Текст
                    У. Р. Стивенс
UNIX
разработка
сетевых приложений
С^ППТЕР


W. Richard Stevens UNIX Network Programming Networking APIs PH PTR Prentice Hall PTR Upper Saddle River, New Jersey 07458 www.phptr.com
У. Р. Стивенс UNIX разработка сетевых приложений МАСТЕР-КЛАСС С^ППТЕР Москва Санкт-Петербург Нижний Новгород Воронеж Ростов-на-Дону Екатеринбург Самара Киев Харьков Минск 2003
ББК 32.973-018.2 УДК 681.3.066 С80 С80 UNIX: разработка сетевых приложений / У. Стивенс. — СПб.: Питер, 2003. — 1088 с.: ил. — (Серия «Мастер-класс»). ISBN 5-318-00535-7 Книга написана известным экспертом по операционной системе UNIX и незаменима для тех. кто занимается созданием web-серверов, клиент-серверных приложений или любого другого сетевого программного обеспечения, так как в ней содержится максимально подробное описание сетевых программных интерфейсов (API), в частности сокетов, которые стали практически стан- дартом для сетевого программирования под Unix. Книга содержит большое количество иллюстрирующих примеров и может использоваться как учебник по программированию в сетях, так и в качестве справочника для опытных програм- мистов. ББК 32.973-018.2 УДК 681.3.066 Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги ISBN 013490012Х (англ.) ISBN 5-318-00535-7 © 1998 Prentice Hall PTR © Перевод на русский язык, ЗАО Издательский дом «Питер», 2003 © Издание на русском языке, оформление, ЗАО Издательский дом «Питер». 2003
Краткое содержание Предисловие......................................26 Часть 1. Введение и протоколы TCP/IP Глава 1. Введение в сетевое программирование.....34 Глава 2. Транспортный уровень: TCP и UDP .......63 Часть 2. Элементарные сокеты Глава 3. Введение в сокеты .....................92 Глава 4. Элементарные сокеты TCP...............118 Глава 5. Пример TCP-соединения клиент-сервер...146 Глава 6. Мультиплексирование ввода-вывода: функции select и poll ........................ 180 Глава 7. Параметры сокетов.......................216 Глава 8. Основные сведения о сокетах UDP.......254 Глава 9. Элементарные преобразования имен и адресов ................................282 Часть 3. Дополнительные возможности сокетов Глава 10. Совместимость IPv4 и IPv6............304 Глава 11. Дополнительные преобразования имен и адресов .....................................315 Глава 12. Процессы-демоны и суперсервер inetd..374 Глава 13. Дополнительные функции ввода-вывода .392 Глава 14. Доменные протоколы Unix..............417
6 Краткое содержание Глава 15. Неблокируемый ввод-вывод...............440 Глава 16. Операции функции ioctl ................470 Глава 17. Маршрутизирующие сокеты ...............490 Глава 18. Широковещательная передача.............514 Глава 19. Многоадресная передача ................534 Глава 20. Дополнительные сведения о сокетах UDP .581 Глава 21. Внеполосные данные ....................617 Глава 22. Управляемый сигналом ввод-вывод .......641 Глава 23. Программные потоки ....................652 Глава 24. Параметры IP...........................687 Глава 25. Символьные сокеты .....................709 Глава 26. Доступ к канальному уровню.............757 Глава 27. Альтернативное устройство клиента и сервера .....................................781 Часть 4. XTI: транспортный интерфейс Х/Ореп Глава 28. XTI: ТСР-клиенты ......................820 Глава 29. XTI: функции имен и адресов............841 Глава 30. XTI: ТСР-серверы.......................855 Глава 31. XTI: клиенты и серверы UDP ............878 Глава 32. Параметры XTI..........................891 Глава 33. Потоки.................................907 Глава 34. XTI: дополнительные функции............926 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6.......................................942
Краткое содержание 7 Приложение Б. Виртуальные сети...................959 Приложение В. Техника отладки ...................964 Приложение Г. Различные исходные коды............977 Приложение Д. Решения некоторых упражнений.......987 Литература .....................................1027 Алфавитный указатель............................1034
Содержание Предисловие ...............................................26 Введение ..................................................26 Изменения по сравнению с первым изданием ..................27 Кому адресована эта книга .................................29 Исходный код и замеченные опечатки ........................30 Благодарности..............................................30 Об авторе..................................................32 От издательства ...........................................32 ЧАСТЬ 1. ВВЕДЕНИЕ И ПРОТОКОЛЫ TCP/IP Глава 1. Введение в сетевое программирование...............34 1.1. Введение .............................................34 1.2. Простой клиент времени и даты ........................37 1.3. Независимость от протокола ...........................41 1.4. Обработка ошибок: функции-обертки.....................43 Значение системной переменной Unix errno .............44 1.5. Простой сервер времени и даты.........................45 1.6. Список примеров технологии клиент-сервер, используемых в книге.48 1.7. Модель OSI............................................50 1.8. История сетей BSD ....................................52 1.9. Сети и узлы, используемые в примерах .................54 Определение топологии сети............................55 1.10. Стандарты Unix.......................................57 Posix ................................................57 Open Group............................................59 Internet Engineering Task Force.......................60 Версии Unix и переносимость ..........................60 1.11. 64-разрядные архитектуры .......................... 60 1.12. Резюме ..............................................61 Упражнения.................................................62 Глава 2. Транспортный уровень: TCP и UDP ..................63 2.1. Введение .............................................63 2.2. Обзор протоколов TCP/IP ..............................63 2.3. UDP: протокол пользовательских дейтаграмм.............66 2.4. TCP: протокол управления передачей....................66
Содержание 9 2.5. Установление и завершение соединения TCP ................68 Трехэтапное рукопожатие..................................68 Параметры TCP ...........................................70 Разрыв соединения TCP ...................................71 Диаграмма состояний TCP..................................72 Просмотр пакетов ........................................73 2.6. Состояние TIME WAIT .....................................75 2.7. Номера портов............................................77 Пара сокетов ............................................79 2.8. Номера портов TCP и параллельные серверы ................79 2.9. Размеры буфера и ограничения.............................82 Отправка по TCP..........................................84 Отправка по UDP .........................................86 2.10. Стандартные службы Интернета ...........................87 2.11. Использование протоколов приложениями Интернета ........88 2.12. Резюме .................................................89 Упражнения....................................................90 ЧАСТЬ 2. ЭЛЕМЕНТАРНЫЕ СОКЕТЫ Глава 3. Введение в сокеты ...................................92 3.1. Введение ................................................92 3.2. Структуры адреса сокетов ................................92 Структура адреса сокета IPv4 ............................92 Универсальная структура адреса сокета ...................95 Структура адреса сокета IPv6 ............................96 Сравнение структур адреса сокетов........................97 3.3. Аргументы типа «значение-результат» .....................98 3.4. Функции определения порядка байтов......................100 3.5. Функции управления байтами..............................103 3.6. Функции inet_aton, inetaddr и inetntoa .................105 3.7. Функции inet_pton и inet_ntop ..........................107 Пример ................................................ 108 3.8. Функция sock ntop и связанные с ней функции.............109 3.9. Функции readn, writen и readline........................Ill 3.10. Функция isfdtype ......................................115 3.11. Резюме ................................................116 Упражнения...................................................117 Глава 4. Элементарные сокеты TCP ............................118 4.1. Введение ...............................................118 4.2. Функция socket..........................................118 Сравнение AF_xxx и PF_xxx ..............................120 4.3. Функция connect ........................................121 4.4. Функция bind............................................123
10 Содержание 4.5. Функция listen...................................................126 4.6. Функция accept...................................................133 Пример: аргументы типа «значение-результат» .....................134 4.7. Функции fork и ехес..............................................136 4.8. Параллельные серверы ............................................138 4.9. Функция close ...................................................141 Счетчик ссылок дескриптора.......................................141 4.10. Функции getsockname и getpeername...............................142 Пример: получение семейства адресов сокета ......................144 4.11. Резюме .........................................................144 Упражнения............................................................145 Глава 5. Пример TCP-соединения клиент-сервер .........................146 5.1. Введение ........................................................146 5.2. Эхо-сервер TCP: функция main.....................................147 5.3. Эхо-сервер TCP: функция str echo ................................148 5.4. Эхо-клиент TCP: функция main ....................................149 5.5. Эхо-клиент TCP: функция str_cli .................................150 5.6. Нормальный запуск................................................151 5.7. Нормальное завершение............................................153 5.8. Обработка сигналов Posix ........................................154 Функция signal ..................................................155 Семантика сигналов Posix.........................................157 5.9. Обработка сигналов SIGCHLD.......................................157 Обработка зомбированных процессов ...............................157 Обработка прерванных системных вызовов ..........................159 5.10. Функции wait и waitpid .........................................160 Различия между функциями wait и waitpid .........................161 5.11. Прерывание соединения перед завершением функции accept .........165 5.12. Завершение процесса сервера ....................................166 5.13. Сигнал SIGPIPE..................................................168 5.14. Сбой на узле сервера............................................170 5.15. Сбой и перезагрузка на узле сервера ............................171 5.16. Выключение узла сервера .......................................,172 5.17. Итоговый пример TCP.............................................172 5.18. Формат данных...................................................174 Пример: передача текстовых строк между клиентом и сервером..174 Пример: передача двоичных структур между клиентом и сервером ... 174 5.19. Резюме .........................................................177 Упражнения.......................................................178 Глава 6. Мультиплексирование ввода-вывода: функции select и poll .....................................180 6.1. Введение ........................................................180 6.2. Модели ввода-вывода .............................................181 Модель блокируемого ввода-вывода............................181
Содержание 11 Модель неблокируемого ввода-вывода ..................182 Модель мультиплексирования ввода-вывода..............182 Модель ввода-вывода, управляемого сигналом...........184 Модель асинхронного ввода-вывода.....................184 Сравнение моделей ввода-вывода ......................185 Сравнение синхронного и асинхронного ввода-вывода ...186 6.3. Функция select.......................................187 При каких условиях дескриптор становится готовым? ...190 Максимальное число дескрипторов для функции select...192 6.4. Функция strcli (продолжение) ........................193 6.5. Пакетный ввод........................................195 6.6. Функция shutdown.....................................198 6.7. Функция str cli (еще раз) ...........................199 6.8. Эхо-сервер TCP (продолжение) ........................201 Атака типа «отказ в обслуживании» ...................206 6.9. Функция pselect .....................................206 6.10. Функция poll .......................................208 6.11. Эхо-сервер TCP (еще раз)............................211 6.12. Резюме .............................................213 Упражнения................................................214 Глава 7. Параметры сокетов................................216 7.1. Введение ............................................216 7.2. Функции getsockopt и setsockopt .....................216 7.3. Проверка наличия параметра и получение значения по умолчанию .... 220 7.4. Состояния сокетов ...................................223 7.5. Общие параметры сокетов..............................224 Параметр сокета SO_BROADCAST ........................224 Параметр сокета SO DEBUG.............................224 Параметр сокета SODONTROUTE .........................224 Параметр сокета SO_ERROR ............................225 Параметр сокета SO KEEPALIVE ........................225 Параметр сокета SO LINGER............................227 Параметр сокета SO OOBINLINE ........................233 Параметры сокета SO_RECVBUF и SO_SNDBUF..............233 Параметры сокета SO RCVLOWAT и SO_SNDLOWAT...........235 Параметры сокета SO_RCVTIMEO и SO_SNDTIMEO ..........235 Параметры сокета SO REUSEADDR и SO REUSEPORT ........236 Параметр сокета SO TYPE .............................239 Параметр сокета SO_USELOOPBACK.......................240 7.6. Параметры сокетов IPv4 ..............................240 Параметр сокета IP HRDINCL ..........................240 Параметр сокета IP OPTIONS...........................240 Параметр сокета IP RECVDSTADDR ......................241 Параметр сокета IP RECVIF ...........................241
12 Содержание Параметр сокета IP_TOS ..............................241 Параметр сокета IPTTL................................241 7.7. Параметр сокета ICMPv6...............................242 Параметр сокета ICMP6 FILTER ........................242 7.8. Параметры сокетов IPv6 ..............................242 Параметр сокета IPv6_ADDRFORM .......................242 Параметр сокета IPv6_CHECKSUM .......................242 Параметр сокета IPv6_DSTOPTS ........................243 Параметр сокета IPv6_HOPLIMIT........................243 Параметр сокета IPv6_HOPOPTS.........................243 IPv6_NEXTHOP ........................................243 Параметр сокета IPv6_PKTINFO ........................243 Параметр сокета IPv6_PKTOPTIONS......................244 Параметр сокета IPv6_RTHDR ..........................244 Параметр сокета IPv6_UNICAST_HOPS ...................244 7.9. Параметры сокетов TCP.............................. 244 Параметр сокета TCP_KEEPALIVE........................244 Параметр сокета TCP_MAXRT ...........................244 Параметр сокета TCP MAXSEG ..........................245 Параметр сокета TCP NODELAY..........................245 Параметр сокета TCP STDURG ..........................248 7.10. Функция fcntl ......................................248 7.11. Резюме .............................................251 Упражнения................................................252 Глава 8. Основные сведения о сокетах UDP..................254 8.1. Введение ............................................254 8.2. Функции recvfrom и sendto............................255 8.3. Эхо-сервер UDP: функция main ........................256 8.4. Эхо-сервер: функция dff echo.........................257 8.5. Эхо-клиент UDP: функция main.........................259 8.6. Эхо-клиент UDP: функция dfi cli......................260 8.7. Потерянные дейтаграммы ..............................261 8.8. Проверка полученного ответа..........................261 8.9. Запуск клиента без запуска сервера...................264 8.10. Итоговый пример клиент-сервера UDP............... 265 8.11. Функция connect для UDP.............................267 Многократный вызов функции connect для сокета UDP....270 Производительность...................................270 8.12. Функция dg cli (продолжение)........................271 8.13. Отсутствие управления потоком в UDP ................272 Приемный буфер сокета UDP............................275 8.14. Определение исходящего интерфейса для UDP ..........276 8.15. Эхо-сервер TCP и UDP, использующий функцию select ..277 8.16. Резюме .............................................280 Упражнения................................................280
Содержание 13 Глава 9. Элементарные преобразования имен и адресов ..............................................282 9.1. Введение ..................................................282 9.2. Система доменных имен......................................282 Записи ресурсов ...........................................283 Распознаватели и серверы имен .............................284 Альтернативы DNS ..........................................285 9.3. Функция gethostbyname......................................285 Пример ................................................... 288 9.4. Параметр распознавателя RESUSEINET6 .......................290 9.5. Функция gethostbyname2 и поддержка IPv6....................291 9.6. Функция gethostbyaddr ................................... 293 Функция gethostbyaddr и поддержка IPv6 ....................294 9.7. Функция uname..............................................294 Пример: определение IP-адресов локального узла.............295 9.8. Функция gethostname .......................................295 9.9. Функции getservbyname и getservbyport......................296 Пример: использование функций gethostbyname и getservbyname .... 297 9.10. Другая информация о сетях ................................300 9.11. Резюме ...................................................301 Упражнения......................................................301 ЧАСТЬ 3. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ СОКЕТОВ Глава 10. Совместимость IPv4 и IPv6 .........................304 10.1. Введение ..............................................304 10.2. Клиент IPv4, сервер IPv6...............................305 10.3. Клиент IPv6, сервер IPv4...............................308 Резюме: совместимость IPv4 и IPv6 ......................310 10.4. Макроопределения проверки адреса IPv6 .................310 10.5. Параметр сокета IPv6_ADDRFORM .........................311 10.6. Переносимость исходного кода ..........................313 10.7. Резюме ................................................314 Упражнения...................................................314 Глава 11. Дополнительные преобразования имен и адресов .................................................315 11.1. Введение ..............................................315 11.2. Функция getaddrinfo....................................315 И.З. Функция gai strerror....................................321 11.4. Функция freeaddrinfo ..................................321 11.5. Функция getaddrinfo: IPv6 и доменный сокет Unix .......322
14 Содержание 11.6. Функция getaddrinfo: примеры ...........................325 11.7. Функция host serv.......................................327 11.8. Функция tcp_connect ....................................328 Пример: клиент времени и даты ...........................329 11.9. Функция tcp listen .....................................331 Пример: сервер времени и даты............................333 Пример: сервер времени и даты с указанием протокола......334 11.10. Функция udp client ....................................336 Пример: не зависящий от протокола клиент времени и даты..337 11.11. Функция udp connect ...................................338 11.12. Функция udpserver .....................................339 Пример: не зависящий от протокола сервер времени и даты .341 11.13. Функция getnameinfo ...................................341 11.14. Функции, допускающие повторное вхождение ..............343 11.15. Функции gethostbyname_r и gethostbyaddr г .............347 11.16. Реализация функций getaddrinfo и getnameinfo ........ 349 Создание структуры addrmfo для каждого адреса ...........357 11.17. Резюме ................................................372 Упражнения....................................................372 Глава 12. Процессы-демоны и суперсервер inetd .. .374 12.1. Введение ...............................................374 12.2. Демон syslogd...........................................375 12.3. Функция sysJog .........................................376 12.4. Функция daemon init.....................................378 Пример: сервер времени и даты в качестве демона..........381 12.5. Демон inetd.............................................382 12.6. Функция daemoninetd ....................................388 Пример: сервер времени и даты, активизированный демоном inetd ... 388 12.7. Резюме .................................................390 Упражнения....................................................390 Глава 13. Дополнительные функции ввода-вывода 392 13.1. Введение ...............................................392 13.2. Тайм-ауты сокета .......................................392 Тайм-аут для функции connect (сигнал SIGALRM) ...........393 Тайм-аут для функции recvfrom (сигнал SIGALRM) ..........394 д. Тайм-аут для функции recvfrom (функция select) ............395 Тайм-аут для функции recvfrom (параметр сокета SO_RCVTIMEO) 397 13.3. Функции recv и send ....................................398 13.4. Функции readv и writev .................................400 13.5. Функции recvmsg и sendmsg...............................400 13.6. Вспомогательные данные .................................405 13.7. Сколько данных находится в очереди? ....................408
Содержание 15 13.8. Сокеты и стандартный ввод-вывод ..............................409 Пример: функция str echo, использующая стандартный ввод-вывод ...........................................410 13.9. Т/ТСР: TCP для транзакций ....................................412 13.10. Резюме.......................................................415 Упражнения..........................................................415 Глава 14. Доменные протоколы Unix...................................417 14.1. Введение .....................................................417 14.2. Структура адреса доменного сокета Unix .......................418 Пример: функция bind и доменный сокет Unix.....................418 14.3. Функция socketpair ...........................................420 14.4. Функции сокетов ..............................................421 14.5. Клиент и сервер потокового доменного протокола Unix ..........422 14.6. Клиент и сервер дейтаграммного доменного протокола Unix.......424 14.7. Передача дескрипторов ........................................426 Пример передачи дескриптора ...................................427 14.8. Получение информации об отправителе ..........................434 Пример ........................................................435 14.9. Резюме .......................................................438 Упражнения..........................................................438 Глава 15. Неблокируемый ввод-вывод..................................440 15.1. Введение .....................................................440 15.2. Неблокируемые чтение и запись: функция str_cli (продолжение) .441 Более простая версия функции str_cli ..........................449 Сравнение времени выполнения различных версий функции strcli........................................452 15.3. Неблокируемая функция connect ................................453 15.4. Неблокируемая функция connect: клиент времени и даты .........454 Прерванная функция connect ....................................457 15.5. Неблокируемая функция connect: клиент Web ....................457 Эффективность одновременных соединений.........................465 15.6. Неблокируемая функция accept .................................466 15.7. Резюме .......................................................468 Упражнения........................................................ 469 Глава 16. Операции функции ioctl ...................................470 16.1. Введение .....................................................470 16.2. Функция ioctl ................................................470 16.3. Операции с сокетами...........................................472 16.4. Операции с файлами............................................473 16.5. Конфигурация интерфейса.......................................473 16.6. Функция get_ifi_info..........................................475 16.7. Операции с интерфейсами ......................................484
16 Содержание 16.8. Операции с кэшем ARP .....................................485 Пример: вывод аппаратного адреса узла .....................486 16.9. Операции с таблицей маршрутизации ........................488 16.10. Резюме ..................................................488 Упражнения......................................................488 Глава 17. Маршрутизирующие сокеты...............................490 17.1. Введение .................................................490 17.2. Структура адреса сокета канального уровня.................491 17.3. Чтение и запись................................'..........492 Пример: получение и вывод записи из таблицы маршрутизации .494 17.4. Операции функции sysctl ..................................500 Пример: определяем, включены ли контрольные суммы UDP .....503 17.5. Функция getifiinfo........................................504 17.6. Функции имени и индекса интерфейса .......................508 Функция if nametoindex.....................................509 1 Функция if_indextoname .....................................510 ‘ Функция if nameindex..........................................511 Функция if freenameindex ................................ 512 17.7. Резюме ...................................................512 Упражнения......................................................513 Глава 18. Широковещательная передача............................514 18.1. Введение .................................................514 18.2. Широковещательные адреса..................................516 18.3. Сравнение направленной и широковещательной передач .......517 18.4. Функция dgcli при использовании широковещательной передачи ... 521 IP-фрагментация и широковещательная передача...............524 18.5. Ситуация гонок............................................524 Применение IPC в обработчике сигнала функции...............530 18.6. Резюме ...................................................532 Упражнения......................................................533 Глава 19. Многоадресная передача ...............................534 19.1. Введение .................................................534 19.2. Адрес многоадресной передачи .............................534 Адреса IPv4 класса D ......................................534 Адреса многоадресной передачи IPv6.........................536 Область действия адресов многоадресной передачи ...........536 19.3. Сравнение многоадресной и широковещательной передач в локальной сети ....................................538 19.4. Многоадресная передача в глобальной сети .................540 19.5. Параметры сокетов многоадресной передачи..................543 19.6. Функция mcast_join и родственные функции .................547 Пример: функция mcastjoin ...............................549 Пример: функция mcast set loop...........................551
Содержание 17 19.7. Функция dg cli, использующая многоадресную передачу..........552 Фрагментация IP и многоадресная передача .....................552 19.8. Получение анонсов сеанса МВопе ..............................553 19.9. Отправка и получение ........................................556 19.10. SNTP: простой синхронизирующий сетевой протокол ............560 19.11. SNTP (продолжение) .........................................564 19.12. Резюме......................................................578 Упражнения.........................................................579 Глава 20. Дополнительные сведения о сокетах UDP ... 581 20.1. Введение ....................................................581 20.2. Получение флагов, IP-адреса получателя и индекса интерфейса .582 Пример: вывод IP-адреса получателя и уведомления о том, что дейтаграмма обрезана ............................586 20.3. Обрезанные дейтаграммы ......................................589 20.4. Когда UDP оказывается предпочтительнее TCP ..................590 20.5. Добавление надежности приложению UDP ........................593 Пример .......................................................596 20.6. Связывание с адресами интерфейсов ...........................604 20.7. Параллельные серверы UDP ....................................609 20.8. Информация о пакете IPv6.....................................612 Исходящий и входящий интерфейс................................613 Адрес отправителя и адрес получателя IPv6 ....................613 Задание и получение предельного количества транзитных узлов.614 Задание адреса следующего транзитного узла....................614 20.9. Резюме ......................................................615 Упражнения.........................................................615 Глава 21. Внеполосные данные.......................................617 21.1. Введение ....................................................617 21.2. Внеполосные данные протокола TCP ............................617 Простой пример использования сигнала SIGURG ..................620 Простой пример использования функции select...................623 21.3. Функция sockatmark ..........................................625 Пример ...................................................... 626 Пример .......................................................628 Пример ...................................................... 631 21.4. Резюме по теме внеполосных данных TCP .......................632 21.5. Клиент-серверные функции проверки пульса ....................634 21.6. Резюме ......................................................639 Упражнения.........................................................640 Глава 22. Управляемый сигналом ввод-вывод .........................641 22.1. Введение ....................................................641
18 Содержание 22 2 Управляемый сигналом ввод-вывод для сокетов 641 Сигнал SIGIO и сокеты UDP 642 Сигнал SIGIO и сокеты TCP 642 22 3 Эхо-сервер UDP с использованием сигнала SIGIO 644 22 4 Резюме 651 Упражнение 651 Глава 23. Программные потоки .................................652 23 1 Введение 652 23 2 Основные функции для работы с потоками создание и завершение потоков 653 Функция pthread_create 653 Функция pthread_join 654 Функция pthread_self 655 Функция pthread_detach 655 Функция pthread_exit 655 23 3 Использование потоков в функции str_cli 656 23 4 Использование потоков в эхо-сервере TCP 658 Передача аргументов новым потокам 660 Функции, безопасные в многопоточной среде 662 23 5 Собственные данные потоков 663 Пример функция readline, использующая собственные данные потока 669 23 6 Web-клиент и одновременное соединение (продолжение) 672 23 7 Взаимные исключения 675 23 8 Условные переменные 680 23 9 Web-клиент и одновременный доступ 684 23 10 Резюме 685 Упражнения 686 Глава 24. Параметры IP........................................687 24 1 Введение 687 24 2 Параметры IPv4 687 24 3 Параметры маршрута от отправителя IPv4 689 Пример 694 Уничтожение маршрута, полученного от отправителя 697 24 4 Заголовки расширения IPv6 698 24 5 Параметры транзитных узлов и параметры получателя IPv6 699 24 6 Заголовок маршрутизации IPv6 703 24 7 «Закрепленные» параметры IPv6 706 24 8 Резюме 707 Упражнения 708 Глава 25. Символьные сокеты ..................................709 25 1 Введение 709 25 2 Создание символьных сокетов 710
Содержание 19 25 3 Вывбд на символьном сокете 710 Особенности символьного сокета версии IPv6 711 Параметр сокета IPv6_CHECKSUM 712 25 4 Ввод через символьный сокет 712 Фильтрация ICMPv6 714 25 5 Программа Ping 715 25 6 Программа Traceroute 727 Пример 739 25 7 Демон сообщений ICMP 740 Эхо-клиент UDP, использующий демон icmpd 743 ' Примеры эхо-клиента UDP 746 Демон icmpd 746 258 Резюме 756 Упражнения 756 Глава 26. Доступ к канальному уровню.........................757 26 1 Введение 757 26 2 BPF пакетный фильтр BSD 757 26 3 DLPI интерфейс поставщика канального уровня 760 26 4 Linux SOCK PАСКЕТ 761 26 5 Libcap библиотека для захвата пакетов 762 26 6 Анализ поля контрольной суммы UDP 762 Пример 779 26 7 Резюме 780 Упражнения 780 Глава 27. Альтернативное устройство клиента и сервера ................................................781 27 1 Введение 781 27 2 Альтернативы для клиента TCP 785 27 3 Тестовый клиент TCP 785 27 4 Последовательный сервер TCP 787 27 5 Параллельный сервер TCP один дочерний процесс для каждого клиента .. 787 27 6 Сервер TCP с предварительным порождением процессов без блокировки для вызова accept 791 Реализация 4 4BSD .. . 794 Эффект наличия слишком большого количества дочерних ’ процессов ... 796 Распределение клиентских соединений между дочерними процессами 796 Коллизии при вызове функции select 797 27 7 Сервер TCP с предварительным порождением процессов и защитой вызова accept блокировкой файла 798 Эффект наличия слишком большого количества дочерних процессов 801
20 Содержание Распределение клиентских соединении между дочерними процессами 801 27 8 Сервер TCP с предварительным порождением процессов и защитой вызова accept при помощи взаимного исключения 802 27 9 Сервер TCP с предварительным порождением процессов передача дескриптора 804 27 10 Параллельный сервер TCP один поток для каждого клиента 809 27 И Сервер TCP с предварительным порождением потоков, каждый из которых вызывает accept 811 27 12 Сервер с предварительным порождением потоков основной поток вызывает функцию accept 813 27 13 Резюме 817 Упражнения 818 ЧАСТЬ 4. XTI: ТРАНСПОРТНЫЙ ИНТЕРФЕЙС X/OPEN Глава 28. XTI: ТСР-клиенты...................................820 28 1 Введение 820 28 2 Функция t open 821 28 3 Функции t_error и t_strerror 825 28 4 Структуры netbuf и структуры протокола XTI 826 28 5 Функция t_bind 827 28 6 Функция t_connect 829 28 7 Функции t rcv и tsnd 830 28 8 Функция t_look 832 28 9 Функции t sndrel и t rcvrel 833 28 10 Функции t_snddis и t rcvdis 834 28 11 Клиент времени и даты для протоколов XTI и TCP 835 Возможность взаимодействия сокетов и XTI 838 28 12 Функция xti_rdwr 838 28 13 Резюме 840 Упражнения 840 Глава 29. XTI: функции имен и адресов .......................841 29 1 Введение 841 29 2 Файл /etc/netconfig и функции netconfig 841 29 3 Переменная NETPATH и функция netpath 843 29 4 Функции netdir 844 29 5 Функции t_alloc и t free 846 29 6 Функции t getprotaddr 848 29 7 Функция xti_ntop 848 29 8 Функция tcp connect 849 Пример 852 29 9 Резюме 853 Упражнения 854
Содержание 21 Глава 30. XTI: ТСР-серверы 855 30 1 Введение 855 30 2 Функция t_hsten 857 30 3 Функция tcplisten 858 30 4 Функция t_accept 860 30 5 Функция xtiaccept 861 30 6 Простой сервер времени и даты 863 30 7 Несколько соединений, ожидающих обработки 865 30 8 Функция xti_accept (еще раз) 867 Длина очереди XTI и аргумент backlog функции listen 874 Установка сервером единичной длины очереди 875 30 9 Резюме 876 Упражнения 876 Глава 31. XTI: клиенты и серверы UDP 878 31 1 Введение 878 312 Функции t rcvudataHt sndudata 878 31 3 Функция udp client 879 Пример клиент времени и даты 882 314 Функция t_rcvuderr асинхронные ошибки 883 Пример сообщение ICMP о недоступности порта 884 31 5 Функция udp_server 886 Пример сервер времени и даты 887 316 Чтение дейтаграммы по частям 888 31 7 Резюме 890 Глава 32. Параметры XTI 891 32 1 Введение 891 32 2 Структура t opthdr 894 32 3 Параметры XTI 895 Параметр XTIDEBUG 895 Параметр XTILINGER 896 Параметры XTI_RCVBUF и XTI RCVLOWAT 896 Параметры XTI_SNDBUF и XTI SNDLOWAT 896 Параметр T IP BROADCAST 896 Параметр T_IP_DONTROUTE 896 Параметр T IP OPTIONS 896 Параметр T IP REUSEADDR 897 Параметр TIPTOS 897 Параметр TIPTTL 897 Параметр T TCP KEEPALIVE 898 Параметр T_TCP_MAXSEG 898 Параметр Т_ТСР_NODELAY 898 Параметр T_UDP_CHECKSUM 898 32 4 Функция t optmgmt 899
22 Содержание 32.5. Проверка наличия параметра и получение значения по умолчанию ... 899 32.6. Получение и установка значений параметров XTI ..........903 Функция xti getopt.......................................903 Функция xti setopt ......................................904 Пример ..................................................906 32.7. Резюме .................................................906 Глава 33. Потоки..............................................907 33.1. Введение ...............................................907 33.2. Обзор ..................................................907 Типы сообщений ..........................................910 33.3. Функции getmsg и putmsg.................................912 33.4. Функции getpmsg и putpmsg...............................913 33.5. Функция ioctl ..........................................914 33.6. TPI: интерфейс поставщика транспортных служб ...........914 33.7. Резюме .................................................925 Упражнение....................................................925 Глава 34. XTI: дополнительные функции.........................926 34.1. Введение ...............................................926 34.2. Неблокируемый ввод-вывод................................926 34.3. Функция t rcvconnect ...................................927 34.4. Функция t getinfo ......................................928 34.5. Функция t getstate .....................................928 34.6. Функция t sync .........................................929 34.7. Функция t_unbind .......................................931 34.8. Функции t rcvv и t_rcwudata.............................931 34.9. Функции t_sndv и t sndvudata ...........................932 34.10. Функции t rcvreldata и t_sndreldata....................932 34.11. Управляемый сигналом ввод-вывод .......................933 34.12. Внеполосные данные ....................................934 Пример использования сигнала SIGPOLL.....................935 Пример использования функции poll .......................938 34.13. Поставщики транспортных служб закольцовки .............939 34.14. Резюме.................................................940 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 ..................................................942 А. 1. Введение ...............................................942 А.2. Заголовок IPv4 ..........................................942 А.З. Заголовок IPv6 ..........................................944 А.4. Адресация IPv4...........................................946 Бесклассовые адреса и CIDR...............................947 Адреса подсетей .........................................948 Адрес закольцовки........................................950
Содержание 23 Неопределенный адрес...................................950 Многоинтерфейсность и псевдонимы адресов...............950 А.5. Адресация IPv6.........................................951 Объединяемые глобальные индивидуальные адреса..........952 Тестовые адреса бЬопе .................................953 Адреса IPv4, преобразованные к виду IPv6...............953 Адреса IPv4, совместимые с IPv6 .......................954 Адрес закольцовки......................................954 Неопределенный адрес...................................954 Адрес локальной связи..................................955 Адрес, локальный на уровне сайта ......................955 А.6. ICMPv4 и ICMPv6: протокол управляющих сообщений в сети Интернет .........................................955 Приложение Б. Виртуальные сети .............................959 Б.1. Введение ..............................................959 Б.2. МВопе .................................................959 Б.З. бЬопе..................................................961 Приложение В. Техника отладки...............................964 В.1. Трассировка системного вызова .........................964 Библиотека потоковых сокетов SVR4 .....................964 Потоковая XTI-библиотека SVR4 .........................966 Сокеты ядра BSD........................................968 Сокеты ядра Solaris 2.6................................968 В.2. Стандартные службы Интернета ..........................969 В.З. Программа sock.........................................969 В.4. Небольшие тестовые программы ..........................972 Пример: определение полосы приоритета внеполосных данных XTI ..........................................972 Пример: определение события для получения внеполосных (срочных) данных XTI.................................973 В.5. Программа tcpdump .....................................974 В.6. Программа netstat ................................... 975 В.7. Программа Isof ........................................975 Приложение Г. Различные исходные коды ......................977 Г.1. Заголовочный файл unp.h ...............................977 Г.2. Заголовочный файл config.h ............................981 Г.З. Заголовочный файл unpxti.h.............................982 Г.4. Стандартные функции обработки ошибок...................983 Приложение Д. Решения некоторых упражнений ,.. 987 Глава 1 ....................................................987 Глава 2 ....................................................988
24 Содержание Глава 3 ........................................................989 Глава 4 ........................................................989 Глава 5 ........................................................989 Глава 6 ........................................................993 Глава 7 ........................................................994 Глава 8 ........................................................999 Глава 9 .......................................................1001 Глава 10 ......................................................1007 Глава 11 ......................................................1007 Глава 12 ......................................................1008 Глава 13 ......................................................1009 Глава 14 ......................................................1009 Глава 15 ......................................................1012 Глава 16 ......................................................1013 Глава 17 ......................................................1013 Глава 18 ......................................................1013 Глава 19 ......................................................1013 Глава 20 ......................................................1016 Глава 21 ......................................................1019 Глава 22 ..................................................... 1020 Глава 23 ..................................................... 1020 Глава 24 ..................................................... 1020 Глава 25 ..................................................... 1021 Глава 26 ..................................................... 1022 Глава 27 ..................................................... 1022 Глава 28 ..................................................... 1022 Глава 29 ................................................... 1023 Глава 30 ..................................................... 1024 ГлаваЗЗ .......................................................1026 Литература ..............................................1027 Алфавитный указатель.....................................1034
Салли, Биллу, Элен и Дэвиду Aloha nui loa
Предисловие Введение Сетевое программирование подразумевает написание программ, взаимодейству- ющих через сеть. Одна из этих программ обычно называется клиентом, а дру- гая — сервером. В большинстве операционных систем имеются предварительно скомпилированные программы, взаимодействующие через сеть — в мире TCP/IP наиболее типичным примером таких программ являются web-клиенты (браузе- ры) и web-серверы, а также клиенты и серверы FTP и Telnet, — однако в этой книге рассказывается о том, как писать собственные сетевые приложения. Сетевые приложения пишутся с использованием программного интерфейса приложений, или API {Application Program Interface). В этой книге мы рассматри- ваем два API для сетевого программирования: 1. Сокеты, иногда называемые Беркли-сокетами (Berkeley), что указывает на их связь с Berkeley Unix. 2. XTI (X/Open Transport Interface — транспортный интерфейс группы X/Open), являющийся модификацией TLI (Transport Layer Interface — интерфейс транс- портного уровня), разработанного группой AT&T. Все примеры в этой книге относятся к операционной системе Unix, хотя ос- новные понятия и концепции сетевого программирования практически не зависят от операционной системы. В примерах используется набор протоколов TCP/IP, причем рассматривается как IP версии 4, так и IP версии 6. Для написания сетевых приложений необходимо знание лежащей в их основе операционной системы и сетевых протоколов. Эта книга опирается на другие мои книги по двум указанным темам: Advanced Programming in the Unix Environment [93]; TCP/IP Illustrated, vol. 1 [94]; TCP/IP Illustrated, vol. 2 [105]; TCP/IP Illustrated, vol. 3 [95]. Это книга, являющаяся вторым изданием книги «UNIX Network Programming», содержит также сведения по операционной системе Unix и по протоколам TCP/IP, но для получения более подробной информации по различным темам в этих об- ластях следует обращаться к четырем перечисленным выше книгам, используя многочисленные ссылки, включенные в текст. В большей степени это относится к книге [105], в которой представлена реализация 4.4BSD функций сетевого про- граммирования для API сокетов (socket, bi nd, connect и т. д.). При понимании того, как реализована та или иная функциональная возможность, ее применение в при- ложениях становится более осмысленным.
Предисловие 27 Изменения по сравнению с первым изданием Второе издание этой книги содержит очень много изменений. Все они стали ре- зультатом моего опыта преподавания данного материала и чтения материалов сетевых конференций Usenet в период с 1990 по 1996 год — это дало мне возмож- ность выявить те темы и концепции сетевого программирования, которые регу- лярно оказываются неверно понятыми. Ниже перечислены основные изменения, внесенные во второе издание: Для всех примеров в этом издании используется ANSI С. Старые главы 6 («Berkeley Sockets» — «Сокеты Беркли») и 8 («Library Rou- tins» — «Библиотечные функции») были расширены, и теперь соответствую- щий материал занимает 25 глав. Это семикратное увеличение (из расчета количества слов), вероятно, является самым значительным изменением со вре- мени первого издания. Большинство разделов прежней главы 6 теперь вырос- ли в отдельные главы, при этом было увеличено количество примеров. Части прежней главы 6, посвященные TCP и UDP, теперь разделены. Снача- ла мы рассматриваем функции TCP и все, что относится к клиент-серверному взаимодействию TCP, а затем — функции UDP и взаимодействие клиента и сервера UDP. Для новичков в этой области такой подход будет проще, чем, например, подробное изучение всех особенностей функции connect с различ- ной семантикой в случае UDP и TCP. Старая глава 7 («System V Transport Layer Interface» — «Интерфейс транспорт- ного уровня System V») была расширена, и теперь соответствующий матери- ал занимает 7 глав. Мы также рассматриваем более новую технологию XTI, пришедшую на смену технологии TLI, о которой шла речь в первом издании. Глава 2 первого издания («The Unix Model» — «Модель Unix») не вошла во второе издание. В этой главе содержался обзор системы Unix, и занимала она 75 страниц. В 1990 году эта глава была необходима, поскольку было не так много книг, адекватно описывающих основной программный интерфейс Unix (в частности, существовавшие на тот период различия между реализациями Беркли и System V). В настоящее время гораздо больше читателей имеют пред- ставление о Unix, и поэтому такие понятия, как идентификатор пользователя, файлы паролей, каталоги и идентификаторы групп пользователей, уже не нуж- даются в особом пояснении. (Для читателей, которым необходима дополни- тельная информация в области программирования под Unix, предназначена моя книга [93], содержащая 700 страниц материала по этим вопросам.) Некоторые более сложные темы из прежней главы 2, предназначенные для опытных программистов, вошли и во второе издание, но они рассматриваются параллельно с применением соответствующей функциональности. Например, при рассмотрении нашего первого параллельного сервера (раздел 4.8) мы по- дробно описываем функцию fork. Когда мы описываем обработку сигнала SIGCHLD, мы рассказываем о многих дополнительных функциональных возмож- ностях обработки сигналов Posix (зомбированные процессы, прерванные сис- темные вызовы и т. д.).
28 Предисловие Везде в книге мы стремились по возможности использовать стандарт Posix. (Более подробно о семействе стандартов Posix говорится в разделе 1.10.) Сюда относятся не только стандарт Posix. 1 для основных функций Unix (управле- ние процессами, сигналы и т. п.), но и более новый стандарт Posix. 1g для соке- тов и XTI и стандарт Posix. 1 1996 года для потоков. При описании таких функций, как socket и connect, термин «системный вы- зов» был заменен на термин «функция» Это связано с принятым в Posix со- глашением о том, что различие между системным вызовом и функцией — это подробность реализации, как правило, не имеющая значения для программи- ста. Прежние главы 4 («А Network Primer» — «Сетевой букварь») и 5 («Commu- nication Protocols» — «Протоколы передачи данных») заменены приложени- ем А, в котором описываются протоколы IP версии 4 (IPv4) и версии 6 (IPv6), и главой 2, в которой рассказывается о протоколах TCP и UDP. Этот новый материал в основном посвящен тем аспектам сетевых протоколов, с которыми наиболее часто встречается разработчик сетевых приложений. В книге также приводится описание протокола IPv6. Хотя на момент написания книги этот протокол только начал применяться, он, вероятно, станет наиболее широко используемым протоколом, когда книга дойдет до читателя. В процессе преподавания сетевого программирования я пришел к выводу, что около 80% всех возникающих в этой области проблем на самом деле не имеет ничего общего с собственно сетевым программированием. Я имею в виду, что эти проблемы связаны не с функциями API, такими как accept или sei ect, а с не- пониманием лежащих в их основе сетевых протоколов. Например, я обнару- жил, что как только студенту удается разобраться с трехэтапным рукопожа- тием {three-way handshake) и последовательностью обмена четырьмя пакетами при завершении соединения, ему становятся понятны и многие аспекты сете- вого программирования. Из второго издания удалены разделы, посвященные XNS, SNA, NetBIOS, про- токолам OSI и UUCP, так как уже в начале 1990-х стало очевидно, что они уступили место протоколам TCP/IP. (Хотя UUCP не устарел и до сих пор пользуется популярностью, в контексте сетевого программирования мало что можно рассказать об этом протоколе.) Во втором издании освещены следующие новые темы: □ Совместимость IPv4 и IPv6 (глава 10). □ Независимые от протокола преобразования имен (глава 11). □ Маршрутизирующие сокеты (глава 17). □ Многоадресная передача (глава 19). □ Потоки (глава 23). □ Параметры IP (глава 24). □ Доступ к канальному уровню (глава 26). □ Альтернативное устройство клиента и сервера (глава 27).
Предисловие 29 □ Виртуальные сети и туннелирование (приложение Б). □ Технологии отладки сетевых программ (приложение В). К первому изданию было сделано столько добавлений, что, к сожалению, по- лучившийся материал не может войти в одну книгу. Поэтому планируется выпу- стить еще как минимум два тома. Второй том1 получит подзаголовок «Взаимодействие процессов» (IPC: Inter- process Communications) и станет расширением главы 3 первого издания. В не- го также войдет описание механизмов IPC реального времени по стандарту Posix.l. Третий том будет иметь подзаголовок «Приложения» («Applications») и бу- дет представлять собой расширение глав 9-18 первого издания. Большинство сетевых приложений будет рассматриваться в третьем томе, но некоторые специальные приложения рассматриваются в этом томе, а именно про- граммы Ping, Traceroute и демон inetd. Кому адресована эта книга Эту книгу можно использовать и как учебное пособие по сетевому программиро- ванию, и как справочник для более опытных программистов. При использовании его как учебника или для ознакомления с сетевым программированием следует уделить особое внимание второй части («Элементарные сокеты», главы 3-9), после чего можно переходить к чтению тех глав, которые представляют наиболь- ший интерес. Во второй части рассказывается об основных функциях сокетов — как для TCP, так и для UDP; кроме того, рассматриваются мультиплексирование ввода-вывода, параметры сокетов и основные преобразования имен и адресов. Всем читателям следует прочесть главу 1, в особенности раздел 1.4, так как в нем описаны некоторые функции-обертки, используемые далее во всей книге. Гла- ва 2 и, возможно, приложение А могут быть использованы по мере необходимо- сти для получения справочных сведений в зависимости от уровня подготовки читателя. Большинство глав в третьей части («Дополнительные возможности сокетов») могут быть прочитаны независимо от других содержащихся в этой же части. Для тех, кто собирается использовать эту книгу в качестве справочного посо- бия, имеется подробный предметный указатель. Для тех, кто будет читать только выборочные главы в произвольном порядке, в книге имеются ссылки на те места, где обсуждаются близкие темы. Хотя API сокетов стал фактическим стандартом сетевого программирования, наравне с ним используется и XTI, иногда с отличным от TCP/IP набором прото- колов. Интерфейсу XTI посвящена четвертая часть. Он описан не так подробно, как интерфейс сокетов во второй и третьей частях. Это объясняется тем, что ос- новные концепции интерфейса сокетов применимы и к XTI. Например, все кон- Этот том также переведен и выпущен издательством «Питер» У. Стивенс UNIX взаимодействие процессов. — СПб: Питер, 2002.
30 Предисловие цепции интерфейса сокетов, относящиеся к использованию неблокируемого вво- да-вывода, широковещательной и многоадресной передачи, управляемого сигна- лом ввода-вывода, внеполосных данных и потоков, остаются в силе и для интер- фейса XTI. Действительно, многие аспекты сетевого программирования схожи в своей основе независимо от того, сокеты или XTI вы используете в своих про- граммах, и вряд ли есть какие-то задачи, которые один API позволяет решить, а другой нет. Концепции остаются неизменными — меняются лишь имена функ- ций и их аргументы. Исходный код и замеченные опечатки Исходный код для всех примеров расположен на моей домашней странице1, ад- рес которой указан в конце предисловия. Чтобы научиться сетевому программи- рованию, лучше всего будет взять эти программы, изменить их и расширить. На самом деле написание программ таким образом является единственным способом овладеть изученными технологиями. В конце каждой главы приводятся упраж- нения, а ответы на большинство из них содержатся в приложении Г. Список найденных опечаток по этой книге также находится на моей домаш- ней странице. Благодарности Для каждого автора самой существенной является поддержка со стороны семьи — без этого не было бы написано ни одной книги! Я благодарен всем членам моей семьи — Салли, Биллу, Элен и Дэвиду — за их поддержку и понимание при напи- сании моей первой книги (первого издания этой книги) и за их терпение, прояв- ленное во время создания этой «маленькой» переделки. Благодаря их любви, под- держке и одобрению стало возможным появление этой книги. Многочисленные рецензенты снабдили меня ценными замечаниями и ука- заниями (всего более 190 страниц текста, или 70 000 слов), обращая мое внима- ние на многочисленные ошибки и те области, которые требовали более подроб- ного изложения, а также предложили альтернативные варианты формулировок, изложения материала и самих программ. Это Рагнвалд Блайндхейм (Ragnvald Blindheim), Джим Баунд (Jim Bound), Гэвин Боуи (Gavin Bowe), Аллен Бриггс (Allen Briggs), Джо Дупник (Joe Doupnik), Вучан Фен (Wuchang Feng), Билл Феннер (Bill Fenner), Боб Фриснан (Bob Friesenhahn), Эндрю Гиерт (Andrew Gierth), Вайн Хэтвей (Wayne Hathaway), Кент Хофер (Kent Hofer), Смоги Джа- мин (Sugih Jamin), Скотт Джонсон (Scott Johnson), Рик Джонс (Rick Jones), Ma- кеш Кэкер (Mukesh Kacker), Марк Лампо (Marc Lampo), Марти Лейснер (Marty Leisner), Джек Макканн (Jack McCann), Крейг Метц (Craig Metz), Боб Нельсон (Bob Nelson), Эви Немис (Evi Nemeth), Джон Нобл (John С. Noble), Стив Рэго (Steve Rago), Джим Рейд (Jim Reid), Чун-Шан Шао (Chung-Shang Shao), Иан Ланс Тейлор (Ian Lance Taylor), Рон Тейлор (Ron Taylor), Андреас Терзис (An- dreas Terzis) и Дейв Телер (Dave Thaler). Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http.// п,гЙГ rnm /download.
Предисловие 31 Особую благодарность я выражаю С. Джамину и его студентам по курсу EECS 489 (Компьютерные сети) в Мичиганском университете, осуществившим «бета-тестирование» одного из первых вариантов текста весной 1997 года. Те, чьи имена указаны далее, отвечали на мои вопросы (иногда весьма много- численные) по электронной почте, что позволило повысить точность формули- ровок и улучшить способ изложения материала: Дейв Бутенхов (Dave Butenhof), Дейв Хансон (Dave Hanson), Джим Хог (Jim Hogue), Макеш Кэкер (Mukesh Каскег), Брайан Керниган (Brian Kernighan), Верн Паксон (Vern Paxson), Стив Рэго (Steve Rago), Дэннис Ритчи (Dennis Ritchie), Стив Саммит (Steve Summit), Поль Викси (Paul Vixie), Джон Вейт (John Wait), Стив Вайс (Steve Wise) и Гари Райт (Gary Wright). Моя особая благодарность Ларри Рафски (Larry Rafsky) и замечательной ко- манде Gari Software за многочисленные интересные обсуждения технических деталей. Спасибо тебе, Ларри! Многие люди и те организации, в которых они работали, шли мне навстречу, предоставляя программное обеспечение или доступ к системе, необходимые для тестирования некоторых примеров к книге. Мег МакРобертс (Meg McPoberts) из SCO предоставил последние выпуски UnixWare, а Дион Джонсон (Dion Johnson), Ясмин Куреши (Yasmin Kureshi), Майкл Таунсенд (Michael Townsend) и Брайан Зиел (Brian Ziel) отвечали на мои многочисленные вопросы. Макеш Кэкер (Mukesh Каскег) из SunSoft обеспечил доступ к бета-версии Solaris 2.6 и ответил на мои многочисленные вопросы о реализации TCP/IP в Solaris. Джим Баунд (Jim Bound), Матт Томас (Matt Thomas), Мэри Клаутер (Магу Clouter) и Барб Гловер (Barb Glover) из Digital Equipment Corp, предостави- ли систему Alpha и доступ к последней версии IPv6 для DigitalUnix. Майкл Джонсон (Michael Johnson) из Red Hat Software предоставил после- дние выпуски программного обеспечения Red Hat Linux. Стив Вайс (Steve Wise) и Джесси Хог (Jessie Haug) из IBM Austin предоста- вили систему RS/6000 и доступ к последней версии IPv6 для AIX. Рик Джонс (Rick Jones) из Hewlett-Packard предоставил доступ к бета-версии HP-UX 10.30, а также совместно с Вильямом Гиллиамом (William Gilliam) отвечал на мои многочисленные вопросы по этой теме. Многие помогали мне при работе с Интернетом. Еще раз хочу поблагодарить сотрудников Национальной оптической астрономической обсерватории (NO АО) — Сиднея Вольфа (Sidney Wolff), Ричарда Вольфа (Richard Wolf) и Стива Гранди (Steve Grandi) — за обеспечение доступа к их сетям и узлам. Дейв Сигал (Dave Siegel), Джастус Аддис (Justus Addiss) и Поль Лачино (Paul Lucchina) отвечали на мои многочисленные вопросы, Фил Кэсло (Phil Kaslo) и Джим Дэвис (Jim Davis) обеспечивали соединение с Mbone, Рэн Аткинсон (Ran Atkinson) и Педро Маркес (Perdo Marques) обеспечивали соединение с бЬопе, а Крейг Метц (Craig Metz) много помогал мне при работе с DNS.
32 Предисловие Сотрудники издательства Prentice Hall, особенно мой редактор Мэри Франц (Mary Franz) вместе с Норин Реджин (Noreen Regina), Софи Папаниколау (Sophie Papanikolaou) и Эйлин Кларк (Eileen Clark), — чудесная находка для автора Я очень признателен за то, что имел возможность столь многое сделать именно так, как мне хотелось Как обычно (но в противоположность общепринятым технологиям), я сделал оригинал-макет книги, используя замечательный пакет grof f, написанный Джейм- сом Кларком (James Clark) Я набрал все 291 972 слова, используя редактор wi, создал 201 иллюстрацию с помощью программы gpic (используя многие из мак- росов Гари Райта), сделал 81 таблицу с помощью программы gtbl, составил пред- метный указатель и подготовил окончательный макет страниц Программа Дей- ва Хансона (Dave Hanson) loom и некоторые сценарии Гари Райта (Gary Wright) использовались для включения кода программ в книгу Набор сценариев на язы- ке awk, написанный Джоном Бентли (Jon Bentley) и Брайаном Керниганом (Brian Kernighan), помогал в создании предметного указателя С нетерпением жду комментарии, предложения и сообщения о замеченных опечатках W Richard Stevens Tucson, Arizona September 1997 rstevens@kohala.com http://www.kohala.com/~rstevens Об авторе УИЛЬЯМ РИЧАРД СТИВЕНС (умер в 1999 году) был автором книги «UNIX Network Programming First Edition», получившей широкое распространение как классическая работа по сетевому программированию в UNIX Он также написал «Advanced Programming in the UNIX Enviroment» и серию книг «TCP/IP Illu- strated» От издательства Ваши замечания, предложения, вопросы отправляйте по адресу электронной по^- ты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Исходные коды всех программ, приведенных в книге, вы можете найти по ад- ресу http.//www.piter.com/download На web сайте издательства http://www.piter.com вы найдете подробную инфор- мацию о наших книгах
ЧАСТЬ 1 ВВЕДЕНИЕ И ПРОТОКОЛЫ TCP/IP
ГЛАВА 1 Введение в сетевое программирование 1.1. Введение Большинство сетевых приложений можно разделить на две группы: клиенты и серверы. Связь между ними демонстрирует рис. 1.1. Рис. 1.1. Сетевое приложение: клиент и сервер Можно привести множество примеров клиентов и серверов, с которыми, ве- роятно, читатель знаком: web-браузер (клиент), соединяющийся с web-сервером; клиент FTP, получающий файлы с сервера FTP; клиент Telnet, используемый нами для того, чтобы войти на удаленный узел через сервер Telnet на этом уда- ленном узле. Обычно за один сеанс клиенты устанавливают соединение с одним сервером, хотя если говорить о web-браузере, мы можем соединиться со множеством раз- личных web-серверов, скажем, в течение 10 минут. Однако с точки зрения серве- ра нет ничего удивительного в том, что в любой момент времени он соединяется Рис. 1.2. Сервер, обслуживающий одновременно множество клиентов
1.1. Введение 35 со множеством клиентов (рис. 1.2). Далее в этой главе будут рассмотрены различ- ные возможности взаимодействия сервера одновременно со множеством клиентов. Взаимодействие клиентского и серверного приложений невозможно без сете- вых протоколов. В этой книге мы сосредоточимся на наборе (стеке) протоколов TCP/IP, называемом также набором протоколов Интернета. Так, например, кли- енты и серверы Web устанавливают соединения, используя протокол TCP. TCP, в свою очередь, использует протокол IP, а протокол IP устанавливает соедине- ние с тем или иным протоколом канального уровня. Например, если и клиент, и сервер находятся в одной сети Ethernet, взаимодействие между ними будет со- ответствовать показанному на рис. 1.3. Пользовательский процесс Стек протоколов - внутри ядра Уровень приложения Транспортный уровень Сетевой уровень Канальный уровень Ethernet Рис. 1.3. Клиент и сервер в сети Ethernet, соединеннные по протоколу TCP Несмотря на то что клиент и сервер устанавливают соединение с использова- нием протокола уровня приложений, транспортные уровни устанавливают со- единение, используя TCP, и т. д„ нужно отметить, что действительный поток ин- формации между клиентом и сервером идет вниз по стеку протоколов на стороне клиента, затем по сети и, наконец, вверх по стеку протоколов на стороне сервера. Отметим также, что клиент и сервер являются типично пользовательскими процессами, в то время как TCP и протоколы IP обычно являются частью стека протоколов внутри ядра. На рис. 1.3 мы выделили четыре уровня процессов. Мы будем обсуждать нс только протоколы TCP и IP. Некоторые клиенты и сер- веры вместо протокола TCP используют UDP, и оба эти протокола более подробно обсуждаются в главе 2. Употребляя термин IP, мы подразумеваем последнюю официальную версию этого протокола — IPv4. Новая версия этого протокола, IP версии 6, была разработана в середине 90-х и, возможно, со временем заменит протокол IPv4. Начальные реализации протокола IPv6 были доступны на момент
36 Глава 1. Введение в сетевое программирование написания этой книги, и в тексте описана разработка сетевых приложении как под IPv4, так и под IPv6. В приложении А приводится сравнение протоколов IPv4 н IPv6 с другими протоколами, с которыми вы встретитесь. Вовсе не обязательно, чтобы клиент и сервер находились, как показано на рис. 1.3, в одной и той же локальной сети {localarea network, LAN). Напротив, кли- ент и сервер могут относиться к разным локальным сетям (рис. 1.4), при этом обе локальных сети должны быть соединены в глобальной сети {wide-area network, Н71ЛГ) с использованием маршрутизаторов. Рис. 1.4. Клиент и сервер в различных локальных сетях, соединенных через глобальную сеть Маршрутизаторы — это блоки, из которых строится гтобальная сеть. На се- годняшний день наибольшей глобальной сетью является Интернет, хотя многие компании создают свои собственные глобальные сети, п эги частные сети могут быть, а могут и не быть подключены к Интернету. Оставшаяся часть этой главы представляет собой обзор различных тем, кото- рые более подробно раскрываются далее по тексту книги. Мы начнем с полного, хотя и простого примера TCP-клиента, демонстрирующего вызовы многих функ- ции и понятия, с которыми мы встретимся в книге. Клиент работает только с про- токолом IP версии 4. Мы покажем изменения, необходимые для работы с прото- колом IP версии 6. Разумнее всего создавать не зависящие от протокола клиенты и серверы, и это решение будет рассмотрено нами в главе 11. В этой главе также показан пример сервера TCP, работающего с нашим клиентом. Чтобы упростить написанный нами код, мы определяем наши собственные функции-обертки {wrapperfunctions) для большинства системных функций, ко- торые будем вызывать. В большинстве случаев мы будем использовать функции- обертки для поиска ошибок, вывода соответствующих сообщении и завершения работы при обнаружении ошибки.
1.2. Простой клиент времени и даты 37 Кроме того, в этой главе мы подробно расскажем о сети, использовавшейся для тестирования примеров из книги, приведем имена узлов, их IP-адреса и на- звания операционных систем, под управлением которых они работают. В настоящее время нельзя говорить о Unix, не упомянув Posix — стандарт, принятый большинством производителей. Мы опишем историю стандарта Posix и расскажем, каким образом он определяет API, рассматриваемые в этой книге наряду с другими конкурирующими стандартами. 1.2. Простой клиент времени и даты Рассматривая этот конкретный пример, мы введем многие понятия и термины, с которыми будем встречаться в процессе изучения этой книги. В листинге 1.1* представлена реализация клиента времени и даты, подключающегося к серверу по протоколу TCP. Этот клиент устанавливаетТСР-соединение с сервером, а сер- вер просто посылает клиенту время и дату в удобочитаемом формате. Листинг 1.1. TCP-клиент для определения времени и даты //intro/daytimetcpcli с 1 #include "unp h" 2 int 3 mainCirt argc. char **argv) 4 { 5 6 7 int sockfd. n char recvline[MAXLINE + 1]; struct sockaddr_in servaddr: 8 9 if (argc l= 2) err_quit("usage a out <IPaddress>"). 10 11 if ( (sockfd = socket(AF_INET SOCK_STREAM. Q>) < 0) err_sys("socket error"). 12 13 14 15 16 bzero(&servaddr. sizeof(servaddr)) servaddr sin_fannly = AF_INET. servaddr sin_port = htons(13). /* сервер для определения времени й даты*/ if (inet_pton(AF_INET. argv[l]. &servaddr sin_addr) <= 0) err_quit('inetjDton error for 3s". argv[l]) 17 18 if (connect(sockfd. (SA *) &servaddr, sizeof(servaddr)) < 0) err_sys("connect error") 19 20 21 22 23 24 25 while ( (n = read(sockfd. recvline. MAXLINE)) > 0) { recvline[n] = 0. /* завершающий нуль */ if (fputs(recvline. stdout) == EOF) err sysC'fputs error"). } if (n < 0) err_sys("read error”), продолжение & 1 Все исходные коды программ, опубликованные в этой книге, вы Мбже+ё йайти по адресу http:// wwwpiter.com/download. 11. I ’> - ,,
38 Глава 1. Введение в сетевое программирование Листинг 1.1 (продолжение) 26 exit(O). 27 } ПРИМЕЧАНИЕ --------------------------------------------------------------- Этот формат мы используем для всех вставок исходного кода в тексте. Каждая непус- тая строка пронумерована. Абзац текста, описывающий некоторую часть кода, начина- ется с двух номеров — начального и конечного номеров тех строк, о которых идет речь в данном абзаце. Как правило, абзацу предшествует короткий заголовок, в котором ре- зюмируется содержание описываемого кода. В начале фрагмента кода указано имя файла исходного кода: в данном примере это файл daytimetcpcli.c в каталоге intro. Поскольку исходный код всех примеров являет- ся свободно доступным (см. предисловие), вы можете найти соответствующие исход- ные файлы. Наилучший способ изучить концепции сетевого программирования — ком- пилировать, запускать и особенно модифицировать эти программы в ходе чтения книги. ПРИМЕЧАНИЕ -------------------------------------------------------- Примечания наподобие этого мы будем использовать для описания особенностей реа- лизации и исторических справок. Если мы откомпилируем эту программу в определенный по умолчанию файл a.out и выполним его, на выходе мы получим следующее: solans % a.out 206.62.226.35 наш ввод Fn Jan 12 14 27 52 1996 вывод программы ПРИМЕЧАНИЕ -------------------------------------------------------- Отображая интерактивный ввод и вывод, мы выделяем то, что вводим, полужирным шрифтом. Комментарии идут справа от вывода курсивом. Название системы мы вклю- чаем в приглашение оболочки (в данном примере Solaris), чтобы показать, на каком узле выполняется команда. На рис. 1.7 показаны системы, используемые для выполне- ния большинства примеров этой книги. Имена узлов обычно соответствуют операци- онным системам. Теперь мы можем рассмотреть подробности этой программы, состоящей из 27 строк. Здесь мы лишь кратко опишем их — на тот случай, если это первая сете- вая программа, с которой вы встретились, — а более полное представление о ней вы сможете получить далее по мере чтения книги. Подключение собственного заголовочного файла 1 Мы подключили наш собственный заголовочный файл, unp.h, текст которого приведен в разделе Г.1. Этот заголовочный файл подключает различные систем- ные заголовочные файлы, необходимые большинству сетевых программ, и опре- деляет используемые нами константы (например, MAXLINE). Аргументы командной строки 2-3 Это определение функции main вместе с аргументами командной строки. При написании кода примеров к этой книге подразумевалось, что для его компиля- ции должен использоваться компилятор ANSI С (American National Standards Institute — Американский национальный институт стандартов).
1.2. Простой клиент времени и даты 39 Создание сокета TCP 10-11 Функция socket создает потоковый сокет (SOCK_STREAM) Интернета. AF INET — это название, иногда используемое для обозначения сокета TCP. Функция возвра- щает целочисленный дескриптор, который мы используем для идентификации сокета во всех последующих вызовах функции (например, в последующих вызо- вах функций connect и read). ПРИМЕЧАНИЕ------------------------------------------------------------ Оператор if содержит вызов функции socket, присваивание возвращаемого значения переменной sockfd и последующую проверку, является ли это присвоенное значение отрицательным. Мы могли разбит ь этот оператор на два оператора С следующим обра- зом; sockfd = socket(AF_INET. SOCK_STREAM. 0). if (sockfd < 0) Однако использованная в листинге запись является типичным для языка С способом объединения двух строк. Поскольку в языке С оператор «меньше чем» (<) имеет более высокий приоритет, чем оператор присваивания, необходимо заключить операции при- сваивания и вызова функции в скобки (как это и сделано в листинге, в строке 10). Между двумя открывающими скобками всегда вставляется пробел как указание па то, что ле- вая часть операции сравнения содержит также операцию присваивания — это элемент собственного стиля автора. (Автор впервые нашел этот стиль в исходном коде Minix [97] и с тех пор всегда им пользуется.) Далее в выражении while также применяется этот стиль. Вы встретите множество различных вариантов применения термина сокет. Во-первых, программный интерфейс приложений (Application Program Interface, API), который мы используем, называется API сокетов. В предыдущем абзаце мы упоминали функцию socket, входщую в API сокетов. Там же мы говорили и о со- кете TCP, что является синонимом точки доступа TCP (TCP endpoint). Если вызов функции socket оказывается неудачным, мы прерываем выполне- ние программы с помощью вызова функции err_sys. Она выдает сообщение об ошибке и ее описание (например, одна из возможных ошибок функции socket связана с отсутствием поддержки протокола) и прерывает выполнение процесса. Эта функция, как и некоторые другие созданные нами функции, начинающиеся с егг_, встречаются далее по тексту книги. Они будут описаны в разделе Г.4. Задание IP-адреса и порта сервера 12-16 Мы заполняем структуру адреса сокета Интернета (структура sockaddr_i п с име- нем servaddr) IP-адресом и номером порта сервера. Сначала мы заполняем струк- туру нулями, используя функцию bzero, затем устанавливаем помер порта, рав- ный 13 (это номер заранее известного порта (well-known port) сервера времени и даты на любом узле TCP/IP, на котором работает эта служба, — табл. 2.1), а так- же присваиваем IP-адресу значение, определенное первым аргументом команд- ной строки (argv[l]). В этой структуре поля IP-адреса и номера портов должны иметь определенный формат: мы вызываем библиотечную функцию htons (host to network short), чтобы преобразовать двоичный номер порта в требуемый фор- мат, и библиотечную функцию i net_pton (presentation to numeric — преобразо-
40 Глава 1 Введение в сетевое программирование вать в двоичное представление), чтобы преобразовать аргумент командной стро- ки в символах ASCII (например, 206 62 226 35 при выполнении данною приме- ра) в двоичный формат ПРИМЕЧАНИЕ ------------------------------------------------------------- Функция bzero нс является функцией ANSI С — она происходи г от более раннего кода сетевого прот раммировапия Берктп Тем пс менее мы используем именно се, а не функ- цию ANSI С memset (с тремя api уметами), потому что с фу икциен b/его работать про- ще Почти каждый производитель, поддерживающий API сокетов также реализует и функцию bzero а на случаи, если такой реализации нет мы приводим се макроопре- деление в нашем заюловочном файле unp h Сам автор допустил ошибку, поменяв местами второй и третий аргументы функции memset в 10 местах в первой редакции |95| Компилятор С ис можщ распознать эту ошибку, поскольку оба артументапринадлежат к одному 1 ину В деиствитечьносгп второй api умент принадлежит к типу mt, а третий — sizet — обычно имеет тип unsigned mt (то есть целое без знака), но заданные значения, соответственно 0 и 16 яв !яются допустимыми для обоих типов api умента Вы юв функции memset все равно осуществ- ля лея но функция факт ически ничего не делала, поскольку задавалось пулевое число инициализируемых баи гов Про! рамма рабо!ала, потому что только некоторые функ- ции сокетов действительно требуют, чюбы последние 8 байтов в сфуктуре адреса со- ке! а были нулевыми Тем не менее эго ошибка, и се можно избежать, используя функ- цию bzero поскольку перестановка двух аргументов функции bzeio будет всегда выяв- лена компилятором С, если используются прототипы функции Возможно, вы впервые встречаете функцию inet_pton Она яв тяется новой, и появи- тесь она в протоко тс IPv6 (о ко i ором бо iee подробно мы пот оворим в приложении А) В более старом коде для преобразования точечпо-десятичнои записи ASCII (dotted- decimal string) в необходимый формат используется функция inet_addr, по у нее есть ряд ограничении, которых не имеет функция inet_pton Не беспокоитесь, если ваша сис- тема (пока еще) не поддерживает >т у функцию, — ее реализация приведена в разделе 3 7 Установление соединения с сервером 1 18 Функция connect, применяемая к сокету TCP, устанавливает соединение по протоколу TCP с сервером, используя адрес сокета из структуры, на которую указывает второй аргумент Мы также должны задать длину структуры адреса в качестве третьего аргумента функции connect, а для структур адреса сокета Ин- тернета мы всегда предоставляем вычисление длины компилятору, используя оператор С sizeof ПРИМЕЧАНИЕ -------------------------------------------------------- В заголовочном файле unp h мы используем директиву #defme SA, чтобы определить SA как struct sockaddr, что соогветствуег общей ст руктуре адреса coKeia Когда одна и з функций сокетов требует указателя на с груктуру адреса сокет а, это г указатель должен 1 быть преобразован в указатель на общую структуру адреса сокета Это происходит по- тому, что функции сокетов появились раньше чем стандарт ANSI С Соответственно, тип указателя void * еще пс бы а доступен в начале 80-х, koi да эти функции разрабаты- вались Проблема состоит в том, что выражение struct sockaddr занимает 15 симво- , лов и часто заставляет выходить строку исходно! о кода за правую границу жрана (или сфапицы книги), — поэтому мы сократили ее до SA Более подробно мы исследуем структуры с адресами сокетов общею типа с помощью лис пипа 3 2
1 3 Независимость от протокола 41 Чтение и отображение ответа сервера 19-25 Мы чш аем ответ сервера и отображаем результат с помощью ст аидартной функ- ции ввода вывода fputs Нужно быть внимательным при использовании TCP, поскольку это потоковый (byte-stream) протокол без границ между записями Обычно ответом сервера яв 1яется 26-байтовая строка с ледующею формата Fri Jan 12 14 27 52 1996\r\n где \г — это возврат каретки, а \п — перевод строки (в символах ASCII) В случае потоковою протокола эти 26 байтов можно получить в нескольких вариантах в виде отдельного сегмента TCP, содержащего все 26 байтов данных, или в виде 26 сегментов каждый из которых содержит по одному байту данных, или в виде любой другой комбинации, в сумме дающей 26 байтов Обычно возвращается один сегмент, содержащий все 26 байтов, но при больших обт>емах данных мы не мо- жем рассчитывать, что ответ сервера будет получен с помощью одного вызова read Следовательно, при чтении из сокета TCP нужно всегда вызывать функцию read циклически и прерывать цикл либо когда функция возвращает 0 (например, соединение было разорвано другой стороной) либо когда возвращенное значе- ние оказывается меньше нуля (функция возвращает ошибку) В приведенном примере конец записи обозначается сервером закрывающим соединение Эта технология используется также протоколом передачи гипертек- ста (Hypeitext Transfer Protocol HTTP) Однако есть и другие способы обозна- чения конца записи Например, протокол передачи файлов (Tile Transfer Protocol, FTP) и простой протокол передачи почты (Simple Mail Transfer Protocol, SMTP) обозначают конец записи 2-баи говой последовательностью, состоящей из ASCII- символов возврата каретки и перевода с троки С 1ужба вызова удаленных проце- дур (Remote Piocedure Call, RPC) и система именования доменов (Domain Name System, DNS) помещают перед каждой записью, отсылаемой по протоколу TCP, двоичное число, соответствующее длине этой записи Здесь важно осознать, что протокол TCP сам по себе не предоставляет никаких меток записей если прило- жение требует отде тять записи одну от дру1 ой, оно должно сделать это самост оя- тельно, и для этого имею гея стандартные методы Завершение программы 26 Функция exit завершает программу Unix все1да закрывав! все открытые де- скрипторы при завершении процесса, поэтому теперь наш TCP-сокет закрыт Как уже отмечалось, далее в тексте книги вы найдете значительно более под- робное обсуждение моментов, о которых мы здесь только упомянули 1.3. Независимость от протокола Наша программа, представленная в листинге 1 1, является зависящей от прото- коза (piotocol dependent) IP версии 4 (IPv4) Мы выделяем и инициализируем структуру sockaddr_in, определяем адрес как относящийся к семейству AF_INET и устанавливаем первый аргумент функции socket равным AF_INET Если мы хотим, чтобы программа работала по протоколу IP версии 6 (IPv6), необходимо модифицировать код В листинге 1 2 показана версия программы,
42 Глава 1. Введение в сетевое программирование работающая по протоколу IPv6, а внесенные в нее изменения отмечены полу- жирным шрифтом. Листинг 1.2. Версия программы из листинга 1.1 для IPv6 //intro/daytimetcpcliv6 с 1 #include "unp h” 2 int 3 main(int argc char **argv) 4 { 5 int sockfd, n, 6 char recvline[MAXLINE + 1]. 7 struct sockaddr_in6 servaddr. ' 8 if (argc '= 2) 9 err_quit("usage a out <IPaddress>"). 10 if ( (sockfd = socket(AFJNET6 SOCK_STREAM, 0)) < 0) 11 err_sys("socket error"). 12 bzerot&servaddr. sizeof(servaddr)). 13 servaddr sin6_family = AF_INET6. 14 servaddr sin6_port = htons(13). /* сервер для определения времени и даты */ 15 if (inet_pton(AF_INET6. argv[l] &servaddr sin6_addr) <= 0) 16 err_quit("inet_pton error for £s" argvfl]) 17 if (connecttsockfd (SA *) &servaddr, sizeof(servaddr)) < 0) 18 err_sys('connect error") 19 while ( (n = read(sockfd recvline MAXLINE)) > 0) { 20 recvlineln] =0 /* завершающий нуль */ 21 if (fputs(recvline stdout) == EOF) 22 err_sys(”fputs error'). 23 ) 24 if (n < 0) 25 err_sys("read error”). 26 exit(0). 27 } Изменились только пять строк, но в результате мы все равно получили про- грамму, зависящую от протокола, в данном случае — от протокола IP версии 6. Полезнее была бы программа, не зависящая от протокола (protocol independent). В листинге 11.3 представлена версия этого клиента, не зависящая от протокола, что реализовано благодаря использованию функции getaddrinfo (вызываемой tcp_connect). Другим недостатком наших программ является то, что пользователь должен вводить IP-адрес сервера в точечно-десятичной записи (например, 206.62.226.35 для версии IPv4). Людям проще работать с именами, чем с числами (например, 1 aptop.kohala com или просто laptop). В главах 9 и И мы обсудим функции, обес- печивающие преобразование имен узлов в IP-адреса и имен служб в номера портов. Мы специально откладываем описание этих функций, продолжая исполь- зовать IP-адреса и номера портов, и поэтому точно знаем, что именно входит
1.4. Обработка ошибок: функции-обертки 43 в структуры адресов сокетов, которые мы должны заполнить и проверить. Это также упрощает наши объяснения сетевого программирования, снимая необхо- димость описывать в подробностях еще один набор функций. 1.4. Обработка ошибок: функции-обертки В любой реальной программе существенным моментом является проверка каж- дого вызова функции на предмет возвращаемой ошибки. В листинге 1.1 мы про- водим поиск ошибок в вызовах функций socket, met pton, connect, read н fputs, и когда ошибка случается, мы вызываем свои собственные функции err_quit и err_sys для вывода сообщения об ошибке и для прерывания выполнения про- граммы. В отдельных случаях, когда функция возвращает ошибку, бывает необ- ходимо выполнить еще какие-либо действия помимо прерывания программы (см., например, листинг 5.9, где требуется проверить прерванный системный вызов). Поскольку прерывание программы из-за ошибки — типичное явление, мы со- кратим наши программы, определив функции-обертки, которые будут вызывать сами рабочие функции, проверять возвращаемые ими значения и прерывать про- грамму при возникновении ошибки. Соглашение, используемое нами, заключа- ется в том, что название функции-обертки пишется с заглавной буквы, например: sockfd - Socket(AF_INET. SOCK_STREAM. 0). Наша функция-обертка для функции socket показана в листинге 1.3. Листинг 1.3. Функция-обертка для функции socket //11b/wrapsock с 172 int 173 Socket(int family. int type, int protocol) 174 { 175 int n. 176 if ( (n = sockettfamily type, protocol)) < 0) 177 err_sys("socket error"). 178 return (n). 179 } ВНИМАНИЕ -------------------------------------------------------------- В тексте книги вам будут встречаться функции с именами, начинающимися с заглав- ной буквы Это будут наши собственные функции-обертки, предназначенные для вы- зова функций с теми же именами, но начинающимися со строчной буквы. При описании исходного кода в тексте книги мы всегда ссылаемся на вызывае- мую функцию низшего уровня (например, socket), а не на функцию-обертку (на- пример, Socket). Хотя вам может показаться, что использование функций-оберток не дает боль- ших преимуществ, на самом деле это не так. Когда мы будем обсуждать потоки в главе 23, вы обнаружите, что при возникновении ошибки функции потоков не присваивают стандартной переменной Unix errno определенную константу, спе- цифическую для произошедшей ошибки. Вместо этого значение переменной errno просто возвращается функцией. Эго значит, что каждый раз, когда мы вызываем
44 Глава 1. Введение в сетевое программирование одну из функций pthread, мы должны разместить в памяти переменную, сохра- нить возвращаемое значение в этой переменной и присвоить его переменной еггпо перед вызовом err_sys. Чтобы избежать загромождения кода скобками, мы мо- жем использовать оператор языка С запятая для объединения присваивания переменной еггпо и вызова err_sys следующим образом: int п. if ( (n = pthread_mutex_lock(&ndone_mutex)) != 0) еггпо = п. err_sys("pthread_mutex_lock error''), В качестве альтернативы мы можем определить новую функцию выдачи со- общений об ошибках, которая в качестве аргумента получает системный номер ошибки. Однако проще всего будет выглядеть код с использованием функции- обертки, если мы определим нашу собственную функцию-обертку, как показано в листинге 1.4: Pthread_mutex_l ock <&ndone_mutex). Листинг 1.4. Функция-обертка для функции pthread_mutex_lock //1ib/wrappthread с 72 void 73 Pthread_mutex_lock(pthread_mutex_t *mptr) 74 { 75 int n 76 if ( (n = pth>"ead_niutex_lock(niptr)) == 0) 77 return 78 errro = n err_sys("pthread_mutex_7ock error'). 80 } ПРИМЕЧАНИЕ ---------------------------------------------------------------------- Если аккуратно программировать на С, можно использовать вместо функций макро- сы, что обеспечивает небольшой выигрыш в производи! ельности, однако фупкиии- обертки крайне редко бывают причиной недостаточной производительности программ. Наш выбор — первая заглавная буква в названии функции — является компромиссом. Мы рассмотрели множество других стилей: подстановка префикса ”е" (как сдела- но па с. 182 [ 56]), добавление суффикса "_с" к имени функции и т. д. Выбранный стиль кажется наименее категоричным, и в то же время он представляет визуальное указание па то, что па самом деле вызывается какая-то другая функция. Эта технология имеет, кроме того, полезный побочный эффект: она позволяет прове- рять возникновение ошибок при выполнении таких функций, ошибки в которых часто остаются незамеченными, например close и listen. Мы всегда будем использовать функции-обертки, кроме тех случаев, когда нам понадобится проверить ошибку явно и обрабатывать ее другим, отличным от прерывания программы способом. Мы не приводим исходный код для всех на- ших функций-оберток, но он полностью доступен (см. предисловие). Значение системной переменной Unix еггпо Когда при выполнении функции Unix (например, одной из функций сокетов) происходит ошибка, глобальной переменной еггпо присваивается положитель-
1.5. Простой сервер времени и даты 45 ное значение, указывающее на тип ошибки, а возвращаемое значение функции при этом обычно равно -1. Наша функция err_sys проверяет значение перемен- ной еггпо и выводит строку с соответствующим сообщением об ошибке (напри- мер, об истечении времени соединения, если значение функции еггпо равно ETIMEDOUT). Переменной еггпо присваивается определенное значение, только если при выполнении функции произошла какая-либо ошибка. Ее значение не определе- но, если функция не возвращает ошибки. Все положительные коды ошибок яв- ляются константами с именами в верхнем регистре, начинающимися с «Е», и обыч- но определяются в заголовке <sys/errno h>. Ни одна ошибка не имеет кода 0. Переменную еггпо нельзя хранить как глобальную переменную, если имеется множество программных потоков, у которых все глобальные переменные явля- ются общими. О решении этой проблемы мы расскажем в главе 23. Говоря для краткости, что «функция connect возвращает ECONNREFUSED», мы под- разумеваем, что при выполнении функции произошла ошибка (обычно при этом возвращаемое значение функции равно -1) и значение переменной еггпо в насто- ящий момент равно указанной константе. 1.5. Простой сервер времени и даты Напишем простую версию сервера TCP для определения времени и даты, кото- рый будет работать с клиентом, описанным в разделе 1.2. Воспользуемся функ- циями-обертками, описанными в предыдущем разделе. Сервер представлен в ли- стинге 1.5. Листинг 1.5. TCP-сервер для определения времени и даты //intro/daytimetcpsrv с 1 #include "unp n" 2 #include <finie h> 3 int 4 maintint argc char **argv) 5 { 6 int listenfd. connfd. 7 struct sockaddr_in servaddr: 8 char buff[MAXLINE] 9 time_t ticks. 10 listenfd = Socket(AFJNET. SOCK_STREAM. 0): 11 bzerot&servaddr sizeof(servaddr)), 12 servaddr sin_family = AF_INET. 13 servaddr sin_addr s_addr = htonl(INADDR_ANY). 14 servaddr sin_port = htons(13) /* сервер времени и даты */ 15 Bind(listenfd. (SA*) &servaddr sizeof(servaddr)): 16 Listend istenfd LISTENQ). 17 for (..) { 18 connfd = Acceptdistenfd. (SA *) NULL. NULL)
46 Глава 1. Введение в сетевое программирование Листинг 1.5(продолжение) 19 ticks - time(NULL): 20 snpnntftbuff. sizeof(buff). "% 24s\erW, ctime(&ticks)). 21 Writetconnfd, buff, strlen(buff)). 22 Close(connfd) 23 } 24 } Создание сокета TCP 10 Создание сокета TCP аналогично созданию клиентского кода. Связывание заранее известного порта сервера с сокетом 1-15 Заранее известный порт сервера (порт номер 13 в случае сервера времени и даты) связывается с сокетом путем заполнения структуры адреса сокета и вызова функ- ции bind. Мы задаем IP-адреса как INADDR_ANY, что позволяет серверу устанавли- вать соединение с клиентом на любом интерфейсе в том случае, если узел сервера имеет несколько интерфейсов. Далее мы рассмотрим, как можно огра- ничить список интерфейсов, через которые может осуществляться подклю- чение клиентов. Преобразование сокета в прослушиваемый сокет 16 С помощью вызова функции 11 sten сокет преобразуется в прослушиваемый сокет, на котором входящие соединения от клиентов принимаются ядром. Вызов функций socket, bind и listen — это обычная последовательность шагов для лю- бого сервера TCP, позволяющая создать прослушиваемый дескриптор (listening descriptor)', в нашем примере это переменная 11 stenfd. Константа LISTENQ взята из нашего заголовочного файла unp h. Она задает мак- симальное количество клиентских соединений, которые ядро ставит в очередь на прослушиваемом сокете. Более подробно создание эгих очередей мы рассмотрим в разделе 4.5. Принятие клиентского соединения, отправка ответа 7-21 Обычно процесс сервера блокируется при вызове функции accept и ждет под- ключения клиента. Соединение TCP устанавливается с помощью так называе- мого трехэтапного рукопожатия (three-way handshake). Когда рукопожатие со- стоялось, функция accept возвращает значение, и это значение является новым дескриптором (connfd), который называется присоединенным дескриптором (connected description). Этот новый дескриптор используется для связи с новым клиентом. Новый дескриптор возвращается функцией accept для каждого клиен- та, соединяющегося с нашим сервером. ПРИМЕЧАНИЕ ----------------------------------------------------------- В книге используется следующий способ обозначения бесконечного цикла- for ( . ){
1.5. Простой сервер времени и даты 47 Текущее время и дата определяются с помощью библиотечной функции time, возвращающей количество секунд с начала эпохи Unix: 00:00:00 1 января 1970 года UTC (Universal Time Coordinated — унивресальное скоординированное вре- мя, среднее время по Гринвичу). Следующая библиотечная функция, ctime, пре- образует целое значение в строку следующего формата, удобного для человече- ского восприятия: Fri Jan 12 14 27 52 1996 Символ возврата каретки и пустую строку добавляет к строке функция snpri ntf, а функция wri te сообщает результат клиенту. .ПРИМЕЧАНИЕ ------------------------------------------------------------- Возможно, вы впервые встречаетесь с функцией snprintf. Во многих случаях использу- ется другая функция — sprintf, но она не в состоянии обеспечить проверку на перепол- нение буфера получателя. Функция snprintf, наоборот, требует, чтобы в качестве второго аргумента указывался размер буфера получателя, что позволяет избежать перепол- нения буфера. Функция snprintf еще не является частью стандарта ANSI С, ио существует вероят- ность ее включения в обновленный стандарт, в настоящее время называемый С9Х. Тем не менее многие поставщики программного обеспечения уже сейчас включают эту функ- цию в стандартную библиотеку языка С. Во многих примерах в нашей книге мы ис- пользуем нашу собственную версию функции snprintf, содержащую обращение к функ- ции sprintf. Отметим, что во многих случаях проникнуть в систему хакерам помогало то, что в ре- зультате отправки данных серверу вызовы функции sprintf переполняли буфер серве- ра. Также следует проявлять осторожность при использовании функций gets, strcat и strcpy — вместо них лучше использовать fgets, strncat и strncpy. Дополнительную ин- формацию по написанию защищенных сетевых программ вы найдете в [30 j. Завершение соединения 22 Сервер закрывает соединение с клиентом, вызывая функцию cl ose. Это иници- ирует обычную последовательность прерывания соединения TCP: пакет FIN по- сылается в обоих направлениях, и каждый пакет FIN распознается на другом конце соединения. Более подробно трехэтаппое рукопожатие и четыре пакета TCP, ис- пользуемые для прерывания соединения, будут описаны в разделе 2.5. Как и в случае клиента в предыдущем разделе, мы только кратко рассмотрели этот сервер, оставив подробности на потом. Запомните следующие моменты: Как и клиент, сервер зависит от протокола IPv4. В листинге 11.5 мы покажем не зависящую от протокола версию, которая использует функцию getaddri nfo. Наш сервер обрабатывает только один запрос клиента за один раз. Если при- близительно в одно время происходит множество клиентских соединений, ядро ставит их в очередь, максимальная длина которой регламентирована, и пере- дает эти соединения функции accept по одному за один вызов. Наш сервер времени и даты, который требует вызова двух библиотечных функций — time и с11 me, — является достаточно быстрым. Но если у сервера обслуживание каж- дого клиента занимает больше времени (допустим, несколько секунд или ми-
48 Глава 1 Введение в сетевое программирование нуту), нам придется некоторым образом организовать одновременное обслу- живание нескольких клиентов. Сервер, показанный в листинге 1.5, называет- ся последовательным, или итеративным, серверам (iteiative server), поскольку он обслуживает клиентов последовательно. Существует несколько техноло- гий написания параллельного сервера {concurrent server), обслуживающего мно- жество клиентов одновременно. Самой простой технологией является вызов функции Unix fork (раздел 4.7), когда для каждого клиента создается по одно- му дочернему процессу. Другой способ — использование npoi раммных пото- ков (threads) вместо функции fork (раздел 23.4) или порождение фиксиро- ванного количества дочерних процессов с помощью функции fork в начале работы (раздел 27.6). f Если мы запустим подобный сервер из командной строки, может по 1 ребовать- ся, чтобы он работал достаточно долго, поскольку часто серверы должны ра- ботать, пока работает система. Для этого нужно модифицировать код сервера таким образом, чтобы он корректно работал как демон {daemon) Unix — про- цесс, способный работать в фоновом режиме, отдельно от терминала. Это ре- шение подробно описано в разделе 12 4. 1.6. Список примеров технологии клиент- сервер, используемых в книге На протяжении всей книги мы иллюстрируем технологии сетевого программи- рования двумя основными приложениями модели клиент-сервер. клиент и сервер времени и даты (их описание мы начали в листингах 1.1, 1.2 и 1.5); эхо-клиент и эхо-сервер (их описание начнется в главе 5). Чтобы обеспечить удобный поиск различных тем, которых мы касаемся в этой книге, мы составили список разработанных нами программ. В табл. 1.1 перечис- лены версии клиента для определения времени н даты, две из которых вы уже видели. В табл. 1.2 перечисляются версии сервера для определения времени и да- ты. В габл. 1 3 представлены версии эхо-клиента, а в табл. 1.4 — версии эхо-сер- вера. Таблица 1.1. Различные версии клиента времени и даты, рассматриваемые в данной книге Листинг Описание 1 1 TCP/IPv4, зависящий от протокола 1 2 TCP/IPv6, зависящим от протокола 9 4 TCP/IPv4 зависящий от протокола вызывает функции gethostbyname ।! и getservbyname 113 > r TCP, не зависящий от протокола, вызывает функции getaddrinfo и tep connect 118 UDP, не зависящий от протокола, вызывает функции getaddrinfo и udp conneQV 15 7 1 СР, использует пеблокируемую функцию connect 28 2 TCP/IPv4, XTI, зависящий от протокола 29 2 TCP, XTI, не зависящий от протокола, вызывает netdn getbyname п tep connect
1 6. Список примеров технологии клиент-сервер, используемых в книге 49 Листинг Описание 312 313 316 33 2 Д 1 Д2 Д7 UDP, XTI, не зависящий от протокола, вызывает netdirgetbyname и udp_client UDP, XTI, не зависящий от протокола, получает асинхронные ошибки UDP, XTI, не зависящий от протокола, читает дейтаграммы по частям TCP, зависящий от протокола, использует TPI вместо сокетов или XTI TCP, зависящий от протокола, генерирует SIGPIPE TCP, зависящий от протокола, выводит размер буфера сокета и MSS TCP, зависящий от протокола, допускает использование имени узла (функция gethostbyname) или IP-адреса Д8 TCP не зависящий от протокола, допускает использование имени узла (функция gethostbyname) Таблица 1.2. Различные версии сервера времени и даты, рассматриваемые в данной книге Листинг Описание 1 5 115 116 И И 12 2 12 4 30 3 315 TCP/IPv4, зависящий от протокола TCP, не зависящий от протокола, вызывает getaddrinfo и tcpjisten TCP. не зависящий от протокола, вызывает getaddrinfo и tcp_listen UDP, не зависящий от протокола, вызывает getaddrinfo и udpserver TCP, не зависящий от протокола, выполняется как автономный демон TCP, не зависящий от протокола, порожденный демоном metd TCP, XTI, не зависящий от проюкола, вызывает netdir getbyname и tcp listen UDP, XTI, не зависящий от протокола, вызывает netdir_getbyname и udp server Таблица 1.3. Различные версии эхо-клиента, рассматриваемые в данной книге Листинг Описание 53 61 62 83 85 87 13 2 13 4 13 5 TCP/IPv4, зависящий oi протокола TCP, использует функцию select TCP, использует функцию select и работает в пакет novi режиме UDP/IPv4, зависящий от протокола UDP, проверяет адрес сервера UDP, вызывает функцию connect для получения асинхронных ошибок UDP, тайм-аут при чтении ответа сервера с использованием сигнала SIGALRM UDP, тайм-аут при чтении ответа сервера с использованием функции select UDP, тайм-аут при чтении ответа сервера с использованием параметра сокета SO_RCVTIMEO 14 4 146 15 1 15 6 1514 1-' 181 v 182 183 Доменный сокет Unix, зависящий от протокола Дейтаграмма домена Unix, зависит от протокола TCP, использует неблокируемую модель ввода-вывода TCP, использует два процесса (функцию fork) TCP, устанавливает соединение, затем посылает сегмент RST UDP, широковещательный, ситуация гонок UDP, широковещательный, ситуация гонок UDP, широковещательный, для устранения ситуации юнок используется функция pselect
50 Глава 1. Введение в сетевое программирование Таблица 1.3 {продолжение) Листинг Описание 18.5 UDP, широковещательный, для устранения ситуации гонок используются функции sigsetjmp и siglongmp 18.6 UDP, широковещательный, для устранения ситуации гонок в обработчике сигнала используется IPC 20.4 UDP, увеличение надежности протокола за счет применения повторной передачи, тайм-аутов и порядковых номеров 21.11 TCP, «проверка пульса» (heartbeat test) сервера с использованием внеполосных данных 23.1 24.4 TCP, использование двух потоков TCP/IPv4, задание маршрута от отправителя Таблица 1.4. Различные версии эхо-сервера, рассматриваемые в данной книге Листинг Описание 5.1 5.9 TCP/IPv4, зависящий от протокола TCP/IPv4, зависящий от протокола, корректно обрабатывает завершение всех дочерних процессов 6.3 TCP/IPv4, зависящий от протокола, использует функцию select, один процесс обрабатывает все клиенты 6.5 TCP/IPv4, зависящий от протокола, использует функцию poll, один процесс обрабатывает все клиенты 8.1 8.14 13 6 143 14 5 14.13 20.3 UDP/IPv4, зависящий от протокола TCP и UDP/IPv4. зависящий от протокола, использует функцию select TCP, использует стандартный ввод-вывод Доменный сокет Unix, зависящий от протокола Дейтаграммный доменный протокол Unix, зависит от протокола Доменный сокет Unix с передачей данных идептифицикации клиента UDP, выводит полученный IP-адрес получателя и имя полученного интерфейса, обрезает дейтаграммы 20.13 21.12 22.2 23.2 23.3 UDP, связывает все адреса интерфейсов TCP, «проверка пульса» клиента с использованием внеполосных данных UDP, использование модели ввода-вывода, управляемого сигналом TCP, одному клиенту соответствует один поток TCP, одному клиенту соответствует один поток, машинно-независимая (переносимая) передача аргумента 24.4 25.20 Д.11 TCP/IPv4, выводит полученный маршрут от отправителя UDP, использует функцию icmpd для получения асинхронных ошибок UDP, связывает все адреса интерфейсов 1.7. Модель OSI Самым общим способом описания уровней сети является предложенная Между- народной организацией по стандартизации (International Standards Organization, ISO) модель взаимодействия открытых систем {Open Systems Interconnection, OSI). Эта семиуровневая модель взаимодействия показана на рис. 1.5, где она срав- нивается со стеком протоколов Интернета.
1.7. Модель OSI 51 Уровень приложения Уровень представления Уровень сеанса Транспортный уровень Сетевой уровень Канальный уровень Физический уровень Набор протоколов Интернета Различные составляющие приложения ▲ Пользовательский процесс А ч______ XTI_____________________ сокеты Ядро Различные составляющие канала связи Модель OSI Рис. 1.5. Уровни модели OSI и набор протоколов Интернета Два нижних уровня модели OSI соответствуют аппаратному обеспечению драйверов устройств и сетей, которое поддерживается системой. Обычно нам не нужно заботиться об этих уровнях, так как достаточно знать только некоторые свойства канального уровня — например, что MTU (максимальная единица пе- редачи) Ethernet, описываемая в разделе 2.9, имеет размер 1500 байт. Сетевой уровень управляется протоколами IPv4 и IPv6. Оба они описывают- ся в приложении А. Из протоколов транспортного уровня мы можем выбирать TCP или UDP (о них рассказывается в главе 2). На рис. 1.5 между TCP и UDP изображен разрыв, означающий, что, возможно, приложение минует транспорт- ный уровень и будет использовать непосредственно IPv4 или IPv6. В таких слу- чаях говорят о символьном, или неструктурированном', сокете (raw socket), кото- рый будет описан в главе 25. Три верхних уровня модели OSI соответствуют уровню приложений. Прило- жением может быть web-клиент (браузер), клиент Telnet, web-сервер, сервер FTP или любое другое используемое нами приложение. В случае протоколов Интер- нета три верхних уровня модели OSI разделяются очень редко. Два программных интерфейса, которые мы описываем в этой книге, — сокеты и XTI — являются интерфейсами между верхними тремя уровнями (уровнем приложений) и транспортным уровнем. Это один из важнейших вопросов книги: как создавать приложения, используя либо сокеты, либо XTI, которые использу- ют, в свою очередь, либо TCP, либо UDP. Мы уже упоминали о символьных со- кетах, и в главе 26 мы увидим, что можем даже полностью миновать уровень IP, чтобы читать и записывать свои собственные кадры канального уровня. 1 Встречаются также термины «сырой сокет», «необработанный сокет» и «дептаграмм-ориентирован- ный сокет». — Примеч. перев
52 Глава 1. Введение в сетевое программирование Почему и сокеты, и XTI предоставляют интерфейс между верхними тремя уровнями модели OSI и транспортным уровнем? Для подобной организации модели OSI имеются две причины, которые мы отобразили на правой стороне рис. 1.5. Прежде всего, три верхних уровня отвечают за все детали приложения (например, FTP, Telnet, HTTP), но мало «знают» о составных частях соедине- ния. Четыре нижних уровня, напротив, мало «знают» о приложении, но отвечают за все составляющие соединения: отправку данных, ожидание распознавания, упорядочивание данных, приходящих не в должном порядке, расчет и проверку контрольных сумм и т. д. Вторая причина: верхние три уровня часто формируют то, что носит название пользовательского процесса (user process), в то время как четыре нижних уровня обычно поставляются как часть ядра операционной сис- темы. Unix, как и многие современные операционные системы, обеспечивает раз- деление пользовательского процесса и ядра. Следовательно, интерфейс между уровнями 4 и 5 является естественным местом для создания программного ин- терфейса приложения (API). 1.8. История сетей BSD Интерфейсы сокетов происходят от системы 4.2BSD (Berkeley Software Distri- bution — программный продукт Калифорнийского университета, в данном случае это адаптированная для Интернета реализация операционной системы Unix, раз- рабатываемая и распространяемая этим университетом), выпущенной в 1983 го- ду. На рис. 1.6 показана эволюция реализаций BSD и отмечены главные эта- пы развития TCP/IP. Некоторые изменения API сокетов также имели место в 1990 году в реализации 4.3BSD Reno, когда протоколы OSI были включены в ядро BSD. На рис. 1.6 представлены реализации системы от 4.2BSD до 4.4BSD, создан- ные группой по разработке компьютерных систем университета Беркли (Computer System Research Group, CSRG). Для использования этих реализаций требовалось, чтобы у получателя уже была лицензия на исходный код Unix. Однако весь код сетевых программ — и поддержка ядра (например, доменные стеки протоколов TCP/IP и Unix, а также интерфейс сокетов), и приложения (такие, как клиенты и серверы Telnet п FTP), — был разработан независимо от кода Unix, созданного AT&T. Поэтому начиная с 1989 года университет Беркли представил первые ре- ализации системы BSD, не ограниченные лицензией на исходный код Unix. Эти реализации распространялись свободно и, в конечном итоге, стали доступны с по- мощью анонимных FTP любому пользователю Интернета. Последними реализациями Беркли стали 4.4BSD-Lite в 1994 и 4.4BSD-Lite2 в 1995 году. Нужно отметить, что эти две реализации были затем использованы в качестве основы для других систем: BSD/OS, FreeBSD, NetBSD и OpenBSD, и все четыре до сих пор активно развиваются и совершенствуются. Более под- робную информацию о различных реализациях BSD, а также общую историю развития различных систем Unix можно найти в главе 1 [671. Многие системы Unix начинались с некоторой версии сетевого кода BSD, включая API сокетов. Мы называем их реализациями, происходящими от Беркли, или Беркли-реализациями (Berkeley-derived implementations). Многие другие ком-
1.8. История сетей BSD 53 мерческие версии Unix основаны на Unix System V Release 4, и некоторые из них содержат сетевые программы, разработанные в университете Беркли (например, UnixWare 2.x), в то время как сетевой код других систем, основанных на SVR4, был разработан независимо (например, Solaris 2.x). Мы также должны отметить, что система Linux, популярная и доступная реализация Unix, не относится к классу происходящих от Беркли: ее сетевой код и API сокетов были разработаны «с нуля». 4.2BSD (1983) Первые широко доступные реализации TCP/IP и API сокетов V 4.3BSD (1986) Усовершенствование TCP 4.3BSD Tahoe (1988) Медленный запуск TCP, предупреждение перегрузки сети, быстрая повторная передача Сетевая система BSD версии 1.0 (1989): Net/1 4.3BSD Reno (1990) Быстрое восстановление, прогнозирование заголовка TCP, сжатие заголовка SLIP, изменение таблицы маршрутизации, добавление информации об управлении в msghdr {}, добавление элемента длины в sockaddr{} Сетевая система BSD версии 2.0 (1991): Net/2 4.4BSD Tahoe (1993) Многоадресная передача, модификации канала с повышенной вместимостью 4.4BSD-Lite (1994) В тексте обозначается как Net/1 Д 5 I •* , 4.4BSD-Lite2 (1995) BSD/OS FreeBSD NetBSD OpenBSD Рис. 1.6. История различных реализаций BSD
54 Глава 1. Введение в сетевое программирование 1.9. Сети и узлы, используемые в примерах На рис. 1.7 показаны различные сети и узлы, используемые в примерах книги. Для каждого узла мы указываем операционную систему и тип аппаратного обес- печения (потому что некоторые операционные системы работают более чем на одном типе аппаратного обеспечения). Имя в каждой рамке — это имя узла, встре- чающееся в тексте. , . Маршрутизатор бЬопе AIX4.2 Digital Unix BSD/OS3.0 Linux 2 0 30 Solans 2.5.1 Solaris 2 5.1 UnixWare (w/IPv6) 4.0B (w/IPv6) (4.4BSD-Lite2) (RedHat 4.2) (w/IPv6) (w/IPv6) 2 1.2 Power PC Alpha Intel x86 Intel x86 Sparc Sparc Intel x86 Подсеть 140.252.1.0/24 Домен tuc.noao. edu Рис. 1.7. Сети и узлы, используемые в примерах Узлы двух верхних сетей Ethernet с адресами подсетей 206.62.32/27 и 206.62.64/ 27 находятся в домене kohala com. Узлы нижней сети Ethernet с адресами подсе- тей 140.252.1.0/24 находятся в домене tuc noao edu, который используется Наци- ональной оптической астрономической обсерваторией. Обозначение /27 и /24 указывает число последовательных битов, начиная с крайнего левого бита адре- са, и используется для идентификации сети и подсети. На рис. А.4 и А.5 эти два адреса IPv4 показаны более подробно, а в разделе А.4 рассказывается о записи /п, используемой в настоящее время для обозначения границ подсети.
1.9. Сети и узлы, используемые в примерах 55 На рис. 1.7 мы по-разному обозначили узлы, работающие как маршрутизато- ры (прямоугольники со скругленными углами), и узлы, которые являются обыч- ными компьютерами (обычные прямоугольники). Этих обозначений мы будем придерживаться па протяжении всей книги, поскольку иногда нужно четко отде- лять обычный узел от маршрутизатора. ПРИМЕЧАНИЕ --------------------------------------------------------- Хотим подчеркнуть, что настоящее имя операционной системы Sun - SunOS 5 х, а не Solans 2.x, однако все называют ее Solans. Определение топологии сети На рис. 1.7 мы показываем топологию сети, состоящей из улов, используемых в качестве примеров книги, но вам нужно знать топологию вашей собственной сети, чтобы запускать в ней примеры и выполнять упражнения. Хотя в настоя- щее время нет стандартов Unix в отношении сетевой конфигурации и админи- стрирования, большинство систем Unix предоставляют две основные команды, которые можно использовать для определения подробностей построения сети: netstat и ifconfig. Мы приводим примеры в различных системах, представлен- ных на рис. 1.7. Изучите руководство, в котором описаны эти команды для ваших систем, чтобы понять различия в той информации, которую вы получите на вы- ходе. Также имейте в виду, что некоторые производители помещают эти коман- ды в административный каталог, например Zsbi п или /usr/sbin, вместо обычного /usr/bin, и этих каталогов может не быть в обычном пути поиска (PATH). 1. netstat -1 предоставляет информацию об интерфейсах. Флаг -п мы использу- ем, чтобы получить численные адреса, а не имена сетей. В результате выво- дится перечень интерфейсов. linux % netstat -ni Kernel Interface table Iface MTU Met RX-OK RX-ERR RK-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flags lo 3584 0 32 000 32 000 BLRU ethO 1500 0 483929 000 449881 0 0 0 BRU Интерфейс закольцовки называется lo, a Ethernet называется ethO. В следую- щем примере показан узел с поддержкой IPv6. alpha % netstat -m Name Mtu Network Address Ipkts lems Opkts Oerrs Coll IrO 1500 <Link> 08 00 2b 37 64 26 11220 0 4893 0 4 InO 1500 DL1 none 11220 0 4893 0 4 InO 1500 206 62 226 206 62 226 42 11220 0 4893 0 4 InO 1500 IPv6 FE80 800 2B37 6424 11220 0 4893 0 4 InO 1500 IPv6 5Flb DFOO СЕЗЕ E200 20 800 2B37 6426 11220 0 4893 0 4 loO 1536 <Link> Link#3 12432 0 12432 0 0 loO 1536 127 127 0 0 1 12432 0 12432 0 0 loO 1536 IPv6 1 12432 0 12432 0 0 turnO 576 <Link> L?nk#4 0 0 0 0 0 turnO 576 IPv6 206 62 226 42 0 0 0 . 0 0
56 Глава 1. Введение в сетевое программирование 2 netstat -г показывает таблицу маршрутизации, которая тоже позволяет опре- делить интерфейсы. Обычно мы задаем флаг -п для вывода численных адре- сов При этом также приводится IP-адрес маршрутизатора, заданного по умол- чанию: { ,,, aix % netstat -rn Routing tat Destinatior lies Flags Refs Use MTU Netif Expire i Gateway Route tree default for Protocol Family 2 206 62 226 62 (Internet) UG 0 0 - enO 127/в 127 0 0 1 U 0 0 - loO 206 62 226 32/27 206 62 226 43 U 4 475 - enO Route tree /96 for Protocol Family 24 0 0 0 0 (Internet UC v6) 0 0 1480 SltO default fe80 2 0 800 207B еЗеЗ UG 0 0 - enO 1 1 UH 0 0 16B96 loO 5flb dfOO сеЗе e200 20 /80 1ink#2 I UC 0 0 1500 enO fe80 /16 1ink#2 » J UC 0 0 1500 enO fe80 2 0 800 2078 еЗеЗ 1ink#2 * f * UHDL 1 0 1500 enO ffOl /16 1 U 0 0 - loO ff02 /16 fe80 800 5afc 2b36 U 1 3 1500 enO ffll /16 1 U 0 0 - loO ffl2 /16 fe80 800 5afc 2b36 U 0 0 1500 enO Длинные строки мы разбили па части, чтобы уместить их на странице. 3. Имея имена интерфейсов, мы выполняем команду if config, чтобы получить подробную информацию для каждого интерфейса. linux % ifconfig ethO ethO Link encap 10Mbps Ethernet HWaddr 00 A0 24 9C 43 34 inet addr 206 62 226 40 Beast 206 62 226 63 Mask 255 255 255 224 UP BROADCAST RUNNING MULTICAST MTU 1500 Metric 1 RX packets 484461 errors 0 dropped 0 overruns 0 TX packets 450113 errors 0 dropped 0 overruns 0 Interrupt 10 Base address 0x300 При этом мы получаем IP-адрес, маску подсети и широковещательный адрес. Флаг MULTICAST часто указывает на то, что узел поддерживает широковеща- тельную передачу. alpha % ifconfig 1п0 InO flags=c63 <UP BROADCAST NOTRAILERS RUNNING MULTICAST S1MPLEX> inet 206 62 226 42 netmask ffffffeO broadcast 206 62 226 63 ipmtu 1500 В некоторых реализациях предоставляется флаг -а, при указании которого выводится информация обо всех сконфигурированных интерфейсах. 4. Одним из способов определить IP-адрес узлов в локальной сети является про- верка широковещательного адреса (найденного нами на предыдущем шаге) • с помощью программы pi ng. bsdi % ping 206.62.226.63 PING 206 62 226 63 (206 62 226 63) 56 data bytes
1 10 Стандарты Unix 57 64 bytes from 206 62 226 35 icmp_seq=0 ttl=255 time-0 316 ms 64 bytes from 206 62 226 40 icmp_seq=0 ttl=64 time=l 369 ms (DUP1) 64 bytes from 206 62 226 34 icmp_seq=0 ttl=255 time=l 822 ms (DUP1) 64 bytes from 206 62 226 42 icmp_seq=0 ttl=64 time=2 27 ms (DUP1) 64 bytes from 206 62 226 37 icmp_seq=0 ttl=64 time=2 717 ms (DUP1) 64 bytes from 206 62 226 33 icmp_seq=0 ttl=255 time=3 281 ms (DUP1) 64 bytes from 206 62 226 62 icmp_seq=0 ttl=255 time=3 731 ms (DUP1) вводим символ прерывания (DEL) --- 206 62 226 63 ping statistics --- 1 packets transmitted 1 packets received +6 duplicates Ot packet loss round-trip min/avg/max = 0 316/2 215/3 731 ms 1.10. Стандарты Unix Стандарты Unix на сегодняшний день определяются Posix и 1Ъе Open Group.i и Posix Название Posix представляет собой сокращение от «Portable Operating System Interface» (интерфейс переносимых операционных систем). Posix не является одиночным стандартом — это семейство стандартов, разрабатываемых организа- цией IEEE (Institute of Electrical and Electronics Engineers — Институт инжене- ров по электротехнике и радиоэлектронике). Стандарты Posix также приняты в ка- честве международных стандартов ISO (International Standards Organization — Международная организация по стандартизации) и IEC (International Electro- technical Commission — Международная электротехническая комиссия), называ- емых ISO/IEC. Первым из стандартов Posix был IEEE Std 1003.1-1988 (317 страниц). Он опре- делял интерфейс между языком С и оболочкой ядра Unix в следующих областях: примитивы процесса (fork, exec, сигналы, таймеры), среда процесса (идентифи- каторы пользователя, группы процессов), файлы и каталоги (все функции ввода- вывода), терминал ввода-вывода, системные базы данных (файл пароля и файл группы) и архивные форматы tar и ерю. ПРИМЕЧАНИЕ----------------------------------------------------- Первый стандарт Posix был пробной версией, выпущенной в 1986 году и известной как IEEEIX Название Posix было предложено Ричардом Столлмэпом (Richard Stallman) Стандарт был обновлен в 1990 году (IEEE Std 1003.2-1990, 356 страниц), и стал международным стандартом (ISO/IEC 9945-1:1990). По сравнению с вер- сией 1988 года в нее были внесены минимальные изменения. К названию было добавлено: «Часть 1: Системный программный интерфейс приложений [язык С]», таким образом отмечалось, что этот стандарт являлся API, написанным на языке С. Следующим стандартом Posix стал IEEE Std 1003.2-1992, и в его названии содержалось: «Часть 2: Оболочка и утилиты». Он был издан в двух томах общим объемом около 1300 страниц. В этой части определяются оболочка (основанная на оболочке System V Bourne) и порядка сотни утилит (программ, обычно запус- каемых из оболочки, от awk и basename до vi и уасс). В книге мы будем обозначать этот стандарт как Posix.2.
58 Глава 1. Введение в сетевое программирование Далее появился стандарт IEEE Std 1003.1b-1993, ранее известный как IEEE 1003.4. Он стал дополнением стандарта 1003.1-1990 и включал расширения реального времени, разработанные группой Р1003.4. Стандарт 1003.1b— 1993 добавил к стан- дарту 1990 года следующие пункты: синхронизация файлов, асинхронный ввод- вывод, семафоры, управление памятью (функция пипар и разделение памяти), пла- нирование выполнения, часы, таймеры и очереди сообщений. Стандарт 1003.1b- 1993 имел объем 590 страниц. Следующий стандарт Posix — IEEE Std 1003.1, редакция 1996 года [40], кото- рый включает 1003.1-1990 (базовый программный интерфейс приложений), 1003.1Ь-1993 (расширения реального времени), 1003.1с—1995 (функции Pthread) и 1003.11—1995 (технические исправления 1003.1b). Этот стандарт также называ- ется ISO/IEC 9945-1:1996. Были добавлены три главы, посвященные программ- ным потокам, и общий объем стандарта составил 743 страницы. Мы будем обо- значать этот стандарт как Posix.1. ПРИМЕЧАНИЕ----------------------------------------------------------- Более четверги из 743 страниц отводится приложению «Rationale and Notes» («Логи- ческое обоснование и замечания»). Эю обоснование содержит историческую инфор- мацию и причины, по которым те или иные свойства были включены или опущены. Часто такое обоснование бывает столь же информативным, как и официальный стан- дарт. Данный стандарт также содержит послесловие, в котором говорится, что стан- дарт ISO/IEC 9945 состоит из следующих частей: Часть 1: Системный программный интерфейс приложения [язык С]. > Часть 2: Оболочка и утилиты. ' Часть 3: Системное администрирование (в стадии разработки). Стандарт Posix, оказавший влияние на большую часть этой книги, — это IEEE Std 1003.1g: Protocol Independent Interfaces (PII) (интерфейсы, нс зависящие от протокола), выпущенный рабочей группой P1003.1g. Это сетевой стандарт API, определяющий два API, называемые DNI (Detailed Network Interfaces — подроб- ные сетевые интерфейсы): 1. DNI/Socket, основанный на API сокетов 4.4BSD. 2. DNI/XTI, основанный на спецификации X/Open XPG4. Работа над этим стандартом началась в 80-х (рабочая группа Р1003.12, позже переименованная в Р1003.1g), но она еще не закончена, хотя и близка к заверше- нию. Проект 6.4 (май 1996) был первым проектом, 75% содержания которого было одобрено при голосовании. Проект 6.6 (март 1997), похоже, является окончатель- ным проектом [41]. В 1998 или 1999 году появится новая версия стандарта IEEE Std 1003.1, которая будет включена в стандарт IEEE Std 1003.1g. Хотя стандарт IEEE Std 1003.1g не является официально законченным, в этой книге используются свойства Проекта 6.6 этого стандарта всюду, где это возмож- но. Этот проект мы будем называть Posix.lg. Например, третий аргумент функ- ции connect (см. раздел 4.3) представлен типом данных sockl en_t, хотя он является новым в Posix.lg. Аналогично мы описываем новую функцию Posix.lg sockatmark
1 10. Стандарты Unix 59 (раздел 21.3) и предоставляем ее реализацию, используя функцию т octi. Мы так- же используем обозначение AF_LOCAL, принятое в протоколе Posix. 1g, вместо AFJJNIX для сокетов домена Unix. Различия между используемыми на сегодняшний день стандартами и Posix. 1g в книге оговариваются. На сегодня ни один из производи- телей не поддерживает Posix. 1g (поскольку это не окончательная реализация), но когда стандарт будет завершен, его поддержка производителями будет обеспе- чена. Работа над всеми стандартами Posix продолжается, и попытки описать эти стандарты можно сравнить со стрельбой по движущейся мишени. О текущем со- стоянии различных стандартов Posix можно узнать на сайте http://www.pasc.org/ standing/sdll.htmL Open Group Open Group (открытая группа) была создана в 1996 году при объединении ор- ганизаций Х/Open Group Company (основанной в 1984 году) и Open Software Foundation (OSF — Фонд открытого программного обеспечения, основанный в 1988 году). Это международный консорциум производителей и конечных по- требителей в сфере промышленности, правительства и образовательных учреж- дений. Группа Х/Open в 1989 году выпустила руководство Х/Ореп Portability Guide, Выпуск 3 (XPG3). Выпуск 4 вышел в 1992 году, а его вторая версия последовала в 1994 году. Эта последняя версия была известна также под именем Специфика- ции 1170 (Spec 1170), и «волшебное» число 1170 было суммой количества сис- темных интерфейсов (926), числа заголовочных файлов (70) и числа команд (174). Последнее название этого набора спецификаций — «Х/Open Single Unix Speci- fication» (Единая спецификация Unix), хотя он также называется «Unix 95». В марте 1997 году вышел анонс версии 2 Единой спецификации Unix. Про- дукты, удовлетворяющие ей, можно называть «Unix 98». Именно так мы называ- ем эту спецификацию в тексте книги. Число интерфейсов, требуемых Unix 98, возросло с 1170 до 1434, хотя в случае рабочей станции дошло до 3030, поскольку включает CDE (Common Desktop Environment — коллективная среда настоль- ных вычислительных средств). Это, в свою очередь, требует наличия системы X Window System и пользовательского интерфейса Motif. Подробности можно получить в книге [47] и по адресу http:/www.opengroup.org/public/tech/unix/version2. Нас интересуют сетевые сервисы, являющиеся частью Unix 98. Они опреде- лены в [74] для API сокетов и XTI. Эта спецификация очень близка к Проекту 6.6 Posix. 1g. ПРИМЕЧАНИЕ--------------------------------------------------------------- К сожалению, Х/Open обозначает свои сетевые стандарты с помощью аббревиатуры XNS: X/Open Networking Services. Например, версия этого документа, в которой опре- деляются сокеты и технологии XTI для Unix 98 [74], называется «XNS Issue 5». Дело в том, что в мире сетевых технологий аббревиатура XNS всегда служила акронимом для Xerox Network Systems (сетевые системы Xerox). Поэтому мы избегаем использо- вания акронима XNS и называем документ Х/Open просто сетевым стандартом API Unix 98.
60 Глава 1. Введение в сетевое программирование Internet Engineering Task Force IETF (Internet Engineering Task Force — целевая группа инженерной поддержки Интернета) — это большое открытое международное сообщество сетевых разра- ботчиков, операторов, производителей и исследователей, работающих в области развития архитектуры Интернета и повышения стабильности его работы. Это сообщество открыто для всех желающих. Стандарты Интернета документированы в RFC 2026 [15]. Обычно стандарты Интернета посвящаются вопросам протоколов, а не программированию API. Тем не менее два документа RFC [32] [96] определяют API сокетов для протокола IP версии 6. Это информационные документы RFC, а не стандарты, и они были вы- пущены для того, чтобы ускорить применение переносимых приложений раз- личными производителями, работающими с более ранними реализациями IPv6. Разработка текстов стандартов занимает все больше времени. Тем не менее интерфейсы API IPv6 будут, возможно, приведены к более строгому стандарту. Версии Unix и переносимость Практически все версии Unix, с которыми можно столкнуться сегодня, соответ- ствуют какому-либо варианту стандарта Posix. 1 или Posix.2. Мы говорим «како- му-либо», потому что после внесения изменений в Posix (например, добавления расширений реального времени в 1993 и потоков в 1996) производителям обыч- но требуется год или два, чтобы подогнать свои программы под эти стандарты. Исторически большинство систем Unix являются потомками либо BSD, либо System V, но различия между ними постепенно стираются, по мере того как про- изводители переходят к использованию стандартов Posix. Основные различия лежат в области системного администрирования, поскольку ни один стандарт Posix на данный момент не описывает эту область. В большинстве примеров этой книги используется стандарт Posix. 1g, и основ- ное внимание мы уделяем API сокетов. По возможности мы везде используем функции Posix. 1.11.64-разрядные архитектуры С середины до конца 90-х годов развивается тенденция к переходу на 64-разряд- ные архитектуры и 64-разрядное программное обеспечение. Одной из причин является более значительная по размеру адресация внутри процесса (например, 64-разрядные указатели), необходимая при использовании больших объемов па- мяти (более 232байт). Обычная модель программирования для существующих 32-разрядных систем Unix называется ILP32. Ее название указывает на то, что целые числа (I), длинные целые числа (L) и указатели (Р) занимают 32 бита. В 64-разрядных системах Unix сейчас завоевывает популярность модель LP64. Ее название говорит о том, что 64 бита требуется только для длинных целых чи- сел (L) и указателей (Р). В табл. 1.5 приводится сравнение этих двух моделей. С точки зрения программирования модель LP64 означает, что мы не можем считать, что указатель хранится как целое число. Мы также должны учитывать влияние модели LP64 на существующие API.
1.12. Резюме 61 Таблица 1.5. Сравнение количества битов для хранения различных типов данных в моделях ILP32 и LP64 Тип данных МодельILP32 Модель LP64 char 8 8 short 16 16 int 32 32 long 32 64 указатель 32 64 В ANSI введен тип данных size_t, который используется, например, в каче- стве аргумента функции mal 1ос (количество байтов, которое данная функция вы- деляет в памяти для размещения какого-либо объекта), а также как третий аргу- мент для функций read и write (число считываемых или записываемых байтов). В 32-разрядной системе size t являегся 32-бптовым значением, но в 64-разряд- пой системе он должен иметь 64-разрядпое значение, чтобы использовать пре- имущество большей модели адресации. Это означает, что в 64-разрядной системе, возможно, size_t будет иметь тип unsigned long (целое число без знака, занимаю- щее 32 бита). Проблемой сетевого API является то, что в некоторых проектах по Posix. 1g определено, что аргументы функции, содержащие размер структур адре- сов сокета, должны иметь тип si ze_t (например, третий аргумент в функциях bi nd и connect). Некоторые элементы структуры XTI также имели тип данных long (например, структуры t_info и t opthdr). Если они оставались неизменными, то должны были изменять 32-разрядпые значения на 64-разрядные, когда система Unix менялась с модели ILP32 на LP64. В обоих случаях не было необходимости в 64-разрядных типах данных: длина структуры адресов сокета занимает макси- мум несколько сотен байтов, и использование типа данных 1 ong для элементов структуры XTI было ошибкой. Мы рассмотрим новые типы данных, введенные для решения подобных про- блем. API сокетов, для того чтобы хранить длину структур адресов сокетов, использует тип данных sockl en_t, a XTI использует типы данных t_scalar_t и t_uscalar_t. Причина, по которой эти 32-разрядные значения не заменяются на 64-разрядные, заключается в том, что таким образом упрощается двоичная со- вместимость с новыми 64-разрядными системами для приложений, скомпилиро- ванных в 32-ра.зрядных системах. 1.12. Резюме В листинге 1.1 показан полностью рабочий, хотя и простой клиент TCP, получа- ющий текущее время и дату с заданного сервера. В листинге 1.5 представлена полная версия сервера. Эти примеры помогают ввести многие из терминов и по- нятий, которые в дальнейшем рассматриваются в книге. Наш клиент зависел от протокола. Мы изменили его, чтобы он использовал IPv6, но при этом получили лишь еще одну зависящую от протокола программу. В главе 11 мы разработаем некоторые функции, которые позволят нам написать код, не зависящий от протокола. Это важно, поскольку в Интернете начинает ис- пользоваться протокол IPv6.
62 Глава 1. Введение в сетевое программирование В книге мы будем использовать функции-обертки, созданные в разделе 1.4, для уменьшения размера нашего кода, хотя по-прежнему каждый вызов функ- ции будет проходить проверку на предмет возвращения ошибки. Все имена на- ших функций-оберток начинаются с заглавной буквы. Стандарты IEEE Posix — Posix.l, описывающий основной интерфейс С и Unix, Posix.2, определяющий стандартные команды, и Posix. 1g, определяющий сетевые API — являются стандартами, к которым постепенно приходит большинство про- изводителей. Однако стандарты Posix активно принимаются и расширяются ком- мерческими стандартами, особенно стандартами Unix, разработанными Open Group, такими как Unix 98. Читатели, которых интересует история сетевого программирования в Unix, могут посмотреть историю развития Unix в книге [89], а в книге [90] представле- на история TCP/IP и Интернета. Упражнения 1. Проделайте все шаги, описанные в конце раздела 1.9, чтобы получить инфор- мацию о топологии вашей сети. 2. Найдите исходный код для примеров из текста (см. предисловие). Откомпи- лируйте и протестируйте клиент времени и даты, представленный в листин- ге 1.1. Запустите программу несколько раз, задавая каждый раз различные IP- адреса в командной строке. 3. Замените первый аргумент функции socket, представленной в листинге 1.1, на 9999. Откомпилируйте и запустите программу. Что происходит? Найдите значение errno, соответствующее выданной ошибке. Как вы можете получить дополнительную информацию по этой ошибке? 4. Измените листинг 1.1 — поместите в цикл while счетчик, который будет счи- тать, сколько раз функция read возвращает значение больше нуля. Выведите значение счетчика перед завершением. Откомпилируйте и запустите свой соб- ственный клиент. 5. Измените листинг 1.5 следующим образом. Сначала поменяйте номер порта, задаваемый при вызове функции sin port, с 13 на 9999. Затем замените один вызов функции write на циклический, при котором функция write вызывает- ся для каждого байта результирующей строки. Откомпилируйте полученный сервер и запустите его в фоновом режиме. Затем измените клиент из преды- дущего упражнения (в котором выводится счетчик перед завершением про- граммы), изменив номер порта, заданный функции sin_port, с 13 на 9999. За- пустите этот клиент, задав в качестве аргумента командной строки IP-адрес узла, на котором работает измененный сервер. Какое значение клиентского счетчика будет выведено? Если есть возможность, попробуйте также запус- тить клиент и сервер на разных узлах.
ГЛАВА 2 Транспортный уровень: TCP и UDP 2.1. Введение В этой главе приводится обзор протоколов семейства TCP/IP, используемых в примерах книги. Наша цель — как можно подробнее описать эти протоколы, чтобы можно было понять, как применять их в сетевом программировании, а так- же дать ссылки на наиболее подробные описания фактического устройства, реа- лизации и истории протоколов. В данной главе речь идет о транспортном уровне и протоколах TCP и UDP, поскольку большинство клиент-серверных приложений используют либо TCP, либо UDP. Эти два протокола, в свою очередь, используют протокол сетевого уровня IP — либо IPv4, либо IPv6. Возможно использовать непосредственно IPv4 или IPv6, минуя транспортный уровень (в таких случах речь идет о символьном, или неструктурированном сокете), но эта технология используется реже. Поэто- му мы даем более подробное описание IPv4 и IPv6 наряду с ICMPv4 и ICMPv6 в приложении А. UDP представляет собой простой и ненадежный протокол передачи дейта- грамм, в то время как TCP является сложным и надежным потоковым протоко- лом. Необходимо понимать, что могут предоставить приложениям эти два транс- портных протокола, так как нам нужно знать, что обрабатывается протоколом, а что мы должны обрабатывать в приложении. Есть ряд свойств TCP, которые при должном понимании упрощают для нас написание надежных клиентов и серверов. Кроме того, с пониманием этих свойств нам будет легче отлаживать наши клиенты и серверы, используя общеупотреби- тельные средства, такие как netstat. В этой главе мы коснемся различных тем, попадающих в эту категорию: трехэтапное рукопожатие TCP, последовательность прерывания соединения TCP, состояние TCP TIME_WAIT, буферизация TCP и UDP уровнем сокетов и т. д. 2.2. Обзор протоколов TCP/IP Хотя набор протоколов и называется TCP/IP, это семейство состоит не только из собственно протоколов TCP и IP. На рис. 2.1 представлен обзор этих прото- колов.
64 Глава 2. Транспортный уровень: TCP и UDP 1Ру6-приложение AF_INET6 sockaddr__xn6{} 1Ру4-лриложение AF-INET sockaddr_in{} Рис. 2.1. Обзор протоколов семейства TCP/IP На этом рисунке представлены и IPv4, и IPv6. Если рассматривать этот рису- нок справа налево, то четыре приложения справа используют IPv6, а о константе AF_INET6 и структуре sockaddr_in6 мы будем говорить в главе 3. Следующие пять приложений используют IPv4. Приложение tcpdump, находящееся в самой левой части рисунка, соединяется непосредственно с канальным уровнем, используя либо BPF (BSD Packet Filter — фильтр пакетов BSD), либо DLPI (Data Link Provider Interface — интерфейс поставщика канального уровня). Мы обозначили штриховую горизонтальную линию под девятью приложениями (интерфейс) как API, что обычно соот- ветствует сокетам или XTI. Интерфейс BPF и DLPI не использует ни соке- ты, ни XTI. ПРИМЕЧАНИЕ ---------------------------------------------------------- Здесь существует исключение, описанное в главе 25’ Linux предоставляет доступ к канальному уровню с помощью сокета специального типа, называемого SOCK_ PACKET. На рис. 2.1 мы также отмечаем, что программа traceroute использует два соке- та: один для IP, другой для ICMP. В главе 25 мы создадим версии IPv4 и IPv6 утилит ping и traceroute.
2.2. Обзор протоколов TCP/IP 65 А сейчас мы опишем каждый из протоколов, представленных на рисунке. Протокол Интернета верст 4. IPv4 (Internet Protocol, version 4), который мы часто обозначаем просто как IP, был «рабочей лошадкой» набора протоколов Интернета с начала 80-х. Он использует 32-разрядную адресацию (см. раздел А.4). IPv4 предоставляет сервис доставки пакетов для протоколов TCP, UDP, ICMP и IGMP. Протокол Интернета версии 6. IPv6 (Internet Protocol, version 6) был разра- ботан в середине 90-х как замена протокола IPv4. Главным изменением является увеличение размера адреса, в случае IPv6 равного 128 битам (см. раздел А.5) для работы с бурно развивающимся в 90-е годы Интернетом. IPv6 предоставляет сер- вис доставки пакетов для протоколов TCP, UDP и ICMPv6. Когда нет необходимости различать IPv4 и IPv6, мы используем аббревиату- ру IP в словах типа «IP-адрес», «1Р-уровень». Протокол управления передачей. TCP (Transmission Control Protocol) являет- ся протоколом, ориентированным на установление соединения и предоставляю- щим надежный двусторонний байтовый поток для пользовательского процесса. Сокеты TCP служат примером потоковых сокетов {stream sockets). TCP обеспе- чивает отправку и прием подтверждений, обработку тайм-аутов, повторную пе- редачу и т. п. Большинство прикладных программ в Интернете используют TCP. Заметим, что протоколом TCP может использоваться как IPv4, так и IPv6. Протокол пользовательских дейтаграмм. UDP (User Datagram Protocol) — это протокол, не ориентированный на установление соединения. Сокеты UDP слу- жат примером дейтаграммных сокетов {datagram sockets). В отличие от TCP, ко- торый является надежным протоколом, в данном случае отнюдь не гарантирует- ся, что дейтаграммы UDP когда-нибудь достигнут заданного получателя. Как и в случае TCP, протоколом UDP может использоваться и IPv4, и IPv6. Протокол управляющих сообщений Интернета. ICMP (Internet Control Mes- sage Protocol) обеспечивает передачу управляющей информации и сведений об ошибках между маршрутизаторами и узлами. Эти сообщения обычно генериру- ются и обрабатываются самостоятельно сетевым программным обеспечением TCP/IP, а не пользовательскими процессами, хотя мы и приводим в качестве примера программу Ping, использующую ICMP. Иногда мы будем называть этот протокол ICMPv4, чтобы отделить его от ICMPv6. Протокол управления группами Интернета. IGMP (Internet Group Manage- ment Protocol) используется для широковещательной передачи (см. главу 19), поддержка которой не является обязательной для IPv4. Протокол разрешения адресов. ARP (Address Resolution Protocol) ставит в со- ответствие аппаратному адресу (например, адресу Ethernet) адрес IPv4. ARP обыч- но используется в широковещательных сетях, таких как Ethernet, Token Ring и FDDI, но не нужен в сетях типа «точка-точка» (point-to-point). Протокол обратного разрешения адресов. RARP (Reverse Address Resolution Protocol) ставит аппаратный адрес в соответствие адресу IPv4. Он иногда исполь- зуется, когда загружается бездисковый узел, такой как Х-терминал. Протокол управляющих сообщений Интернета, версия 6. ICMPv6 (Internet Control Message Protocol version 6) объединяет функциональные возможности протоколов ICMPv4, IGMP и ARP.
66 Глава 2. Транспортный уровень: TCP и UDP Фильтр пакетов BSD. Этот интерфейс предоставляет доступ к канальному уровню для процесса. Обычно он поддерживается ядрами, произошедшими от BSD. Интерфейс поставщика канального уровня. DLPI (Data Link Provider Interface) предоставляет доступ к канальному уровню и обычно поставляется с SVR4 (Sys- tem V Release 4). Все протоколы Интернета определяются в документах RFC {Request For Com- ments), которые играют роль формальной спецификации. Решение к упражне- нию 2.1 показывает, как можно получить документы RFC. Узел, поддерживающий как IPv4, так и IPv6, мы будем называть узлом IPv4/ IPv6 (IPv4/IPv6 host) или двухстековым узлом {dual-stack host). Дополнительные подробности собственно по протоколам TCP/IP можно най- ти в [94]. Реализация TCP/IP в 4.4BSD описывается в [105]. 2.3. UDP: протокол пользовательских дейтаграмм UDP — это простой протокол транспортного уровня. Он описывается в RFC 768 [80]. Приложение записывает в сокет UDP дейтаграмму {datagram), которая инкапсулируется {encapsulate), или, иначе говоря, упаковывается либо в дейтаграм- му IPv4, либо в дейтаграмму IPv6 и затем посылается получателю. При этом не гарантируется, что дейтаграмма UDP когда-нибудь дойдет до указанного получателя. Проблема, с которой мы сталкиваемся в процессе сетевого программирова- ния с использованием UDP, заключается в его недостаточной надежности. Если нам нужна уверенность в том, что дейтаграмма дошла до получателя, мы должны встроить в наше приложение множество функций: подтверждение приема, тайм- ауты, повторные передачи и т. п. Каждая дейтаграмма UDP имеет конкретную длину, и мы можем рассматри- вать дейтаграмму как запись {record). Если дейтаграмма корректно доходит до конечного получателя (то есть пакет приходит без ошибки контрольной суммы), длина дейтаграммы передается принимающему приложению. Мы уже отмечали, что TCP является потоковым {byte-stream) протоколом, без каких бы то ни было границ записей (см. раздел 1.2), что отличает его от UDP. Мы также отметили, что UDP предоставляет сервис, не ориентированный на установление соединения {connectionless), поскольку в установлении долгосроч- ной связи между клиентом и сервером UDP нет необходимости. Например, кли- ент UDP может создать сокет и послать дейтаграмму данному серверу, а затем срезу же послать через тот же сокет дейтаграмму другому серверу. Аналогично сервер UDP может получить пять дейтаграмм подряд через один и тот же сокет UDP от пяти различных клиентов. 2.4. TCP: протокол управления передачей Сервис, предоставляемый приложению протоколом TCP, отличается от сервиса протокола UDP. (TCP описывается в RFC 793 [83].) Прежде всего TCP обеспе- чивает установление соединений {connections) между клиентами и серверами.
2.4, TCP: протокол управления передачей 67 Клиент TCP устанавливает соединение с сервером, обменивается с ним данными по этому соединению и затем разрывает соединение. TCP также обеспечивает надежность (reliability). Когда TCP отправляет дан- ные на другой конец связи, он требует, чтобы в случае их получения было высла- но подтверждение. Если подтверждение не приходит, TCP автоматически пере- дает данные повторно и увеличивает время ожидания подтверждения. После некоторого количества повторных передач TCP оставляет попытки отправить эти данные. В среднем суммарное время попыток отправки данных занимает от 4 до 10 минут (в зависимости от реализации). TCP содержит алгоритмы, позволяю- щие динамически прогнозировать время (период) обращения (round-trip time, RTT) между клиентом и сервером, и таким образом узнавать, сколько времени необхо- димо для получения подтверждения. Например, RTT в локальной сети может иметь значение порядка миллисекунд, в то время как для глобальной сети эта величина может достигать нескольких секунд. Более того, в определенный мо- мент времени ТСР может получить значение RTT между данными клиентом и сер- вером, равное одной секунде, а затем через 30 секунд измерить RTT на том же соединении и получить значение, равное 2 секундам, что объясняется различия- ми сетевого трафика. TCP также упорядочивает данные, связывая некоторый порядковый номер с каждым отправляемым байтом. Предположим, например, что приложение за- писывает 2048 байт в сокет TCP, что приводит к отправке двух сегментов TCP. Первый из них содержит данные с порядковыми номерами 1-1024, второй — с но- мерами 1025-2048. (Сегмент (segment) — это блок данных, передаваемых прото- колом TCP протоколу IP.) Если какой-либо сегмент приходит вне очереди (то есть если нарушается последовательность сегментов), принимающий TCP зано- во упорядочит сегменты, основываясь на их порядковых номерах, прежде чем от- править данные принимающему приложению. Если TCP получает дублирован- ные данные (допустим, компьютер на другом конце ошибочно решил, что сегмент потерян, и передал его заново, когда на самом деле он потерян не был, а просто сеть была перегружена), он может выявить эту ситуацию (на основе порядковых номеров), и дублированные данные будут проигнорированы. ПРИМЕЧАНИЕ----------------------------------------------------------------- Протокол UDP не обеспечивает надежности. UDP сам по себе не имеет ничего похо- жего на подтверждения, порядковые номера, определение RTT, тайм-ауты или повтор- ные передачи. Если дейтаграмма UDP дублируется в сети, на принимающий узел могут быть доставлены оба экземпляра. Также если клиент UDP отправляет две дейтаграм- мы одному и тому же получателю, их порядок может быть изменен сетью, и они будут доставлены с нарушением исходного порядка. Приложения UDP должны обрабаты- вать все подобные случаи, как это показано в разделе 20.5. TCP обеспечивает управление потоком (flow control). TCP всегда сообщает своему собеседнику, сколько именно байтов он хочет получить от него. Это назы- вается объявлением окна (window). В любой момент времени окно соответствует свободному пространству в приемном буфере. Таким образом гарантируется, что отправитель не переполнит буфер получателя. Окно изменяется динамически
68 Глава 2. Транспортный уровень: TCP и UDP с течением времени: по мере того как приходят данные от отправителя, размер окна уменьшается, но по мере считывания данных принимающим приложением окно увеличивается. Окно может стать нулевым: если приемный буфер TCP для данного сокета заполнен, он должен подождать, когда приложение считает дан- ные из буфера, перед тем как сможет получать другие данные. ПРИМЕЧАНИЕ ------------------------------------------------------------- UDP не обеспечивает управления потоком. Быстрый отправитель UDP может с лег- . костью передавать дейтаграммы с такой скоростью, с которой не может работать полу- чатель UDP, как это показано в разделе 8.13. Наконец, соединение TCP также является двусторонним (full-duplex). Это значит, что приложение может отправлять и принимать данные в обоих направ- лениях через заданное соединение в любой момент времени. Поэтому TCP дол- жен отслеживать состояние таких характеристик, как порядковые номера и раз- меры окна, для каждого направления потока данных: отправки и приема. ПРИМЕЧАНИЕ ------------------------------------------------------------- UDP может быть (а может и не быть) двусторонним. 2.5. Установление и завершение соединения TCP Чтобы упростить понимание функций connect, accept и cl ose и чтобы нам было легче отлаживать приложения TCP с помощью программы netstat, необходимо разобраться с тем, как устанавливаются и разрываются соединения TCP. Также необходимо познакомиться с диаграммой перехода состояний TCP. Это будет примером того, как глубокое знание протоколов способно помочь в сетевом про- граммировании. Трехэтапное рукопожатие При установлении соединения TCP действия развиваются по следующему сце- нарию: 1. Сервер должен быть подготовлен для того, чтобы принять входящее соедине- ние. Обычно это достигается путем вызова функций socket, bi nd и 11 sten и на- зывается пассивным открытием (passive open). 2. Клиент выполняет активное открытие (active open), вызывая функцию connect. Это заставляет клиент TCP послать сегмент SYN (от слова «synchronize» — синхронизировать), чтобы сообщить серверу начальный порядковый номер данных, которые клиент будет посылать по соединению. Обычно с сегментом SYN не посылается никаких данных: он содержит только заголовок IP, заго- ловок TCP и, возможно, параметры TCP (о которых мы вскоре поговорим). 3. Сервер должен подтвердить получение клиентского сегмента SYN, а также послать свой собственный сегмент SYN, содержащий начальный порядковый номер для данных, которые сервер будет посылать по соединению. Сервер
2,5. Установление и завершение соединения TCP 69 посылает SYN и АСК — подтверждение приема (от слова «acknowledgement») клиентского SYN — в виде единого сегмента. 4. Клиент должен подтвердить получение сегмента SYN от сервера. Для подобного обмена нужно как минимум три пакета, поэтому он называет- ся трехэтапным рукопожатием TCP (TCP three-way handshake). На рис. 2.2 пред- ставлена схема такого обмена. На рис. 2.2 J — это начальный порядковый номер клиента, а К — начальный порядковый номер сервера. Номер подтверждения в сегменте АСК — это следу- ющий предполагаемый порядковый номер на том конце связи, который отпра- вил сегмент АСК. Поскольку сегмент SYN занимает 1 байт пространства поряд- ковых номеров, номер подтверждения в сегменте АСК каждого сегмента SYN — это начальный порядковый номер, увеличенный на единицу. Аналогично сегмент АСК каждого сегмента FIN — это увеличенный на единицу порядковый номер сегмента FIN. ПРИМЕЧАНИЕ----------------------------------------------------- Повседневной аналогией установления соединения TCP может служить система теле- фонной связи [73]. Функция socket эквивалентна включению используемого телефо- на. Функция bind дает возможность другим узнать ваш телефонный номер, чтобы они могли позвонить вам. Функция listen включает звонок, и вы можете услышать, когда происходит входящий звонок. Функция connect требует, чтобы мы знали чей-то номер телефона и могли до него дозвониться. Функция accept — аналогия ответа на входя- щий звонок. Идентифицирующие данные, возвращаемые функцией accept (это IP- адрес и номер порта клиента), аналогичны телефонному номеру звонящего по телефо- ну. Однако имеется отличие, состоящее в том, что функция accept возвращает иденти- фицирующие данные клиента только после того, как соединение установлено, а во время телефонного звонка после определения номера телефона звонящего мы можем выбрать, отвечать на звонок или нет. Если используется система DNS (см. главу 9), она предо- ставляет услуги, аналогичные телефонной книге. Действие функции getsockbyname аналогично отысканию номера телефона по конкретному имени, а действие функции getsockbyaddr можно сравнить с отысканием имени по упорядоченному списку теле- фонных номеров.
70 Глава 2. Транспортный уровень: TCP и UDP Параметры TCP Каждый сегмент SYN может содержать параметры TCP. Ниже перечислены наи- более общеупотребительные параметры TCP. Параметр MSS. Этот параметр TCP позволяет узлу, отправляющему сегмент SYN, объявить свой максимальный размер сегмента (maximum segment size, MSS) — максимальное количество данных, которое он будет принимать в каж- дом сегменте TCP по этому соединению. Мы покажем, как получить и устано- вить этот параметр TCP с помощью параметра сокета TCP MAXSEG (см. раздел 7.9). Параметр масштабирования окна (Window scale option). Максимальный раз- мер окна, который может быть установлен в заголовке TCP, равен 65 535, по- скольку соответствующее поле занимает 16 бит. Но высокоскоростные соеди- нения (45 Мбит/с и больше, как сказано в RFC 1323 [45]) или линии с большой задержкой (спутниковые сети) требуют большего размера окна для достиже- ния максимально возможной пропускной способности. Этот параметр, появив- шийся не так давно, определяет, что объявленная в заголовке TCP величина окна должна быть масштабирована — сдвинута влево на 0-14 бит1, предостав- ляя максимально возможное окно размером почти в гигабайт (65 535 х 21,4). Для использования параметра масштабирования окна в соединении необхо- дима его поддержка обоими связывающимися узлами Мы увидим, как задей- ствовать этот параметр с помощью параметра сокета SO_RCVBUF (см. раздел 7.5). ПРИМЕЧАНИЕ -------------------------------------------------- Чтобы обеспечить совместимость с более ранними реализациями, в которых не под- держивается этот параметр, применяются следующие правила. TCP может отправить ’ 1 параметр со своим сегментом SYN в процессе активного открытия сокета Но он может ’ масштабировать свое окно, только если с другого конца связи также будет отправлен соответствующий параметр со своим сегментом SYN Эта логика предполагает, что не- доступные в данной реализации параметры просто игнорируются. Это общее и необ- ходимое требование, но, к сожалению, его выполнение не гарантировано для всех реа- лизаций • Отметка времени (Timestamp option). Этот параметр необходим для высоко- скоростных соединений, чтобы предотвратить возможное повреждение дан- ных, вызванное потерей пакетов. Поскольку это один из недавно появившихся ( параметров, его обработка производится аналогично параметру масштабиро- вания окна. Программисту сетевых приложений не нужно беспокоиться об этом параметре. Параметр MSS поддерживается в большинстве реализаций, в то время как параметр масштабирования окна и временная метка являются более новыми. Последние два параметра иногда называются «параметрами RFC 1323», поскольку описываются в этом документе [45]. Они также часто называются параметрами для «канала с повышенной вместимостью», поскольку сеть с широкой полосой пропускания либо с большой задержкой называется каналом с повышенной вмес- тимостью (long fat pipe). В главе 24 [94] эти новые параметры описаны более подробно. 1 Что соответствует умножению на 2" — Примеч научн ред
2,5. Установление и завершение соединения TCP 71 Разрыв соединения TCP В то время как для установления соединения необходимо три сегмента, для его разрыва требуется четыре сегмента, 1. Одно из приложений первым вызывает функцию close, и мы в этом случае говорим, что данный узел1 выполняет активное закрытие (active close). TCP этого узла отправляет сегмент FIN, обозначающий прекращение передачи дан- ных. 2. Другой узел, получающий сегмент FIN, выполняет пассивное закрытие (passive close). Полученный сегмент FIN подтверждается TCP. Полученный сегмент FIN передается приложению как признак конца файла (после данных, кото- рые уже стоят в очереди, ожидая приема приложением), поскольку получе- ние сегмента FIN означает для приложения то. что оно уже не получит ника- ких данных по этому соединению. 3. Через некоторое время после того как приложение получило признак конца файла, оно вызывает функцию cl ose для закрытия своего сокета. При этом его TCP отправляет сегмент FIN. 4. TCP системы, получающей окончательный сегмент FIN (то есть того узла, на 1 котором произошло активное закрытие), подтверждает получение сегмента FIN. Поскольку сегменты FIN и АСК передаются в обоих направлениях, обычно требуется четыре сегмента. Мы говорим «обычно», поскольку в ряде сценариев сегмент FIN на первом шаге отправляется вместе с данными. Кроме того, оба сег- мента, отправляемых на шаге 2 и 3, отправляются с узла, выполняющего пассив- ное закрытие, и могут быть объединены в один сегмент. Эти пакеты изображены на рис. 2.3 Рис. 2.3. Обмен пакетов при закрытии соединения TCP Сегмент FIN занимает 1 байт пространства порядковых номеров аналогично SYN. Следовательно, сегмент АСК каждого сегмента FIN — это порядковый но- мер FIN, увеличенный на единицу. 1 Здесь под узлом понимается точка доступа TCP (TCP endpoint) — Примеч научн ред
72 Глава 2. Транспортный уровень: TCP и UDP Возможно, что между шагами 2 и 3 некоторые данные перейдут от узла, вы- полняющего пассивное закрытие, к узлу, выполняющему активное закрытие. Это явление называется половинным закрытием (half-close), и мы рассмотрим его в подробностях при описании функции shutdown в разделе 6.6. Отправка каждого сегмента FIN происходит при закрытии сокета. Мы указы- вали, что приложение вызывает для этого функцию close, но при этом мы пони- маем, что когда процесс Unix прерывается либо произвольно (при вызове функ- ции exit или при завершении функции main), либо непроизвольно (при получении сигнала, прерывающего процесс), все его открытые дескрипторы закрываются, что также вызывает отправку сегмента FIN каждому открытому на данный мо- мент соединению TCP. На рис. 2.3 мы продемонстрировали, как клиент выполняет активное закры- тие. Но активное закрытие может выполнять любой узел — и клиент, и сервер. Часто активное закрытие выполняет клиент, но с некоторыми протоколами (осо- бенно HTTP) активное закрытие выполняет сервер. Диаграмма состояний TCP Операции TCP при установлении и разрыве соединения можно определить с по- мощью диаграммы состояний TCP (state transition diagram). Ее мы изобразили на рис. 2.4. Для соединения определено 11 различных состояний. Правила TCP предпи- сывают переходы из одного состояния в другое, основываясь на текущем состоя- нии и сегменте, полученном в этом состоянии. Например, если приложение вы- полняет активное открытие в состоянии CLOSED (закрыто), TCP отправляет сегмент SYN, и новым состоянием становится SYN SENT (отправлен SYN). Если затем TCP получает сегмент SYN с сегментом АСК, он отправляет сегмент АСК, и следующим состоянием становится ESTABLISHED (соединение установлено). В этом последнем состоянии происходит большая часть обмена данными. Две стрелки, идущие от состояния ESTABLISHED, относятся к разрыву со- единения. Если приложение вызывает функцию cl ose перед получением призна- ка конца файла (активное закрытие), происходит переход к состоянию FINWAITl (ожидание FIN 1). Но если приложение получает сегмент FIN в состоянии ESTAB- LISHED (пассивное закрытие), происходит переход в состояние CLOSE WAIT (ожидание закрытия). Мы отмечаем нормальные переходы клиента с помощью обычной линии, а нор- мальные переходы сервера — с помощью штриховой линии. Необходимо отме- тить, что существует два перехода, о которых мы ранее не говорили: одновремен- ное открытие (когда оба конца связи отправляют сегменты SYN приблизительно в одно время, и эти сегменты пересекаются в сети) и одновременное закрытие (когда оба конца связи отправляют сегменты FIN). В главе 18 [94] имеются при- меры и описания этих сценариев, которые встречаются достаточно редко. Одна из причин, по которым мы приводим здесь диаграмму перехода состоя- ний, заключается в том, что мы хотим показать все 11 состояний TCP и их назва- ния. Эти состояния отображаются программой netstat, которая является полез- ным средством отладки клиент-серверных приложений. Для отслеживания изменений состояния мы также используем программу netstat (см. главу 5).
2.5. Установление и завершение соединения TCP 73 Начальная точка CLOSED SYN_RCVD appl. пассивное окно send: «nothing? te041'. . WAIT CLOSING FIN_WAIT_1 TIME recv: ACK send: «nothing? _rec\£HN_ send: ACK или тайм- аут Пассивное открытие recv: FIN send: АСК recv: FIN send. ACK recv: SYN_______ send: SYN, ACK одновременное открытие Пассивное открытие ESTABLISHED Состояние передачи данных \appl. close SYN_SENT ) ~ ) __________/или тайм- Активное открытие аут appl. close send: FIN recv: ACK recv: ACK send: <nothing> -----► Нормальные переходы для клиента -----► Нормальные переходы для сервера арр<: Переходы между состояниями, возникающие, когда приложение выполняет указанную операцию recv: Переходы между состояниями, возникающие, когда получен указанный сегмент send: Указывает, что отправляется при этом переходе Рис. 2.4. Диаграмма состояний TCP Просмотр пакетов На рис. 2.5 представлен реальный обмен пакетами, происходящий во время со- единения TCP: установление соединения, передача данных и разрыв соединения. Показаны также состояния TCP, через которые проходит каждый узел.
74 Глава 2. Транспортный уровень: TCP и UDP В этом примере клиент объявляет максимальный размер сегмента (MSS) 1460 байт (обычное значение для IPv4 в Ethernet), а сервер — 1024 байт (обыч- ное значение для более поздних Беркли-реализаций в Ethernet). MSS в каждом направлении отличаются (см. также упражнение 2.5). Как только соединение установлено, клиент формирует запрос и посылает его серверу. Мы считаем, что этот запрос соответствует одиночному сегменту TCP (то есть его размер меньше 1024 байт — анонсированного размера MSS сервера). Сервер обрабатывает запрос и отправляет ответ, и мы также считаем, что ответ соответствует одиночному сегменту (в данном примере меньше 1460 байт). Оба сегмента данных мы отобразили более жирными линиями. Обратите вни- мание, что подтверждение запроса отправляется клиенту вместе с ответом серве- ра. Это называется вложенным подтверждением (piggybacking) и обычно проис-
2,6. Состояние TIME WAIT 75 ходит, когда время, требуемое серверу для обработки запроса и генерации ответа, меньше приблизительно 200 миллисекунд. Если серверу требуется больше вре- мени — скажем, 1 секунда — ответ будет приходить после подтверждения. (Дина- мика потока данных TCP подробно описана в главах 19 и 20 [94].) Затем мы показываем четыре сегмента, закрывающих соединение. Обратите внимание, что узел, выполняющий активное закрытие (в данном сценарии кли- ент), входит в состояние TIME_WAIT. Мы рассмотрим это в следующем разделе. Относительно рис. 2.5 важно отметить, что если целью данного соединения было отправить запрос, занимающий один сегмент, и получить ответ, также за- нимающий один сегмент, то при использовании TCP будет задействовано всего восемь сегментов. Если же используется UDP, произойдет обмен только двумя сегментами: запрос и ответ. Но при переходе от TCP к UDP теряется надежность, которую TCP предоставляет приложению, и множество задач по обеспечению надежности транспортировки данных переходит с транспортного уровня (TCP) на уровень приложения. Другое важное свойство, предоставляемое протоколом TCP, — это управление в условиях перегрузки, которое в случае протокола UDP также должно принимать на себя приложение. Тем не менее важно понимать, что для многих приложений, обменивающихся небольшими объемами данных, ис- пользование UDP позволяет избежать накладных расходов, возникающих при установлении и разрыве соединения TCP. ПРИМЕЧАНИЕ--------------------------------------------------------- Альтернативой UDP в этом сценарии служит Т/ТСР — TCP для транзакций. Его мы описываем в разделе 13 9 2.6. Состояние TIME_WAIT Без сомнений, самым сложным для понимания аспектом TCP в отношении сете- вого программирования является состояние TIME_WAIT (время_ожидания). На рис. 2.4 мы видим, что узел, выполняющий активное закрытие, проходит это со- стояние. Продолжительность этого состояния конечной точки равна двум MSL (maximum segment lifetime — максимальное время жизни сегмента), иногда этот период называется 2MSL. В каждой реализации TCP выбирается определенное значение MSL Рекомен- дуемое значение, приведенное в RFC 1122 [9], равно 2 минутам, хотя Беркли- реализации традиционно использовали значение 30 секунд. Это означает, что продолжительность состояния TIME_WAIT — от 1 до 4 минут. MSL — это мак- симальное количество времени, в течение которого дейтаграмма IP может оста- ваться в объединенной сети. Это время ограничено, поскольку каждая дейтаграмма содержит 8-разрядное поле предельного количества транзитных узлов, или прыж- ков (hop limit) (поле TTL IPv4 на рис. А. 1 и поле предельного количества транзит- ных узлов IPv6 на рис. А.2), максимальное значение которого равно 255. Хотя этот предел ограничивает количество транзитных узлов, а не время пребывания пакета в сети, считается, что пакет с максимальным значением этого предела (ко- торое равно 255) не может существовать в объединенной сети дольше, чем пред- писывает значение MSL.
76 Глава 2. Транспортный уровень: TCP и UDP В результате различных аномалий, происходящих в объединенных сетях, пакеты обычно теряются. Если возникает сбой на маршрутизаторе или отключа- ется связь между двумя маршрутизаторами, то для стабилизации и поиска аль- тернативного пути требуются секунды или минуты. В течение этого периода могут возникать петли маршрутизации (маршрутизатор А отправляет пакеты маршру- тизатору В, а маршрутизатор В отправляет их обратно маршрутизатору А), и па- кеты теряются в этих петлях. Если потерянный пакет — это сегмент TCP, то по истечении установленного времени ожидания отправляющий узел снова переда- ет пакет, и этот заново переданный пакет доходит до конечного получателя по некоему альтернативному пути. Но если спустя некоторое время (не превосходя- щее MSL) после начала передачи потерянного пакета петля маршрутизации ис- правляется, пакет, потерянный в петле, отправляется к конечному получателю. Начальный пакет называется потерянной копией или дубликатом (lost duplicate), а также блуждающей копией или дубликатом (wandering duplicate). TCP должен обрабатывать потерянные пакеты. Состояние TIME_WAIT позволяет добиться двух целей: 1. Обеспечить надежность разрыва двустороннего соединения TCP. 2. Подождать, когда истечет время жизни в сети старых дублированных сегмен- тов. Первую причину можно объяснить, обратившись к рис. 2.5 с предположени- ем, что последний сегмент АСК потерян. Сервер еще раз отправит свой послед- ний сегмент FIN, и клиент должен будет обработать эту информацию. Он отве- тит сегментом RST (другой тип сегмента TCP), что сервер интерпретирует как ошибку. Если TCP выполняет всю работу, необходимую для корректного разры- ва потока данных в обоих направлениях соединения (его двустороннего закры- тия), он должен корректно обрабатывать потерю любого из этих четырех сегмен- тов. Этот пример также объясняет, почему узел, выполняющий активное закрытие, остается в состоянии TIME_WAIT: зто тот узел, который, возможно, должен бу- дет передать повторно последний сегмент АСК. Чтобы понять вторую причину, по которой необходимо состояние TIME_WAIT, предположим, что у нас имеется соединение между IP-адресом 206.62.226.33, порт 1500, и IP-адресом 198.69.10.2, порт 21. Это соединение закрывается, и спу- стя некоторое время мы устанавливаем другое соединение между теми же IP- адресами и портами: 206.62.226.33, порт 1500, и 198.69.10.2, порт 21. Последнее соединение называется новым воплощением (incarnation) предыдущего соедине- ния, поскольку использует те же IP-адреса и порты. TCP должен предотвратить появление старых дубликатов, относящихся к данному соединению, в новом во- площении соединения. Чтобы выполнить это, TCP не инициирует новое вопло- щение соединения, которое в данный момент находится в состоянии TIME W AIT. Поскольку продолжительность состояния TIME_WAIT равна двум MSL, это по- зволяет удостовериться, что истечет и время жизни пакетов, посланных в одном направлении, и время жизни пакетов, посланных в ответ. Используя это прави- ло, мы гарантируем, что в момент успешного установления соединения TCP вре- мя жизни в сети всех старых дубликатов от предыдущих воплощений этого со- единения уже истекло.
2.1. Номера портов 77 ПРИМЕЧАНИЕ---------------------------------------------------------- Из этого правила существует исключение. Реализации, происходящие от Беркли, ини- циируют новое воплощение соединения, которое в настоящий момент находится в со- стоянии T1ME_WAIT, если приходящий сегмент SYN имеет порядковый номер «больше» конечного номера из предыдущего воплощения. На с. 958-959 [105] об этом рассказано более подробно. Для этого требуется, чтобы сервер выполнил ак- тивное закрытие, поскольку состояние TIME WAIT должно существовать на узле, получающем следующий сегмент SYN. Эта возможность используется командой rsh. В RFC 1185 [46] рассказывается о некоторых ловушках, которые могут вас при этом подстерегать. 2.7. Номера портов В любой момент времени множество процессов может использовать либо прото- кол UDP, либо протокол TCP. И TCP, и UDP используют 16-разрядные целые номера портов {port numbers), что позволяет им различать эти процессы. Когда клиент хочет соединиться с сервером, он должен идентифицировать этот сервер. И для TCP, и для UDP определена группа заранее известных портов (well- known ports) для идентификации известных служб. Например, каждая реализа- ция TCP/IP, поддерживающая FTP, присваивает заранее известный порт 21 (де- сятичное значение) серверу FTP. Серверам TFTP (Trivial File Transfer Protocol — простейший протокол передачи файлов) присваивается порт UDP 69. Кроме того, клиенты используют динамически назначаемые, или эфемерные (ephemeral), порты, то есть порты с непродолжительным временем жизни. Эти номера портов обычно присваиваются клиенту автоматически протоколами UDP или TCP. Клиенту обычно не важно фактическое значение динамически назна- чаемого порта; клиент лишь должен быть уверен, что динамически назначаемый порт является уникальным на клиентском узле. Реализации TCP и UDP гаран- тируют такую уникальность. Документ RFC 1700 [87] содержит список номеров портов, присвоенных агент- ством IANA (Internet Assigned Numbers Authority — Агентство по выделению имен и уникальных параметров протоколов Интернета). Более современная ин- формация о них содержится в файле ftp://ftp.isi.edu/in-notes/iana/assignments/port- numbers, а не в RFC. Номера портов делятся на три диапазона: 1. Заранее известные порты, от 0 до 1023. Эти номера портов управляются и при- сваиваются агентством IANA. Когда это возможно, один и тот же номер порта присваивается данному сервису и для TCP, и для UDP. Например, порт 80 присвоен web-серверу для обоих протоколов, хотя в настоящее время все реа- лизации используют только TCP. 2. Зарегистрированные порты, от 1024 до 49 151. Они не управляются IANA, но IANA регистрирует и составляет списки использования этих портов для удоб- ства потребителей. Когда это возможно, один и тот же порт присваивается данному сервису и для TCP, и для UDP. Например, порты с номерами от 6000 до 6063 присвоены серверу X Window для обоих протоколов, но в настоящее время все реализации используют только TCP. Верхний предел (49 151) для этих портов является новым, так как в RFC 1700 [87] он был равен 65 535.
78 Глава 2. Транспортный уровень: TCP и UDP 3. Динамически назначаемые, или частные, порты: от 49 152 до 65 535. IANA ни- чего не говорит об этих портах. Эти порты иногда называются эфемерными. («Волшебное» число 49 152 составляет три четверти от 65 536.) На рис. 2.6 показано разделение портов на диапазоны и общее распределение номеров портов. Заранее известные порты IANA н- --я 1023 ЗареКгетриромнныв порты IANA Н--------------------7—---------------------Я 1024 49151 Динамически Динамические, или частные, порты IANA |<— Я 49152 65535 Зарезервированные назначаемые порты BSD порты BSD 1023 1024 5000 5001 1 rresvport BSD-серверы (непривилегированные) Динамически назначаемые порты Solans 65535 513 1023 32768 32768 Рис. 2.6. Распределение номеров портов На этом рисунке мы отмечаем следующие моменты: В системах Unix имеется понятие зарезервированного порта (reserved port) — это порт с номером меньше 1024. Эти порты может присвоить сокету только привилегированный процесс. Все заранее известные порты IANA являются зарезервированными портами; следовательно, сервер, желающий использовать этот порт (такой, как сервер FTP), должен обладать правами привилегиро- ванного пользователя. Исторически сложилось так, что Беркли-реализации (начиная с 4.3BSD) по- зволяют динамически выделять порты в диапазоне от 1024 до 5000. Это было приемлемо в начале 80-х, когда серверы не могли обрабатывать много клиен- тов одновременно, но сегодня можно легко найти сервер, поддерживающий более 3977 клиентов в любой момент времени. Поэтому некоторые системы выделяют динамически назначаемые порты по-другому (например, Solaris, как показано на рис. 2.6), чтобы предоставить больше динамически назначаемых портов. ПРИМЕЧАНИЕ----------------------------------------------------------- Как выяснилось, значение 5000 для верхнего предела динамически назначаемых пор- тов, реализованное в настоящее время во многих системах, было типографской ошиб- кой [6]. Этот предел должен был быть равен 50 000 t Существует несколько клиентов (не серверов), которые запрашивают заре- зервированный порт для аутентификации в режиме клиент-сервер: типичным примером могут служить клиенты rlogin и rsh. Эти клиенты вызывают биб- лиотечную функцию rresvport для создания сокета TCP и присваивают соке- ту неиспользованный номер порта из диапазона от 513 до 1023 Эта функция
2 8. Номера портов TCP и параллельные серверы 79 обычно пытается связаться с портом 1023, а если попытка оказывается неудач- ной — с портом 1022 и т. д., пока не будет достигнут желаемый результат или пока не будут перебраны все порты вплоть до порта 513. ПРИМЕЧАНИЕ----------------------------------------------------------- Заметим, что и зарезервированные порты BSD, и порты функции rresvport частично перекрывают верхнюю половину заранее известных портов IANA Это происхо- дит потому, что известные порты IANA когда-то заканчивались на 255. В RFC 1340 под названием «Assigned numbers» в 1992 году началось присваивание заранее извест- ных портов в диапазоне от 256 до 1023. В предыдущем документе RFC под названием «Assigned numbers» за номером 1060 от 1990 г. эти порты назывались стандартными службами Unix (Unix Standard Services) Существует множество Беркли-серве- ров, номера портов которых были заданы в 80-х годах и начинались с 512 (таким обра- зом, номера с 256 по 511 были пропущены) Функция rresvport начинает выбор с вер- хушки диапазона 512-1023 и направляется вниз. Пара сокетов Пара сокетов {socketpair) для соединения TCP — это кортеж (группа взаимосвя- занных элементов данных или записей) из четырех элементов, определяющий две конечных точки соединения: локальный IP-адрес, локальный порт TCP, уда- ленный IP-адрес и удаленный порт TCP. Пара сокетов однозначно идентифици- рует каждое соединение TCP в объединенной сети. Два значения, идентифицирующих конечную точку, — IP-адрес и номер пор- та — часто называют сокетом. Мы можем распространить понятие пары сокетов на UDP, даже учитывая то, что этот протокол не ориентирован на установление соединения. Когда мы будем говорить о функциях сокетов (bind, connect, getpeername и т. д.), мы покажем, ка- кими функциями задаются конкретные элементы пары сокетов Например, функ- ция bind позволяет приложению задавать локальный IP-адрес и локальный порт для сокетов как TCP, так и UDP. 2.8. Номера портов TCP и параллельные серверы Что случится при наличии параллельного сервера, когда основным циклом сер- вера порождается дочерний процесс для обработки каждого нового соединения, если дочерний процесс будет продолжать использовать заранее известный номер порта при обслуживании длительного запроса? Проанализируем типичную по- следовательность. Пусть сервер запускается на узле bsdi (рис. 1.7), который яв- ляется многоинтерфейсным (mutlihomed), то есть имеет несколько сетевых интерфейсов с IP-адресами 206.62.226.35 и 206.62.226.66, и выполняет пассивное открытие, используя свой заранее известный номер порта (в данном примере 21). Теперь он ждет запроса клиента. Эта ситуация изображена на рис. 2.7. Для указания пары сокетов сервера мы используем обозначение {*.21, *.*}. Сервер ждет запроса соединения на порте 21 любого локального интерфейса (пер- вая звездочка). Удаленный IP-адрес и удаленный порт не заданы, и мы обознача-
80 Глава 2. Транспортный уровень: TCP и UDP ; Сервер ; । । । ------------ । {*.21,*.*} —!------► Прослушиваемый I__________________1 сокет Рис. 2.7. Сервер TCP с пассивным открытием на порте 21 ем их *. *. В данном случае можно сказать, что речь идет о прослушиваемом сокете (listening socket). ПРИМЕЧАНИЕ------------------------------------------------------------------ Чтобы отделить IP-адрес от номера порта, мы используем точку, поскольку это обо- значение принято в программе netstat. Иногда это вызывает путаницу, поскольку де- сятичные точки используются и в доменных именах (solaris.kohala.com. 21), и в точеч- но-десятичной записи IPv4 (206 62.226.33.21). Локальный IP-адрес, задаваемый с помощью звездочки (символа подстанов- ки1, wildcard), — называется универсальным адресом. Если узел, на котором запу- щен сервер, является многоинтерфейсным (как в нашем примере), сервер может указать, что он хочет принимать входящие соединения, которые приходят только для одного определенного локального интерфейса. Сервер должен либо выбрать один определенный интерфейс, либо принимать запросы от всех интерфейсов, то есть он не может задать список, состоящий из нескольких адресов. Локальный адрес, заданный с помощью символов подстановки, соответствует выбору произ- вольного адреса из определенного множества. В листинге 1.5 перед вызовом функ- ции bi nd IP-адрес в структуре адреса сокета задан с помощью константы INADDR_ANY. Позже клиент запускается на узле с IP-адресом 198.69.10.2 и выполняет ак- тивное открытие соединения с IP-адресом сервера 206.62.226.35. В этом примере мы считаем, что динамически назначаемый порт, выбранный клиентом TCP, — это порт 1500, что отражено на рис. 2.8. Под клиентом мы показываем его пару сокетов. » 206.62.226.35 198.69.10.2 206.62.226.66 ! ; Запрос ; ' и I на соединение 1 Клиент -----1-------“---------[---> I ! 206.62.226.35 ; ; {198.69.10.2.1500,1 port21 I 206.62.226.35.21} | Рис. 2.8. Запрос на соединение от клиента к Сервер {*. 21, *. *)-j— серверу 1 Для термина «wildcard» пока не существует устоявшегося перевода Встречаются такие варианты, как «метасимвол», «символ замены», «символ подстановки», «расширитель», «универсальный сим- вол», «генератор», «групповой символ» и даже «джокер», «уальдкард» и «дикая карта» Под этими разнообразными названиями подразумевается специфический символ (как правило, звездочка), который заменяет любой символ или группу символов — Примеч перев
2.8. Номера портов TCP и параллельные серверы 81 Когда сервер получает и принимает соединение клиента, он создает с помощью функции fork свою копию, давая возможность дочернему процессу обработать запрос клиента, как показано на рис. 2.9 (функцию fork мы описываем в разде- ле 4.7). 206.62.226.35 198.69.10.2 206.62.226.35.21 Прослушиваемый сокет Присоединенный сокет Рис. 2.9. Параллельный сервер, дочерний процесс которого обрабатывает запрос клиента ' >' На этом этапе мы должны провести различие между прослушиваемым соке- том и присоединенным сокетом на стороне сервера. Обратите внимание на то, что присоединенный сокет использует тот же локальный порт (21), что и прослу- шиваемый сокет. Кроме того, на узле с несколькими сетевыми интерфейсами ло- кальный адрес заполняется для присоединенного сокета (206.62.226.35), как толь- ко устанавливается соединение. При выполнении следующего шага подразумевается, что другой клиентский процесс на клиентском узле запрашивает соединение с тем же сервером. Код ТСР- клиента задает новому сокету клиента неиспользованный номер динамически назначаемого порта, скажем 1501. Мы получаем сценарий, представленный на рис. 2.10. На стороне сервера различаются два соединения: пара сокетов для пер- вого соединения отличается от пары сокетов для второго соединения, поскольку TCP-клиент выбирает неиспользованный порт (1501) для второго соединения. Из этого примера видно, что TCP не может демультиплексировать входящие сегменты, просматривая только номера портов получателя. TCP должен рассмат- ривать все четыре элемента в паре сокетов, чтобы определить, для какой конеч- ной точки предназначен приходящий сегмент. На рис. 2.10 представлены три со- кета с одним и тем же локальным портом (21). Если сегмент приходит с IP-адреса 198.69.10.2, порт 1500, и предназначен для IP-адреса 206.62.226.35, порт 21, он доставляется первому дочернему процессу. Если сегмент приходит с IP-адреса 198.69.10.2, порт 1501, и предназначен для IP-адреса 206.62.226.35, порт 21, он доставляется второму дочернему процессу. Все другие сегменты TCP, предназ- наченные для порта 21, доставляются исходному серверу с прослушиваемым со- кетом.
82 Глава 2. Транспортный уровень: TCP и UDP 198.69.10.2 206.62.226.35 206.62.226.66 Прослушиваемый сокет Присоединенный сокет Присоединенный сокет Рис. 2.10. Второе соединение клиента с тем же сервером 2.9. Размеры буфера и ограничения Существует несколько ограничений, влияющих на размер дейтаграмм IP. Снача- ла мы опишем эти ограничения, а затем свяжем их вместе, чтобы показать, как они влияют на данные, которые может передавать приложение. Максимальный размер дейтаграммы IPv4 — 65 535 байт, включая заголовок IPv4. Это связано с тем, что размер дейтаграммы ограничен 16-разрядным полем общей длины (рис. А.1). Максимальный размер дейтаграммы IPv6 — 65 575 байт, включая 40-разряд- ный заголовок IPv6. Это ограничение связано с 16-раздрядным полем длины полезных данных (рис. А.2). Обратите внимание, что поле длины полезных данных IPv6 не включает размер заголовка IPv6, в то время как в случае IPv4 длина заголовка включается. IPv6 поддерживает передачу увеличенного объема полезных данных (jumbo payload), при этом поле длины полезных данных расширяется до 32 битов. Од- нако эта функция поддерживается только на тех канальных уровнях, на кото- рых максимальная единица передачи (MTU) превышает 65 535. Это свойство разработано для соединений между двумя узлами, таких как HIPPI (High- Performance Parallel Interface — высокоскоростной параллельный интерфейс), у которых часто нет собственных ограничений на MTU. Во многих сетях определена MTU (maximum transmission unit — максималь- ная единица передачи), величина которой диктуется аппаратными средствами. Например, размер MTU для Ethernet равен 1500 байтам. Другие канальные уровни, такие как связь «точка-точка» с использованием протокола PPP (Point-
2.9. Размеры буфера и ограничения 83 to-Point Protocol), имеют конфигурируемую MTU. Более ранние соединения по протоколу SLIP (Serial Line Internet Protocol — межсетевой протокол для последовательного канала) часто использовали MTU, равную 296 байтам. Минимальная величина канальной MTU (link MTU) для IPv4 — 68 байт. Ми- нимальная величина канальной MTU для IPv6 — 576 байтам. Наименьшая величина MTU в пути между двумя узлами называется транс- портной MTU (path MTU). В настоящее время MTU Ethernet, равная 1500 бай- там, часто является и транспортной MTU. Величина транспортной MTU между любыми двумя узлами не обязательно одинакова в обоих направлениях, по- скольку маршрутизация в Интернете часто асимметрична [77], то есть марш- рут от А до В может отличаться от маршрута от В до А. Если размер дейтаграммы превышает канальную MTU, то и IPv4, и IPv6 вы- полняют фрагментацию (fragmentation). Фрагменты не соединяются заново (reassemble), пока они не достигнут конечного получателя. Узлы IPv4 выпол- няют фрагментацию генерируемых дейтаграмм, а маршрутизаторы IPv4 выполняют фрагментацию передаваемых дейтаграмм. Но в случае IPv6 дейта- граммы фрагментируются только узлами, а маршрутизаторы IPv6 не фраг- ментируют передаваемые ими дейтаграммы. ПРИМЕЧАНИЕ--------------------------------------------------------------- Будьте внимательны при использовании данной терминологии. Узел, помеченный как маршрутизатор IPv6, может все равно выполнять фрагментацию, но только для дей- таграмм, которые этот маршрутизатор генерирует сам, однако он никогда не фрагмен- тирует передаваемые им дейтаграммы. Когда этот узел генерирует дейтаграммы IPv6, он на самом деле выступает в роли узла (а не маршрутизатора). Например, большин- ство маршрутизаторов поддерживают протокол Telnet, используемый администрато- рами для настройки. Дейтаграммы IP, генерируемые сервером Telnet маршрутизатора, генерируются маршрутизатором, поэтому он может выполнять их фрагментацию Вы можете заметить, что в заголовке IPv4 (рис. А 1) существуют поля для выполнения 1Р\’4-фрагментации, но в заголовке IPv6 (рис. А.2) полей для фрагментации нет. По- скольку фрагментация скорее исключение, чем правило, IPv6 может содержать допол- нительный заголовок с информацией о фрагментации. Если бит DF (Don’t Fragment — не фрагментировать) в заголовке IPv4 уста- новлен (см. рис. АД), это означает, что данная дейтаграмма не должна быть фрагментирована ни отправляющим узлом, ни каким-либо маршрутизатором на ее пути. Маршрутизатор, получающий дейтаграмму IPv4 с установленным битом DF, размер которой превышает MTU исходящей линии, генерирует сообщение ICMPv4 о недоступности получателя Fragmentati on needed but DF bit set (Необходима фрагментация, но установлен бит DF) (табл. А.З). Поскольку маршрутизаторы IPv6 не выполняют фрагментации, можно счи- тать, что во всех дейтаграммах IPv6 установлен бит DF. Когда маршрутизатор IPv6 получает дейтаграмму, размер которой превышает MTU исходящей ли- ниии, он генерирует сообщение об ошибке ICMPv6 Packet too big (Слишком большой пакет) (табл. А.4). Бит DF протокола IPv4 и его аналог в IPv6 могут использоваться для обнару- жения транспортной MTU (path MTU discovery) (RFC 1191 [71] для IPv4 и RFC
84 Глава 2. Транспортный уровень: TCP и UDP 1981 [62] для IPv6). Например, если TCP использует эту технологию с IPv4, он отправляет все дейтаграммы с установленным битом DF. Если же какой- нибудь промежуточный маршрутизатор возвращает сообщение ICMP Frag- mentation needed but DF bit set (Необходима фрагментация, но установлен бит DF), TCP уменьшает количество данных, которые он отправляет в каждой дейтаграмме, и передает их повторно. Обнаружение транспортной MTU не обязательно для IPv4, но должно поддерживаться всеми реализациями IPv6. IPv4 и IPv6 определяют минимальный размер буфера сборки (minimum reassem- bly buffer size): минимальный размер дейтаграммы, который гарантированно поддерживает любая реализация. Для IPv4 этот размер равен 576 байт, для IPv6 он увеличен до 1500 байт. Например, в случае IPv4 мы не знаем, может ли данный получатель принять дейтаграмму в 577 байт. Поэтому многие при- ложения IPv4, использующие UDP (DNS, RIP, TFTP, ВООТР, SNMP) предот- вращают генерацию дейтаграмм IP, превышающих этот размер. Ж Для протокола TCP определен максимальный размер сегмента (maximum segment size, MSS). MSS указывает собеседнику максимальный объем данных, кото- рые можно отправлять в каждом сегменте TCP. Параметр MSS был показан в сегментах SYN на рис. 2.5. Цель параметра MSS — сообщить собеседнику дей- ствительный размер буфера сборки и попытаться предотвратить фрагмента- цию. В качестве MSS часто используется значение, равное MTU интерфейса минус фиксированные размеры заголовков IP и TCP. В Ethernet при исполь- зовании IPv4 это будет 1460, а в Ethernet при использовании IPv6 — 1440 (за- головок TCP для обоих протоколов имеет длину 20 байт, но заголовок IPv4 имеет длину 20 байт, а заголовок IPv6 — 40 байт). Значение MSS в TCP представлено 16-разрядным полем, ограничивающим значение до 65 535. Это допустимо для IPv4, поскольку максимальное коли- чество данных TCP в дейтаграмме IPv4 равно 65 495 (65 535 минус 20-байто- вый заголовок IPv4 и минус 20-байтовый заголовок TCP). Но в случае увели- ченного объема полезных данных дейтаграммы IPv6 используется другая технология (RFC 2147 [7]). Прежде всего, максимальное количество дан- ных TCP в дейтаграмме IPv6 без увеличения объема полезных данных равно 65 515 байтам (65 535 минус 20-байтовый заголовок IPv6). Значение MSS, равное 65 535, считается особым случаем, обозначающим «бесконеч- ность». Это значение используется только вместе с параметром увеличения объема полезных данных, когда требуется размер MTU, превышающий 65 535. Если TCP использует увеличение объема полезных данных и получает от со- беседника объявление размера MSS, равного 65 535 байтам, то предельный раз- мер дейтаграммы, посылаемой им, будет равен MTU интерфейса. Если оказы- вается, что этот размер слишком велик (например, на пути существует канал с меньшим размером MTU), при обнаружении транспортной MTU будет ус- тановлено меньшее значение. Отправка по TCP Приняв все вышеизложенные термины и определения, обратимся к рис. 2.11, где показано, что происходит, когда приложение записывает данные в сокет TCP.
2.9. Размеры буфера и ограничения 85 Рис. 2.11. Этапы записи данных в рокет JCP и буферы, используемые при этой записи У каждого сокета TCP есть буфер отправки, размер которого мы можем изме- нять с помощью функции сокета SO_SNDBUF (см. раздел 7.5). Когда приложение вызывает функцию write, ядро копирует данные из буфера приложения в буфер отправки сокета. Если для всех данных приложения недостаточно места в буфе- ре сокета (либо буфер приложения больше буфера отправки сокета, либо в буфере отправки сокета уже имеются данные), этот процесс приостанавливается (пере- ходит в состояние ожидания). Подразумевается, что мы используем обычный блокируемый сокет (о неблокируемых сокетах мы поговорим в главе 15). Ядро возвращает управление из функции write только после того, как последний байт в буфере приложения будет скопирован в буфер отправки сокета. Следователь- но, успешное возвращение управления из функции write в сокет TCP говорит нам лишь о том, что мы можем снова использовать наш буфер приложения. Оно не говорит о том, получил ли собеседник отправленные данные, или получило ли их приложение-адресат (более подробно мы рассмотрим это при описании пара- метра сокета SO_LINGER в разделе 7.5). TCP помещает данные в буфер отправки сокета и отправляет их TCP собесед- нику, основываясь на всех правилах передачи данных TCP (см. главы 19и20 [94]). Собеседник TCP должен подтвердить данные, и только когда от него придет сег- мент АСК, подтверждающий прием данных, наш TCP сможет удалить подтверж- денные данные из буфера отправки сокета. TCP должен хранить копию данных, пока их прием не будет подтвержден получателем. TCP отправляет данные IP порциями размером MSS или меньше, добавляя свой заголовок TCP к каждому сегменту. Здесь MSS — это значение, анонсиро- ванное собеседником, или 536, если собеседник не указал значение MSS. IP до- бавляет свой заголовок, ищет в таблице маршрутизации IP-адрес получателя (со- ответствующая запись в таблице маршрутизации задает исходящий интерфейс, то есть интерфейс для исходящих пакетов) и передает дейтаграмму на соответ- ствующий канальный уровень. IP может выполнить фрагментацию перед пере-
86 Глава 2. Транспортный уровень: TCP и UDP дачей дейтаграммы, но, как мы отмечали выше, одна из целей параметра MSS — не допустить фрагментации; более новые реализации также используют обнару- жение транспортной MTU. У каждого канального соединения имеется очередь вывода, и если она заполнена, пакет игнорируется и вверх по стеку протоколов возвращается ошибка: от канального уровня к IP и затем от IP к TCP. TCP учтет эту ошибку и попытается отправить сегмент позже. Приложение не информиру- ется об этом временном состоянии. Отправка по UDP На рис. 2.12 показано, что происходит, когда приложение записывает данные в со- кет UDP. На этот раз буфер отправки сокета избражен пунктирными линиями, поскольку он (буфер) на самом деле не существует. У сокета UDP есть размер буфера отправки (который мы можем изменить с помощью функции сокета SO_SNDBUF, см. раздел 7.5), но это просто верхний предел размера дейтаграммы UDP, которая может быть записана в сокет. Если приложение записывает дейтаграмму размером больше буфера отправки сокета, возвращается сообщение об ошибке EMSGSIZE. Поскольку протокол UDP не является надежным, ему не нужно хра- нить копию данных приложения. Ему также не нужно иметь настоящий буфер отправки (данные приложения обычно копируются в буфер ядра по мере их дви- жения вниз по стеку протоколов, но эта копия сбрасывается канальным уровнем после передачи данных). UDP просто добавляет свой 8-байтовый заголовок и передает дейтаграмму протоколу IP. IPv4 или IPv6 добавляет свой заголовок, определяет исходящий интерфейс, выполняя функцию маршрутизации, а затем либо добавляет дейта- грамму в очередь вывода канального уровня (если размер дейтаграммы не пре- восходит MTU), либо фрагментирует дейтаграмму и добавляет каждый фрагмент в очередь вывода канального уровня. Рис. 2.12. Этапы записи данных в сокет UDP и буферы, используемые при этой записи
2,10. Стандартные службы Интернета 87 Если приложение UDP отправляет большие дейтаграммы (например, 2000- байтовые), существует гораздо большая вероятность фрагментации, чем в случае TCP, поскольку TCP разбивает данные приложения на порции, равные по разме- ру MSS, которому нет аналога в UDP. Успешное завершение функции записи в сокет UDP говорит о том, что либо дейтаграмма, либо фрагменты дейтаграммы были добавлены к очереди вывода канального уровня. Если недостаточно места для дейтаграммы или одного из ее фрагментов, приложению часто возвращается сообщение ENOBUFS. ПРИМЕЧАНИЕ----------------------------------------------------------- К сожалению, некоторые реализации не возвращают этой ошибки, не предоставляя приложению никаких указаний на то, что дейтаграмма была проигнорирована еще до начала передачи. 2.10. Стандартные службы Интернета В табл. 2.1 перечислены некоторые стандартные службы, предоставляемые боль- шинством реализаций TCP/IP. Обратите внимание, что все они предоставляют- ся с использованием и TCP, и UDP, и номер порта для обоих протоколов один и тот же. Таблица 2.1. Стандартные службы TCP/IP, предоставляемые в большинстве реализаций »»ие«да<»«9«»оиома Имя Порт TCP Порт UDP RFC Описание echo 7 7 862 Сервер возвращает то, что посылает клиент discard 9 9 863 Сервер игнорирует все данные, присланные клиентом daytime 13 13 867 Сервер возвращает время и дату в формате, удобном для восприятия человеком chargen 19 19 864 TCP-сервер посылает непрерывный поток символов, пока соединение не будет разорвано клиентом. UDP-сервер посылает дейтаграмму со случайным количеством символов каждый раз, когда клиент посылает дейтаграмму time 37 37 868 Сервер возвращает текущее время в виде двоичного 32-разрядного числа. Это число представляет собой количество секунд, про- шедших с момента 00.00'00 1 января 1900 года (UTC)' Эти службы часто предоставляются демоном inetd на узлах Unix (см. раз- дел 12.5). Стандартные службы обеспечивают возможность простого тестирова- ния при использовании стандартного клиента Telnet. Вот, например, тесты для сервера, определяющего время и дату, и для эхо-сервера. UTC — Universal Time Coordinated, универсальное скоординированное время (среднее время по Гринвичу). — Примеч. перев.
88 Глава 2. Транспортный уровень: TCP и UDP solans % telnet bsdi daytime Trying 206 62 226 35 Connected to bsdi kohala com Escape character is ’*]’ Tue Mar 19 11 06 49 1996 Connection closed by foreign host solans * telnet bsdi echo Trying 206 62 226 35 Connected to bsdi kohala com Escape character is ’*]•. hello, world hello world A] telnet> quit Connection closed выходные данные клиента Telnet выходные данные клиента Telnet выходные данные клиента Telnet выходные данные сервера daytime выходные данные клиента Telnet (сервер закрывает соединение) выходные данные клиента Telnet выходные данные клиента Telnet выходные данные клиента Telnet это набираем мы это возвращает сервер мы вводим символы ''и ] для обращения к клиенту Telnet и сообщаем ему, что закончили на этот раз клиент закрывает соединение В этих двух примерах мы вводим имя узла и название службы (daytime и echo). Названия служб сопоставляются с номерами портов, показанными в табл. 2.1, с по- мощью файла/etc/services, о котором речь пойдет в разделе 9.9. Обратите внимание, что когда мы соединяемся с сервером daytime, сервер вы- полняет активное закрытие. В случае эхо-сервера клиент выполняет активное закрытие. Вспомним рис. 2.4, где показано, что узел, выполняющий активное за- крытие, — это узел, проходящий через состояние TIME_WAIT. 2.11. Использование протоколов приложениями Интернета Таблица 2.2 иллюстрирует использование протоколов типичными приложения- ми Интернета. Таблица 2.2. Использование протоколов типичными приложениями Интернета Приложение IP ICMP UDP TCP Ping • Traceroute • • OSFP (протокол маршрутизации) • RIP (протокол маршрутизации) • BGP (протокол маршрутизации) • ВООТР (протокол bootstrap — протокол - • дистанционной загрузки и запуска устройств в сети) DHCP (протокол bootstrap) • NTP (синхронизирующий сетевой протокол) • TFTP (простейший протокол передачи файлов) , • SNMP (управление сетью) • SMTP (электронная почта) • Telnet (удаленный вход в систему) •
2.12. Резюме 89 Приложение IP ICMP UDP TCP FTP (передача файлов) • HTTP (протокол передачи HTML-файлов • по сети WWW) NNTP (сетевой протокол передачи новостей) • DNS (система доменных имен) • • NFS (сетевая файловая система) • • Sun RPC (удаленный вызов процедур) • • DCE RPC (удаленный вызов процедур) • • Первые два приложения, Ping и Traceroute, являются диагностическими, и они используют протокол ICMP. Traceroute создает свои собственные пакеты UDP для отправки и считывает ответы ICMP. Три популярных протокола маршрути- зации демонстрируют многообразие транспортных протоколов, которые исполь- зуются протоколами маршрутизации. Алгоритм OSPF (Open Shortest Path First — первоочередное открытие кратчайших маршрутов) использует IP непосредствен- но через символьный сокет, в то время как RIP (Routing Information Protocol — протокол маршрутной информации) использует UDP, a BGP (Border Gateway Protocol — протокол граничных шлюзов) использует TCP. Далее идут пять приложений, основанные на UDP, а за ними следуют прило- жения TCP. Последние четыре приложения используют и UDP, и TCP. 2.12. Резюме UDP является простым ненадежным протоколом, не ориентированным на уста- новление соединения, в то время как TCP — это сложный, надежный, ориентиро- ванный на установление соединения протокол. Хотя большинство приложений в Интернете используют протокол TCP (Web, Telnet, FTP, электронная почта), существует необходимость в обоих протоколах. В разделе 20.4 мы рассматрим причины, по которым иногда вместо TCP выбирается UDP. TCP устанавливает соединение с помощью трехэтапного рукопожатия и раз- рывает соединение, используя обмен четырьмя пакетами. Когда соединение TCP установлено, оно переходит из состояния CLOSED в состояние ESTABLISHED. При разрыве соединения оно переходит обратно в состояние CLOSED. Суще- ствует всего 11 состояний, в которых может находиться соединение TCP. Прави- ла перемещения между этими состояниями определяет диаграмма переходов. Понимание этой диаграммы существенно для диагностики проблем при исполь- зовании программы netstat. Важно также понимать, что происходит, когда мы вызываем такие функции, как connect, accept и close. Состояние TCP TIME_WAIT — неиссякаемый источник путаницы, возника- ющей у разработчиков сетевых приложений. Это состояние существует для того, чтобы реализовать разрыв двустороннего соединения TCP (например, для реше- ния проблем, возникающих в случае потери последнего сегмента АСК), а также чтобы дождаться, когда истечет время жизни старых дублированных сегментов в сети.
90 Глава 2 Транспортный уровень TCP и UDP Упражнения 1 Мы говорили о протоколах IPv4 и IPv6 А что произошло с версией 5, и каковы были версии 0, 1, 2 и 3? {Подсказка найдите последний документ RFC «As- signed numbers» («Присвоенные номера») Можете сразу переходить к реше- нию (приложение Д), если вы не знаете, как получать документы RFC в элек- тронном виде ) 2 Где вы будете искать дополнительную информацию о протоколе, которому присвоено название «1Р версии 5»? 3 Описывая рис 2 11, мы отметили, что TCP считает MSS равным 536, если не получает от собеседника величину параметра MSS Почему используется имен- но это значение? 4 Нарисуйте рисунок, аналогичный рис 2 5 для клиент-серверного приложения времени и даты из главы 1, считая, что сервер возвращает 26 байт данных в от- дельном сегменте TCP 5 Допустим, что установлено соединение между узлом в Ethernet, чей TCP объяв- ляет MSS, равный 1460, и узлом в Token Ring, чей TCP объявляет MSS, рав- ный 4096 Ни один из узлов не пытается обнаружить, чему равна транспорт- ная MTU При просмотре пакетов мы никогда не видим более 1460 байтов данных в любом направлении Почему? 6 Описывая табл 2 2, мы отметили, что протокол OSFP использует IP непосред- ственно Каково значение поля протокола в заголовке IPv4 (рис А 1) для дей- таграмм OSFP?
ЧАСТЬ 2 ЭЛЕМЕНТАРНЫЕ СОКЕТЫ
ГЛАВА 3 Введение в сокеты 3.1. Введение Эта глава начинается с описания программного интерфейса приложения (API) сокетов. Мы начнем со структур адресов сокетов, которые будут встречаться по- чти в каждом примере на протяжении всей книги. Эти структуры можно переда- вать в двух направлениях: от процесса к ядру и от ядра к процессу. Последний случай — пример аргумента, через который передается возвращаемое значение, и далее в книге мы встретимся с другими примерами таких аргументов. Перевод текстового представления адреса в двоичное значение, входящее в структуру адреса сокета, осуществляется функциями преобразования адресов. В большей части существующего кода IPv4 используются функции inet_addr и 1 net_ntoa, но две новых функции, i net pton и i net_ntop, работают и с IPv4, и с IPv6. Одной из проблем этих функций является то, что они зависят от протокола, так как для них имеет значение тип преобразуемого адреса — IPv4 или IPv6. Мы разработали набор функций, названия которых начинаются с sock_, работающих со структурами адресов сокетов независимо от протокола. Эти функции мы и бу- дем использовать, чтобы сделать наш код не зависящим от протокола. 3.2. Структуры адреса сокетов Большинство функций сокетов используют в качестве аргумента указатель на структуру адреса сокета. Каждый набор протоколов определяет свою собствен- ную структуру адреса сокетов. Имена этих структур начинаются с sockaddr_ и за- канчиваются уникальным суффиксом для каждого набора протоколов. Структура адреса сокета IPv4 Структура адреса сокета IPv4, обычно называемая структурой адреса сокета Ин- тернета, именуется sockaddr_in и определяется в заголовочном файле <net.i net/ in h>. В листинге 3.11 представлено определение Posix. 1g. Листинг 3.1. Структура адреса сокета Интернета (IPv4): sockaddrjn struct in_addr { in_addr_t s_addr /* 32-разрядный адрес IPv4 */ /* порядок байтов в сети */ 1 Вес исходные коды программ, опубликованные в згой книге, вы можете найти по адресу http:// www piter com/download
3.2. Структуры адреса сокетов 93 struct sockaddr_ jn { uint8_t sin_len. /* длина структуры (16) */ sa_famly_t sin_famly. /* AFJNET */ т n_port_t sin_port. /* 16-битовый номер порта TCP или UDP */ /* порядок байтов в сети */ struct in_addr sin_addr. /* 32-битовый адрес IPv4 */ /* порядок байтов в сети */ char sin_zero[8]. /* не используется */ Есть несколько моментов, касающихся структур адреса сокета в целом, кото- рые мы покажем на примере. > Элемент длины st п_1 еп появился в версии 4.3BSD-Reno, когда была добавле- на поддержка протоколов OSI (см. рис. 1.6). До этой реализации первым эле- ментом был STn_farmly, который исторически имел тип unsigned short (целое без знака). Не все производители поддерживают поле длины для структур адреса сокета, и в Posix. 1g, например, не требуется наличия этого элемента. Типы данных, подобные uint8_t, введены в Posix.lg (табл. 3.1). Наличие поля длины упрощает обработку структур адреса сокета с переменной дли- ной. Даже если поле длины присутствует, нам не придется устанавливать и прове- рять его значение, пока мы не имеем дела с маршрутизирующими сокетами (см. главу 17). Оно используется внутри ядра процедурами, работающими со структурами адресов сокетов из различных семейств протоколов (например, код таблицы маршрутизации). ПРИМЕЧАНИЕ ------------------------------------------------------ Четыре функции, передающие структуру адреса сокета от процесса к ядру, — bind, con- nect, sendto и sendmsg — используют функцию sockargs в реализациях, ведущих проис- хождение от Беркли [105, с. 452 ]. Эта функция копирует структуру адреса сокета из процесса и затем явно присваивает элементу sin_len значение размера структуры, пе- реданной в качестве аргумента этим четырем функциям Пять функций, передающих структуру адреса сокета от ядра к процессу, — accept, recvfrom, recvmsg, getpeername и getsockname — устанавливают элемент sinjen перед возвращением управления процессу. К сожалению, обычно не существует простого теста, выполняемого в процессе компи- ляции и определяющего, задает ли реализация поле длины для своих структур адреса сокета. В нашем коде мы тестируем собственную константу HAVE_SOCKADDR_SA_ LEN, (листинг Г.2), но для того чтобы определить, задавать эту константу или нет, тре- буется откомпилировать простую тестовую программу, использующую необязатель- ный элемент структуры, и проверить, успешно ли выполнена компиляция. В листин- ге 3.3 мы увидим, что от реализаций IPv6 требуется задавать SIN6_LEN, если структура адреса сокета имеет поле длины. В некоторых реализациях IPv4 (например, Digital Unix) поле длины предоставляется для приложений, основанных на параметре времени ком- пиляции (например, _SOCKADDR_LEN). Это свойство обеспечивает совместимость с другими более ранними программами. Posix. 1g требует наличия только трех элементов структуры: st n_fann ly, st n_addr и st n port. Posix-совместимая реализация может определять дополнительные
94 Глава 3. Введение в сокеты элементы структуры, и это норма для структуры адреса сокета Интернета. Почти все реализации добавляют элемент sin_zero, так что все структуры ад- реса сокета имеют размер как минимум 16 байт. И Типы элементов s_addr, sin_fami 1у и sin_port мы указываем согласно Posix.lg. Тип данных 1 n_addr_t соответствует целому числу без знака длиной как мини- мум 32 бита, 1 n_port_t — целому числу без знака длиной как минимум 16 бит, a sa_family_t — это произвольное целое число без знака. Последнее обычно представляет собой 8-разрядное целое без знака, если реализация поддержи- вает поле длины, либо 16-разрядное целое, если поле длины не поддерживает- ся. В табл. 3.1 перечислены эти три типа данных Posix вместе с некоторыми другими типами данных Posix. 1g, с которыми мы встретимся. Таблица 3.1. Типы данных, требуемые Posix.lg Тип данных Описание Заголовочный файл int8_t 8-разрядное целое co знаком <sys/types.h> uint8_t 8-разрядное целое без знака <sys/types.h> intl6_t 16-разрядное целое со знаком <sys/types.h> uintl6_t 16-разрядное целое без знака <sys/types.h> int32_t 32-разрядное целое со знаком <sys/types.h> uint32_t 32-разрядное целое без знака <sys/types.h> sa family_t семейство адресов структуры адреса сокета <sys/socket.h> socklen_t длина структуры адреса сокета, обычно типа uint32_t <sys/socket.h> inaddrt 1Ру4-адрес, обычно типа uint32_c <netinet/in.h> in_port_t порт TCP или UDP, обычно типа uintl6_t <netinet/in h> Вы также встретите типы данных u char, u_short, u_int и u_long, которые не имеют знака. Posix. 1g определяет их с замечанием, что они устарели. Они пре- доставляются в целях обратной совместимости. И адрес IPv4, и номер порта TCP и UDP всегда хранятся в структуре в соот- ветствии с порядком байтов, определенным в сети. Об этом нужно помнить при использовании этих элементов (более подробно о разнице между поряд- ком байтов узла и порядком байтов в сети мы поговорим в разделе 3.4). ПРИМЕЧАНИЕ -------------------------------------------------------- Причина того, что sin_addr является структурой, а не просто целым числом без знака, носит исторический характер. В более ранних реализациях (например, 4.2BSD) струк- тура inaddr определялась как объединение (union) различных структур, чтобы сде- лать возможным доступ к каждому из четырех байтов 32-разрядного 1Ру4-адреса, а так- же к обоим входящим в него 16-разрядным значениям. Эта возможность использова- лась в адресах классов А, В и С для выборки соответствующих байтов адреса. Но с появлением подсетей и последующим исчезновением различных классов адре- сов (см. раздел А.4) и введением бесклассовой адресации (classless addressing) необ- ходимость в объединении структур отпала. В настоящее время большинство систем отказались от использования объединения и просто определяют in addr как структу- ру, содержащую один элемент типа unsigned long.
3.2. Структуры адреса сокетов 95 г К 32-разрядному адресу IPv4 можно обратиться двумя путями. Например, если serv — это структура адреса сокета Интернета, то serv sin_addr указывает на 32-разрядный адрес IPv4 как на структуру i n_addr, в то время как serv. si n_ addr. s addr указывает на тот же 32-разрядный адрес IPv4 как на значение типа 1 n_addr_t (обычно это 32-разрядное целое число без знака). . Элемент si n_zero не используется, но мы всегда устанавливаем его в нуль при заполнении одной из этих структур. Перед заполнением структуры мы всегда обнуляем все ее элементы. ПРИМЕЧАНИЕ ------------------------------------------------------------- В большинстве случаев при использовании этой структуры не требуется, чтобы эле- мент sin_zero был равен нулю, но, например, при связывании конкретного адреса IPv4 (а не произвольного интерфейса) этот элемент обязательно должен быть нулевым. - Структуры адреса сокета используются только на данном узле: сама структу- ра не передается между узлами, хотя определенные поля (например, поля IP- адреса и порта) используются для соединения. Универсальная структура адреса сокета Структуры адреса сокета всегда передаются по ссылке при передаче в качестве аргумента для любой функции сокета. Но функции сокета, принимающие один из этих указателей в качестве аргумента, должны работать со структурами адреса сокета из любого поддерживаемого семейства протоколов. Проблема в том, как объявить тип передаваемого указателя. Для ANSI С ре- шение простое: void * является указателем на неопределенный (универсальный) тип (generic pointer type). Но функции сокетов существовали до появления ANSI С, и в 1982 году было принято решение определить универсальную структу- ру адреса сокета (generic socket address structure) в заголовочном файле <sys/ socket h>, которая показана в листинге 3.2. Листинг 3.2. Универсальная структура адреса сокета: sockaddr struct sockaddr { uint8_t sa_len. sa_family_t sa_family. /* семейство адресов: константа AF_xxx */ char sa_data[14], /* адрес, специфичный для протокола */ }. Функции сокетов определяются таким образом, что их аргументом является указатель на общую структуру адреса сокета, как показано в прототипе функции bind (ANSI С): int bind(int, struct sockaddr *. socklen_t). При этом требуется, чтобы для любых вызовов этих функций указатель на структуру адреса сокета, специфичную для протокола, был преобразован в ука- затель на универсальную структуру адреса сокета. Например: struct sockaddr_in serv. /* структура адреса сокета IPv4 */ /* заполняем serv{} */ bindlsockfd. (struct sockaddr *) &serv, sizeof(serv));
96 Глава 3. Введение в сокеты Если мы не выполним преобразование (struct sockaddr *), компилятор С сге- нерирует предупреждение в форме "Warm ng passmgarg2 of‘bind’ from incompatible poi nter type" (Передается указатель несовместимого типа). Здесь мы предполага- ем, что в системных заголовочных файлах имеется прототип ANSI С для функ- ции bi nd. С точки зрения разработчика приложений универсальная структура адреса сокета используется только для преобразования указателей на структуры адре- сов конкретных протоколов. ПРИМЕЧАНИЕ ------------------------------------------------------------ Вспомните, что в нашем заголовочном файле unp.h (см. раздел 1.2) мы определили SA ’ как строку "struct sockaddr", чтобы сократить код, который мы написали для преобра- ' зования этих указателей. С точки зрения ядра основанием использовать в качестве аргументов указатели на уни- версальные структуры адреса сокетов является то, что ядро должно получать указа- тель вызывающей функции, преобразовывать его в struct sockaddr, а затем по значе- <.' нию элемента sajfamily определять тип структуры. Но разработчику приложений было бы проще работать с указателем void *, поскольку это избавило бы его от необходимо- сти выполнять явное преобразование указателя. Структура адреса сокета IPv6 Структура адреса сокета IPv6 задается при помощи включения заголовочного файла <netinet/in h>, как показано в листинге 3.3. Листинг 3.3. Структура адреса сокета IPv6: sockaddr_in6 struct in6_addr { uint8_t st adclr116]: /* 128-разрядный адрес IPv6 */ /* порядок байтов сетевой */ }. #define SIN6 LEN /* требуется для проверки во время компиляции */ struct sockaddr_in6 { uint8_t sin6_len: /* длина этой структуры (24) */ sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port. /* номер порта транспортного уровня */ /* порядок байтов сетевой */ uint32_t sin6_flowinfo. /* приоритет и метка потока */ /* порядок байтов сетевой */ struct in6_addr sin6_addr. /* 1Ру6-адрес */ /* порядок байтов сетевой */ }. ПРИМЕЧАНИЕ ------------------------------------------------------ Расширения API сокетов для IPv6 описаны в RFC 2133 [32]. В Posix.lg ничего не ска- зано об IPv6. Однако некоторые типы данных в листинге 3.3 отличаются от приведен- ных в RFC, поскольку используемые нами типы данных были окончательно определе- ны в проекте Posix.lg, опубликованном после выхода RFC 2133. Отметим следующие моменты относительно листинга 3.3: & Константа S IN6_LEN должна быть задана, если система поддерживает поле дли- ны для структур адреса сокета.
3.2. Структуры адреса сокетов 97 3 Семейством IPv6 является AF_INET6, в то время как семейство IPv4 — AF INET. Элементы в структуре упорядочены таким образом, что если структура sockaddr_i пб выровнена по 64 битам, то так же выровнен и 128-разрядный эле- мент sin6_addr. На некоторых 64-разрядных процессорах доступ к данным с 64- разрядными значениями оптимизирован, если данные выровнены так, что их адрес кратен 64. ж Элемент si n6_f 1 owi nfо разделен на три поля: □ 24 бита младшего порядка — это метка потока; □ следующие 4 бита обозначают приоритет; □ следующие 4 бита зарезервированы. Поле метки потока и поле приоритета рассматриваются в описании рис. А.2. Отметим, что использование поля приоритета еще не определено. Сравнение структур адреса сокетов На рис. 3.1 показано сравнение четырех структур адресов сокетов, с которыми мы встретимся в тексте, предназначенных для IPv4, IPv6, доменного сокета Unix и канального уровня (см. листинг 17.1). Подразумевается, что все структуры ад- реса сокета содержат 1-байтовое поле длины, поле семейства также занимает 1 байт и длина любого поля, размер которого ограничен снизу, в точности равна этому ограничению. Две структуры адреса сокета имеют фиксированную длину, а структура доменного сокета Unix и структура канального уровня — перемен- ную. При обработке структур переменной длины мы передаем функциям сокетов IPv4 sockaddr_in{} Длина | 16-битовый порт 32-битовый 1Ри4-адрес IPv6 sockaddr_in6{} Длина |af_inet6 16-битовый порт 32-битовая метка потока Unix sockaddr_un{} Длина | AF LOCAL (Не используется) Фиксированная длина (16 байт) Листинг 3.1 128-битовый 1Ру4-адрес Полное имя (не более 104 байт) Канальный уровень sockaddr_dl{} Длина AF_LINK Индекс интерфейса Тип Длина имени Длина адреса Длина селектора Имя интерфейса и адрес канального уровня Переменная длина Листинг 17.1 Фиксированная длина (24 байта) Листинг 3.4 Переменная длина Листинг 14.1 Рис. 3.1. Сравнение различных структур адресов сокетов
98 Глава 3. Введение в сокеты указатель на структуру адреса сокета, а в другом аргументе передаем длину этой структуры. Под каждой структурой фиксированной длины мы показываем ее размер в байтах (для реализации 4.4BSD). ПРИМЕЧАНИЕ ----------------------------------------------------------- Сама структура sockaddr_un имеет фиксированную длину, но объем информации в ней — длина полного имени (pathname) — может быть переменным. Передавая ука- затели на эти структуры, следует соблюдать аккуратность при обработке поля длины — как длины в структуре адреса сокета (если поле длины поддерживается данной реали- зацией), так и длины данных, передаваемых ядру и принимаемых от него Этот рисунок служит также иллюстрацией стиля, которого мы придерживаемся в этой книге: названия структур на рисунках всегда выделяются полужирным шрифтом, а за ними следуют фигурные скобки. Ранее отмечалось, что в реализации 4.3BSD Reno ко всем структурам адресов сокетов было добавлено поле длины. Если бы поле длины присутствовало в оригинальной ре- ализации сокетов, то не возникло бы необходимости передавать аргумент длины функ- циям сокетов (третий аргумент функций bind и connect) Вместо этого размер структу- ры мог бы храниться в поле длины структуры. 3.3. Аргументы типа «значение-результат» Мы отмечали, что когда структура адреса сокета передается какой-либо из функ- ций сокетов, она всегда передается по ссылке, то есть в качестве аргумента пере- дается указатель на структуру. Длина структуры также передается в качестве аргумента. Но способ, которым передается длина, зависит от того, в каком на- правлении передается структура: от процесса к ядру или наоборот. 1. Три функции — bind, connect и sendto — передают структуру адреса сокета от процесса к ядру. Один из аргументов этих функций — указатель на структуру адреса сокета, другой аргумент — это целочисленный размер структуры, как показано в следующем примере: struct sockaddr_in serv. /* заполняем serv{} */ connect(sockfd (SA *) &serv, sizeof(serv)) Поскольку ядру передается и указатель, и размер структуры, на которую он указывает, становится точно известно, какое количество данных нужно ско- пировать из процесса в ядро. На рис. 3.2 показан этот сценарий. В следующей главе мы увидим, что размер структуры адреса сокета в действи- тельности имеет тип socklen_t, а не int, но Posix. 1g рекомендует, чтобы ис- пользовался тип mnt32_t. 2. Четыре функции — accept, recvfnom, getsockname и getpeername — передают струк- туру адреса сокета от ядра к процессу. Этим функциям передается указатель на структуру адреса сокета и указатель на целое число, содержащее размер структуры, как показано в следующем примере: struct sockaddr_un cli. /* домен Unix */ socklen t len.
3.3. Аргументы типа «значение-результат 99 len = sizeof(cli) /* len- это значение */ getpeername(umxfd. (SA*) &cli. &len). /* значение len могло измениться */ Пользовательский процесс Рис. 3.2. Структура адреса сокета, передаваемая от процесса к ядру Причина замены типа для аргумента «длина» с целочисленного на указатель состоит в том, что размер является и значением при вызове функции (сообщает ядру размер структуры, так что ядро при заполнении структуры знает, где нужно остановиться), и результатом, когда функция возвращает значение (сообщает процессу, какой объем информации ядро действительно сохранило в этой струк- туре). Такой тип аргумента называется аргументом типа «значение-результат» (value-result argument). На рис. 3.3 представлен этот сценарий. Пользовательский процесс Рис. 3.3. Структура адреса сокета, передаваемая от ядра к процессу Пример аргументов типа «значение-результат» вы увидите в листинге 4.2. Если при использовании аргумента типа «значение-результат» для длины структуры структура адреса сокета имеет фиксированную длину (рис. 3.1), то значение, возвращаемое ядром, будет всегда равно этому фиксированному раз-
100 Глава 3. Введение в сокеты меру: 16 для sockaddr_in IPv4 и 24 для sockaddr_in6 IPv6. Для структуры адреса сокета переменной длины (например, sockaddr_un домена Unix) возвращаемое значение может быть меньше максимального размера структуры (вы увидите это в листинге 14.2). ПРИМЕЧАНИЕ --------------------------------------------------------- Мы говорили о структурах адресов сокетов, передаваемых между процессом и ядром. Для такой реализации, как 4.4BSD, где все функции сокетов являются системными вызовами внутри ядра, это верно. Но в некоторых реализациях, особенно в System V, функции сокетов являются лишь библиотечными функциями, которые выполняются как часть обычного пользовательского процесса. То, как эти функции взаимодейству- ют со стеком протоколов в ядре, относится к деталям реализации, которые обычно нас не волнуют. Тем не менее для простоты изложения мы будем продолжать говорить об этих структурах как о передаваемых между процессом и ядром такими функциями, как bind и connect. (В разделе В. 1 вы увидите, что реализации System V действительно передают пользовательские структуры адресов сокетов между процессом и ядром, по как часть сообщений потоков.) Существует еще две функции, передающие структуры адресов сокетов: это recvmsg и sendmsg (см. раздел 13.5). Однако при их вызове поле длины не является отдельным аргументом функции, а передается как одно из полей структуры. В сетевом программировании наиболее общим примером аргумента типа «зна- чение-результат» может служить длина возвращаемой структуры адреса сокета. Вы встретите и другие аргументы типа «значение-результат»: & Три средних аргумента функции sei ect (см. раздел 6.3). • Аргумент длины для функции getsockopt (см. раздел 7.2). » Элементы msg_namelen и msg_control len структуры msghdr при использовании с функцией reevmsg (см. раздел 13.5). Л Элемент т fc_l еп структуры i fconf (см. листинг 16.1). Первый из двух аргументов длины в функции sysctl (см. раздел 17.4). 3.4. Функции определения порядка байтов Рассмотрим 16-разрядное целое число, состоящее из двух байтов. Возможны два способа хранения двух байтов в памяти. Такое расположение, когда первым идет младший байт, называется прямым порядком байтов (little-endian), а когда пер- вым расположен старший байт — обратным порядком байтов (big-endian). На рис. 3.4 показаны оба варианта. Сверху на этом рисунке изображены адреса, возрастающие справа налево, а снизу — слева направо. Старший бит (most significant bit, MSB) является в 16-раз- рядном числе крайним слева, а младший бит (least significant bit, LSB) — крайним справа. ПРИМЕЧАНИЕ --------------------------------------------------------- Термины «прямой порядок байтов» и «обратный порядок байтов» указывают, какой конец многобайтового значения — младший байт или старший — хранится в качестве начального адреса значения.
3.4. Функции определения порядка байтов 101 Возрастание адресов в памяти Прямой порядок байтов: Адрес А+1 Адрес А Старший байт Младший байт | MSB 16-битовое значение LSB Обратный порядок байтов. Старший байт | Младший байт Адрес А Адрес А+1 Возрастание адресов в памяти Рис. 3.4. Прямой и обратный порядок байтов для 16-битового целого числа К сожалению, не существует единого стандарта порядка байтов, и можно встре- тить системы, использующие оба формата. Способ упорядочивания байтов, ис- пользуемый в конкретной системе, мы называем порядком байтов узла {host byte order). Программа, представленная в листинге 3.4, выдает порядок байтов узла. Листинг 3.4. Программа для определения порядка байтов узла //intro/byteorder с 1 include "unp h" 2 int 3 maintint argc, char **argv) 4 { 5 union { 6 short s. 7 char clsizeof(short)]: 8 } un. 9 un s = 0x0102. 10 pnntf(”«s " CPU_VENDOR_OS). 11 if (sizeof(short) == 2) { 12 if (un c[0] == 1 && un.c[l] == 2) 13 printf("big-endian\en"), 14 else if (un.c[0J = 2 && un c[l] == 1) 15 printfClittle-endianlen”), 16 else 17 printfC’unknownXen") 18 } else 19 printf("sizeof(short) = Idler". sizeof(short)): 20 exit(0) 21 } Мы помещаем двухбайтовое значение 0x0102 в переменную типа short (корот- кое целое) и проверяем значения двух байтов этой переменной: с [0] (адрес А на рис. 3.4) и с [1] (адрес А+1 на рис. 3.4), чтобы определить порядок байтов.
102 Глава 3. Введение в сокеты Константа CPU_VENDOR_OS определяется программой GNU (аббревиатура GNU рас- крывается рекурсивно — GNU’s Not Unix) autoconf в процессе конфигурации, необходимой для выполнения программ из этой книги. В этой константе хранит- ся тип центрального процессора, а также сведения о производителе и реализации операционной системы На рис. 1.7 представлены некоторые примеры вывода этой программы при запуске ее в различных системах, aix % byteorder powerpc-ibm-атх4 200 big-endian alpha % byteorder alpha-dec-osf4 0 little-endian bsdi % byteorder i386-pc-bsdi3 0 little-endian genii m % byteorder sparc-sun-sunos4 1 4 big-endian hpux % byteorder hppal 1-hp-hpuxlO 30 big-endian linux X byteorder 1586-pc-linux-gnu little-endian solans X byteorder sparc-sun-solaris2 5 1 big-endian umxware % byteorder 1386-umvel-sysv4 2MP little-endian Все, что было сказано об определении порядка байтов 16-разрядного целого числа, конечно, справедливо и в отношении 32-разрядного целого. ПРИМЕЧАНИЕ ------------------------------------------------------------------------- Существуют системы, в которых возможен переход от прямого к обратному порядку байтов [26] либо при перезапуске системы (MIPS 2000), либо в любой момент выпол- нения программы (Intel i860). Разработчикам сетевых приложений приходится обрабатывать различия в опре- делении порядка байтов, поскольку в сетевых протоколах используется сетевой порядок байтов (network byte order). Например, в сегменте TCP есть 16-разряд - ный номер порта и 32-разрядный адрес IPv4. Стеки отправляющего и принимаю- щего протоколов должны согласовывать порядок, в котором передаются байты этих многобайтовых полей. Протоколы Интернета используют обратный поря- док байтов. Теоретически реализация Unix могла бы хранить поля структуры адреса со- кета в порядке байтов узла, а затем выполнять необходимые преобразования при перемещении полей в заголовки протоколов и обратно, позволяя нам не беспоко- иться об этом. Но исторически и с точки зрения Posix. 1g определяется, что для некоторых полей в структуре адреса сокета порядок байтов всегда должен быть сетевым. Поэтому наша задача — выполнить преобразование из порядка байтов узла в сетевой порядок и обратно. Для этого мы используем следующие четыре функции:
3.5. Функции управления байтами 103 ^include <netniet/ni h> inntl6_t htons(unitl6_t hostl6bitvalue') uint32_t htonl(uint32_t host32bitvalue) Обе функции возвращают значение, записанное в сетевом порядке байтов uintl6_t ntohs(uintl6_t netlbbitvalue) uint32_t ntohl(uint32_t net32bitvalue) Обе функции возвращают значение, записанное в порядке байтов узла В названиях этих функций h обозначает узел, п обозначает сеть, s — тип short, 1 — тип long. Термины short и /объявляются наследием времен реализации 4.2BSD Digital VAX. Следует воспринимать s как 16-разрядное значение (как, например, номер порта TCP или UDP), а 1 — как 32-разрядное значение (например, адрес IPv4). В самом деле, в 64-разрядной системе Digital Alpha длинное целое занима- ет 64 разряда, а функции htonl и ntohl оперируют 32-разрядными значениями (несмотря на то, что используют тип long). Используя эти функции, мы можем не беспокоиться о реальном порядке бай- тов на узле и в сети. Для преобразования порядка байтов в конкретном значении следует вызвать соответствующую функцию. В системах с таким же порядком байтов, как в протоколах Интернета (обратным), эти четыре функции обычно определяются как пустой макрос. Мы еще вернемся к проблеме определения порядка байтов, обсуждая данные, содержащиеся в сетевом пакете, и сравнивая их с полями в заголовках протоко- ла, в разделе 5.18 и упражнении 5 8. Мы до сих пор не определили термин байт. Его мы будем использовать для обозначения 8 бит, поскольку практически все современные компьютерные сис- темы используют 8-битовые байты. Однако в большинстве стандартов Интерне- та для обозначения 8 бит используется термин октет. Началось это на заре TCP/IP, поскольку большая часть работы выполнялась в системах типа DEC-10, в кото- рых не применялись 8-битовые байты. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Типичной ошибкой среди программистов сетевых приложений начала 80-х, разраба- тывающих код на рабочих станциях Sun (Motorola с обратным порядком байтов), было забыть вызвать одну из указанных четырех функций. На этих рабочих станциях про- граммы работали нормально, но при переходе на машины с прямым порядком байтов они переставали работать. 3.5. Функции управления байтами Существует две группы функций, работающих с многобайтовыми полями без преобразования данных и без интерпретации их как строк языка С с завершаю- щим нулем. Они необходимы нам при обработке структур адресов сокетов, по- скольку такие поля этих структур, как IP-адреса, могут содержать нулевые бай-
104 Глава 3. Введение в сокеты ты, но при этом не являются строками С. Строки с завершающим нулем обраба- тываются функциями языка С, имена которых начинаются с аббревиатуры str. Эти функции подключаются с помощью файла <string h>. Первая группа функций, названия которых начинаются с b (от слова «byte» — «байт»), взята из реализации 4.2BSD и все еще предоставляется практически любой системой, поддерживающей функции сокетов. Вторая группа функций, названия которых начинаются с mem (от слова «memory» — память), взята из стан- дарта ANSI С и доступна в любой системе, обеспечивающей поддержку библио- теки ANSI С. Сначала мы представим функции, которые берут начало от реализации Берк- ли, хотя в книге мы будем использовать только одну из них — bzero. (Дело в том, что она имеет только два аргумента и ее проще запомнить, чем функцию memset с тремя аргументами, как объяснялось в разделе 1.2.) Две другие функции, Ьсору и bcmp, могут встретиться вам в существующих приложениях. #include <strings h> void bzerotvoid *dest. sizet nbytes). void bcopy(const void *src. void *dest. size_t nbytes). int bcmp(const void *ptrl const void *ptr2. sizet nbytes): Возвращает 0 в случае равенства, ненулевое значение в случае неравенства ПРИМЕЧАНИЕ -------------------------------------------------------------------- Мы впервые встречаемся с квалификатором1 const. В приведенном примере он служит признаком того, что значения, на которые указывает указатель, то есть src, ptrl и ptr2, не изменяются функцией. Другими словами, область памяти, на которую указывает указатель с квалификатором const, считывается функцией, но не изменяется. Функция bzero обнуляет заданное число байтов в указанной области памяти. Мы часто используем эту функцию для инициализации структуры адреса сокета нулевым значением. Функция Ьсору перемещает заданное число байтов из источ- ника в место назначения. Функция bcmp сравнивает две произвольных последо- вательности байтов и возвращает нулевое значение, если две байтовых строки идентичны, и ненулевое в противном случае. Следующие функции являются функциями ANSI С: #include <string h> void *memset(void *dest, int c. size_t len) void *memcpy(void *dest. const void *src. size_t nbytes). int memcmp(const void *ptrl const void *ptr2 sizet nbytes). Возвращает 0 в случае равенства, значение <0 или >0 в случае неравенства (см текст) Функция memset присваивает заданному числу байтов значение с. Функция memcpy аналогична функции Ьсору, но имеет другой порядок двух аргументов. 1 Встречается и другой перевод термина «qualifier» — «определитель». — Примеч перев.
3.6. Функции inet aton, inetaddr и inet ntoa 105 функция bcopy корректно обрабатывает перекрывающиеся поля, в то время как поведение функции memcpy не определено, если источник и место назначения пе- рекрываются. В случае перекрывания полей должна использоваться функция ANSI С memmove (упражнение 30.3). ПРИМЕЧАНИЕ ----------------------------------------------------------------- Чтобы запомнить порядок аргументов функции memcpy, подумайте о том, что он сов- падает с порядком аргументов в операторе присваивания (справа — оригинал, слева — копия). dest = see. Последним аргументом этой функции всегда является длина области памяти. Функция mememp сравнивает две произвольных последовательности байтов и возвращает нуль, если они идентичны. В противном случае знак возвращаемо- го значения определяется знаком разности между первыми несовпадающими бай- тами, на которые указываютptr1 и ptr2. Предполагается, что сравниваемые байты принадлежат к типу unsigned chars. 3.6. Функции inet_aton, inet addr и inet_ntoa Существует две группы функций преобразования адресов, которые мы рассмат- риваем в этом и следующем разделах. Они выполняют преобразование адресов Интернета из строк ASCII (удобных для человеческого восприятия) в двоичные значения с сетевым порядком байтов (эти значения хранятся в структурах адре- сов сокетов). 1. Функции inet_aton, inetjitoa и inet addr преобразуют адрес IPv4 из точечно- десятичной записи (например, 206.62.226.33) в 32-разрядное двоичное значе- ние в сетевом порядке байтов. Возможно, вы встретите эти функции в много- численных существующих программах. 2. Более новые функции i net_pton и i net_ntop работают и с адресами IPv4, и с адре- сами IPv6. Эти функции, описываемые в следующем разделе, мы используем в книге. #include <arpa/inet h> int inet_aton(const char *strptr struct in_addr *addrptr). Возвращает 1. если строка преобразована успешно. О в случае ошибки in_addr_t inet_addr(const char ★strptr'). Возвращает 32-разрядный адрес IPv4 в сетевом порядке байтов: INADDR_NONE в случае ошибки char *inet_ntoa(struct in_addr inaddr). Возвращает указатель на строку с адресом в точечно-десятичной записи
106 Глава 3 Введение в сокеты Первая из названных функций, i net aton, преобразует строку, на которую ука- зывает strptr, в 32-разрядное двоичное число, записанное в сетевом порядке бай- тов, передаваемое через указатель addrptr. При успешном выполнении возвраща- емое значение равно 1, иначе возвращается нуль. ПРИМЕЧАНИЕ --------------------------------------------------------- Функция inet_aton обладает одним недокументированным свойством: если addrptr — пустой указатель (null pointer), функция все равно выполняет проверку допустимости адреса, содержащегося во входной строке, но не сохраняет результата Функция inet addr выполняет то же преобразование, возвращая в качестве значения 32-разрядное двоичное число в сетевом порядке байтов. Проблема при использовании этой функции состоит в том, что все 232 возможных двоичных зна- чения являются действительными IP-адресами (от 0.0.0.0 до 255.255.255.255), но в случае возникновения ошибки функция возвращает константу INADDR_NONE (обыч- но представленную двоичным числом, состоящим из 32 битов, установленных в единицу). Это означает, что точечно-десятичная запись 255.255.255 255 (огра- ниченный адрес для широковещательной передачи IPv4, см. раздел 18.2) не мо- жет быть обработана этой функцией, поскольку ее двоичное значение выглядит как указание на сбой при выполнении функции. ПРИМЕЧАНИЕ --------------------------------------------------------- Многие ранние версии утилиты Ping выводят ошибку Host unknown, если мы пытаем- ся выполнить команду ping 255 255 255 255 Причина возникновения этой некоррект- ной ошибки в том, что функция inetaddr выполняется неверно, после чего программа пытается интерпретировать IP-адрес в точечно-десятичной записи как имя узла, что тоже не срабатывает. Другой возможной проблемой, сопровождающей выполнение функции inet addr, мо- жет стать то, что, как утверждается в некоторых руководствах, в случае ошибки она возвращает значение -1 вместо INADDR_NONE Это может вызвать проблемы при сравнении возвращаемого значения функции (значение без знака) с отрицатель- ной константой, зависящие от компилятора С На сегодняшний день функция т net_addr является нерекомендуемой, или ус- таревшей, и в создаваемом коде вместо нее должна использоваться функция 1 net_aton. Еще лучше использовать более новые функции, описанные в следую- щем разделе, работающие и с IPv4, и с IPv6. Функция inet_ntoa преобразует 32-разрядный двоичный адрес IPv4, храня- щийся в сетевом порядке байтов, в точечно-десятичную строку. Строка, на кото- рую указывает возвращаемый функцией указатель, находится в статической па- мяти. Это означает, что функция не допускает повторного вхождения, то есть не является повторно входимой (reentrant), что мы обсудим в разделе 11 14. Нако- нец, отметим, что эта функция принимает в качестве аргумента структуру, а не указатель на структуру. ПРИМЕЧАНИЕ --------------------------------------------------------- Функции, принимающие действующие структуры в качестве аргументов, встречаются редко. Более общим способом является передача указателя на структуру
3.7 Функции inet pton и inet ntop 107 3.7. Функции inet_pton и inet_ntop Эти функции появились с IPv6 и работают как с адресами IPv4, так и с адресами IPv6. Их мы будем использовать в книге. Символы р и п обозначают соответствен- но формат представления и численный формат. Формат представления адреса часто является строкой ASCII, а численный формат — это двоичное значение, входящее в структуру адреса сокета. include <arpa/inet h> int inet_pton(int family const char *strptr void ★addrptr') Возвращает 1 в случае успешного выполнения функции 0 если входная строка имела неверный формат представления -1 в случае ошибки const char *inet_ntop(int family const void *addrptr char *strptr sizet 7en): Возвращает указатель на результат если выполнение функции прошло успешно NULL в случае ошибки Значением аргумента farm 1 у для обеих функций может быть либо AF_I NET, либо AF_INET6. Если family не поддерживается, обе функции возвращают ошибку со значением переменной еггпо, равным EAFNOSUPPORT. Первая функция пытается преобразовать строку, на которую указывает strpt г, сохраняя двоичный результат с помощью указателя addrptr. При успешном выпол- нении ее возвращаемое значение равно 1 Если входная строка находится в неверном формате представления для заданного семейства (farm Ту), возвращается нуль. Функция 1 net_ntop выполняет обратное преобразование — из численного фор- мата (addrptr) в формат представления (strptr) Аргумент 1еп -- это размер при- Формат представления численный формат in_addr(} 32-битовый двоичный адрес IPv4 IPv4-aflpec в точечно- десятичной записи in6_addr(} 128-битовый двоичный IPv4-aflpec, преобразованный к виду IPv64 или совместимый с ним in_addr(} 128-битовый двоичный адрес IPv6 x:x:x:x:x:x:a.b.c.d х:х:х:х:х:х:х:х Рис. 3.5. Функции преобразования адресов
108 Глава 3. Введение в сокеты нимающей строки, который передается, чтобы функция не переполнила буфер вызывающего процесса. Чтобы облегчить задание этого размера, в заголовочный файл <netinet/in h> включаются следующие определения: #define INET_ADDRSTRLEN 16 /* для точечно-десятичной записи IPv4-aflpeca */ #define INET6_AD0RSTRLEN 46 /* для шестнадцатеричной записи IPv6-aflpeca*/ Если аргумент 1 еп слишком мал для хранения результирующего формата пред- ставления вместе с символом конца строки (terminating null), возвращается пус- той указатель и переменной еггпо присваивается значение ENOSPC. Аргумент strptr функции inet_ntop не может быть пустым указателем. Вызы- вающий процесс должен выделить память для хранения преобразованного зна- чения и задать ее размер. При успешном выполнении функции возвращаемым значением является этот указатель. На рис. 3.5 приведена схема действия пяти функций, описанных в этом и пре- дыдущем разделах. Пример Даже если ваша система еще не включает поддержку IPv6, вы можете использо- вать новые функции, заменив вызовы вида foo sin_addr s_addr = inet_addr(cp). ,, на inet_pton(AF_INET, ср. &foo sin_addr). » а также заменив вызовы вида ptr = inet_ntoa(foo sin_addr). на char strCINET ADDRSTRLEN]: ptr = inet_ntop(AF_INET. &foo sin_addr. str. sizeof(str)), В листинге 3.5 представлено простое определение функции i net_pton, поддер- живающее только IPv4, а в листинге 3.6 — версия inet_ntop, поддерживающая только IPv4. Листинг 3.5. Простая версия функции inet pton, поддерживающая только IPv4 //1зbfгее/зnet—pton_ipv4 с 10 int И inet_pton(int family, const char *strptr void *addrptr) 12 { 13 if (family == AF_INET) { 14 struct in_addr in_val. 15 if (inet_aton(strptr. &in_val)) { 16 memcpy(addrptr. &in_val sizeoffstruct in_addr)): 17 return (1). IB } 19 return (0). 20 } 21 errno = EAFNOSUPPORT. 22 return (-1), 23 }
3.8. Функция sock ntop и связанные с ней функции 109 Листинг 3.6 Простая версия функции inet_ntop, поддерживающая только IPv4 //libfree/inet_ntop_ipv4 с 8 const char * 9 inet_ntop(int family, const void *addrptr, char *strptr, size_t len) 10 { 11 const u_char *p = (const u_char *) addrptr, 12 if (family == AFJNET) { 13 char temp[INET_ADDRSTRLEN], 14 snprintf(temp. sizeof(temp). "fcd fcd Id Id". 15 p[0L p[l]. p[2], p[3]). 16 if (strlen(temp) >= len) { 17 errno = ENOSPC. 18 return (NULL). 19 } 20 strcpy(strptr. temp), 21 return (strptr). 22 } 23 errno = EAFNOSUPPORT. 24 return (NULL). 25 } 3.8. Функция sock_ntop и связанные с ней функции Основная проблема, связанная с функцией i net_ntop, состоит в том, что вызыва- ющий процесс должен передать ей указатель на двоичный адрес. Этот адрес обычно содержится в структуре адреса сокета, поэтому вызывающему процессу необходимо знать формат структуры и семейство адресов. Следовательно, чтобы использовать эту функцию, нужно написать код следующего вида для IPv4: struct sockaddr_in addr. inet_ntop(AF_INET &addr sin_addr. str. sizeof(str)) или такого вида для IPv6: struct sockaddr_in6 addr6. inet_ntop(AF_INET6. &addr6 sin6_addr. str. sizeof(str)). Как видите, код становится зависящим от протокола. Чтобы решить эту проблему, напишем собственную функцию и назовем ее sock_ntop. Она получает указатель на структуру адреса сокета, исследует эту струк- туру и вызывает соответствующую функцию для того, чтобы возвратить формат представления адреса. #include "unp h" char *sock_ntop(const struct sockaddr *sockaddr. socklen_t addrlerT): Возвращает непустой указатель, если функция выполнена успешно. NULL в случае ошибки sockaddr указывает на структуру адреса сокета, длина которой равна значению add г 1 еп. Функция sock_ntop использует свой собственный статический буфер для хранения результата и возвращает указатель на этот буфер.
110 Глава 3. Введение в сокеты ПРИМЕЧАНИЕ --------------------------------------------------------------------------- Обратите внимание, что при статическом хранении результата функция не допускает повторного вхождения (не является повторно входимой) и не может быть использова- на несколькими программными потоками (не является безопасной в многопоточной среде — thread-safe). Более подробно мы поговорим об этом в разделе 11.14. Мы допус- тили такое решение для этой функции, чтобы ее было легче вызывать из простых про- грамм, приведенных в книге. Формат представления — либо точечно-десятичная форма записи адреса IPv4, либо шестнадцатеричная формазаписи адреса IPv6, за которой следует заверша- ющий символ (мы используем точку, как в программе netstat), затем десятичный номер порта, а затем завершающий нуль. Следовательно, размер буфера должен быть равен как минимум INET_ADDRSTRLEN плюс 6 байт для IPv4 (16 + 6 = 22) либо INET6_ADDRSTRLEN плюс 6 байт для IPv6 (46 + 6 = 52). В листинге 3.7 представлен исходный код только для случая AF_INET. Листинг 3.7. Наша функция sock_ntop //1ib/sock_ntop с 5 char * 6 sock_ntop(const struct sockaddr *sa, socklen_t salen) 1 { 8 char portstr[7]; 9 static char str[128] /* максимальный домен Unix */ 10 switch (sa->sa_family) { 11 case AFJNET { 12 struct sockaddr_in *sin = (struct sockaddr_in *) sa: 13 if (inet_ntop(AF_INET. &sin->sin_addr. str. sizeof(str)) == NULL) 14 return (NULL). 15 if (ntohs(sin->sin_port) '= 0) { 16 snpnntf(portstr sizeof(portstr). " fcd". ntohs(sin->sin_port)): 17 strcat(str. portstr). 18 } 19 return (str). 20 } Для работы co структурами адресов сокетов мы определяем еще несколько функций, которые упростят переносимость нашего кода между IPv4 и IPv6. #include "unp h" int sock_bind_wild(int sockfd. int family). Возвращает 0 в случае успешного выполнения функции. -1 в случае ошибки int sock_cmp_addr(const struct sockaddr *sockaddrl const struct sockaddr *sockaddr2, socklen_t addrlen). Возвращает 0. если адреса относятся к одному семейству и совпадают, ненулевое значение в противном случае int sock_cmp_port(const struct sockaddr *sockaddrl. const struct sockaddr *sockaddr2. socklen_t addrlen):
3.9. Функции readn, writen и readline 111 Возвращает 0 если адреса относятся к одному семейству и порты совпадают, ненулевое значение в противном случае int sock_get_port(const struct sockaddr *sockaddr, socklen_t addrlen). Возвращает неотрицательный номер порта для адресов IPv4 или IPv6, иначе -1 char *sock_ntop_host(const struct sockaddr *sockaddr. socklen_t addrlen). Возвращает непустой указатель в случае успешного выполнения функции NULL в случае ошибки void sock_set_addr(const struct sockaddr *sockaddr. socklen_t addrlen. void *ptr); void sock_set_port(const struct sockaddr *sockaddr. socklen_t addrlen. int port). void sock_set_wild(struct sockaddr *sockaddr, socklen_t addrlen). Функция sock_bi nd_wi 1 d связывает универсальный адрес и динамически на- значаемый порт с сокетом. Функция sock_cmp_addr сравнивает адресные части двух структур адреса сокета, а функция sock_cmp_port сравнивает номера портов из них. Функция sock_get_port возвращает только номер порта, а функция sock_ntop_host преобразует к формату представления только ту часть структуры адреса сокета, которая относится к узлу (все, кроме порта, то есть IP-адрес узла). Функция sock_set_addr присваивает адресной части структуры значение, указанное аргу- ментом ptr, а функция sock_set_port задает в структуре адреса сокета только но- мер порта. Функция sock_set_wi 1 d задает адресную часть структуры через символы подстановки. Как обычно, мы предоставляем для всех этих функций функции- обертки, которые возвращают значение, отличное от типа voi d, и в наших про- граммах обычно вызываем именно обертки. Мы не приводим в данной книге исход- ный код для этих функций, так как он свободно доступен (см. предисловие). 3.9. Функции readn, writen и readline Потоковые сокеты (например, сокеты TCP) демонстрируют с функциями read и wri te поведение, отличное от обычного ввода-вывода файлов. Функция read или write на потоковом сокете может ввести или вывести немного меньше байтов, чем запрашивалось, но это не будет ошибкой. Причиной может быть достижение границ буфера для сокета в ядре. Все, что требуется в этой ситуации, — чтобы процесс повторил вызов функции read или write для ввода или вывода оставших- ся байтов. (Некоторые версии Unix ведут себя аналогично при записи в канал (pipe) более 4096 байт.) Этот сценарий всегда возможен на потоковом сокете при выполнении функции read, но с функцией write он обычно наблюдается только если сокет неблокируемый. Тем не менее вместо write мы всегда вызываем функ- цию writer) на тот случай, если в данной реализации возможно возвращение мень- шего количества данных, чем мы запрашиваем. Введем три функции для чтения и записи в потоковый сокет. include "unp h" ssize_t readn(int filedes. void *buff, size_t nbytes).
112 Глава 3. Введение в сокеты ssize_t writen(int filedes const void *buff size_t nbytes) ssize_t readline(int filedes, void ★buff. size_t mxlerf) Все функции возвращают количество считанных или записанных байтов -1 в случае ошибки В листинге 3.8 представлена функция readn, в листинге 3.9 — функция writer), а в листинге 3.10 — функция resell i ne. Листинг 3.8. Функция readn: считывание п байтов из дескриптора //1ib/readn с 1 #include "unp h' 2 ssize_t /* Считывает n байтов из дескриптора */ 3 readnOnt fd. void *vptr. size_t n) 4 { 5 size_t nleft. 6 ssize_t nread 7 char *ptr, 8 ptr = vptr. 9 nleft = n. 10 while (nleft > 0) { 11 if ( (nread = read(fd ptr nleft)) < 0) { 12 if (errno == Elf.TR) 13 nread = 0. /* и вызывает снова функцию readO */ 14 else 15 return (-1). 16 } else if (nread == 0) 17 break. /* EOF */ 18 nleft -= nread, 19 ptr += nread. 20 } 21 return (n - nleft) /* возвращает значение >= 0 */ 22 } Листинг 3.9. Функция writen: запись п байтов в дескриптор //lib/writen с 1 #mclude "unp h" 2 ssize_t /* Записывает n байтов в дескриптор */ 3 writen(int fd const void *vptr size_t n) 4 { 5 size_t nleft. 6 ssize^t nwritten. 7 const char *ptr. 8 ptr = vptr. 9 nleft = n. 10 while (nleft > 0) { 11 if ( (nwritten = write(fd. ptr. nleft)) <= 0) { 12 if (errno — EINTR) 13 nwritten = 0. /* и снова вызывает функцию write!) */ 14 else 15 return (-1), /* ошибка */ 16 }
3.9. Функции readn, wnten и readhne 113 17 nleft -= nwntten. 18 ptr += nwntten. 19 } 20 return (n) 21 } Листинг 3.10. Функция readline: считывание следующей строки из дескриптора/ по 1 байту за один раз //test/readlinel с 1 include "unp h’ 2 ssize_t 3 readlineCmt fd. void *vptr. size_t maxlen) 4 { 5 ssize_t n rc 6 char c *ptr 7 ptr = vptr 8 for (n = 1, n < maxlen n++) { 9 again 10 if ( (rc = readffd &c. 1)) == 1) { 11 *ptr++ = c. 12 if (c == '\en') 13 break /* записан символ новой строки как в fgetsC) */ 14 } else if (rc == 0) { 15 if (n == 1) 16 return (0). /* EOF данные не считаны */ 17 else 18 break /* EOF некоторые данные были считаны */ 19 } else { 20 if (еггпо == EINTR) 21 goto again 22 return (-1) /* ошибка еггпо задается функцией readO */ 23 } 24 } 25 *ptr = 0 /* завершаем нулем как в fgetsO */ 26 return (n) 27 } Если функция чтения или записи ( read или wri te) возвращает ошибку, то наши функции проверяют, не совпадает ли код ошибки с EINTR (прерывание систем- ного вызова сигналом, см. раздел 5.9). В этом случае прерванная функция вызы- вается повторно. Мы обрабатываем ошибку, не позволяя вызывающему процессу снова вызвать read или write, поскольку целью наших функций является предот- вратить обработку нехватки данных вызывающим процессом. В листинге 13.3 мы покажем, что вызов функции recv с флагом MSG_WAITALL позволяет обойтись без использования отдельной функции readn. Заметим, что наша функция read] те вызывает системную функцию read один раз для каждого байта данных. Это неэффективно, что показано в разделе 3.9 [93]. Нам хотелось бы буферизовать данные с помощью вызова функции read, чтобы получить столько данных, сколько мы можем, а затем проверять буфер по 1 бай- ту за один раз. Одно из решений заключается в использовании стандартной биб- лиотеки ввода-вывода, о чем пойдет речь в разделе 13.8.
114 Глава 3. Введение в сокеты В листинге 3.11 представлена более быстрая версия функции headline, кото- рая считывает до MAXLINE байтов за один вызов, а затем возвращает по одному символу за один вызов. Листинг 3.11. Улучшенная версия функции readhne // 1ib/readline с 1 include "unp h" 2 static ssize_t 3 my_read(int fd. char *ptr) 4 { 5 static int read_cnt = 0, 6 static char *read_ptr. 7 static char read_buf[MAXLINE]: 8 if (read_cnt <= 0) { 9 again 10 if ( (read_cnt = read(fd, read_buf, s1zeof(read_buf))) < 0) { 11 if (errno — EINTR) 12 goto again, 13 return (-1). 14 } else if (read_cnt “ 0) 15 return (0). 16 readjatr = read_buf. 17 } 18 read_cnt-- 19 *ptr = *read_ptr++, 20 return (1). 21 } 22 ssize_t 23 readline(int fd. void *vptr. size t maxlen) 24 { 25 ssize_t n, rc. 26 char c. *ptr. 27 ptr = vptr, 28 for (n = 1. n < maxlen, n++) { 29 if ( (rc = my_read(fd, &с)) == 1) { 30 *ptr++ = с. 31 if (с == ’\еп’) 32 break. /* записан символ новой строки, как в fgetsO */ 33 } else if (rc == 0) { 34 if (n == 1) 35 return (0); /* EOF. данные не считаны */ 36 else 37 break. /* EOF было считано некоторое количество данных */ 38 } else 39 return (-1). /* ошибка, еггпо задается readO */ 40 } 41 *ptr = 0 /* завершающий нуль, как в fgetsO */ 42 return (n). 43 } 2-21 Внутренняя функция my_read считывает до MAXLINE символов за'один вйзов и затем возвращает их по одному.
3.10. Функция isfdtype 115 29 Единственное изменение самой функции read] те заключается в том, что те- перь она вызывает функцию my_read вместо read. Такая незначительная модификация функции read] we приводит к большим изменениям в результатах ее работы. Если мы измерим время, требуемое старой и новой версиям для чтения файла, состоящего из 2781 строки (135 816 байт), то окажется, что для старой версии будет потрачено 8,8 секунд процессорного вре- мени, а для новой — 0,3 секунды. Это различие в основном объясняется разницей во времени, проведенном внутри ядра, то есть разницей в системном времени, поскольку старая версия выполняет 135 816 системных вызовов, в то время как новая версия только 34 (135 816, деленное назначение MAXLINE, равное 4096). ПРИМЕЧАНИЕ----------------------------------------------------------- К сожалению, использование переменных типа static в функции my_read для поддержки информации о состоянии при последовательных вызовах приводит к тому, что функ- ция больше не является безопасной в многопоточной системе (thread-safe) и повторно входимой (reentrant). Мы обсуждаем это в разделах 11.14 и 23.5. Мы предлагаем вер- сию, безопасную в многопоточной системе, основанную на собственных данных про- граммных потоков, в листинге 23.5. 3.10. Функция isfdtype Бывают ситуации, когда нужно протестировать дескриптор, чтобы определить, относится ли он к некоторому заданному типу. Исторически это осуществлялось с помощью вызова функции Posix. 1 fstat и последующего тестирования возвра- щенного значения st_mode одним из макросов S_ISxxx [93, с. 73-76 ]. Многие, но не все реализации определяют макрос S_ISSOCK, проверяющий, является ли дескрип- тор сокетом. Поскольку существует несколько реализаций, не способных сообщить, является ли дескриптор сокетом, на основании только той информации, которую возвращает функция fstat, Posix.lg предоставляет новую функцию isfdtype. #include <sys/stat h> int isfdtype(int fd. int fdtype) Возвращает 1. если дескриптор имеет указанный тип 0 . если это не так -1 в случае ошибки Для проверки, является ли дескриптор сокетом, необходимо, чтобы аргумент fdtype имел тип S_IFSOCK. Одно из применений этой функции — программа, вы- полняемая другой программой с помощью функции ехес (см. раздел 4.7) для про- верки, действительно ли ожидаемый дескриптор является сокетом. В листинге 3.12 представлена примерная реализация этой функции. Предпо- лагается, что реализация поддерживает упомянутый макрос, позволяющий про- верить тип дескриптора с помощью функции fstat. Листинг 3.12. Реализация isfdtype с использованием функции fstat //lib/isfdtype с 1 #include "unp h" 2 #ifndef SJFSOCK
116 Глава 3. Введение в сокеты Листинг 3.12(продолжение) 3 #error S_IFSOCK not defined 4 #endif 5 int 6 isfdtype(int fd int fdtype) 7 { 8 struct stat buf 9 if (fstat(fd &buf) < 0) 10 return (-1) 11 if ( (buf stjnode & S—IFMT) — fdtype) 12 return (1). 13 else 14 return (0), 15 } Существуют другие константы S_IFxxx, определяемые в заголовочном файле <sys/stat h>, и наша реализация позволяет их использовать. Однако в Posix.lg указано, что эта функция работает только когда fdtype имеет тип S IFSOCK. 3.11. Резюме Структуры адресов сокетов являются неотъемлемой частью каждой сетевой про- граммы. Мы выделяем для них место в памяти, заполняем их и передаем указате- ли на них различным функциям сокетов. Иногда мы передаем указатель на одну из этих структур функции сокета, и она сама заполняет поля структуры. Мы все- гда передаем эти структуры по ссылке (то есть передаем указатель на структуру, а не саму структуру) и всегда передаем размер структуры в качестве дополни- тельного аргумента. Когда функция сокета заполняет структуру, длина также передается по ссылке, и ее значение может быть изменено функцией, поэтому мы называем такой аргумент «значение-результат» (value-result). Структуры адресов сокетов являются самоопределяющимися, поскольку они всегда начинаются с поля family, которое идентифицирует семейство адресов, содержащихся в структуре. Более новые реализации, поддерживающие структу- ры адресов сокетов переменной длины, также содержат поле, которое определяет длину всей структуры. Две функции, преобразующие IP-адрес из формата представления (который мы записываем в виде последовательности символов ASCII) в численный фор- мат (который входит в структуру адреса соке га) и обратно — это i net_pton и i net_ ntop. Эти функции являются зависящими от протокола. Более совершенной ме- тодикой является работа со структурами адресов сокетов как с непрозрачными (opaque) объектами, когда известны лишь указатель на структуру и ее размер. Мы разработали набор функций sock_, которые помогут сделать наши програм- мы не зависящими от протокола. Создание наших не зависящих от протокола средств мы завершим в главе 11 функциями getaddrinfo и getnameinfo. Сокеты TCP предоставляют приложению поток байтов, лишенный маркеров записей. Возвращаемое значение функции read может быть меньше запрашивае- мого, но это не обязательно является ошибкой. Чтобы упростить считывание и за- пись потока байтов, мы разработали три функции — readn, writer и readline, — которые мы используем в книге.
Упражнения 117 Упражнения 1. Почему аргументы типа «значение-результат», такие как длина структуры адреса сокета, должны передаваться по ссылке? 2. Почему и функция readn, и функция writen копируют указатель void* в указа- тель char*? 3. Функции inet_aton и inet_addr характеризуются традиционно нестрогим от- ношением к тому, что они принимают в качестве точечно-десятичной записи адреса IPv4: допускаются от одного до четырех десятичных чисел, разделен- ных точками; также допускается задавать шестнадцатеричное число с помощью начального Ох или восьмеричное число с помощью начального 0 (выполните команду tel net Охе, чтобы увидеть поведение этих функций). Функция i net_pton намного более строга в отношении адреса IPv4 и требует наличия именно че- тырех чисел, разделенных точками, каждое из которых является десятичным числом от 0 до 255. Функция i net_pton не разрешает задавать точечно-деся- тичный формат записи адреса, если семейство адресов — AF_INET6, хотя суще- ствует мнение, что это можно было бы разрешить, и тогда возвращаемое зна- чение было бы адресом IPv4, преобразованным к виду IPv6 (рис. А.8). На- пишите новую функцию 1 net_pton_l oose, реализующую такой сценарий: если используется семейство адресов AF INET и функция i net_pton возвращает нуль, вызовите функцию i net_aton и посмотрите, успешно ли она выполнится. Ана- логично, если используется семейство адресов AF_INET6 и функция inet_pton возвращает нуль, вызовите функцию i net_aton, и если она выполнится успеш- но, возвратите адрес IPv4, преобразованный к виду IPv6.
ГЛАВА 4 Элементарные сокеты TCP 4.1. Введение В этой главе описываются элементарные функции сокетов, необходимые для на- писания полностью работоспособного клиента и сервера TCP. Сначала мы опи- шем все элементарные функции сокетов, которые будем использовать, а затем в следующей главе создадим клиент и сервер. С этими приложениями мы будем работать на протяжении всей книги, постоянно их совершенствуя (см. табл. 1 3 и табл. 1 4). Мы также опишем параллельные (concurrent) серверы — типичную техноло- гию Unix, когда множество клиентов одновременно соединяются с одним и тем же сервером. Подключение очередного клиента заставляет сервер выполнить функцию fork, порождающую новый серверный процесс именно для этого кли- ента. В этой главе применительно к использованию функции fork мы будем рас- сматривать модель «каждому клиенту — один процесс», а в главе 23 при обсужде- нии программных потоков расскажем о модели «каждому клиенту — один поток». На рис. 4.1 представлен типичный сценарий взаимодействия, происходящего между клиентом и сервером. Сначала запускается сервер, затем спустя некото- рое время запускается клиент, который соединяется с сервером. Предполагается, что клиент посылает серверу запрос, сервер этот запрос обрабатывает и посылает клиенту ответ. Так продолжается, пока клиентская сторона не закроет соедине- ние, посылая при этом серверу признак конца файла. Затем сервер закрывает свой конец соединения и либо завершает работу, либо ждет подключения нового кли- ента. 4.2. Функция socket Чтобы обеспечить сетевой ввод-вывод, процесс должен начать работу с вызова функции socket, задав тип желаемого протокола (TCP с использованием IPv4, UDP с использованием IPv6, доменный сокет Unix и т. д.). include <sys/socket h> int socket(int family int type int protocol) Возвращает неотрицательный дескриптор, если функция выполнена успешно -1 в случае ошибки Константа family задает семейство протоколов. Ее возможные значения при- ведены в табл. 4.1. Значения константы type (тип) перечислены среди других кон-
4.2. Функция socket 119 TCP-сервер Рис. 4.1. Функции сокетов для элементарного клиент-серверного соединения TCP стант в табл. 4.2. Обычно аргумент protocol функции socket равен нулю, за ис- ключением символьных сокетов. Их мы рассмотрим в главе 25. Не все сочетания констант farm ly и type допустимы. В табл. 4 3 показаны до- пустимые сочетания, а также протокол, соответствующий каждой паре. Ячейки таблицы, содержащие «Да», соответствуют допустимым комбинациям, для кото- рых нет удобных сокращений Пустая ячейка означает, что данное сочетание не поддерживается. Таблица 4.1. Константы протокола (family) для функции socket Семейство сокетов (family) Описание AFINET AFINET6 AFLOCAL AFROUTE AF KEY Протоколы IPv4 Протоколы IPv6 Протоколы доменных сокетов Unix (см главу 14) Маршрутизирующие сокеты (см главу 17) Ключевой сокет
120 Глава 4 Элементарные сокеты TCP Таблица 4.2. Тип сокета для функции socket Тип (type) Описание SOCKSTREAM Потоковый сокет SOCKDGRAM Сокет дейта! рамм SOCKRAW Символьный (неструктурированный) сокег Таблица 4.3. Сочетания констант family и type для функции socket AFJNET AFJNET6 AF_LOCAL AF_ROUTE AF.KEY SOCK_STREAM TCP TCP Да SOCKDGRAM UDP UDP Да SOCK RAW IPv4 IPv6 Да Да ПРИМЕЧАНИЕ ---------------------------------------------------------- В качестве первого аргумента функции socket вы также можете встретить константу PFxxx Подробнее об этом мы расскажем в конце данного раздела Кроме того, вам можег встретиться название AF_UNIX (исторически сложившееся в Unix) вместо AF_LOCAL (название из Posix 1g), и более подробно мы поговорим об этом в главе 14 Для ар1ументов family и type существуют и друше значения Например, 4 4BSD под- Л держивает и AF_NS (протоколы Xerox NS, часто называемые XNS), и AF ISO (прото- колы OSI) Но сегодня очень немногие используют какой-либо из этих протоколов Аналогично значение type для SOCK_SEQPАСКЕТ, сокета последовательных паке- тов, реализуется и протоколами Xerox NS, и протоколами OSI Но протокол TCP яв- ляется потоковым и поддерживает только сокеты SOCK_STREAM Linux поддерживает новый тип сокетов — SOCK PACKET, предоставляющий доступ к канальному уровню, — аналогично BPF и DLPI на рис 2 1 Об этом более подробно рассказывается в главе 26 Ключевой сокет AF_KEY является новшеством IPv6 требует поддержки криптогра- ' фической защиты, и возможно, многие системы будут поддерживать этот сокет и для IPv4 Аналогично тому как маршрутизирующий сокет (AF ROUTE) является интер- фейсом к таблице маршрутизации ядра, ключевой сокет — это инте|х|>ейс к таблице ключей ядра Предварительная документация на это семейство содержится в [65] и [66] При успешном выполнении функция socket возвращает неотрицательное це- лое число, аналогичное дескриптору файла Мы называем это число дескрипто- ром сокета {socket descriptor), или sockfd Чтобы получить дескриптор сокета, до- статочно указать лишь семейство протоколов (IPv4, IPv6 или Unix) и тип сокета (потоковый, символьный или дейтаграммный) Мы еще не задали ни локальный адрес протокола, ни удаленный адрес протокола Сравнение AF_xxx и PF_xxx Префикс AF_ обозначает семейство адресов (address family), a PF_ — семейство протоколов (protocol family) Исторически ставилась такая цель, чтобы отдельно взятое семейство протоколов могло поддерживать множество семейств адресов, и значение PF_ использовалось для создания сокета, а значение AF_ — в структу-
4 3 Функция connect 121 pax адресов сокетов Но в действительности семейства протоколов, поддержива- ющего множество семейств адресов, никогда не существовало, и поэтому в заго- ловочном файле <sys/socket h> значение PF_ для протокола задается равным зна- чению AF_ Не гарантируется, что это равенство будет всегда справедливо, но при попытке изменить ситуацию для существующих протоколов большая часть на- писанного кода потеряет работоспособность В целях согласования с существую- щей практикой программирования мы используем в тексте только константы AF_, хотя вы можете встретить и значение PF_, в основном в вызовах функции socket ПРИМЕЧАНИЕ -------------------------------------------------------------- Просмотр 137 программ с вызовами функции socket в реализации BSD/OS 2 1 пока- зывает, что в 143 случаях вызова задается значение AF_ и только в 8 случаях — зна- чение PF_ Причина создания аналогичных наборов констант с префиксами AF_ и PF_ восходит к 4 IcBSD [59] и к версии функции socket, предшествующей описываемой нами вер- сии (которая появилась с 4 2BSD) Версия функции socket в 4 IcBSD получала четыре аргумента, одним из которых был указатель на структуру sockproto Первый элемент этой структуры назывался spfamily, и его значение было одним из значений PF_ Второй элемент, spprotocol, был номером протокола аналогично третьему аргументу нынешней функции socket Единственный способ задать семейство протоколов за- ключался в том, чтобы задать эту структуру Следовательно, в этой системе значения PF_ использовались как элементы для задания семейства протоколов в структуре sockproto Значения AF_ играли роль элементов для задания семейства адресов в струк- турах адресов сокетов Структура sockproto еще присугствует в 4 4BSD [105, с 626- 627], по служит только для вну греннего использования ядром Начальное определение содержало для элемента sp family комментарий «семейство протоколов», но в исходном коде 4 4BSD он был изменен на «семейство адресов» Еще большую путаницу в эту ситуацию вносит то, что в Беркли-реализации структура данных ядра, содержащая значение, которое сравнивается с первым аргументом функ- ции socket (элемент dom_family структуры domain [105, с 187]), сопровождается ком- ментарием, где сказано, что в этой структуре содержится значение AF_ Но некоторые структуры domain внутри ядра инициализированы с помощью константы AF_ [105, с 192], в то время как другие — с помощью PF_ [105, с 646], [95, с 229] Еще одно историческое замечание Страница руководства по 4 2BSD от июля 1983 года, посвященная функции socket, называет ее первый аргумент af и перечисляет его воз- можные значения как константы AF_ Наконец, отметим, что Posix 1g задает первый аргумент функции socket как значение PF_, а значение AF_ использует для структуры адреса сокета Но далее в структуре addnnfo определяется только одно значение семейства (см раздел 11 2), предназначен- ное для использования либо в вызове функции socket, либо в структуре адреса сокета 4.3. Функция connect Функция connect используется клиентом TCP для установления соединения с сервером TCP #include <sys/socket h> int connecttint sockfd const struct sockaddr *servaddr socklen_t addrlen) Возвращает 0 в случае успешного выполнения функции -1 в случае ошибки
122 Глава 4. Элементарные сокеты TCP Аргумент sockfd — это дескриптор сокета, возвращенный функцией socket. Второй и третий аргументы — это указатель на структуру адреса сокета и ее раз- мер, (см. раздел 3.3). Структура адреса сокета должна содержать IP-адрес и номер порта сервера. Пример применения этой функции был представлен в листинге 1.1. Клиенту нет необходимости вызывать функцию bind (которую мы описываем в следующем разделе) до вызова функции connect: при необходимости ядро вы- берет и динамически назначаемый порт, и IP-адрес отправителя. В случае сокета TCP функция connect инициирует трехэтапное рукопожатие TCP (см. раздел 2.5). Функция возвращает значение только если установлено соединение или произошла ошибка. Возможно несколько ошибок: 1. Если клиент TCP не получает ответа на свой сегмент SYN, возвращается со- общение ETIMEDOUT. 4.4BSD, например, отправляет один сегмент SYN, когда вызывается функция connect, второй — 6 секунд спустя, и еще один — через 24 секунды [105, с. 828]. Если ответ не получен в течение 75 секунд, возвра- щается ошибка. Некоторые системы позволяют администратору устанавливать значение вре- мени ожидания; см. приложение Е [94]. 2. Если на сегмент SYN сервер отвечает сегментом RST, это означает, что ни один процесс на узле сервера не находится в ожидании подключения к указанному нами порту (например, нужный процесс может быть не запущен). Это устой- чивая неисправность {hard error), и клиенту возвращается сообщение ECONNRE - FUSED сразу же по получении им сегмента RST. RST (от «reset» — сброс) — это сегмент TCP, отправляемый собеседнику при возникновении ошибок. Вот три условия, при которых генерируется RST: сег- мент SYN приходит для порта, не имеющего прослушивающего сервера (что мы только что описали); TCP хочет разорвать существующее соединение; TCP получает сегмент для несуществующего соединения (дополнительная инфор- мация содержится на с. 246-250 [94]). 3. Если сегмент SYN клиента приводит к получению сообщения ICMP о недо- ступности получателя от какого-либо промежуточного маршрутизатора, это считается случайным сбоем (soft error). Клиентское ядро сохраняет сообщение об ошибке, но продолжает отправлять сегменты SYN с теми же временными ин- тервалами, что и в первом сценарии. Но если по истечении определенного фик- сированного времени (75 секунд для 4.4BSD) ответ не получен, сохраненная ошибка ICMP возвращается процессу либо как EHOSTUNREACH, либо как ENETUNREACH. ПРИМЕЧАНИЕ ----------------------------------------------------------- Многие более ранние системы, такие как 4.2BSD, некорректно прерывали попыт- ки установления соединения при получении сообщения ICMP о недоступности полу- чателя. Это было неверно, поскольку данная ошибка ICMP может указывать на вре- менную неисправность. Например, может быть так, что эта ошибка вызвана проблемой маршрутизации, которая исправляется в течение 15 секунд. Обратите внимание, что ENETUNREACH в табл. А.З не включается даже когда ошиб- ка указывает, что сеть получателя недоступна. Недоступность сети считается устарев- шей ошибкой, и даже если 4.4BSD получает такое сообщение, приложению возвраща- ется EHOSTUNREACH.
4.4. Функция bind 123 Эти ошибки мы можем наблюдать на примере нашего простого клиента, со- зданного в листинге 1.1. Сначала мы указываем адрес нашего собственного узла (127.0.0.1), на котором работает сервер времени и даты, и видим обычный вывод: solans % daytlmetcpcll 127.0.0.1 Tue Jan 16 16.45 07 1996 Чтобы увидеть возвращаемый ответ в другом формате, мы обращаемся к ло- кальному маршрутизатору Cisco (см. рис. 1.7): solans % daytlmetcpcll 206.62.226.62 Tuesday, May 7. 1996 11-01.33-MST Затем мы задаем IP-адрес в локальной подсети (206.62.226) с несуществую- щим адресом узла (55). Когда клиент посылает запросы ARP (запрашивая аппа- ратный адрес узла), он не получает никакого ответа: solans % daytlmetcpcll 206.62.226.55 connect error Connection timed out Мы получаем сообщение об ошибке только по истечении времени выполне- ния функции connect (которое, как мы говорили, для Solaris 2.5 составляет 3 ми- нуты). Обратите внимание, что наша функция err_sys выдает текстовое сообще- ние, соответствующее коду ошибки ETIMEDOUT. В следующем примере мы пытаемся обратиться к шлюзу (gateway), представ- ляющему собой маршрутизатор Cisco, на котором не запущен сервер времени и даты: solans % daytlmetcpcll 140.252.1.4 connect error Connection refused Сервер немедленно отвечает, отправляя сегмент RST. В последнем примере мы пытаемся обратиться к недоступному адресу из сети Интернет. Просмотрев пакеты с помощью программы tcpdump, мы увидим, что маршрутизатор через шесть повторных передач возвращает сообщение ICMP о не- доступности узла: solans % daytlmetcpcll 192.3.4.5 connect error No route to host Как и в случае ошибки ETIMEDOUT, в этом примере функция connect возвращает ошибку EHOSTUNREACH только после ожидания в течение определенного времени. В терминах диаграммы перехода состояний TCP (см. рис. 2.4) функция connect переходит из состояния CLOSED (состояния, в котором сокет начинает работать при создании с помощью функции socket) в состояние SYN_SENT, а затем, при успешном выполнении, в состояние ESTABLISHED. Если выполнение функции connect окажется неудачным, сокет больше не используется и должен быть за- крыт. Мы не можем снова вызвать функцию connect для сокета. В листинге 11.2 вы увидите, что когда функция connect выполняется в цикле, проверяя каждый IP-адрес данного узла, пока он не заработает, то каждый раз, когда выполнение функции оказывается неудачным, мы должны закрыть дескриптор сокета с по- мощью функции cl ose и снова вызвать функцию socket. 4.4. Функция bind Функция bind связывает сокет с локальным адресом протокола. В случае прото- колов Интернета адрес протокола — это комбинация 32-разрядного адреса IPv4 ПО ---------------------_ л г~ _______ ____
124 Глава 4. Элементарные сокеты TCP include <sys/socket h> int bind(int sockfd. const struct sockaddr ★myaddr, socklen_t addrlen). Возвращает 0 в случае успешного выполнения. -1 в случае ошибки ПРИМЕЧАНИЕ ------------------------------------------------------------- В руководстве при описании функции bind говорилось: «функция bind присваивает имя неименованному сокету». Использование термина «имя» спорно, обычно оно вы- зывает ассоциацию с доменными именами (см. главу 9), такими как foo.bar.com. Функ- ция bind не имеет ничего общего с именами. Она задает сокету адрес протокола, а то, что он значит, зависит от самого протокола. Вторым аргументом является указатель на специфичный для протокола ад- рес, а третий аргумент — это размер структуры адреса. В случае TCP вызов функ- ции bi nd позволяет нам задать номер порта, IP-адрес, а также задать оба эти пара- метра или вообще не указывать ничего. Серверы связываются со своим заранее известным портом при запуске. Мы видели это в листинге 1.5. Если клиент или сервер TCP не делает этого, ядро выбирает динамически назначаемый порт для сокета либо при вызове функ- ции connect, либо при вызове функции 11 sten. Клиент TCP обычно позволяет ядру выбирать динамически назначаемый порт, если приложение не требует зарезервированного порта (см. рис. 2.6), но сервер TCP достаточно редко пре- доставляет ядру такую возможность, так как обращение к серверам произво- дится через заранее известные порты. ПРИМЕЧАНИЕ ------------------------------------------------------------- Исключением из этого правила являются серверы удаленного вызова процедур RPC (Remote Procedure Call). Обычно они позволяют ядру выбирать динамически назнача- емый порт для их прослушиваемого сокета, поскольку затем этот порт регистрируется программой отображения портов RPC. Клиенты должны соединиться с этой програм- мой, чтобы получить номер динамически назначаемого порта до того, как они смогут соединиться с сервером с помощью функции connect. Это также относится к серверам RPC, использующим протокол UDP. Процесс с помощью функции bind может связать конкретный IP-адрес с соке- том. IP-адрес должен соответствовать одному из интерфейсов узла. Так опре- деляется IP-адрес, который будет использоваться для отправляемых через сокет IP-дейтаграмм. При этом для сервера TCP на сокет накладывается огра- ничение: он может принимать только такие входящие соединения клиента, которые предназначены именно для этого IP-адреса. Обычно клиент TCP не связывает IP-адрес с сокетом при помощи функции bind. Ядро выбирает IP-адрес отправителя в момент подключения клиента к со- кету, основываясь на используемом исходящем интерфейсе, который, в свою оче- редь, зависит от маршрута, требуемого для обращения к серверу [105, с. 737]. Если сервер TCP не связывает IP-адрес с сокетом, ядро назначает ему IP-ад- рес (указываемый в исходящих пакетах), который совпадает с адресом получате- ля сегмента SYN, полученного от клиента [105, с. 943]. Как мы уже говорили, вызов функции bi nd позволяет нам задать IP-адрес и порт (вместе или по-отдельности) либо не задавать никаких аргументов. В табл. 4.4
4.4. Функция bind 125 приведены все возможные значения, которые присваиваются аргументам si n_addr и si n port либо sin6_addr и sin6_port в зависимости от желаемого результата. Таблица 4.4. Результаты задания IP-адреса и/или номера порта в функции bind Процесс задает: Результат IP-адрес Порт Универсальный 0 Ядро выбирает IP-адрес и порт Универсальный Ненулевое значение Ядро выбирает IP-адрес, процесс задает порт Локальный , 0 Процесс задает IP-адрес, ядро выбирает порт Локальный Ненулевое значение Процесс задает IP-адрес и порт Если мы зададим нулевой номер порта, то при вызове функции bind ядро вы- берет динамически назначаемый порт. Но если мы зададим IP-адрес с помощью символов подстановки, ядро не выберет локальный IP-адрес, пока либо к сокету не присоединится клиент (TCP), либо на сокет не будет отправлена дейтаграмма (UDP). В случае IPv4 универсальный адрес, состоящий из символов подстановки (wild- card), задается константой INADOR_ANY, значение которой обычно нулевое. Это ука- зывает ядру на необходимость выбора IP-адреса. Такое пример вы видели в лис- тинге 1.5: struct sockaddr_in servaddr, servaddr sin_addr s_addr = htonl(INADDR_ANY), /* универсальный */ Этот прием работает с IPv4, где IP-адрес является 32-разрядным значением, которое можно представить как простую численную константу (в данном слу- чае 0), но воспользоваться им при работе с IPv6 мы не можем, поскольку 128-раз- рядный адрес IPv6 хранится в структуре. (В языке С мы не можем поместить структуру в правой части оператора присваивания.) Эта проблема решается сле- дующим образом: struct sockaddr_in6 serv serv sin6_addr = in6addr_any. /* универсальный */ Система выделяет место в памяти и инициализирует переменную in6addr_any, присваивая ей значение константы IN6A00R_ANY_INIT. Объявление внешней кон- станты in6addr_any содержится в заголовочном файле <netinet/in.h>. Значение INAOOR ANY (0) не зависит от порядка байтов, поэтому использование функции htonl в действительности не требуется. Но поскольку все константы INAODR_, определенные в заголовочном файле <netinet/in.h>, задаются в порядке байтов узла, с любой из этих констант следует использовать функцию htonl. Если мы поручаем ядру выбрать для нашего сокета номер динамически на- значаемого порта, то функция bind не возвращает выбранное значение. В самом деле, она не может возвратить это значение, поскольку второй аргумент функции bi nd имеет квалификатор const. Чтобы получить значение динамически назнача- емого порта, заданного ядром, потребуется вызвать функцию getsockname, кото- рая возвращает локальный адрес протокола.
126 Глава 4. Элементарные сокеты TCP Типичным примером процесса, связывающего с сокетом конкретный IP-ад- рес, служит узел, на котором работают web-серверы нескольких организаций (см. раздел 14.2 [95]). Прежде всего у каждой организации есть свое собственное доменное имя, например www.organization.com. Доменному имени каждой орга- низации сопоставляется некоторый IP-адрес; различным организациям сопо- ставляются различные адреса, но обычно из одной и той же подсети. Напри- мер, если адрес подсети 198.69.10, то IP-адресом первой организации может быть 198.69.10.128, следующей — 198.69.10.129 и т. д. Все эти IP-адреса затем становятся псевдонимами, или альтернативными именами (alias), одного се- тевого интерфейса (например, при использовании параметра alias команды ifconfig в 4.4BSD). В результате уровень IP будет принимать входящие дейта- граммы, предназначенные для любого из адресов, являющихся псевдонимами. Наконец, для каждой организации запускается по одной копии сервера HTTP, и каждая копия связывается с помощью функции bi nd только с IP-адресом опреде- ленной организации. ПРИМЕЧАНИЕ ------------------------------------------------------------ В качестве альтернативы можно запустить одиночный сервер, связанный с универсаль- ным адресом. Когда происходит соединение, сервер вызывает функцию getsockname, чтобы получить от клиента IP-адрес получателя, который (см. наше обсуждение выше) может быть равен 198.69.10.128,198.69.10.129 и т. д. Затем сервер обрабатывает запрос клиента на основе именно того IP-адреса, к которому было направлено это соединение. Одним из преимуществ связывания с конкретным IP-адресом является то, что демуль- типлексирование данного IP-адреса с процессом сервера выполняется ядром. Следует внимательно относиться к различию интерфейса, на который приходит пакет, и IP-адреса получателя этого пакета. В разделе 8.8 мы поговорим о моделях систем с гибкой привязкой (weak end system) и с жесткой привязкой (strong end system). Боль- шинство реализаций используют первую модель, то есть считают обычным явлением принятие пакета на интерфейсе, отличном от указанного в IP-адресе получателя. (При этом подразумевается узел с несколькими сетевыми интерфейсами.) При связывании с сокетом конкретного IP-адреса на этом сокете будут приниматься дейтаграммы с за- данным IP-адресом получателя, и только они. Никаких ограничений на принимающий интерфейс не накладывается — эти ограничения возникают только в случае если ис- пользуется модель системы с жесткой привязкой. Общей ошибкой выполнения функции bi nd является EADDRINUSЕ, указывающая на то, что адрес уже используется. Более подробно мы поговорим об этом в разде- ле 7.5, когда будем рассматривать параметры сокетов SO_REUSEAOOR и SO_REUSEPORT. 4.5. Функция listen Функция listen вызывается только сервером TCP и выполняет два действия. 1. Когда сокет создается с помощью функции socket, считается, что это актив- ный сокет, то есть клиентский сокет, который запустит функцию connect. Функ- ция 11 sten преобразует неприсоединенный сокет в пассивный сокет, запросы на подключение к которому начинают приниматься ядром. В терминах диа- граммы перехода между состояниями TCP (см. рис. 2.4) вызов функции 11 sten пепеволит сокет из состояния CLOSED в состояние LISTEN.
4.5. Функция listen 127 2. Второй аргумент этой функции задает максимальное число соединений, кото- рые ядро может помещать в очередь этого сокета. include <sys/socket.h> int listen(int sockfd. int backlog'). Эта функция обычно вызывается после функций socket и bind. Она должна вызываться перед вызовом функции accept. Чтобы уяснить смысл аргумента backlog, необходимо понять, что для данного прослушиваемого сокета ядро поддерживает две очереди: 1. Очередь не полностью установленных соединений (incomplete connection queue), содержащая запись для каждого сегмента SYN, пришедшего от клиента, для которого сервер ждет завершения трехэтапного рукопожатия TCP. Эти соке- ты находятся в состоянии SYN_RCVD (см. рис. 2.4). 2. Очередь полностью установленных соединений (complete connection queue), содержащая запись для каждого клиента, с которым завершилось трехэтап- ное рукопожатие TCP. Эти сокеты находятся в состоянии ESTABLISHED (см. рис. 2.4). На рис. 4.2 представлены обе эти очереди для прослушиваемого сокета. Прибытие сегмента SYN Рис. 4.2. Две очереди, поддерживаемые прослушиваемым сокетом TCP На рис. 4.3 показан обмен пакетами во время установления соединения с ис- пользованием этих очередей. Когда от клиента приходит сегмент SYN, TCP создает новую запись в очереди не полностью установленных соединений, а затем отвечает вторым сегментом трехэтапного рукопожатия, посылая сегмент SYN вместе с сегментом АСК, под- тверждающим прием клиентского сегмента SYN (см. раздел 2.5). Эта запись ос- танется в очереди не полностью установленных соединений, пока не придет тре- тий сегмент трехэтапного рукопожатия (клиентский сегмент АСК для сегмента сервера SYN) или пока не истечет время жизни этой записи. (В реализациях, про- исходящих от Беркли, время ожидания (тайм-аут) для элементов очереди не
128 Глава 4 Элементарные сокеты TCP Клиент Создание записи в очереди не полностью установленных соединений Сервер RTT Запись перемещается из очереди не полностью установленных соединений в очередь полностью установленных, функция accept может завершиться Рис. 4.3. Обмен пакетами в процессе установления соединения с применением очередей полностью установленных соединений равно 75 секундам ) Если трехэтапное ру- копожатие завершается нормально, запись переходит из очереди не полностью установленных соединений в конец очереди полностью установленных соедине- ний Когда процесс вызывает функцию accept (о которой мы поговорим в следую- щем разделе), ему возвращается первая запись из очереди полностью установлен- ных соединений, а если очередь пуста, процесс переходит в состояние ожидания до появления записи в ней Есть несколько важных моментов, которые нужно учитывать при работе с эти- ми очередями Аргумент backlog функции 1 т sten исторически задавал максимальное суммар- ное значение для обеих очередей ПРИМЕЧАНИЕ -------------------------------------------------------- Формального определения аргумента backlog никогда не существовало В руководстве 4 2BSD сказано, что «он определяет максимальную длину, до которой может вырасти очередь не полностью установленных соединений» Многие руководства и даже Posix 1g копируют это определение дословно, но в нем не говорится, в каком состоянии должно находиться соединение — в состоянии SYN RCVD, ESTABLISHED (до вызова accept) или же в любом из них Определение, приведенное выше, относится к реализации Бер кли, пакет 4 2BSD, и копируется многими другими реализациями Беркли-реализации включают поправочный множитель для аргумента back 1 од, равный 1,5 [94, с 257], [105, с 462] Например, при типичном значении аргу- мента backlog = 5 в таких системах допускается до восьми записей в очередях, как показано в табл 4 6 ПРИМЕЧАНИЕ -------------------------------------------------------- Причина возникновения этого множителя теряется в истории [48] Но если мы рас- сматриваем backlog как способ задания максимального числа установленных соедине- ний, которые ядро помещает в очередь прослушиваемого сокета (об этом вскоре будет рассказано) этот множитель нужен для учета не полностью установленных соедине- ний, находящихся в очереди [8]
4 5 Функция listen 129 Не следует задавать нулевое значение аргументу backlog, поскольку различ- ные реализации интерпретируют это по-разному (табл 4 6) Некоторые реа- лизации допускают помещение в очередь одного соединения, в то время как в других вообще невозможно помещать соединения в очередь Если вы не хо- тите, чтобы какие-либо клиенты соединялись с вашим прослушиваемым со- кетом, просто закройте прослушиваемый сокет * Если трехэтапное рукопожатие завершается нормально (то есть без потерян- ных сегментов и повторных передач), запись остается в очереди не полностью установленных соединений на время одного периода обращения (round-trip time, RTT), какое бы значение ни имел этот параметр для конкретного соеди- нения между клиентом и сервером В разделе 14 4 [95] показано, что для од- ного web-сервера средний период RTT равен 187 миллисекундам (Это среднее значение, и отдельные значения могут намного его превосходить ) В коде при- меров всегда используется традиционное значение backlog, равное 5, поскольку это было максимальное значение, которое поддерживалось в системе 4 2BSD Это было актуально в 80-х, когда загруженные серверы могли обрабатывать только несколько сотен соединений в день Но с ростом Сети (WWW), когда серверы обрабатывают миллионы соединении в день, столь малое число стало абсолютно неприемлемым [95, с 187-192] Серверам HTTP необходимо на- много большее значение аргумента backlog, и новые ядра должны поддержи- вать такие значения ПРИМЕЧАНИЕ ------------------------------------------------------------- В настоящее время многие системы позволяют админис 1 раторам изменять максималь- ное значение аргумента backlog Возникает вопрос какое значение аргумента backlog должно задавать прило- жение, если значение 5 часто является неадекватным? На этот вопрос нет про- стого ответа Серверы HTTP сейчас задают большее значение, но если заданное значение является в исходном коде константой, то для увеличения константы требуется перекомпиляция сервера Другой способ — принять некоторое зна- чение по умолчанию и предоставить возможность изменять его с помощью параметра командной строки или переменной окружения Всегда можно за- давать значение больше того, которое поддерживается ядром, так как ядро должно обрезать значение до максимального, не возвращая при этом ошибку [105, с 456] Мы приводим простое решение этой проблемы, изменив нашу функцию-оберт- ку для функции 11 sten В листинге 4 11 представлен действующий код Пере- менная окружения LISTENQ позволяет переопределить значение по умолчанию. Листинг 4.1. Функция-обертка для функции listen, позволяющая переменной окружения переопределить аргумент backlog //lib/wrapsock с 74 void л л. продолжение гх 1 Вес исходные коды прырачм опубликованные в этой кнше, вы можете найти по адресу htip// w ww piter coin/download
130 Глава 4. Элементарные сокеты TCP Листинг 4.1(продолжение) 75 Listentint fd. int backlog) 76 { 77 char *ptr. 78 /* может заменить второй аргумент на переменную окружения */ 79 if ( (ptr = getenvfLISTENQ")) !- NULL) 80 backlog - atoi(ptr) 81 if (listened. backlog) < 0) 82 err_sys("listen error"). 83 } й Традиционно в руководствах и книгах утверждалось, что помещение фикси- рованного числа соединений в очередь позволяет обрабатывать случай загру- женного серверного процесса между последовательными вызовами функции accept. При этом подразумевается, что из двух очередей больше записей будет содержаться, вероятнее всего, в очереди полностью установленных соедине- ний. Но оказалось, что для действительно загруженных web-серверов это не так. Причина задания большего значения backlogs том, что очередь не полно- стью установленных соединений растет по мере поступления сегментов SYN от клиентов; элементы очереди находятся в состоянии ожидания завершения трехэтапного рукопожатия. В табл. 4.5 показано действительное число записей в каждой очереди, изме- ренное на умеренно загруженном web-сервере. Эти значения были получены при проверке двух счетчиков для прослушиваемого сокета HTTP приблизи- тельно каждые 84 миллисекунды в течение 2 часов в середине рабочего дня. Таблица 4.5. Количество записей в очереди полностью и не полностью установ- ленных соединений Количество записей в очереди Сколько раз это количество наблюдалось для очереди не полностью установленных соединений Сколько раз это количество наблюдалось для очареди полностью установленных соединений 0 3033 90 358 1 7158 107 2 10 551 59 3 12 960 52 4 11 949 38 5 9836 27 6 7754 31 7 6165 22 8 4829 30 9 3687 35 10 2674 30 11 1893 25 12 1431 29 13 1083 25 14 1065 49 15 980 7
4,5. Функция listen 131 Количество записей Сколько раз это количество Сколько раз это количество в очереди наблюдалось для очереди не полностью установленных соединений наблюдалось для очереди полностью установленных соединений 16 784 17 696 18 514 19 382 20 294 21 248 22 161 23 152 24 121 25 77 26 48 27 33, 28 79 29 78 30 90 31 70 32 29 33 16 34 4 90 924 90 924 Очередь полностью установленных соединений была пуста 99,4% времени, но в некоторые периоды в нее все же попадали записи. Система, в которой рабо- тал сервер (BSD/OS 2.0.1), имела максимальное значение backlog, равное 64, хотя представленные значения не достигли этого предела. Если очереди заполнены, когда приходит клиентский сегмент SYN, то TCP игнорирует приходящий сегмент SYN [105, с. 930-931] и не посылает RST. Это происходит потому, что состояние считается временным, и TCP клиента снова передаст свой сегмент SYN, для которого в ближайшее время, вероятно, найдется место в очереди. Если бы TCP сервера послал RST, функция connect клиента сразу же возвратила бы ошибку, заставив приложение обработать это условие, вместо того чтобы позволить TCP выполнить повторную передачу. Кроме того, клиент не может увидеть разницу между сегментами RST в ответе на сегмент SYN, означающими, что на данном порте нет сервера либо на дан- ном порте есть сервер, но его очереди заполнены. ПРИМЕЧАНИЕ--------------------------------------------------------- Posix. 1g разрешает оба варианта реагирования па эту ситуацию: игнорирование нового сегмента SYN или ответ на новый сегмент SYN с помощью RST. Все реализации, происходящие от Беркли, игнорировали новый сегмент SYN. Данные, которые приходят после завершения трехэтапного рукопожатия, но до того, как сервер вызывает функцию accept, должны быть помещены в оче- редь TCP-сервера, пока не будет заполнен приемный буфер.
132 Глава4 ЭлементарныесокетыTCP В табл 4 6 показано действительное число установленных в очередь соедине- ний для различных значений аргумента backlog в различных операционных сис- темах, показанных на рис 1 7 Девять различных операционных систем помещены в шесть различных колонок, что иллюстрирует многообразие значении аргумен- та backlog Таблица 4.6. Действительное количество соединений в очереди для различных значений аргумента backlog backlog AIX 4.2, BSD/OS 3.0 DUnix4.0, Linux 2.0.27, Uware2.1.2 HR-UX 10.30 SunOs 4.1.4 Solans 2.5.1 Solans 2.6 0 1 0 1 1 1 1 1 2 1 1 2 2 3 2 4 2 3 4 3 4 3 5 3 4 5 4 6 4 7 4 6 7 . 5 7 5 8 5 7 8 6 9 6 10 6 9 8 7 10 7 И 7 10 8 8 12 8 13 8 12 8 9 13 9 14 9 13 8 10 15 10 16 10 15 8 11 16 И 17 И 16 8 12 18 12 19 12 18 8 13 19 13 20 13 18 8 14 21 14 22 14 19 8 15 22 Системы AIX, BSD/ОХ и SunOS реализуют традиционный алгоритм Беркли, хотя последний не допускает значения аргумента backlog больше пяти В систе- мах HP-UX и Solans 2 6 используется другой поправочный множитель к аргу- менту backlog Системы Digital Unix, Linux и UnixWare воспринимают этот аргу- мент буквально, то есть не используют поправочный множитель, а в Solans 2 51 к аргументу backlog просто добавляется единица ПРИМЕЧАНИЕ------------------------------------------------------------ Linux допускает неограниченное число соединении в случае backlog = 0 чго является ошибкой Программа для измерения этих значений представлена в решении упражнения 14 5 Как мы отмечали, традиционно аргумент backlog задавал максимальное значение для суммы обеих очередей В течение 1996 года была предпринята новая атака через Ин- тернет, названная SYN flooding (лавинная адресация сегмента SYN) Написанная ха- кером программа отправляет жертве сегменты SYN с высокой частотой, заполняя оче- редь не полностью установленных соединении для одного или нескольких портов TCP (Хакером мы называем атакующего, как сказано в предисловии к [ 19] ) Кроме того, IP- адрес отправителя каждого сегмента SYN задается случайным числом — формируют- ся вымышленные IP-адреса (IP spoofing), что ведет к получению доступа обманным путем Таким образом, сегмент сервера SYN/АСК уходит в никуда Это не позволяет серверу узнать реальный IP-адрес хакера Очередь не полностью установленных со-
4 6 Функция accept 133 единений заполняется ложными сегментами SYN, в результате чего для подлинных сегментов SYN в ней не хватает места Таким образом, происходит отказ в обслужива- нии (denial of service) нормальных клиентов Существует два типичных способа про- тивостояния этим атакам [8] Но самое интересное в этом примечании — это еще одно обращение к вопросу о том, чю па самом деле означащ api умент backlog функции listen Он должен задавать максимальное число установленных соединений для данного со- кета, которые ядро помещает в очередь Oi раниченпе количества установленных со- единений имеет целью приостановить получение ядром новых запросов на соединение для данного сокета, когда их не принимает приложение (по любой причине) Если сис- тема реализует именно такую интерпретацию, как, например, BSD/OS 3 0, то прило- жению не нужно задавать большие значения api умента backlog только потому, что сер- вер обрабашваег множество клиентских запросов (например, занятый меЬ-сервер), или для защиты от «наводнения» SYN (лавинном адресации сегмента SYN) Ядро об- 9 рабазываег множество не полностью установленных сое щпенни вне зависимости от то! о, являются ли они законными или приходят от хакера Но даже в такой интерпре- тации мы видим (см табл 4 5), что имеют место сценарии, koi да очередь полнощью установленных соединении накапливает записи (в данном случае до 15), и значения 5 туг явно недостаточно 4.6. Функция accept Функция accept вызывается сервером TCP для возвращения следующего уста- новленною соединения из начала очереди полностью установленных соедине- нии (см рис 4 2) Если очередь полностью установленных соединении пуста, процесс переходит в состояние ожидания (по умолчанию предполагается блоки- руемый сокет) include <sys/socket h> int accept(int sockfd struct sockaddr *cliaddr socklen_t ★addrlen') Возвращает неотрицательный дескриптор в случае успешного выполнения функции 1 в случае ошибки Аргументы cl iaddr и addrl еп используются для возвращения адреса протокола подключившегося процесса (клиента) Аргумент addrl ел — это аргумент типа «зна- чение-результат» (см раздел 3 3) Перед вызовом мы присваиваем целому чис- лу на которое указывает *addlen, размер структуры адреса сокета, на которую указывает аргумент cl i addr, и по завершении функции это целое число содержит действительное число байтов, хранимых ядром в структуре адреса сокета Если выполнение функции accept прошло успешно, он возвращает новый де- скриптор, автоматически созданный ядром Этот дескриптор используется для обращения к соединению TCP с конкретным клиентом При описании функции accept мы называем ее первый аргумент прослушиваемым сокетом (hstemngsocket) — это дескриптор, созданный функцией socket и затем используемый в качестве ар- гумента для функций bi nd и 11 sten, — а значение, возвращаемое этой функцией, мы называем присоединенным сокетом (connected socket) Сервер обычно создает только один прослушиваемый сокет, который существует в течение времени жиз- ни сервера Затем ядро создает по одному присоединенному сокету для каждого клиентского соединения, принятого с помощью функции accept (для которого
134 Глава 4. Элементарные сокеты TCP завершено трехэтапное рукопожатие TCP). Когда сервер заканчивает предостав- ление сервиса данному клиенту, сокет закрывается. Эта функция возвращает до трех значений: целое число, которое является либо дескриптором сокета, либо кодом ошибки, а также адрес протокола клиентского процесса (через указатель cl i addr) и размер адреса (через указатель addrlen). Если нам не нужно, чтобы был возвращен адрес протокола клиента, следует сделать указатели cliaddr и addrlen пустыми указателями. В листинге 1.5 показаны эти моменты. Присоединенный сокет закрывается при каждом прохождении цикла, но прослушиваемый сокет остается открытым в течение времени жизни сервера. Мы также видим, что второй и третий аргу- менты функции accept являются пустыми указателями, поскольку нам не нужно идентифицировать клиент. Пример: аргументы типа «значение-результат» В листинге 4.2 представлен измененный код листинга 1.5 (используемого для вывода IP-адреса и номера порта клиента), обрабатывающий аргумент типа «зна- чение-результат» функции accept. Листинг 4.2. Сервер определения времени и даты, сообщающий IP-адрес и номер порта клиента //intro/daytimetcpsrvl с 1 include 'unp h" 2 include <time h> 3 int 4 main(int argc. char **argv) 5 { 6 int listenfd connfd. 7 socklen_t len. 8 struct sockaddr_in servaddr cliaddr. 9 char buff[MAXLINE] 10 time_t ticks 11 listenfd = Socket(AF_INET SOCK_STREAM. 0) 12 bzero(&servaddr. sizeof(servaddr)). 13 servaddr sin_family = AF_INET 14 servaddr sin_addr s_addr = htonl(INADDR_ANY) 15 servaddr sin_port = htons(13) /* daytime server */ 16 Bind(listenfd (SA *) &servaddr sizeof(servaddr)). 17 Listen(listenfd LISTEhO). 18 for (. ) { 19 len = sizeof(cliaddr) 20 connfd = Accept(listenfd. (SA *) &cliaddr, &len). 21 printfC'connection from $s. port $d\en", 22 Inet_ntop(AF_lNET &cliaddr sm_addr buff sizeof(buff)). 23 ntohs(cliaddr sin_port)), 24 ticks = time(NULL)
4.6. Функция accept 135 25 snprintf(buff sizeof(buff). "% 24s\er\en" ctimet&ticks)). 26 Wnte(connfd. buff, strlen(buff)). 27 Close(connfd). 28 } 29 } Новые объявления 7-8 Мы определяем две новых переменных: Теп, которая будет переменной типа «значение-результат», и cliaddr, которая будет содержать адрес протокола кли- ента. Принятие соединения и вывод адреса клиента 19-23 Мы инициализируем переменную Теп, присвоив ей значение, равное размеру структуры адреса сокета, и передаем указатель на структуру cliaddr и указатель на 1 еп в качестве второго и третьего аргументов функции accept. Мы вызываем функцию inetjitop (см. раздел 3.7) для преобразования 32-битового 1Р-адреса в структуре адреса сокета в строку ASCII (точечно-десятичную запись), а затем вызываем функцию ntohs (см. раздел 3.4) для преобразования сетевого порядка байтов в 16-битовом номере порта в порядок байтов узла. ПРИМЕЧАНИЕ------------------------------------------------------------------ При вызове функции sock_ntop вместо inet_ntop наш сервер станет меньше зависеть от протокола, однако он все равно зависит от IPv4. Мы покажем версию этого сервера, не зависящего от протокола, в листинге 11.5. Если мы запустим наш новый сервер, а затем запустим клиент на том же узле, то, дважды соединившись с сервером, мы получим от клиента следующий вывод: solans % daytimetcpcl1 127.0.0.1 Wed Jan 17 15 42 35 1996 solans % daytimetcpcli 206.62.226.33 Wed Jan 17 15 42 53 1996 Сначала мы задаем IP-адрес сервера как адрес закольцовки на себя (loopback address) (127.0 0.1), азатем как его собственный IP-адрес (206.62.226.33). Вот со- ответствующий вывод сервера: solans # daytimetcpsrvl connection from 127 0 0 1 port 33188 connection from 206 62 226 33. port 33189 Обратите внимание на то, что происходит с IP-адресом клиента. Поскольку наш клиент времени и даты (см. листинг 1.1) не вызывает функцию bind, как ска- зано в разделе 4.4, ядро выбирает IP-адрес отправителя, основанный на исполь- зуемом исходящем интерфейсе. В первом случае ядро задает IP-адрес равным адресу закольцовки, во втором случае — равным IP-адресу интерфейса Ethernet. Кроме того, мы видим, что динамически назначаемый порт, выбранный ядром Solans, — это 33 188, а затем 33 189 (см. рис. 2.6). На конечном шаге приглашение оболочки изменяется на знак # — это пригла- шение к вводу команды для привилегированного пользователя. Наш сервер дол- жен обладать правами привилегированного пользователя, чтобы с помощью функ-
136 Глава 4. Элементарные сокеты TCP ции bind связать зарезервированный порт 13. Если у нас нет прав привилегиро- ванного пользователя, вызов функции bi nd оказывается неудачным: solans % daytimetcpsrvl bind error Permission denied 4.7. Функции fork и exec Прежде чем рассматривать создание параллельного сервера (что мы сделаем в сле- дующем разделе), необходимо описать функцию Unix fork. Эта функция являет- ся единственным способом создания нового процесса в Unix. #include <umstd h> pid_t fork(void). Возвращает 0 в дочернем процессе идентификатор дочернего процесса в родительском процессе. -1 в случае ошибки Если вы никогда не встречались с этой функцией, трудным для понимания может оказаться то, что она вызывается один раз, а возвращает два значения. Одно значение эта функция возвращает в вызывающем процессе (который также на- зывается родительским процессом) — этим значением является идентификатор созданного процесса (который также называется дочерним процессом). Второе значение (нуль) она возвращает в дочернем процессе. Следовательно, по возвра- щаемому значению можно определить, является ли данный процесс родитель- ским или дочерним. Причина того, что функция fork возвращает в дочернем процессе нуль, а не идентификатор родительского процесса, заключается в том, что у дочернего про- цесса есть только один родитель, и дочерний процесс всегда может получить иден- тификатор родительского, вызвыв функцию get ppi d. У родителя же может быть любое количество дочерних процессов, и способа получить их идентификаторы не существует. Если родительскому процессу требуется отслеживать идентифи- каторы своих дочерних процессов, он должен записывать возвращаемые значе- ния функции fork. Все дескрипторы, открытые в родительском процессе перед вызовом функ- ции fork, становятся доступными дочерним процессам после ее завершения. Вы увидите, как это свойство используется сетевыми серверами: родительский про- цесс вызывает функцию accept, а затем функцию fork. Затем присоединенный сокет совместно используется родительским и дочерним процессами. Обычно дочерний процесс использует присоединенный сокет для чтения и записи, а ро- дительский процесс только закрывает присоединенный сокет. Существует два типичных случая применения функции fork' 1. Процесс создает свою копию таким образом, что каждая из копий может обра- батывать одно задание. Это типичная ситуация для сетевых серверов. Далее в тексте вы увидите множество подобных примеров. 2. Процесс хочет запустить другую программу. Поскольку единственный спо- соб создать новый процесс — это вызвать функцию fork, процесс сначала вы- зывает функцию fork, чтобы создать свою копию, а затем одна из копий (обычно дочерний процесс) вызывает функцию ехес (ее описание следует за описали-
4 7. Функции fork и exec 137 ем функции fork), чтобы заменить себя новой программой. Этот сценарий ти- пичен для таких программ, как интерпретаторы командной строки. Единственный способ запустить в Unix на выполнение какой-либо файл — вызвать функцию ехес (Мы будем часто использовать общее выражение «функ- ция ехес», когда неважно, какая из шести функций ехеслх вызывается.) Функция ехес заменяет копию текущего процесса новым программным файлом, причем в новой программе обычно запускается функция main. Идентификатор процесса при этом не изменяется. Процесс, вызывающий функцию ехес, мы будем назы- вать вызывающим процессом, а выполняемую при этом программу — новой про- граммой. В старых описаниях и книгах новая программа ошибочно называется новым процессом. Это неверно, поскольку новый процесс не создается. Различие между шестью функциями ехес заключается в том, что они допуска- ют различные способы задания аргументов. выполняемый программный файл может быть задан или именем файла (filename), или полным именем (pathname)-, ’ аргументы новой программы перечисляются один за другим, либо на них име- ется ссылка через массив указателей; новой программе либо передается окружение вызывающего процесса, либо задается новое окружение. #inciude <umstd h> int exec!(const char ^pathname const char *argO /* (char *) 0 */ ). int execv(const char *pathname char *const argi/fj). int execle(const char *pathname const char *argO /* (char *) 0. char *const ewp[] */ ) int execve(const char ^pathname char *const argi/[] char *const ewp[]). int execlp(const char ^filename const char *argO /* (char *) 0 */ ), int execvp(const char *filename char *const argvf]) Все шесть функций возвращают -1 в случае ошибки если же функция выполнена успешно то ничего не возвращается Эти функции возвращают вызывающему процессу значение -1 только если происходит ошибка. Иначе управление передается в начало новой программы, обычно функции main. Отношения между этими шестью функциями показаны на рис. 4.4. Обычно только функция exeeve является системным вызовом внутри ядра, а остальные представляют собой библиотечные функции, вызывающие exeeve. Отметим различия между этими функциями: 1. Три верхние функции (рис. 4.4) принимают каждую строку как отдель- ный аргумент, причем перечень аргументов завершается пустым указателем (так как их количество может быть различным). У трех нижних функций
138 Глава 4. Элементарные сокеты TCP имени файла в полное имя вызов Рис. 4.4. Отношения между шестью функциями ехес имеется массив argv, содержащий указатели на строки. Этот массив должен содержать пустой указатель, определяющий конец массива, поскольку размер массива не задается. 2. Две функции в левой колонке получают аргумент filename. Он преобразуется в pathname с использованием текущей переменной окружения PATH. Если аргу- мент filename функции execlp или execvp содержит косую черту (/) в любом месте строки, переменная PATH не используется. Четыре функции в двух пра- вых колонках получают полностью определенный аргумент pathname. 3. Четыре функции в двух левых колонках не получают точного указателя окру- жения. Вместо этого с помощью текущего значения внешней переменной environ создается список окружения, который передается новой программе. Две функции в правой колонке получают точный список окружения. Массив указателей envp должен быть завершен пустым указателем. Дескрипторы, открытые в процессе перед вызовом функции ехес, обычно оста- ются открытыми во время ее выполнения. Мы говорим «обычно», поскольку это свойство может быть отключено при использовании функции fent 1 для установ- ки флага дескриптора FD_CLDEXEC. Оно необходимо серверу i netd, о котором пой- дет речь в разделе 12.5. 4.8. Параллельные серверы Сервер, представленный в листинге 4.2, является последовательным (итератив- ным) сервером. Для такого простого сервера, как сервер времени и даты, это допу- стимо. Но когда обработка запроса клиента занимает больше времени, мы не можем связывать один сервер с одним клиентом, поскольку нам хотелось бы об- рабатывать множество клиентов одновременно. Простейшим способом написать параллельный сервер под Unix является вызов функции fork, порождающей до- черний процесс для каждого клиента. В листинге 4.3 представлен типичный па- раллельный сервер. Листинг 4.3. Типичный параллельный сервер pid_t pid. int listenfd. connfd. listenfd = Socket! . ):
4.8. Параллельные серверы 139 /* записываем в sockaddr_in{} параметры заранее известного порта сервера */ Bind(1istenfd. ). Ltsten(1тstenfd. LISTENQ). for ( . . ) { connfd = Accept(1istenfd, ); /* вероятно, блокировка */ if ( (pid = ForkO) == 0) { Close(listenfd). /* дочерний процесс закрывает прослушиваемый сокет */ dOTt(connfd). /* обработка запроса */ Close(connfd). /* с этим клиентом закончено */ exit(O). /* дочерний процесс завершен */ } Close(connfd). родительский процесс закрывает присоединенный сокет */ Когда соединение установлено, функция accept возвращает управление, сер- вер вызывает функцию fork и затем дочерний процесс занимается обслуживани- ем клиента (по присоединенному сокету connfd), а родительский процесс ждет другого соединения (на прослушиваемом сокете 1 т stenfd). Родительский процесс закрывает присоединенный сокет, поскольку новый клиент обрабатывается до- черним процессом. Мы предполагаем, что функция doit в листинге 4.3 выполняет все, что требу- ется для обслуживания клиента. Когда эта функция возвращает управление, мы явно закрываем присоединенный сокет с помощью функции close в дочернем процессе. Это не требуется, так как следующее выражение вызывает функцию exit, и прекращение процесса подразумевает, в частности, закрытие ядром всех открытых дескрипторов. Включать явный вызов функции close или нет — дело вкуса программиста. В разделе 2.5 мы сказали, что вызов функции close на сокете TCP вызывает отправку сегмента FIN, за которой следует обычная последовательноегь прекра- щения соединения TCP. Почему же функция close(connfd) из листинга 4.3, вы- званная родительским процессом, не завершает соединение с клиентом? Чтобы понять происходящее, мы должны учитывать, что у каждого файла и сокета есть счетчик ссылок (reference count). Для счетчика ссылок поддерживается своя за- пись в таблице файла [93, с. 57-60]. Эта запись содержит значения счетчика де- скрипторов, открытых в настоящий момент, которые соответствуют этому файлу или сокету. В листинге 4.3 после завершения функции socket запись в таблице файлов, связанная с li stenfd, содержит значение счетчика ссылок, равное 1. Но после завершения функции fork дескрипторы дублируются (для совместного использования и родительским, и дочерним процессом), поэтому записи в табли- це файла, ассоциированные с этими сокетами, теперь содержат значение 2. Сле- довательно, когда родительский процесс закрывает connfd, счетчик ссылок умень- шается с 2 до 1. Но фактического закрытия дескриптора не произойдет, пока счетчик ссылок не станет равен 0. Это произойдет несколько позже, когда дочер- ний процесс закроет connfd. Можно проиллюстрировать сокеты и соединение, представленные в листин- ге 4.3, следующим образом. Прежде всего, на рис. 4.5 показано состояние клиента и сервера в тот момент, когда сервер блокируется при вызове функции accept и от клиента приходит запрос на соединение.
140 Глава 4. Элементарные сокеты TCP Клиент Сервер listenfd connect() Рис. 4.5. Состояние соединения клиент-сервер перед завершением вызванной функции accept Сразу же после завершения функции accept мы получаем сценарий, изобра- женный на рис. 4.6. Соединение принимается ядром и создается новый сокет — connfd. Это присоединенный сокет, и теперь данные могут считываться и записы- ваться по этому соединению. Клиент Сервер Рис. 4.6. Состояние соединения клиент-сервер после завершения функции accept Следующим шагом параллельного сервера является вызов функции fork. На рис. 4.7 показано состояние соединения после вызова функции fork. Клиент Сервер Рис. 4.7. Состояние соединения клиент-сервер после вызова функции fork Обратите внимание, что оба дескриптора, 1 т stenfd и connfd, совместно исполь- зуются родительским и дочерним процессами. Далее родительский процесс закрывает присоединенный сокет, а дочерний процесс закрывает прослушиваемый сокет. Это показано на рис. 4.8. Желательно, чтобы это было последнее состояние сокетов. Дочерний процесс управляет соединением с клиентом, а родительский процесс может снова вызвать функцию accept на прослушиваемом сокете, чтобы обрабатывать следующее кли- ентское соединение.
4.9. Функция close 141 Клиент Сервер (родительский) Рис. 4.8. Состояние соединения клиент-сервер после закрытия родительским и дочерним процессами соответствующих сокетов 4.9. Функция close Обычная функция Unix cl ose также используется для закрытия сокета и завер- шения соединения TCP. include <umstd h> int close(int sockfd). По умолчанию функция close отмечает сокет TCP как закрытый и немедлен- но возвращает управление процессу. Дескриптор сокета больше не используется процессом и не может быть передан в качестве аргумента функции read или write. Но TCP попытается отправить данные, которые уже установлены в очередь, и пос- ле того как отправка произойдет, осуществится нормальная последовательность завершения соединения TCP (см. раздел 2.5). В разделе 7.5 рассказывается о параметре сокета SO_LINGER, который позволяет нам изменять действие с сокетом TCP, выполняемое по умолчанию. В этом раз- деле мы также назовем действия, благодаря которым приложение TCP может получить гарантию того, что приложение-собеседник получило данные, постав- ленные в очередь на отправку, но еще не отправленные. Счетчик ссылок дескриптора В конце раздела 4.8 мы отметили, что когда родительский процесс на нашем па- раллельном сервере закрывает присоединенный сокет с помощью функции cl ose, счетчик ссылок дескриптора уменьшается лишь на единицу. Поскольку счетчик ссылок при этом все еще оставался больше нуля, вызов функции cl ose не иници- ировал последовательность завершения TCP-соединения, состоящую из четырех пакетов. Нам нужно, чтобы наш параллельный сервер с присоединенным соке- том, разделяемым между родительским и дочерним процессами, работал по тако- му принципу.
142 Глава 4. Элементарные сокеты TCP Если мы хотим отправить сегмент FIN по соединению TCP, вместо функции cl ose должна использоваться функция shutdown (см. раздел 6.6). Причины мы рас- смотрим в разделе 6.5. Необходимо также знать, что происходит с нашим параллельным сервером, если родительский процесс не вызывает функцию cl ose для каждого присоеди- ненного сокета, возвращаемого функцией accept. Прежде всего, родительский процесс в какой-то момент израсходует все дескрипторы, поскольку обычно чис- ло дескрипторов, которые могут быть открыты процессом, ограничено. Но что более важно, ни одно из клиентских соединений не будет завершено. Когда до- черний процесс закрывает присоединенный сокет, его счетчик ссылок уменьша- ется с 2 до 1 и остается равным 1, поскольку родительский процесс не закрывает присоединенный сокет с помощью функции cl ose. Это помешает выполнить по- следовательность завершения соединения TCP, и соединение останется от- крытым. 4.10. Функции getsockname и getpeername Эти две функции возвращают либо локальный (функция getsockname), либо уда- ленный (функция getpeername) адрес протокола, связанный с сокетом. include <sys/socket h> int getsockname!int sockfd struct sockaddr *localadd socklen_t *addrlen). int getpeernameCint sockfd struct sockaddr *peeraddr socklen_t *addrlen). Обратите внимание, что последний аргумент обеих функций относится к типу «значение-результат», то есть обе функции будут заполнять структуру адреса со- кета, на которую указывает аргумент localaddr или peeraddr. ПРИМЕЧАНИЕ ------------------------------------------------------------- Обсуждая функцию bind, мы отметили, что термин «имя» используется некорректно Эти две функции возвращают адрес протокола, связанный с одним из концов сетевого соединения, что для протоколов IPv4 и IPv6 является сочетанием IP-адреса и номера порта. Эти функции также не имеют ничего общего с доменными именами (глава 9) Функции getsockname и getpeername необходимы нам по следующим соображе- ниям: После успешного выполнения функции connect и возвращения управления в клиентский процесс TCP, который не вызывает функцию bind, функция getsockname возвращает IP-адрес и номер локального порта, присвоенные со- единению ядром. После вызова функции bi nd с номером порта 0 (что является указанием ядру на необходимость выбрать номер локального порта) функция getsockname воз- вращает номер локального порта, который был задан Функцию getsockname можно вызвать, чтобы получить семейство адресов со- кета, как это показано в листинге 4.4. Сервер TCP, который с помощью функции Ы nd связывается с универсальным IP-адресом (см. листинг 1.5), как только устанавливается соединение с кли-
4,10, Функции getsockname и getpeername 143 ентом (функция accept успешно выполнена), может вызвать функцию getsockname, чтобы получить локальный IP-адрес соединения. Аргумент sockfd (дескриптор сокета) в этом вызове должен содержать дескриптор присоеди- ненного, а не прослушиваемого сокета. Когда сервер запускается с помощью функции ехес процессом, вызывающим функцию accept, он может идентифицировать клиент только одним способом — вызвать функцию getpeername. Это происходит, когда функция i netd (см. раз- дел 12.5) вызывает функции fork и ехес для создания сервера TCP. Этот сцена- рий представлен на рис. 4.9. Функция i netd вызывает функцию accept (верхняя левая рамка), после чего возвращаются два значения: дескриптор присоеди- ненного сокета connfd (это возвращаемое значение функции), а также IP-ад- рес и номер порта клиента, отмеченные на рисунке небольшой рамкой с под- писью «адрес собеседника» (структура адреса сокета Интернета). Далее вызывается функция fork и создается дочерний процесс функции inetd. По- скольку дочерний процесс запускается с копией содержимого памяти роди- тельского процесса, структура адреса сокета доступна дочернему процессу, как и дескриптор присоединенного сокета (так как дескрипторы совместно исполь- зуются родительским и дочерним процессами). Но когда дочерний процесс с помощью функции ехес запускает выполнение реального сервера (скажем, сервера Telnet), содержимое памяти дочернего процесса заменяется новым программным файлом для сервера Telnet (то есть структура адреса сокета, содержащая адрес собеседника, теряется). Однако во время выполнения функ- ции ехес дескриптор присоединенного сокета остается открытым. Один из первых вызовов функции, который выполняет сервер Telnet, — это вызов функ- ции getpeername для получения IP-адреса и номера порта клиента. C?pnfd inetd Адрес собеседника = accept(1) inetd (дочерний процесс) Адрес собеседника ехес Сервер Telnet connfd Рис. 4.9. Пример функции inetd, порождающей сервер Очевидно, что в приведенном примере сервер Telnet при запуске должен знать значение функции connfd. Этого можно достичь двумя способами Во-первых,
144 Глава 4. Элементарные сокеты TCP процесс, вызывающий функцию ехес, может отформатировать номер дескрипто- ра как символьную строку и передать ее в виде аргумента командной строки про- грамме, выполняемой с помощью функции ехес. Во-вторых, можно заключить соглашение относительно определенных дескрипторов: некоторый дескриптор всегда присваивается присоединенному сокету перед вызовом функции ехес. Последний случай соответствует действию функции i netd — она всегда присваи- вает дескрипторы 0, 1 и 2 присоединенным сокетам. Пример: получение семейства адресов сокета Функция sockfd_to_fапл 1у, представленная в листинге 4.4, возвращает семейство адресов сокета. Листинг 4.4. Возвращаемое семейство адресов сокета //lib/sockfd_to_family с 1 include "unp h" 2 int 3 sockfd_to_famly(int sockfd) 4 { 5 union { 6 struct sockaddr sa. 7 char data[MAXSOCKADDR]: В } un. 9 socklen_t len. 10 len = MAXSOCKADDR 11 if (getsocknameCsockfd (SA *) un.data. &len) < 0) 12 return (-1). 13 return (un sa sa_famly). 14 } Выделение пространства для наибольшей структуры адреса сокета 5-8 Поскольку мы не знаем, какой тип структуры адреса сокета нужно будет разме- стить в памяти, мы используем в нашем заголовочном файле unp h константу MAXSOCKADDR, которая представляет собой размер наибольшей структуры адреса сокета в байтах. Мы определяем массив типа char такого размера в объединении, включающем универсальную структуру адреса сокета. Вызов функции getsockname .0-13 Мы вызываем функцию getsockname и возвращаем семейство адресов. Поскольку Posix. 1g позволяет вызывать функцию getsockname на пеприсоеди- ненном сокете, эта функция должна работать для любого дескриптора открытого сокета. 4.11. Резюме Все клиенты и серверы начинают работу с вызова функции socket, возвращаю- щей дескриптор сокета. Затем клиенты вызывают функцию connect, в то время как серверы вызывают функции bi nd, 11 sten и accept. Сокеты обычно закрывают-
Упражнения 145 ся с помощью стандартной функции close, хотя в разделе 6.6 вы увидите другой способ закрытия, реализуемый с помощью функции shutdown. Мы также прове- рим влияние параметра сокета SO_LINGER (см. раздел 7.5). Большинство серверов TCP являются параллельными. При этом для каждого клиентского соединения, которым управляет сервер, вызывается функция fork. Вы увидите, что большинство серверов UDP являются последовательными. Хотя обе эти модели успешно использовались на протяжении ряда лет, имеются и дру- гие параметры создания серверов с использованием программных потоков и про- цессов, которые мы рассмотрим в главе 27. Упражнения 1. В разделе 4.4 мы утверждали, что константы INADDR_, определенные в заголо- вочном файле <netinet/in h>, расположены в порядке байтов узла. Каким об- разом мы можем это определить? 2. Измените листинг 1.1 так, чтобы вызвать функцию getsockname после успеш- ного завершения функции connect. Выведите локальный IP-адрес и локаль- ный порт, присвоенный сокету TCP, используя функцию sock_ntop. В каком диапазоне (см. рис. 2.6) будут находиться динамически назначаемые порты ва- шей системы? 3. Предположим, что на параллельном сервере после вызова функции fork за- пускается дочерний процесс, который завершает обслуживание клиента пе- ред тем, как результат выполнения функции fork возвращается родительскому процессу. Что происходит при этих двух вызовах функции cl ose в листинге 4.3? 4. В листинге 4.2 сначала измените порт сервера с 13 на 9999 (так, чтобы для запуска программы вам не потребовались права привилегированного пользо- вателя). Удалите вызов функции 1i sten. Что происходит? 5. Продолжайте предыдущее упражнение. Удалите вызов функции bind, но ос- тавьте вызов функции 11 sten. Что происходит?
ГЛАВА 5 Пример ТСР-соединения клиент-сервер 5.1. Введение Напишем простой пример пары клиент-сервер, используя элементарные функ- ции из предыдущей главы. Наш простой пример — это эхо-сервер, функциониру- ющий следующим образом: 1. Клиент считывает строку текста из стандартного потока ввода и отправляет ее серверу. 2. Сервер считывает строку из своего сетевого ввода и отсылает эту строку об- ратно клиенту. 3. Клиент считывает отраженную строку и помещает ее в свой стандартный по- ток вывода. На рис. 5.1 изображена пара клиент-сервер вместе с функциями, используе- мыми для ввода и вывода. fgets stdin > TCP- writen readline TCP- 4 — - сервер stdout -Ч клиент readline writen fputs Рис. 5.1. Простой эхо-клиент и эхо-сервер Между клиентом и сервером мы показали две стрелки, но на самом деле это одно двустороннее соединение TCP. Функции fgets и fputs имеются в стандарт- ной библиотеке ввода-вывода, а функции writen и readline приведены в разде- ле 3.9. Мы разрабатываем нашу собственную реализацию эхо-сервера, однако боль- шинство реализаций TCP/IP предоставляют готовый эхо-сервер, используя как TCP, так и UDP (см. раздел 2.10). С нашим собственным клиентом мы также бу- дем использовать и готовый сервер. Соединение клиент-сервер, отражающее вводимые строки, является коррект- ным, хотя и простым примером сетевого приложения. На этом примере можно проиллюстрировать все основные действия, необходимые для реализации соеди- нения клиент-сервер. Все, что вам нужно сделать, чтобы применить его к вашему приложению, — это изменить операции, которые выполняет сервер с принимае- мыми от клиентов данными.
5.2. Эхо-сервер TCP: функция main 147 С помощью этого примера мы можем не только проанализировать запуск на- шего клиента и сервера в нормальном режиме (ввести строку и посмотреть, как она отражается), но и исследовать множество «граничных условий»: выяснить, что происходит в момент запуска клиента и сервера; что происходит, когда кли- ент нормальным образом завершает работу; что происходит с клиентом, если про- цесс сервера завершается до завершения клиента или если возникает сбой на узле сервера, и т. д. Рассмотрев эти сценарии, мы сможем понять, что происходит на уровне сети и как это представляется для API-сокетов, и научиться писать при- ложения так, чтобы они умели обрабатывать подобные ситуации. Во всех рассматриваемых далее примерах присутствуют специфичные для протоколов жестко заданные (hard coded) константы, такие как адреса и порты. Это обусловлено двумя причинами. Во-первых, нам необходимо точно понимать, что нужно хранить в специфичных для протоколов структурах адресов. Во-вто- рых, мы еще не рассмотрели библиотечные функции, которые сделали бы наши программы более переносимыми. Эти функции рассматриваются в главах 9 и 11. В последующих главах код клиента и сервера будет претерпевать многочис- ленные изменения, по мере того как вы будете больше узнавать о сетевом про- граммировании (см. табл. 1.2 и 1.3). 5.2. Эхо-сервер TCP: функция main Наши клиент и сервер TCP используют функции, показанные на рис. 4.1.-Про- грамма параллельного сервера представлена в листинге 5.11. Листинг 5.1. Эхо-сервер TCP (улучшенный в листинге 5.9) //tcpcIiserv/tcpservOl с 1 include "unp h" 2 int 3 main(int argc, char **argv) 4 { 5 int listenfd. connfd. 6 pid_t chi1 dpid. 7 socklen_t cl lien. 8 struct sockaddr_in cliaddr. servaddr. 9 listenfd = SockettAFJNET. SOCK_STREAM. 0). 10 bzero(&servaddr. sizeof(servaddr)). 11 servaddr sin_fannly = AF_INET, 12 servaddr sin_addr s_addr = htonl(INADDR_ANY); , 13 servaddr sin_port - htons(SERV_PORT). 14 BindClistenfd. (SA *) &servaddr, sizeof(servaddr)); 15 Listendistenfd. LISTENQ). 16 for (..) ( 17 clilen = sizeof(cliaddr). продолжение^ 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter.com/download.
148 Глава 5. Пример TCP-соединения клиент-сервер Листинг 5.1(продолжение) 18 connfd = Accept(listenfd. (SA *) &cliaddr &clilen) 19 if ( (childpid = ForkO) = 0) { /* дочерний процесс */ 20 Close(listenfd) /* закрываем прослушиваемый сокет */ 21 str_echo(connfd) /* обрабатываем запрос */ 22 exit(0) 23 } 24 Close(connfd) /* родительский процесс закрывает присоединенный сокет */ 25 } 26 } Создание сокета, связывание с известным портом сервера 3 15 Создается сокет TCP. В структуру адреса сокета Интернета записывается уни- версальный адрес (I NADDR_ANY) и номер заранее известного порта сервера (SERV_PORT, который определен как 9877 в нашем заголовочном файле unp h). В результате связывания с универсальным адресом системе сообщается, что мы примем со- единение, предназначенное для любого локального интерфейса в том случае, если система имеет несколько сетевых интерфейсов. Наш выбор номера порта TCP основан на рис. 2.6. Он должен быть больше 1023 (нам не нужен зарезервирован- ный порт), больше 5000 (чтобы не допустить конфликта с динамически назнача- емыми портами, которые выделяются многими реализациями, происходящими от Беркли), меньше 49 152 (чтобы избежать конфликта с «правильным» диапа- зоном динамически назначаемых портов) и не должен конфликтовать ни с од- ним зарегистрированным портом. Сокет преобразуется в прослушиваемый при помощи функции 11 sten. Ожидание завершения клиентского соединения .7-18 Сервер блокируется в результате вызова функции accept, ожидая подключения клиента Параллельный сервер 9-24 Для каждого клиента функция fork порождает дочерний процесс, и дочерний процесс обслуживает запрос этого клиента. Как мы говорили в разделе 4.8, до- черний процесс закрывает прослушиваемый сокет, а родительский процесс за- крывает присоединенный сокет. Затем дочерний процесс вызывает функцию str_echo (см. листинг 5.2) для обработки запроса клиента. 5.3. Эхо-сервер TCP: функция str_echo Функция str_echo, показанная в листинге 5.2, выполняет серверную обработку запроса каждого клиента — считывание строк от клиента и отражение их обратно клиенту. Листинг 5.2. функция str_echo: отраженные строки на сокете //lit>/str_echo с 1 #include 'unp h'
5.4. Эхо-клиент TCP, функция main 149 3 str_echo(int sockfd) 4 { 5 ssize_t n 6 char line[MAXLINE] 7 for ( ) { 8 if ( (n = Readline(sockfd line MAXLINE)) == 0) 9 return /* соединение закрыто с другого конца */ 10 Writentsockfd line n) И } 12 } Чтение строки и ее отражение 7-ц функция readline считывает очередную строку из сокету, после чего строка отражается обрат но клиенту с помощью функции writen. Если клиент закрывает соединение (нормальный сценарий), то при получении клиентского сегмента FIN функция дочернего процесса readl i пе возвращает нуль. После этого происходит возврат из функции str_echo, и далее завершается дочерний процесс, приведен- ный в листинге 5.1. 5.4. Эхо-клиент TCP: функция main В листинге 5 3 показана функция main ТСР-клиента. Листинг 5.3. Эхо-клиент TCP //tcpc1iserv/tcpcli01 с 1 include unp h" 2 int 3 main(int argc char **argv) 4 { 5 int sockfd 6 struct sockaddr_in servaddr. 7 if (argc '= 2) 8 err_quit('usage tcpcli <IPaddress>'). 9 sockfd = Socket(AF_INET SOCK_STREAM 0) 10 bzero(&servaddr sizeoftservaddr)) 11 servaddr sin_family = AF_INET 12 servaddr sin_port = htons(SERV_PORT) 13 Inet_pton(AF_INET argv[l] &servaddr sin_addr) 14 Connect(sockfd (SA *) &servaddr sizeof(servaddr)) 15 str_cli(stdin sockfd) /* зта функция выполняет все необходимые действия со стороны клиента */ 16 exit(0): 17 }
150 Глава 5. Пример TCP-соединения клиент-сервер Создание сокета, заполнение структуры его адреса )-13 Создается сокет ТС Р, и структура адреса сокета заполняется I P-адресом серве- ра и номером порта. IP-адрес сервера мы берем из командной строки, а извест- ный номер порта сервера (SERV_PORT) — из нашего заголовочного файла unp.h. Соединение с сервером 14-15 Функция connect устанавливает соединение с сервером. Затем функция str_cl 1 (см. листинг 5.4) выполняет все необходимые действия со стороны клиента. 5.5. Эхо-клиент TCP: функция str_cli Эта функция, показанная в листинге 5.4, обеспечивает отправку запроса клиента и прием ответа сервера в цикле 0. Функция считывает строку текста из стандарт- ного потока ввода, отправляет ее серверу и считывает отраженный ответ сервера, после чего помещает отраженную строку в стандартный поток вывода. Листинг 5.4. Функция str_cli: цикл формирования запроса клиента //lib/str_cli с 1 #include "unp h” 2 voi d 3 str_cli(FILE *fp. int sockfd) 4 { 5 char sendline[MAXLINE], recvline[MAXLINE] 6 while (Fgets(sendline, MAXLINE, fp) != NULL) { 7 Writen(sockfd. sendline strlen(sendline)). 8 if (Readline(sockfd, recvline, MAXLINE) == 0) 9 err_quit("str_cli server terminated prematurely"): 10 Fputs(recvline. stdout), И ) 12 } Считывание строки, отправка серверу >-7 Функция fgets считывает строку текста, а функция wri ten отправляет эту стро- ку серверу. Считывание отраженной сервером строки, запись в стандартный поток вывода -10 Функция headline принимает отраженную сервером строку, а функция fputs записывает ее в стандартный поток вывода. Возврат в функцию main 1-12 Цикл завершается, когда функция fgets возвращает пустой указатель, что про- исходит, когда либо достигается конец файла, либо происходит ошибка. Наша функция-обертка Fgets проверяет наличие ошибки, и если ошибка обнаружена,
5.6. Нормальный запуск 151 прерывает выполнение программы, то есть функция Fgets возвращает пустой ука- затель только при достижении конца файла. 5.6. Нормальный запуск Наш небольшой пример TCP (около 150 строк кода для функций main, str_echo, str_cl т, read 1i ne и wn ten) позволяет нам понять, как запускаются и завершаются клиент и сервер и, что наиболее важно, как развиваются события, если произо- шел сбой на узле клиента или в клиентском процессе, потеряна связь в сети и т. д. Только при понимании этих «граничных условий» и их взаимодействия с прото- колами TCP/IP мы сможем обеспечить устойчивость клиентов и серверов, кото- рые смогут справляться с подобными ситуациями. Сначала мы запускаем сервер в фоновом режиме на узле bsdi. bsdi X tcpservOl & [1] 21130 Когда сервер запускается, он вызывает функции socket, bi nd, 11 sten и accept, а затем блокируется в вызове функции accept. (Мы еще не запустили клиент.) Перед тем как запустить клиент, мы запускаем программу netstat, чтобы прове- рить состояние прослушиваемого сокета сервера. bsdi X netstat -а Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp 00 * 9877 * * LISTEN Здесь мы показываем только первую строку вывода и интересующую нас стро- ку. Эта команда показывает состояние всех сокетов в системе, поэтому вывод может быть большим. Для просмотра прослушиваемых сокетов следует указать параметр -а. Результат совпадает с нашими ожиданиями. Сокет находится в состоянии LISTEN, локальный IP-адрес задан с помощью символа подстановки (то есть яв- ляется универсальным), и указан локальный порт 9877. Функция netstat выво- дит звездочку для нулевого IP-адреса (INADDR_ANY, универсальный адрес) или для нулевого порта. Затем на том же узле мы запускаем клиент, задав IP-адрес сервера 127.0.0.1. Мы могли бы задать здесь и адрес 206.62.226.35 (см. рис. 1.7). bsdi X tcpcliOl 127.0.0.1 Клиент вызывает функции socket и connect, последняя вызывает трехэтапное рукопожатие TCP. Когда рукопожатие TCP завершается, функция connect воз- вращает управление процессу-клиенту, а функция accept — процессу-серверу. Соединение установлено. Затем выполняются следующие шаги: 1. Клиент вызывает функцию str_cli, которая блокируется в вызове функции fgets, поскольку мы еще ничего не ввели. 2. Когда функция accept возвращает управление процессу-серверу, последний вызывает функцию fork, а дочерний процесс вызывает функцию str_echo. Она вызывает функцию readl i ne, которая, в свою очередь, вызывает функцию read, блокируемую в ожидании получения данных от клиента. 3. Родительский процесс сервера снова вызывает функцию accept и блокирует- ся в ожидании подключения следующего клиента.
152 Глава 5. Пример TCP-соединения клиент-сервер У нас имеется три процесса, и все они находятся в состоянии ожидания (бло- кированы): клиент, родительский процесс сервера и дочерний процесс сервера. ПРИМЕЧАНИЕ --------------------------------------------------------- Мы специально поставили первым пунктом (после завершения трехэтапного рукопо- жатия) вызов функции str cli, происходящий па стороне клиента, а затем уже пере- числили действия на стороне сервера Причину объясняет рис. 2.5: функция connect возвращает управление, когда клиент получает второй сегмент рукопожатия. Однако функция accept не возвращает управление до тех пор, пока сервер не получит третий сегмент рукопожатия, то есть пока не пройдет половина периода RTT после заверше- ния функции connect. Мы намеренно запускаем и клиент, и сервер на одном узле — так проще всего экспериментировать с клиент-серверными приложениями. Поскольку клиент и сервер запущены на одном узле, функция netstat отображает теперь две допол- нительные строки вывода, соответствующие соединению TCP: bsdi % netstat -а Proto Recv-Q Send-Q Local Address Foreign Address (state) tep 0 0 localhost 9877 localhost 1052 ESTABLISHED tep 0 0 localhost 1052 localhost 9877 ESTABLISHED tep 0 0 * 9877 * * LISTEN Первая из строк состояния ESTABLISHED соответствует дочернему сокету серве- ра, поскольку локальным портом является порт 9877. Вторая строка ESTABLI SHED — это клиентский сокет, поскольку локальный порт — порт 1052. Если мы запуска- ем клиент и сервер на разных узлах, на узле клиента будет отображаться только клиентский сокет, а на узле сервера — два серверных сокета. Для проверки состояния и отношений между этими процессами можно также использовать команду ps: bsdi % ps -1 PID PPID WCHAN STAT TT TIME COMMAND 19130 19129 wait Is Pl 0 04 99 -ksh (ksh) 21130 19130 netcon 1 Pl 0 00 06 tcpservOl 21131 19130 ttyin 1+ Pl 0 00 09 tcpcliOl 127.0.0.1 21132 21130 net io 1 Pl 0 00 01 tcpservOl 21134 21133 wait Ss P2 0 03 50 -ksh (ksh) 21149 21134 - R+ P2 0 00 05 ps -1 (Из вывода мы удалили несколько столбцов, не представляющих интереса в данном обсуждении.) Мы запустили клиент и сервер из одного окна (pl, обо- значающий псевдотерминал 1), а из второго окна запустили команду ps (р2). В колонках PID и PPID показаны отношения между родительским и дочерним про- цессами. Можно точно сказать, что первая строка tcpservOl соответствует роди- тельскому процессу, а вторая строка tcpservOl — дочернему, поскольку PPID до- чернего процесса — это PID родительского. Кроме того, PPID родительского процесса совпадает с PID интерпретатора команд (ksh). Колонка STAT для всех трех сетевых процессов отмечена символом I. Это озна- чает, что процессы находятся в состоянии ожидания (idle). Знак + в конце двух ячеек STAT говорит о том, что данный процесс находится в группе основных про- цессов его управляющего терминала. Если процесс находится в состоянии ожи- дания, колонка WCHAN сообщит нам о том, чем он занят В 4.4BSD значение netcon
5.7. Нормальное завершение 153 выводится, если процесс блокируется функцией accept или connect, значение neti о — если процесс блокируется при вводе или выводе через сокет, a ttyin или ttyout — если процесс блокируется при терминальном вводе-выводе. Значения WCHAN имеют смысл для наших трех сетевых процессов. 5.7. Нормальное завершение На этом этапе соединение установлено, и все, что мы ни вводили бы на стороне клиента, отражается обратно. bsdi % tcpcliOl 127.0.0.1 эту строку мы показывали раньше hello, world наш ввод hello, world отраженная сервером строка good bye good bye AD Ctrl-D- наш завершающий символ для обозначения конца файла Мы вводим две строки, каждая из них отражается, затем мы вводим символ конца файла (EOF) Ctrl-D, который завершает работу клиента. Если мы сразу же выполним команду netstat, то увидим следующее: bsdi % netstat -а | grep 9877 tcp 0 0 local host 1052 local host 9877 TIME_WAIT tcp 0 0 * 9877 * * LISTEN Клиентская часть соединения (поскольку локальный порт — это порт 1052) входит в состояние TIME_WAIT (см. раздел 2.6), и прослушивающий сервер все еще ждет подключения другого клиента. (На этот раз мы передаем вывод netstat программе grep, чтобы вывести только строки с заранее известным портом наше- го сервера. Но при этом также удаляется строка заголовка.) Перечислим этапы нормального завершения работы нашего клиента и сервера. 1. Когда мы набираем символ EOF, функция fgets возвращает пустой указатель, и функция str_cl 1 возвращает управление (см. листинг 5.4). 2. Когда функция str_cli возвращает управление клиентской функции main (см. листинг 5.3), последняя завершает работу, вызывая функцию exit. 3. При завершении процесса выполняется закрытие всех открытых дескрипто- ров, так что клиентский сокет закрывается ядром. При этом серверу посыла- ется сегмент FIN, на который TCP сервера отвечает сегментом АСК. Это первая половина последовательности завершения работы соединения TCP. На этом этапе сокет сервера находится в состоянии CLOSE_WAIT, а клиентский со- кет — в состоянии FIN WAIT 2 (см. рис. 2.4 и 2.5). 4. Когда TCP сервера получает сегмент FIN, дочерний процесс сервера блокиру- ется в вызове функции readline (см. листинг 5.2), а затем функция readline возвращает нуль. Это заставляет функцию str_echo вернуть управление функ- ции mai п дочернего процесса сервера. 5. Дочерний процесс сервера завершается с помощью вызова функции exi t (лис- тинг 5.1). 6. Все открытые дескрипторы в дочернем процессе сервера закрываются. Закры- тие присоединенного сокета дочерним процессом вызывает отправку двух последних сегментов завершения соединения TCP: FIN от сервера клиенту
154 Глава 5. Пример TCP-соединения клиент-сервер и АСК от клиента (см. рис. 2.5). На этом этапе соединение полностью завер- шается. Клиентский сокет входит в состояние TIMEWAIT. 7. Другая часть завершения процесса относится к сигналу SIGCHLD. Он отправ- ляется родительскому процессу, когда завершается дочерний процесс. Это происходит и в нашем примере, но мы не перехватываем данный сигнал в коде и по умолчанию он игнорируется. Дочерний процесс входит в состояние зом- би (zombie). Мы можем проверить это с помощью команды ps. bsdi % PS PID ТТ STAT TIME COMMAND 19130 Pl Ss 0 05 08 -ksh (ksh) 21130 Р1 I 0 00 06 tcpservOl 21132 Р1 z 0 00 00 (tcpservOl) 21167 Р1 R+ 0-00 10 PS Теперь дочерний процесс находится в состоянии Z (зомби). Процессы-зомби нужно своевременно удалять, а это требует работы с сигна- лами Unix. Поэтому в следующем разделе мы сделаем обзор управления сигнала- ми, а затем продолжим рассмотрение нашего примера. 5.8. Обработка сигналов Posix Сигнал — это уведомление процесса о том, что произошло некое событие. Иногда сигналы называют программными прерываниями (software interrupts). Подразу- мевается, что процесс не знает заранее о том, когда придет сигнал. Сигналы могут посылаться в следующих направлениях: одним процессом другому процессу (или самому себе); ** ядром процессу. Сигнал SIGCHLD, упомянутый в конце предыдущего раздела, ядро посылает ро- дительскому процессу при завершении дочернего. Для каждого сигнала существует определенное действие (action или disposi- tion — характер). Действие, соответствующее сигналу, задается с помощью вы- зова функции sigaction (ее описание следует чуть ниже) и может быть выбрано тремя способами: 1. Мы можем предоставить функцию, которая вызывается при перехвате опре- деленного сигнала. Эта функция называется обработчиком сигнала (signal handler), а действие называется перехватыванием сигнала (catching). Сигналы SIGKILL и SIGSTOP перехватить нельзя. Наша функция вызывается с одним це- лочисленным аргументом, который является номером сигнала, и ничего не возвращает. Следовательно, прототип этой функции имеет вид: void handler (int signo) Для большинства сигналов вызов функции sigaction и задание функции, вы- зываемой при получении сигнала, — это все, что требуется для обработки сиг- нала. Но дальше вы увидите, что для перехватывания некоторых сигналов, в частности SIGIO, SIGPOLL и SIGURG, требуются дополнительные действия со сто- роны процесса.
5.8. Обработка сигналов Posix 155 2. Мы можем игнорировать сигнал, если действие задать как SIG_IGN. Сигналы SIGKILL и SIGST0P не могут быть проигнорированы. 3. Мы можем установить действие для сигнала по умолчанию, задав его как S IG_DFL. Действие сигнала по умолчанию обычно заключается в завершении процесса по получении сигнала, а некоторые сигналы генерируют копию области памя- ти процесса в его текущем каталоге (так называемый дамп — core dump). Есть несколько сигналов, для которых действием по умолчанию является игнори- рование. Например, SIGCHLD и SIGURG (посылаются по получении внеполосных данных, см. главу 21)— это два сигнала, игнорируемых по умолчанию, с кото- рыми мы встретимся в тексте. Функция signal Согласно Posix, чтобы определить действие для сигнала, нужно вызвать функ- цию sigaction. Однако это достаточно сложно, поскольку один аргумент этой функции — это структура, для которой необходимо выделение памяти и запол- нение. Поэтому проще задать действие сигнала с помощью функции signal. Пер- вый ее аргумент — это имя сигнала, а второй — либо указатель на функцию, либо одна из констант SIG_IGN и SIGJ3FL. Но функция signal существовала еще до появ- ления Posix. 1, и ее различные реализации имеют разную семантику сигналов с целью обеспечения обратной совместимости. В то же время Posix четко диктует семантику при вызове функции sigaction. Это обеспечивает простой интерфейс с соблюдением семантики Posix. Мы включили эту функцию в нашу собственную библиотеку вместе функциями егг_ХХХ и функциями-обертками, которые мы ис- пользуем для построения всех наших программ. Она представлена в листинге 5.5. Листинг 5.5. Функция signal, вызывающая функцию Posix sigaction //lib/signal с 1 #include ’’unp h" 2 Sigfunc * 3 signal(int signo, Sigfunc *func) 4 { 5 struct sigaction act oact. 6 act sajiandler = func. 7 sigemptyset(&act sajnask), 8 act sa_flags = 0. 9 if (signo == SIGALRM) { 10 #lfdef SAJNTERRUPT 11 act sa flags |= SAJNTERRUPT: /* SunOS 4 x */ 12 #endif 13 } else { 14 #ifdef SA_RESTART 15 act sa flags |= SA_RESTART. /* SVR4, 44BSD */ 16 #endif 17 } 18 if (sigaction(signo. &act, &oact) < 0) 19 return (SIGJRR). 20 return (oact.sajiandler), 21 }
156 Глава 5. Пример TCP-соединения клиент-сервер Упрощение прототипа функции при использовании typedef 2-3 Обычный прототип для функции signal усложняется наличием вложенных ско- бок: void (*signal(int signo void (*/wc)(int)))(int). Чтобы упростить эту запись, мы определяем тип Sigfunc в нашем заголовоч- ном файле unp.h следующим образом: typedef void Sigfunc(mt) указывая тем самым, что обработчики сигналов — это функции с целочисленным аргументом, ничего не возвращающие (voi d). Тогда прототип функции выглядит следующим образом: Sigfunc *signal(int signo Sigfunc ★func). Указатель на функцию, являющуюся обработчиком сигнала, — это второй ар- гумент функции и, в то же время, возвращаемое функцией значение. Установка обработчика 6 Элемент sajiandler структуры sigaction устанавливается равным аргументу tunc функции signal. Установка маски сигнала для обработчика 7 Posix позволяет нам задавать набор сигналов, которые будут блокированы при вызове обработчика сигналов. Любой блокируемый сигнал не может быть до- ставлен (deliver) процессу. Мы устанавливаем элемент sajnask равным пустому набору. Это означает, что во время работы обработчика дополнительные сигна- лы не блокируются. Posix гарантирует, что перехватываемый сигнал всегда бло- кирован, пока выполняется его обработчик. Установка флага SA_RESTART -17 Флаг SA_RESTART не является обязательным, и если он установлен, то систем- ный вызов, прерываемый этим сигналом, будет автоматически снова выполнен ядром. (В продолжении нашего примера мы более подробно поговорим о пре- рванных системных вызовах.) Если перехватываемый сигнал не является сигна- лом SIGALRM, мы задаем флаг SA RESTART, если он определен. (Причина, по которой сигнал SIGALRM выделяется в особый случай, состоит в том, что обычно цель его генерации — ввести временное ограничение в операцию ввода-вывода, как пока- зано в листинге 13.2. В этом случае мы хотим, чтобы блокированный системный вызов был прерван сигналом.) Более ранние системы, особенно SunOS 4.x, авто- матически перезапускают прерванный системный вызов по умолчанию и затем определяют дополнительный флаг SA_INTERRUPT. Если этот флаг задан, мы уста- навливаем его при перехвате сигнала SIGALRM. Вызов функции sigaction 3-20 Мы вызываем функцию sigaction, азатем возвращаем старое действие сигнала как результат функции signal. В книге мы везде используем Лункнию si anal из листинга 5 5.
5.9. Обработка сигналов SIGCHLD 157 Семантика сигналов Posix Сведем воедино следующие моменты, относящиеся к обработке сигналов в сис- теме, совместимой с Posix. Однажды установленный обработчик сигналов остается установленным (в более ранних системах обработчик сигналов удалялся каждый раз по вы- полнении). На время выполнения функции-обработчика сигнала доставляемый сигнал блокируется. Более того, любые дополнительные сигналы, заданные в наборе сигналов sajnask, переданном функции sigaction при установке обработчика, также блокируются. В листинге 5.5 мы устанавливаем sajnask равным пусто- му набору, что означает, что никакие сигналы, кроме перехватываемого, не блокируются. % Если сигнал генерируется один или несколько раз, пока он блокирован, то обычно после разблокирования он доставляется только один раз, то есть по умолчанию сигналы Unix не устанавливаются в очередь. Пример мы рассмот- рим в следующем разделе. Стандарт Posix реального времени 1003.1b опреде- ляет набор надежных сигналов, которые помещаются в очередь, но в этой кни- ге мы их не используем. S Существует возможность выборочного блокирования и разблокирования на- бора сигналов с помощью функции sigprocmask. Это позволяет нам защитить критическую область кода, не допуская перехватывания определенных сигна- лов во время ее выполнения. 5.9. Обработка сигналов SIGCHLD Назначение состояния зомби — сохранить информацию о дочернем процессе, чтобы родительский процесс мог ее впоследствии получить. Эта информация включает идентификатор дочернего процесса, статус завершения и данные об использовании ресурсов (время процессора, память и т. д.). Если у завершающе- гося процесса есть дочерний процесс в зомбированном состоянии, идентифика- тору родительского процесса всех зомбированных дочерних процессов присваи- вается значение 1 (процесс i m t), что позволяет унаследовать дочерние процессы и сбросить их (то есть процесс imt будет ждать (wait) их завершения, благодаря чему будут удалены зомби). Некоторые системы Unix в столбце COMMAND выводят для зомбированных процессов значение <defunct>. Обработка зомбированных процессов Очевидно, что нам не хотелось бы оставлять процессы в виде зомби. Они занима- ют место в ядре, и в конце концов у нас может не остаться никаких процессов, кроме зомбированных. Когда мы выполняем функцию fork для дочерних процес- сов, необходимо с помощью функции wait дождаться их завершения, чтобы они не превратились в зомби. Для этого мы устанавливаем обработчик сигналов для перехватывания сигнала SIGCHLD и внутри обработчика вызываем функцию wait. (Функции wait и waitpid мы опишем в разделе 5.10.)
158 Глава 5. Пример TCP-соединения клиент-сервер Signal(SIGCHLD. sig_chld). в листинге 5.1, после вызова функции 11 sten. (Необходимо сделать это за некото- рое время до вызова функции fork для первого дочернего процесса, причем толь- ко один раз.) Затем мы определяем обработчик сигнала — функцию sig_chld, пред- ставленную в листинге 5.6. Листинг 5.6. Версия обработчика сигнала SIGCHLD, вызывающая функцию wait (усовершенствованная версия находится в листинге 5.8) //tcpcliserv/sigchldwait с 1 include “unp h” 2 void 3 sig_chld(int signo) 4 { 5 pid_t pid 6 int stat. 7 pid = wait(&stat). 8 printfCchild 8d terminated\en' pid). 9 return. 10 ) ВНИМАНИЕ -------------------------------------------------------------------------- В обработчике сигналов не рекомендуется вызов стандартных функций ввода-вывода, таких как pnntf, по причинам, изложенным в разделе 11.14. В данном случае мы вызы- ваем функцию pnntf как средство диагностики, чтобы увидеть, когда завершается до- черний процесс. В системах System V7 и Unix 98 дочерний процесс не становится зомби, если процесс задает действие SIG_IGN для SIGCHLD. К сожалению, это верно только для System V7 и Unix 98. В Posix 1 прямо сказано, что такое поведение этим стандартом не преду- смотрено. Переносимый способ обработки зомби состоит в том, чтобы перехватывать сигналы SIGCHLD и вызывать функцию wait или waitpid. Если мы откомпилируем в Solaris 2.5 программу, представленную в листин- ге 5.1, вызывая функцию Signal с нашим обработчиком sig_chld, и будем исполь- зовать функцию signal из системной библиотеки (вместо нашей версии, показан- ной в листинге 5 5), то получим следующее: solans % tcpserv02 & [2] 16939 solans % tcpcliOl 127.0.0.1 hi there hi there 'D child 16942 terminated accept error Interrupted system call запускаем сервер в фоновом режиме затем клиент набираем эту строку и она отражается сервером вводим символ конца файла функция pnntf из обработчика сигнала выводит эту строку но функция main преждевременно прекращает выполнение Последовательность шагов в этом примере такова: 1. Мы завершаем работу клиента, вводя символ EOF. TCP клиента посылает сегмент FIN серверу, и сервер отвечает сегментом АСК. 2. Получение сегмента FIN доставляет EOF ожидающей функции readline до- чернего процесса. Дочерний процесс завершается.
5.9. Обработка сигналов SIGCHLD 159 3. Родительский процесс блокирован в вызове функции accept, когда доставля- ется сигнал SIGCHLD. Функция sig_chld (наш обработчик сигнала) выполняет- ся, функция wait получает PID дочернего процесса и статус завершения, по- сле чего из обработчика сигнала вызывается функция printf. Обработчик сигнала возвращает управление. 4. Поскольку сигнал был перехвачен родительским процессом, в то время как родительский процесс был блокирован в системном вызове (функция accept), ядро заставляет функцию accept возвратить ошибку EINTR (прерванный сис- темный вызов). Родительский процесс не обрабатывает эту ошибку коррект- но (см. листинг 5.1), поэтому функция main преждевременно завершается. Цель данного примера — показать, что при написании сетевых программ, пе- рехватывающих сигналы, необходимо получать информацию о прерванных сис- темных вызовах и обрабатывать их. В этом специфичном для Solaris 2.5 примере функция signal из стандартной библиотеки С не приводит к автоматическому перезапуску прерванного вызова ядром, то есть флаг SA_RESTART, установленный нами в листинге 5.5, не устанавливается функцией signal из системной библио- теки. Некоторые другие системы автоматически перезапускают прерванный сис- темный вызов. Если мы запустим тот же пример в 4.4BSD, используя ее библио- течную версию функции si gnal, ядро перезапустит прерванный системный вызов и функция accept не возвратит ошибки. Одна из причин, по которой мы опреде- ляем нашу собственную версию функции signal и используем ее далее, — реше- ние этой потенциальной проблемы, возникающей в различных операционных системах (см. листинг 5.5). Кроме того, мы всегда программируем явную функцию return для наших об- работчиков сигналов (см. листинг 5.6), даже если функция ничего не возвращает (void), чтобы избежать предупреждений о возможном прерывании системного вызова возвратом. Обработка прерванных системных вызовов Термином медленный системный вызов (slow system call), введенным при описа- нии функции accept, мы будем обозначать любой системный вызов, который мо- жет быть заблокирован навсегда. Такой системный вызов может никогда не за- вершиться. В эту категорию попадает большинство сетевых функций. Например, пет никакой гарантии, что вызов функции accept сервером когда-нибудь будет завершен, если нет клиентов, которые соединятся с сервером. Аналогично вызов нашим сервером функции read (через readl i пе) в листинге 5.2 никогда не возвра- тит управление, если клиент никогда не пошлет серверу строку для отражения. Другие примеры медленных системных вызовов — чтение и запись в случае про- граммных каналов и терминальных устройств. Важным исключением является дисковый ввод-вывод, который обычно завершается возвращением управления вызвавшему процессу (в предположении, что не происходит фатальных аппарат- ных ошибок). Основное применяемое здесь правило связано с тем, что когда процесс, бло- кированный в медленном системном вызове, перехватывает сигнал, а затем обра- ботчик сигналов завершает работу, системный вызов может возвратить ошибку
160 Глава 5. Пример TCP-соединения клиент-сервер EINTR. Некоторые ядра автоматически перезапускают некоторые прерванные си- стемные вызовы. Для обеспечения переносимости программ, перехватывающих сигналы (большинство параллельных серверов перехватывает сигналы SIGCHLD), следует учесть, что медленный системный вызов может возвратить ошибку EINTR. Проблемы переносимости связаны с приведенными выше «могут» и «некото- рые» и тем фактом, что поддержка флага Posix SA RESTART не является обязатель- ной. Даже если реализация поддерживает флаг SA RESTART, не все прерванные си- стемные вызовы могут автоматически перезапуститься. Например, большинство реализаций, происходящих от Беркли, никогда автоматически не перезапускают функцию sei ect, а некоторые из этих реализаций никогда не перезапускают функ- ции accept и recvfrom. Чтобы обработать прерванный вызов функции accept, мы изменяем вызов функции accept, приведенной в листинге 5.1, в начале цикла for следующим об- разом: for ( ) { clilen = sizeof(cliaddr) if ( (connfd = accept(listenfd. (SA *) &cliaddr &clilen)) < 0) { if (errno == EINTR) continue. /* назад в for() */ else err_sys( "accept error") } Обратите внимание, что мы вызываем функцию accept, а не функцию-обертку Accept, поскольку мы должны обработать неудачное выполнение функции само- стоятельно. В этой части кода мы сами перезапускаем прерванный системный вызов. Это допустимо для функции accept и таких функций, как read, write, select и open. Но есть функция, которую мы не можем перезапустить самостоятельно, — это функция connect. Если она возвращает ошибку EINTR, мы не можем снова вызвать ее, поскольку в этом случае немедленно возвратится еще одна ошибка. Когда функ- ция connect прерывается перехваченным сигналом и не перезапускается автома- тически, нужно вызвать функцию sei ect, чтобы дождаться завершения соедине- ния (см. раздел 15.3). 5.10. Функции wait и waitpid В листинге 5.7 мы вызываем функцию wait для обработки завершенного дочер- него процесса. #include <sys/wait h> pid_t wait(int *statloc), pid_t waitpid(pid_t pid int *statloc int options) Обе функции возвращают ID процесса в случае успешного выполнения -1 в случае ошибки Обе функции, и wait, и waitpid, возвращают два значения. Возвращаемое зна- чение каждой из этих функций — это идентификатор завершенного дочернего процесса, а через указатель st at 1 ос передается статус завершения дочернего про-
5.10. Функции wait и waitpid 161 цесса (целое число). Для проверки статуса завершения можно вызвать три мак- роса, которые сообщают нам, что произошло с дочерним процессом: дочерний процесс завершен нормально, уничтожен сигналом пли только приостановлен программой управления заданиями (job-control). Дополнительные макросы по- зволяют получить состояние выхода дочернего процесса, а также значение сиг- нала, уничтожившего или остановившего процесс. В листинге 14.8 мы использу- ем макроопределения WIFEXITED и WEXITSTATUS. Если у процесса, вызывающего функцию wait, нет завершенных дочерних про- цессов, но есть один или более выполняющихся, функция wait блокируется до тех пор, пока первый из дочерних процессов не завершится. Функция waitpid предоставляет более гибкие возможности выбора ожидае- мого процесса и его блокирования. Прежде всего, в аргументе pid задается иден- тификатор процесса, который мы будем ожидать. Значение -1 говорит о том, что нужно дождаться завершения первого дочернего процесса. (Существуют и дру- гие значения идентификаторов процесса, но здесь они нам не понадобятся.) Аргумент options позволяет задавать дополнительные параметры. Наиболее об- щеупотребительным является параметр WNOHANG: он сообщает ядру, что не нужно выполнять блокирование, если нет завершенных дочерних процессов. Различия между функциями wait и waitpid Теперь мы проиллюстрируем разницу между функциями wait и waitpid, исполь- зуемыми для сброса завершенных дочерних процессов. Для этого мы изменим код нашего клиента TCP так, как показано в листинге 5.7. Клиент устанавливает пять соединений с сервером, а затем использует первое из них (sockfdt0]) в вызо- ве функции str_cli. Несколько соединений мы устанавливаем для того, чтобы породить от параллельного сервера множество дочерних процессов, как показа- но на рис. 5.2. Клиент Рис. 5.2. Клиент, установивший пять соединений с одним и тем же параллельны^! сервером Листинг 5.7. Клиент TCP, устанавливающий пять соединений с сервером //tcpcliserv/tcpcliO4 с 1 #include "unp h" 2 int 3 main(int argc. char **argv) 4 { продолжение
162 Глава 5. Пример TCP-соединения клиент-сервер Листинг 5.7(продолжение) 5 int 1. sockfd[5], 6 struct sockaddr_in servaddr, 7 if (argc !- 2) 8 err_quit("usage. tcpcli <IPaddress>“). 9 for (i - 0. i < 5. i++) { 10 sockfdti] - Socket(AF_INET, SOCK_STREAM. 0): 11 Dzero(&servaddr, sizeof(servaddr)), 12 servaddr sinjamily - AFJNET, 13 servaddr sin_port - htons(SERV_P0RT). 14 I net_pton( AFJNET. argv[l], &servaddr sin_addr), 15 Connect(sockfd[i], (SA *) &servaddr. sizeof(servaddr)) 16 } 17 str_cli(stdin. sockfd[0]). /* эта функция выполняет все необходимые действия для формирования запроса клиента*/ 18 exit(0). 19 } Когда клиент завершает работу, все открытые дескрипторы автоматически закрываются ядром (мы не вызываем функцию cl ose, а пользуемся только функ- цией exit) и все пять соединений завершаются приблизительно в одно и то же время. Это вызывает отправку пяти сегментов FIN, по одному на каждое соеди- нение, что, в свою очередь, вызывает примерно одновременное завершение всех пяти дочерних процессов. Это приводит к доставке пяти сигналов SIGCHLD прак- тически в один и тот же момент, что показано на рис. 5.3. Рис. 5.3. Клиент завершает работу, закрывая все пять соединений и завершая все пять дочерних процессов Доставка множества экземпляров одного и того же сигнала вызывает пробле- му, к рассмотрению которой мы и приступим.
5.10. Функции wait и waitpid 163 Сначала мы запускаем сервер в фоновом режиме, азатем — новый клиент. Наш сервер, показанный в листинге 5.1, несколько модифицирован — теперь в нем вызывается функция signal для установки обработчика сигнала SIGCHLD, приве- денного в листинге 5.6. bsdi * tcpserv03 & [1] 21282 bsdi Ж tcpcl104 206.62.226.35 hello мы набираем эту строку hello и она отражается сервером AD мы набираем символ конца файла child 21288 terminated выводится сервером Первое, что мы можем заметить, — данные выводит только одна функция printf, хотя мы предполагаем, что все пять дочерних процессов должны завер- шиться. Если мы выполним программу ps, то увидим, что другие четыре дочер- них процесса все еще существуют как зомби. PID ТТ STAT TIME COMMAND 21282 Р1 S 0 00 09 tcpserv03 21284 Р1 Z 0 00 00 (tcpserv03) 21285 Р1 Z 0 00 00 (tcpserv03) 21286 Р1 z - 0 00 00 (tcpserv03) 21287 Р1 Z 0 00 00 (tcpserv03) Установки обработчика сигнала и вызова функции wait из этого обработчика недостаточно для предупреждения появления зомби. Проблема состоит в том, что все пять сигналов генерируются до того как выполняется обработчик сигна- ла, и вызывается он только один раз, поскольку сигналы Unix обычно не помеща- ются в очередь. Более того, эта проблема является недетерминированной. В при- веденном примере с клиентом и сервером на одном и том же узле обработчик сигнала выполняется один раз, оставляя четыре зомби Но если мы запустим кли- ент и сервер на разных узлах, то обработчик сигналов, скорее всего, выполнится дважды: один раз в результате генерации первого сигнала, а поскольку другие четыре сигнала приходят во время выполнения обработчика, он вызывается по- вторно тоже только один раз. При этом остаются три зомби. Но иногда в зависи- мости от точного времени получения сегментов FIN па узле сервера обработчик сигналов может выполниться три или даже четыре раза. Корректным решением будег вызвать функцию wai tpid вместо wait.В листин- ге 5.8 представлена версия нашей функции sig_chld, корректно обрабатывающая сигнал SIGCHLD. Эта версия работает, потому что мы вызываем функцию waitpid в цикле, получая состояние любого из дочерних процессов, которые завершились. Необходимо задать параметр WNOHANG: это указывает функции waitpid, что не нуж- но блокироваться, если существуют выполняемые дочерние процессы, которые еще не завершились. В листинге 5.6 мы не могли вызвать функцию wait в цикле, поскольку нет возможности предотвратить блокирование функции wait при на- личии выполняемых дочерних процессов, которые еще не завершились. В листинге 5.9 показана окончательная версия нашего сервера. Он корректно обрабатывает возвращение ошибки EINTR из функции accept и устанавливает об- работчик сигнала (листинг 5.8), который вызывает функцию waitpid для всех за- вершенных дочерних процессов.
164 Глава 5, Пример TCP-соединения клиент-сервер Листинг 5.8. Окончательная (корректная) версия функции sig_chld, вызывающая функцию waitpid //tcpcliserv/sigchldwaitpid.с 1 include "unp h” 2 3 4 5 6 void sig chld(int signo) { pid_t pid. int stat. 7 8 9 10 while ( (pid = waitpid(-l. &stat. WNOHANG)) > 0) printfl"chiId %d terminated\en”. pid) return. } Листинг 5.9. Окончательная (корректная) версия TCP-сервера, обрабатывающего ошибку EINTR функции accept //tcpcliserv/tcpserv04 с 1 #include "unp h" 2 3 4 5 6 7 8 9 int mainOnt argc. char **argv) { int listenfd. connfd. pid_t childpid. socklen_t cl lien, , struct sockaddrjn cliaddr. servaddr: void sig_chld(int). 10 listenfd = Socket(AF_INET. SOCKJ5TREAM. 0): 11 12 13 14 bzerol&servaddr. sizeof(servaddr)). servaddr sin_family = AF_INET. servaddr sin_addr s_addr = htonl(INADDR_ANY). servaddr sin_port = htons(SERV_PORT). 15 Bindllistenfd. (SA*) &servaddr. sizeof(servaddr)): 16 Listen(listenfd. LISTENQ), 17 Signal (SIGCHLD. sig_chld). /* нужно вызвать waitpidO */ 18 19 20 21 22 23 24 25 26 27 28 29 for (..) { clilen = sizeof(cliaddr). if ( (connfd = accept(listenfd. (SA *) 8cliaddr. 8clilen)) < 0) { if (errno =*= EINTR) continue. /* назад к for() */ else err sysC'accept error”). } if ( (childpid = ForkO) = 0) { /* дочерний процесс */ Close(listenfd). /* закрываем прослушиваемый сокет */ str_echo(connfd). 7* обрабатываем запрос */ exit(0).
5.11. Прерывание соединения перед завершением функции accept 165 30 } 31 Close(connfd); /* родитель закрывает присоединенный сокет */ 32 } 33 } Целью этого раздела было продемонстрировать три сценария, которые могут встретиться в сетевом программировании. 1. При выполнении функции fork, порождающей дочерние процессы, следует перехватить сигнал SIGCHLD. 2. При перехватывании сигналов мы должны обрабатывать прерванные систем- ные вызовы. 3. Обработчик сигналов SIGCHLD должен быть создан корректно с использовани- ем функции waitpid, чтобы не допустить появления зомби. Окончательная версия нашего сервера TCP (см. листинг 5.9) вместе с обра- ботчиком сигналов SIGCHLD в листинге 5.8 обрабатывает все три сценария. 5. 11. Прерывание соединения перед завершением функции accept Существует другое условие, аналогичное прерванному системному вызову, при- мер которого был описан в предыдущем разделе. Оно может привести к возвраще- нию функцией accept нефатальной ошибки, в случае чего следует заново вызвать функцию accept. Последовательность пакетов, показанная на рис. 5.4, встречает- ся на загруженных серверах (эта последовательность типична для загруженных web-серверов). Рис. 5.4. Получение сегмента RST для состояния соединения ESTABLISHED перед вызовом функции accept Трехэтапное рукопожатие TCP завершается, устанавливается соединение, а за- тем TCP клиента посылает сегмент RST. На стороне сервера соединение ставит- ся в очередь в ожидании вызова функции accept, который последует при получе- нии сегмента RST процессом сервера. Спустя некоторое время процесс сервера вызывает функцию accept.
166 Глава 5. Пример TCP-соединения клиент-сервер ПРИМЕЧАНИЕ------------------------------------------------------ Этот сценарий очень просто имитировать. Запустите сервер, который должен вызвать функции socket, bind и listen, а затем перед вызовом функции accept переведите сервер па короткое время в состояние ожидания Пока процесс сервера находится в состоя- нии ожидания, запустите клиент, который вызовет функции socket и connect. Как только функция connect завершится, установите параметр сокета SO_LINGER, чтобы сгене- рировать сегмент RST (который мы описываем в разделе 7.5 и демонстрируем в лис- тинге 15.14), и завершите процессы. К сожалению, принцип обработки прерванного соединения зависит от реали- зации. Реализации, происходящие от Беркли, обрабатывают прерванное соеди- нение полностью внутри ядра прозрачно для процесса сервера. Большинство ре- ализаций SVR4, однако, возвращают процессу ошибку, и эта ошибка зависит от реализации. При этом переменная еггпо принимает значение EPROPTO (ошибка протокола), между тем как в Posix. 1g указано, что должна возвращаться ошибка ECONNABORTED (прерывание соединения). Posix. 1g иначе определяет эту ошибку, так как ошибка EPROTO возвращается еще и в том случае, когда в подсистеме пото- ков происходят какие-либо фатальные события, имеющие отношение к протоко- лу. Возвращение той же ошибки для нефатального прерывания установленного соединения клиентом приводит к тому, что сервер не знает, вызывать снова функ- цию accept или нет. В случае ошибки ECONNABORTED сервер может игнорировать ошибку и снова вызывать функцию accept. ПРИМЕЧАНИЕ ----------------------------------------------------- В Solans 2.6 реализованы требования Posix.lg. В [105] описана обработка этой ошибки в Беркли-ядрах (Berkeley-denvcd kernels), ко- торые никогда не передают ее процессу Обработка RST с вызовом функции tcp_close представлена на с. 964 [105]. Эта функция вызывает функцию in_pcbdetach со с. 897 [105], которая, в свою очередь, вызывает функцию sofree со с. 719 [ 105] Функция sofree [105, с. 473] обнаруживает, что сокет все еще находится в очереди полностью установ- ленных соединений прослушиваемого сокета Она удаляет этот сокет из очереди и ос- вобождает сокет. Когда сервер, наконец, вызовет функцию accept, он не сможет узнать, что установленное соединение было удалено из очереди Мы вернемся к подобным прерванным соединениям в разделе 15.6 и покажем, какие проблемы они могут порождать совместно с функцией select и прослуши- ваемым сокетом в нормальном режиме блокирования. 5. 12. Завершение процесса сервера Теперь мы запустим соединение клиент-сервер и уничтожим дочерний процесс сервера. Это симулирует сбой (crash) процесса сервера, благодаря чему мы смо- жем выяснить, что происходит с клиентом в подобных ситуациях. (Следует точ- но различать сбой процесса сервера, который мы рассмотрим здесь, и сбой на са- мом узле сервера, о котором речь пойдет в разделе 5.14.) События развиваются так: 1. Мы запускаем сервер и клиент на разных узлах и вводим на стороне клиента одну строку, чтобы проверить, все ли в порядке. Строка отражается дочерним процессом сервера.
5,12. Завершение процесса сервера 167 2. Мы находим идентификатор дочернего процесса сервера и уничтожаем его с помощью программы kill. Одним из этапов завершения процесса является закрытие всех открытых дескрипторов в дочернем процессе. Это вызывает отправку сегмента FIN клиенту, и TCP клиента отвечает сегментом АСК. Это первая половина завершения соединения TCP. 3. Родительскому процессу сервера посылается сигнал SIGCHLD, и он корректно обрабатывается (см. листинг 5.9). 4. С клиентом ничего не происходит. TCP клиента получает от TCP сервера сег- мент FIN и отвечает сегментом АСК, но проблема состоит в том, что клиентский процесс блокирован в вызове функции fgets в ожидании строки от терминала. 5. Запуск программы netstat на этом шаге из другого окна на стороне клиента показывает состояние клиентского сокета: solans % netstat | grep 9877 Local Address Remote Address Swind Send-Q Rwind Recv-Q State solans 34673 bsdi 9877 8760 0 8760 0 CLOSE_WAIT (Здесь мы впервые показываем вывод программы netstat в Solaris, поэтому мы добавили строку-заголовок. Формат несколько отличается от формата вывода BSD, но информация та же.) Мы также запускаем netstat из другого окна на стороне сервера: bsdi % netstat | grep 9877 tep 0 0 bsdi 9877 solans 34673 FIN_WAIT_2 Как видите, согласно рис. 2.4, осуществилась половина последовательности завершения соединения TCP. 6. Мы можем снова ввести строку на стороне клиента. Вот что происходит на стороне клиента (начиная с шага 1): solans % tcpcliOl 206.62.226.35 запускаем клиент hello первая строка, которую мы ввели hello она корректно отражается теперь мы уничтожаем(к111) дочерний процесс сервера на узле сервера another line затем мы вводим следующую строку на стороне клиента str_cl1 server terminated prematurely Когда мы вводим следующую строку, функция str_cli вызывает функцию wri ten, и TCP клиента отправляет данные серверу. TCP это допускает, посколь- ку получение сегмента FIN протоколом TCP клиента только указывает, что процесс сервера закрыл свой конец соединения и больше не будет отправлять данные. Получение сегмента FIN не сообщает протоколу TCP клиента, что процесс сервера завершился (хотя в данном случае он завершился). Мы вер- немся к этому вопросу в разделе 6.6, когда будем говорить о половинном за- крытии TCP. Когда TCP сервера получает данные от клиента, он отвечает, посылая сегмент RST, поскольку процесс, у которого был открытый сокет, завершился. Мы мо- жем проверить, что этот сегмент RST отправлен, просмотрев пакеты с помощью поогпаммы tcodumo.
168 Глава 5. Пример TCP-соединения клиент-сервер 7. Однако процесс клиента не увидит сегмента RST, поскольку он вызывает функ- цию read 11 ne сразу же после вызова функции wri ten, и readl i ne сразу же воз- вращает 0 (признак конца файла) по причине того, что на шаге 2 был получен сегмент FIN. Наш клиент не предполагает получать признак конца файла на этом этапе (см. листинг 5.3), поэтому он завершает работу, сообщая об ошиб- ке Server terminated prematurely (Сервер завершил работу преждевременно). 8. Когда клиент завершает работу (вызывая функцию err_quit в листинге 5.4), все его открытые дескрипторы закрываются. ПРИМЕЧАНИЕ---------------------------------------------------------- Шаги описанной последовательности действий также зависят от синхронизации вре- мени. Когда мы запускаем сервер и клиент па разных узлах, отправка данных от клиен- та к серверу (следующая строка в примере) и получение клиентом RST сервера зани- мают несколько миллисекунд. Поэтому клиентский вызов функции readlinc возвращает нуль, так как сегмент FIN, полученный рапыие, уже готов к тому, чтобы быть считан- ным. Но если мы запустим клиент и сервер па одном узле или если нам нужно сделать незначительную паузу перед вызовом функции readlinc на стороне клиента, то полу- ченный от сервера сегмент RST имеет приоритет по отношению к сегменту FIN, полу- ченному раньше. В результате этого функция readhne возвратит ошибку, а значение переменной еггпо будет равно ECONNRESET (Connection reset by the peer — Соеди- нение сброшено собеседником). Проблема заключается в том, что клиент блокируется в вызове функции fgets, когда сегмент FIN приходит на сокет. Клиент в действительности работает с дву- мя дескрипторами — дескриптором сокета и дескриптором ввода пользователя, и поэтому он должен блокироваться при вводе из любого источника (сейчас в функции str cl 1 он блокируется при вводе только из одного источника). Обес- печить подобное блокирование — это одно из назначений функций sei ect и pol 1, о которых рассказывается в главе 6. Когда в разделе 6.4 мы перепишем функцию str_cl 1, то как только мы уничтожим с помощью программы kill дочерний про- цесс сервера, клиенту будет отправлено уведомление о полученном сегменте FIN. 5. 13. Сигнал SIGPIPE Что происходит, если клиент игнорирует возвращение ошибки из функции readl i ne и отсылает следующие данные серверу? Это может произойти, если, например, клиенту нужно выполнить две операции по отправке данных серверу перед счи- тыванием данных от сервера, причем первая операция отправки данных вызыва- ет RST. Применяется следующее правило: когда процесс производит запись в сокет, получивший сегмент RST, процессу посылается сигнал SIGPIPE. По умолчанию действием этого сигнала является завершение процесса, так что процесс должен перехватить сигнал, чтобы не произошло непроизвольного завершения. Если процесс либо перехватывает сигнал и возвращается из обработчика сиг- нала, либо игнорирует сигнал, то операция записи возвращает ошибку EPIPE. Чтобы увидеть, что происходит с сигналом SIGPIPE, изменим код нашего кли- ента так, как показано в листинге 5.10.
5.13. Сигнал SIGPIPE 169 ПРИМЕЧАНИЕ-------------------------------------------------------- Часто задаваемым вопросом (FAQ) в Usenet является такой: как получить этот сигнал при первой, а не при второй операции записи? Это невозможно. Как следует из приве- денных выше рассуждений, первая операция записи выявляет cci мепт RST, а вторая — сигнал. Если запись в сокет, получивший сегмент FIN, допускается, то запись в сокет, получивший сегмент RST, является ошибочной. Листинг 5.10. Функция str_ch, дважды вызывающая функцию writen //tcpcliserv/str_clill с 1 #include "unp h" 2 void 3 str_cli(FILE *fp int sockfd) 4 { 5 char sendline[MAXLINE], recvline[MAXLINE], 6 while (Fgets(sendline MAXLINE fp) '= NULL) { 7 Writen(sockfd. sendline. 1). 8 sleep(l), 9 Writen(sockfd, sendline + 1. strlen(sendline) - 1). 10 if (Readlinelsockfd recvline. MAXLINE) == 0) 11 err_quit('str_cli server terminated prematurely"). 12 Fputs(recvline stdout) 13 } 14 } 7-9 Все изменения, которые мы внесли, — это повторный вызов функции writen: сначала в сокет записывается первый байт данных, за этим следует пауза в 1 се- кунду и далее идет запись остатка строки. Наша цель — выявить сегмент RST при первом вызове функции writen и генерировать сигнал SIGPIPE при втором вызове. Если мы запустим клиент на нашем узле BSD/OS, мы получим: bsdi X tcpclill 206.62.226.34 hi there мы вводим эту строку hi there и она отражается сервером здесь мы завершаем дочерний процесс сервера bye затем мы вводим эту строку bsdi % echo $? что возвращает KornShell7 269 269 = 256 + 13 bsdi % CBgrep SIGPIPE /usr/include/sys/signal.h #define SIGPIPE 13 /* write on a pipe with no one to read it */ Мы запускаем клиент, вводим одну строку, видим, что строка отражена кор- ректно, и затем завершаем дочерний процесс сервера на узле сервера. Затем мы вводим другую строку (bye), но ничего ие отражается, и мы получаем только при- глашение командной строки. Поскольку действием сигнала SIGPIPE по умолча- нию является завершение процесса без записи дампа, программа KornShell ничего не выводит. Это проблема, встречающаяся в программах, завершаемых с помощью сигнала SIGPIPE: даже интерпретатор командной строки ничего не выводит, так что нет возможности узнать, что произошло.
170 Глава 5. Пример TCP-соединения клиент-сервер Чтобы вывести значение, возвращаемое интерпретатором команд, нам при- дется выполнить команду echo $?. Это значение равно 269. Затем мы выводим численное значение константы SIGPIPE и видим, что возвращаемое значение Korn- Shell равно 256 плюс номер сигнала. Но если мы выполним программу в Digital Unix 4.0, Solaris 2.5 или UnixWare 2.12, то возвращаемое значение KornShell бу- дет равно 141, то есть 128 плюс 13. ПРИМЕЧАНИЕ------------------------------------------------------- Версия 11/16/88 KornShell возвращала сумму числа 128 и номера сигнала, в то время как более новые версии возвращают сумму числа 256 и номера сигнала. Posix 2 опреде- ляет лишь то, что возвращаемое значение должно быть больше 128. Другое интерпре- таторы команд могут возвращать различные значения. Рекомендуемый способ обработки сигнала SIGPIPE зависит оттого, что прило- жение собирается делать, когда получает этот сигнал. Если ничего особенного делать не нужно, проще всего установить действие SIG_IGN, предполагая, что по- следующие операции вывода перехватят ошибку EPIPE и завершатся. Если при появлении сигнала необходимо проделать специальные действия (возможно, за- пись в системный журнал), то сигнал следует перехватить и выполнить требуе- мые действия в обработчике сигнала. Однако отдавайте себе отчет в том, что если используется множество сокетов, то при доставке сигнала мы не получаем ин- формации о том, на каком сокете произошла ошибка. Если нам нужно знать, ка- кая именно операция write вызвала ошибку, следует либо игнорировать сигнал, либо вернуть управление из обработчика сигнала и обработать ошибку EPIPE из функции write. 5.14. Сбой на узле сервера В следующем примере мы проследим за тем, что происходит в случае сбоя на узле сервера. Чтобы мы могли имитировать эту ситуацию, клиент и сервер долж- ны работать на разных узлах. Затем мы запускаем сервер, запускаем клиент, вво- дим строку на стороне клиента для проверки работоспособности соединения, от- соединяем узел сервера от сети и вводим другую строку на стороне клиента. Этот сценарий охватывает также ситуацию, в которой узел сервера становится недо- ступен во время отправки данных клиентом (например, после того как соедине- ние установлено, выключается некий промежуточный маршрутизатор). События развиваются следующим образом: 1. Когда происходит сбой на узле сервера, по существующим сетевым соедине- ниям от сервера не отправляется никакой информации, то есть мы считаем, что на узле происходит сбой, а не выключение компьютера оператором (что мы рассмотрим в разделе 5.16). 2. Мы вводим строку на стороне клиента, она записывается с помощью функции writen (см. листинг 5.3) и отправляется протоколом TCP клиента как сегмент данных. Затем клиент блокируется в вызове функции readl i пе в ожидании отраженного ответа. 3. Если мы понаблюдаем за сетью с помощью программы tepdump, то увидим, что TCP клиента последовательно осуществляет повторные передачи сегмента
5.15. Сбой и перезагрузка на узле сервера 171 данных, пытаясь получить сегмент АСК от сервера. В разделе 25.11 [105] по- казан типичный образец повторных передач TCP: реализации, происходящие от Беркли, делают попытки передачи сегмента данных 12 раз, ожидая около 9 минут перед прекращением попыток. Когда TCP клиента наконец прекра- щает попытки ретрансляции (считая, что узел сервера за это время не переза- гружался или что он все еще недоступен, если на узле сервера сбоя не было, но он был недоступен по сети), клиентскому процессу возвращается ошибка. Поскольку клиент блокирован в вызове функции readl т пе, он возвращает ошиб- ку. Если на узле сервера произошел сбой, и на все сегменты данных клиента не было ответа, будет возвращена ошибка ETIMEDOUT. Но если некий промежу- точный маршрутизатор определил, что узел сервера был недоступен, и отве- тил сообщением ICMP о недоступности получателя, клиент получит либо ошибку EHOSTUNREACH, либо ошибку ENETUNREACH. Хотя наш клиент в конце концов обнаруживает, что собеседник выключен или недоступен, бывает, что нужно определить это прежде чем пройдут условленные девять минут. Чтобы предусмотреть эту ситуацию, нужно поместить тайм-аут в вызов функции readl те, о чем рассказывается в разделе 13.2. В описанном сценарии сбой на узле сервера можно обнаружить только послав данные на этот узел. Если мы хотим обнаружить сбой на узле сервера, не посылая активно данные, требуется другая технология. Мы рассмотрим параметр сокета SO_KEEPALlVE в разделе 7.5 и некоторые функции проверки пульса (heartbeatfunctions) клиент-серверного соединения в разделе 21.5. 5.15. Сбой и перезагрузка на узле сервера В этом сценарии мы устанавливаем соединение между клиентом и сервером и за- тем считаем, что на узле сервера происходит сбой, после чего узел перезагружа- ется. В предыдущем разделе узел сервера был выключен, когда мы отправляли ему данные. Здесь же перед отправкой данных серверу узел сервера перезагру- зится. Простейший способ имитировать такую ситуацию — установить соедине- ние, отсоединить сервер от сети, выключить узел сервера и перезагрузить его, а затем снова присоединить узел сервера к сети. Как было сказано в предыдущем разделе, если клиент не посылает активно данные серверу, то он не узнает о произошедшем на узле сервера сбое. (При этом считается, что мы не используем параметр сокета SO_KEEPALIVE.) 1. Мы запускаем сервер, затем — клиент и вводим строку для проверки установ- ленного соединения. 2. Узел сервера выходит из строя и перезагружается. 3. Мы вводим строку на стороне клиента, которая посылается как сегмент дан- ных TCP на узел сервера. 4. Когда узел сервера перезагружается после сбоя, его TCP теряет информацию о существовавших до сбоя соединениях. Следовательно, TCP сервера отвеча- ет на полученный от клиента сегмент данных, посылая RST. 5. Наш клиент блокирован в вызове функции readl i пе, когда приходит сегмент RST, заставляющий функцию readl me возвратить ошибку ECONNRESET.
172 Глава 5. Пример TCP-соединения клиент-сервер Если для нашего клиента важно диагностировать выход из строя узла сервера даже когда клиент активно не посылает данные, то требуется другая технология (с использованием параметра сокета SO_KEEPALIVE или некоторых функций, про- веряющих наличие связи в клиент-серверном соединении). 5.16. Выключение узла сервера В двух предыдущих разделах рассматривался выход из строя узла сервера или недоступность узла сервера в сети. Теперь мы рассмотрим, что происходит, если узел сервера выключается оператором в то время, когда на этом узле выполняет- ся наш серверный процесс. Когда система Unix выключается, процесс i m t обычно посылает всем процес- сам сигнал SIGTERM (мы можем перехватить этот сигнал), ждет в течение некото- рого фиксированного времени (часто от 5 до 20 секунд), а затем посылает сигнал SIGKILL (который мы перехватить не можем) всем еще выполняемым процессам. Это дает всем выполняемым процессам короткое время для завершения работы. Если мы не перехватили сигнал SIGTERM и завершили выполнение процессов, ра- боту нашего сервера завершит сигнал SIGKILL. При завершении процесса закры- ваются все открытые дескрипторы, а затем мы проходим ту же последователь- ность шагов, что описывалась в разделе 5.12. Как отмечалось в разделе 5.12, в нашем клиенте следует использовать функцию sei ect или pol 1, чтобы клиент определил завершение процесса сервера, как только оно произойдет. 5.17. Итоговый пример TCP Прежде чем клиент и сервер TCP смогут взаимодействовать друг с другом, каж- дый из них должен определить пару сокетов для соединения: локальный IP-ад- рес, локальный порт, удаленный IP-адрес, удаленный порт. На рис. 5.5 мы схема- тически изображаем эти значения черными кружками. На этом рисунке ситуация представлена с точки зрения клиента. Удаленный IP-адрес и удаленный порт должны быть заданы клиентом при вызове функции connect. Два локальных зна- чения обычно выбираются ядром тоже при вызове функции connect. У клиента есть выбор: он может задать только одно из локальных значений или оба, выз- вав функцию bi nd перед вызовом функции connect, однако второй подход не- типичен. Как мы отмечали в разделе 4.10, клиент может получить два локальных значе- ния, выбранных ядром, вызвав функцию getsockname после установления соеди- нения. На рис. 5.6 показаны те же четыре значения, но с точки зрения сервера. Локальный порт (заранее известный порт сервера) задается функцией bi nd. Обычно сервер также задает в этом вызове универсальный IP-адрес, хотя может и ограничиться получением соединений, предназначенных для одного опреде- ленного локального интерфейса, путем связывания с IP-адресом, записанным без символов подстановки (то есть не универсальным). Если сервер связывается с универсальным IP-адресом на узле с несколькими сетевыми интерфейсами, он может определить локальный IP-адрес (указываемый как адрес отправителя
5.17. Итоговый пример TCP 173 socket () accept () Рис. 5.5. TCP-соединение клиент-сервер с точки зрения клиента listen () socket () accept!) bind() Рис. 5.6. TCP-соединение клиент-сервер с точки зрения сервера в исходящих пакетах) при помощи вызова функции getsockname после установле- ния соединения (см. раздел 4.10). Два значения удаленного адреса возвращают- ся серверу при вызове функции accept. Как мы отмечали в разделе 4.10, если сер- вером, вызывающим функцию accept, выполняется с помощью функции ехес другая программа, то эта программа может вызвать функцию getpeername, чтобы при необходимости определить IP-адрес и порт клиента.
174 Глава 5. Пример TCP-соединения клиент-сервер 5.18. Формат данных В нашем примере сервер никогда не исследует запрос, который он получает от клиента. Сервер лишь читает все данные, включая символ перевода строки, и от- правляет их обратно клиенту, отслеживая только разделитель строк. Это исклю- чение, а не правило, так как обычно необходимо принимать во внимание формат данных, которыми обмениваются клиент и сервер. Пример: передача текстовых строк между клиентом и сервером Изменим наш сервер так, чтобы он, по-прежнему принимая текстовую строку от клиента, предполагал, что строка содержит два целых числа, разделенных пробе- лом, и возвращал сумму этих чисел. Функции шатл наших клиента и сервера оста- ются прежними, как и функция str_cli. Меняется только функция str_echo, что мы показываем в листинге 5.11. Листинг 5.11. Функция str_echo, суммирующая два числа //tcpcliserv/str_echo08 с 1 include 'unp h" 2 void 3 str_echo(int sockfd) 4 { 5 long argl arg2. 6 ssize_t n. 7 char line[MAXLINE] 8 for ( .) { 9 if ( (n = Readlinetsockfd. line. MAXLINE)) = 0) 10 return. /* соединение закрывается удаленным концом */ 11 if (sscanfdine. "BldBld". 8argl &arg2) == 2) 12 snprintf(line, sizeof(line) "£ld\en' argl + arg2) 13 else 14 snpnntflline. sizeof(line) "input errorlen"). 15 n = strlent line) 16 Writentsockfd. line. n). 17 } 18 } 1-14 Мы вызываем функцию sscanf, чтобы преобразовать два аргумента из тексто- вых строк в целые числа типа 1 ong, а затем функцию snpri ntf для преобразования ре <\’льтата в текстовую строку. Эти клиент и сервер работают корректно вне зависимости от порядка байтов на их узлах. Пример: передача двоичных структур между клиентом и сервером Теперь мы изменим код клиента и сервера, чтобы передавать через сокет не тек- стовые стпоки. а двоичные значения. Мы увидим, что клиент и сеовео оаботают
5.18. Формат данных 175 некорректно, когда они запущены на узлах с различным порядком байтов или на узлах, не согласовывающих размер целых чисел типа long (см. табл. 1.5). Функции main наших клиента и сервера не изменяются. Мы определяем одну структуру для двух аргументов, другую структуру для результата и помещаем оба определения в наш заголовочный файл unp.h, представленный в листинге 5.12. В листинге 5.13 показана функция str_cl i. Листинг 5.12. Заголовочный файл unp.h //tcpcliserv/sum h 1 struct args { 2 long argl. 3 long arg2, 4 }• 5 struct result { 6 long sum. 7 }• Листинг 5.13. Функция str_cli, отправляющая два двоичных целых числа серверу //tcpcliserv/str_cli09 с 1 #include "unp h" 2 #include “sum h" 3 void 4 str_cli(FILE *fp. int sockfd) 5 { 6 char sendline[MAXLINE], 7 struct args args. 8 struct result result. 9 while (Fgets(sendline. MAXLINE, fp) l= NULL) { 10 if (sscanf(sendline ’^Id^ld", &args argl. Sargs arg2) '= 2) { 11 printfCinvalid input 2s". sendline), 12 continue 13 } 14 Writen(sockfd. &args, sizeof(args)). 15 if (Readn(sockfd. &result sizeof(result)) == 0) 16 err_quit("str_cli server terminated prematurely"): 17 printf(“Xld\en". result sum). 18 } 19 } 10-14 Функция sscanf преобразует два аргумента из текстовых строк в двоичные, и мы вызываем функцию writen для отправки структуры серверу. 15-17 Мы вызываем функцию readn для чтения ответа и выводим результат с помощью функции printf. В листинге 5.14 показана наша функция str_echo. Листинг 5.14. Функция str_echo, складывающая два двоичных целых числа //tcpcliserv/str_echo09 с 1 #include "unp h” 2 #include "sum h"
176 Глава 5 Пример TCP-соединения клиент-сервер Листинг 5.14 (продолжение) 3 void 4 str_echo(int sockfd) 5 { 6 ssize_t n 7 struct args args. 8 struct result result: 9 for ( ) { 10 if ( (n - Readntsockfd. &args sizeof(args))) — 0) 11 return /* соединение закрыто удаленным концом */ 12 result sum = args argl + args arg2 13 Writen(sockfd Sresult sizeof(result)) 14 } 15 } 14 Мы считываем аргументы при помощи вызова функции readn, вычисляем и за- поминаем сумму и вызываем функцию writen для отправки результирующей структуры обратно. Если мы запустим клиент и сервер на двух машинах с аналогичной архитекту- рой, — допустим, solans и sunos5 (см. рис. 1 7), — все будет работать нормально. Вот взаимодействие с клиентом: sunos5 % tcpcli09 206.62.226.33 11 22 мы вводим эти числа 33 а это ответ сервера -11 -44 -55 Но если клиент и сервер работают на машинах с разными архитектурами — например, сервер в системе Sparc solans, в которой используется обратный по- рядок байтов (big-endian), а клиент в системе Intel bsdi с прямым порядком бай- тов (little-endian), — результат будет неверным: bsdi % tcpcl109 206.62.226.33 1 2 мы вводим эти числа 3 и сервер дает правильный ответ -22 -77 потом мы вводим эти числа -16777314 и сервер дает неверный ответ Проблема заключается в том, что два двоичных числа передаются клиентом через сокет в формате с прямым порядком байтов, а сервер интерпретирует их как целые числа, записанные с обратным порядком байтов. Мы видим, что это допустимо для положительных целых чисел, но для отрицательных такой подход не срабатывает (см. рис. 5.1). Действительно, в подобной ситуации могут возник- нуть три проблемы: 1. Различные реализации хранят двоичные числа в различных форматах. Наи- более характерный пример — прямой и обратный порядок байтов, описанный в разделе 3.4. 2. Различные реализации могут хранить один и тот же тип данных языка С по- разному. Например, большинство 32-разрядных систем Unix используют 32 бита для типа 1 ong, ио 64-разрядные системы обычно используют 64 бита для того же типа данных (см табл. 1.5). Нет никакой гарантии, что типы short, л 1 nt или 1 ong имеют какой-либо определенный размер.
5 19. Резюме 177 3. Различные реализации по-разному упаковывают структуры в зависимости от числа битов, используемых для различных типов данных, и ограничений по выравниванию для данного компьютера. Следовательно, неразумно переда- вать через сокет двоичные структуры. Есть два общих решения проблемы, связанной с различными форматами данных: 1. Передавайте все численные данные как текстовые строки. Это то, что мы де- лали в листинге 5.11. При этом предполагается, что у обоих узлов один и тот же набор символов. 2. Явно определяйте двоичные форматы поддерживаемых типов данных (число битов и порядок байтов) и передавайте все данные между клиентом и серве- ром в этом формате. Пакеты удаленного вызова процедур (Remote Procedure Call, RPC) обычно используют именно эту технологию. В RFC 1832 [92] опи- сывается стандарт представления внешних данных (External Data Representa- tion, XDR), используемый с пакетом Sun RPC. 5.19. Резюме Первая версия наших эхо-клиента и эхо-сервера содержала около 150 строк (вклю- чая функции readl i пе и writen), но многие ее детали пришлось модифицировать. Первой проблемой, с которой мы столкнулись, было превращение дочерних про- цессов в зомби, и для обработки этой ситуации мы перехватывали сигнал SIGCHLD. Затем наш обработчик сигнала вызывал функцию waitpid, и мы показали, что должны вызывать именно эту функцию вместо более старой функции wait, по- скольку сигналы Unix не помещаются в очередь. В результате мы рассмотрели некоторые подробности обработки сигналов Posix, а за дополнительной инфор- мацией по этой теме вы можете обратиться к главе 10 [93] Следующая проблема, с которой мы столкнулись, состояла в том, что клиент не получал уведомления о завершении процесса сервера Мы видели, что TCP нашего клиента получал уведомление, но оно не доходило до клиентского про- цесса, поскольку он был блокирован в ожидании ввода пользователя. В главе 6 для обработки этого сценария мы будем использовать функцию sei ect или pol 1, чтобы ожидать готовности любого из множества дескрипторов вместо блокиро- вания при обращении к одному дескриптору. Мы также обнаружили, что если узел сервера выходит из строя, мы не можем определить это до тех пор, пока клиент не пошлет серверу какие-либо данные. Некоторые приложения должны узнавать об этом факте раньше, о чем мы пого- ворим далее, когда будем рассматривать в разделе 7.5 параметр сокета SOJCEEPALIVE, а в разделе 21.5 мы создадим набор функций проверки пульса (heartbeat functions) для клиент-серверного соединения. В нашем простом примере происходил обмен текстовыми строками, и посколь- ку от сервера не требовалось просматривать отражаемые им строки, все работало нормально. Отправка численных данных между клиентом и сервером может при- вести к ряду новых проблем, что и было продемонстрировано.
178 Глава 5. Пример TCP-соединения клиент-сервер Упражнения 1. Создайте сервер TCP на основе листингов 5.1 и 5.2 и клиент TCP на основе листингов 5.3 и 5.4. Запустите сервер, затем запустите клиент. Введите несколь- ко строк, чтобы проверить, что клиент и сервер работают. Завершите работу клиента, введя символ конца файла, и заметьте время. Используйте програм- му netstat на узле клиента для проверки того, что клиентский конец соедине- ния проходит состояние TIME_WAIT. Запускайте netstat примерно каждые 5 секунд, чтобы посмотреть, когда закончится состояние TIME_WAIT. Како- во время MSL для этой реализации? 2. Что происходит с нашим соединением клиент-сервер, если мы запускаем кли- ент и подключаем к стандартному потоку ввода двоичный файл? 3. В чем разница между нашим соединением клиент-сервер и использованием клиента Telnet для взаимодействия с нашим эхо-сервером? 4. В нашем примере в разделе 5.12 мы проверили, что первые два сегмента за- вершения соединения отправляются (сегмент FIN от сервера, на который затем клиент отвечает сегментом АСК) при просмотре состояний сокета с помощью программы netstat. Происходит ли обмен двумя последними сегментами (FIN от клиента, на который затем сервер отвечает сегментом АСК)? Если да, то когда? Если нет, то почему? 5. Что произойдет с примером, рассмотренным в разделе 5.14, если между шага- ми 2 и 3 мы перезапустим сервер на узле сервера? 6. Чтобы проверить, что происходит с сигналом SIGPIPE в разделе 5.13, измените листинг 5.3 следующим образом. Напишите обработчик сигнала для SIGPIPE, который будет просто выводить сообщение и возвращать управление. Уста- новите этот обработчик сигнала перед вызовом функции connect. Измените номер порта сервера на 13 (порт сервера времени и даты). Когда соединение установится, с помощью функции si еер войдите в состояние ожидания на 2 се- кунды, с помощью функции write запишите несколько байтов в сокет, прове- дите в состоянии ожидания (sleep) еще 2 секунды и с помощью функции wri te запишите еще несколько байтов. Запустите программу. Что происходит? 7. Что произойдет на рис. 5.5, если IP-адрес узла сервера, заданный клиентом при вызове функции connect, является IP-адресом, связанным с крайним пра- вым канальным уровнем на стороне сервера, а не IP-адресом, связанным с край- ним левым канальным уровнем? 8. В нашем примере эхо-сервера, осуществляющего сложение двух целых чисел (см. листинг 5.14), когда клиент и сервер принадлежат системам с различным порядком байтов, для небольших положительных чисел получается правиль- ный ответ, но для небольших отрицательных чисел ответ неверен. Почему? (Подсказка: нарисуйте схему обмена значениями через сокет, аналогичную рис. 3.4.) 9. В нашем примере в листингах 5.13 и 5.14 можем ли мы решить проблему, свя- занную с различным порядком байтов на стороне клиента и на стороне серве- ра, если клиент преобразует два аргумента в сетевой порядок байтов, исполь-
Упражнения 179 зуя функцию htonl, а сервер затем вызывает функцию ntohl для каждого ар- гумента перед сложением и выполняет аналогичное преобразование ре- зультата? 10. Что произойдет в листингах 5.13 и 5.14, если в качестве узла клиента исполь- зуется компьютер Sparc, где данные типа 1 ong занимают 32 бита, а в качестве узла сервера — Digital Alpha, где данные типа long занимают 64 бита? Изме- нится ли что-либо, если клиент и сервер поменяются местами? 11. На рис. 5.5 указано, что IP-адрес клиента выбирается IP на основе маршрути- зации. Что это значит?
ГЛАВА 6 Мультиплексирование ввода- вывода: функции select и poll 6.1. Введение В разделе 5.12 мы видели, что наш TCP-клиент обрабатывает два входных пото- ка одновременно: стандартный поток ввода и сокет TCP. Проблема, с которой мы столкнулись, состояла в том, что пока клиент был блокирован в вызове функции fgets (чтение из стандартного потока ввода), процесс сервера уничтожался. TCP сервера корректно отправляет сегмент FIN протоколу TCP клиента, но посколь- ку процесс клиента блокирован при чтении из стандартного потока ввода, он не получит признак конца файла, пока не считает даппые из сокета (возможно, зна- чительно позже). Нам нужна возможность сообщить ядру, что мы хотим полу- чить уведомления о том, что выполняется одно или несколько условий для вво- да-вывода (например, присутствуют данные для считывания или дескриптор готов к записи новых данных). Эта возможность называется мультиплексированием (multiplexing) ввода-вывода и обеспечивается функциями sei ect и pol 1. Мы рас- смотрим также более новый вариант функции sei ect, входящей в стандарт Posix. 1g, называемый pselect. Мультиплексирование ввода-вывода обычно используется сетевыми прило- жениями в следующих случаях: Когда клиент обрабатывает множество дескрипторов (обычно интерактивный ввод и сетевой сокет), должно использоваться мультиплексирование ввода- вывода. Это сценарий, который мы только что рассмотрели выше. Возможно, хотя это и редкий случай, что клиент одновременно обрабатывает множество сокетов. Такой пример мы приведем в разделе 15.5 при использо- вании функции select в контексте клиента Web. Если сервер TCP обрабатывает и прослушиваемый сокет, и присоединенные сокеты, обычно используется мультиплексирование ввода-вывода, как это показано на рис. 6.7. Если сервер работает и с TCP, и с UDP, обычно также используется мульти- плексирование ввода-вывода. Такой пример мы приводим в листинге 8.3. Если сервер обрабатывает множество служб и, возможно, множество прото- колов (например, демон т netd, который описан в разделе 12.5), обычно исполь- гея мутьтиплексирование ввода-вывода.
6.2. Модели ввода-вывода 181 Область применения мультиплексирования ввода-вывода не ограничивается только сетевым программированием. Любому нетривиальному приложению ча- сто приходится использовать эту технологию. 6.2. Модели ввода-вывода Прежде чем начать описание функций sei ect и pol 1, мы должны вернуться назад и уяснить основные различия между пятью моделями ввода-вывода, доступны- ми нам в Unix: блокируемый ввод-вывод; неблокируемый ввод-вывод; мультиплексирование ввода-вывода (функции select И poll)*' ввод-вывод, управляемый сигналом (сигнал SIGIO); асинхронный ввод-вывод (функции Posix.lg зю_). Возможно, вы захотите пропустить этот раздел при первом прочтении, а затем вернуться к нему по мере знакомства с различными моделями ввода-вывода, под- робно рассматриваемыми в дальнейших главах. Как вы увидите в примерах этого раздела, обычно различаются две фазы опе- рации ввода: 1. Ожидание готовности данных. 2. Копирование данных от ядра процессу. Первый шаг операции ввода па сокете обычно включает ожидание прихода данных по сети. Когда пакет приходит, он копируется в буфер внутри ядра. Вто- рой шаг — копирование этих данных из буфера ядра в буфер приложения. Модель блокируемого ввода-вывода Наиболее распространенной моделью ввода-вывода является модель блокируе- мого ввода-вывода, которую мы использовали для всех примеров в тексте книги. По умолчанию все сокеты являются блокируемыми. Используя в наших приме- рах сокет дейтаграмм, мы получаем сценарий, показанный на рис. 6.1. В этом примере вместо TCP мы используем UDP, поскольку в случае UDP признак готовности данных очень прост: получена вся дейтаграмма или нет. В слу- чае TCP он становится сложнее, поскольку приходится учитывать дополнитель- ные переменные, например минимальный объем данных в сокете (low water-mark). В примерах этого раздела мы говорим о функции recvfrom как о системном вызове, поскольку делаем различие между нашим приложением и ядром. Вне за- висимости от того, как реализована функция recvfrom (как системный вызов в ядре, происходящем от Беркли, или как функция, активизирующая системный вызов функции getmsg в ядре System V), она обычно выполняет переключение между работой в режиме приложения и работой в режиме ядра, за которым через опре- деленный промежуток времени следует возвращение в режим приложения. На рис. 6.1 процесс вызывает функцию recvfrom, и системный вызов не воз- вращает управление, пока дейтаграмма не придет и не будет скопирована в бу- фер приложения либо пока не произойдет ошибка. Наиболее типичная ошибка —
182 Глава 6. Мультиплексирование ввода-вывода: функции select и poll Приложение Системный Ядро recvfrom ВЫЗОВ Дейтаграмма не готова Ожидание данных Процесс / блокирован / до завершения ' функции recvfrom ▼ Дейтаграмма готова \ Обработка дейтаграммы Нормальное завершение функции Копирование дейтаграммы^ I Копирование I данных / из ядра ▼ I в буфер Копирование завершено I приложения Рис. 6.1. Модель блокируемого ввода-вывода это прерывание системного вызова сигналом, о чем рассказывалось в разделе 5.9. Процесс блокирован в течение всего времени с момента, когда он вызывает функ- цию recvfrom, до момента, когда эта функция завершается. Когда функция recvfrom выполняется нормально, наше приложение обрабатывает дейтаграмму. Модель неблокируемого ввода-вывода Когда мы определяем сокет как неблокируемый, мы тем самым сообщаем ядру следующее: «когда запрашиваемая нами операция ввода-вывода не может быть за- вершена без перевода процесса в состояние ожидания, следует не переводить про- цесс в состояние ожидания, а возвратить ошибку». Неблокируемый ввод-вывод мы описываем подробно в главе 15, а на рис. 6.2 мы лишь демонстрируем его свойства. В первых трех случаях вызова функции recvfrom данных для возвращения нет, поэтому ядро немедленно возвращает ошибку EWOULDBLOCK. Когда мы в четвертый раз вызываем функцию recvfrom, дейтаграмма готова, поэтому она копируется в буфер приложения и функция recvfrom успешно завершается. Затем мы обраба- тываем данные. Такой процесс, когда приложение находится в цикле и вызывает функцию recvfrom на неблокируемом дескрипторе, называется опросом (polling). Приложе- ние последовательно опрашивает ядро, чтобы увидеть, что какая-то операция может быть выполнена. Часто это пустая трата времени процессора, но такая модель все же иногда используется, обычно в системах, выделяемых для одной функции. Модель мультиплексирования ввода-вывода В случае мультиплексирования ввода-вывода мы вызываем функцию select или poll, и блокирование происходит в одном из этих двух системных вызовов, а не в действительном системном вызове ввода-вывода. На рис. 6.3 обобщается мо- дель мультиплексирования ввода-вывода. Процесс блокируется в вызове функции sei ect, ожидая, когда дейтаграммный сокет будет готов для чтения. Когда функция sei ect возвращает сообшение, что сокет готов для чтения, процесс вызывает функцию recvfrom, чтобы скопировать дейтаграмму в наш буфер приложения.
6.2. Модели ввода-вывода 183 Приложение Ядро recvfrom Системный вызов EWOULDBLOCK Процесс несколько раз вызывает функцию recvfrom до ее успешного завершения (опрос) recvfrom recvfrom recvfrom Системный вызов EWOULDBLOCK Системный вызов EWOULDBLOCK Дейтаграмма не готова Дейтаграмма не готова Дейтаграмма готова Системный вызов Копирование дейтаграммы Нормальное завершение функции Копирование завершено Копирование данных > из ядра в буфер приложения ' Обработка дейтаграммы Рис. 6.2. Модель неблокируемого ввода-вывода Приложение Ядро Процесс блокирован / select до завершения I функции select I и ожидает, когда J один из (возможной нескольких сокетов I станет готов для / чтения \ (recvfrom Системный вызов --------------> Дейтаграмма не готова Функция select сообщает, что сокет готов для чтения Дейтаграмма готова Системный вызов Ожидание данных Процесс блокирован, пока данные копируются в буфер приложения Нормальное завершение функции Копирование дейтаграммы Копирование завершено Копирование данных из ядра в буфер приложения Обработка дейтаграммы Рис. 6.3. Модель мультиплексирования ввода-вывода Сравнивая рис. 6.3 и 6.1, мы не найдем в модели мультиплексирования ввода- вывода каких-либо преимуществ, более того, она даже обладает незначительным недостатком, поскольку использование функции sei ect требует двух системных
184 Глава 6. Мультиплексирование ввода-вывода: функции select и poll вызовов вместо одного. Но преимущество использования функции sei ect, кото- рое мы увидим далее в этой главе, состоит в том, что мы сможем ожидать готов- ности не одного дескриптора, а нескольких. Модель ввода-вывода, управляемого сигналом Мы можем сообщить ядру, что необходимо уведомить процесс о готовности де- скриптора с помощью сигнала SIGIO. Такая модель имеет название ввода-вывода, управляемого сигналом (signal-driven I/O). Она представлена в обобщенном виде на рис. 6.4. Приложение Процесс продолжает выполняться Установка обработчика сигнала sigio Системный вызов sigaction Возвращение Ядро Ожидание данных Процесс блокирован, пока данные < копируются в буфер приложения Обработчик сигнала recvfrom Передача сигнала sigio Системный вызов Дейтаграмма готова Копирование дейтаграммы Нормальное завершение функции Обработка -4---------------- дейтаграммы ▼ Копирование завершено Копирование данных из ядра в буфер приложения Рис. 6.4. Модель управляемого сигналом ввода-вывода Сначала мы включаем на сокете управляемый сигналом ввод-вывод (об этом рассказывается в разделе 22.2) и устанавливаем обработчик сигнала при помощи системного вызова sigaction. Возвращение из этого системного вызова происхо- дит незамедлительно, и наш процесс продолжается (он не блокирован). Когда дейтаграмма готова для чтения, для нашего процесса генерируется сигнал SIGIO. Мы можем либо прочитать дейтаграмму из обработчика сигнала с помощью вы- зова функции recvfrom и затем уведомить главный цикл о том, что данные готовы для обработки (см. раздел 22.3), либо уведомить основной цикл и позволить ему прочитать дейтаграмму. Независимо от способа обработки сигнала эта модель имеет то преимущество, что во время ожидания дейтаграммы не происходит блокирования. Основной цикл может продолжать выполнение, ожидая уведомления от обработчика сигнала о том, что данные готовы для обработки либо дейтаграмма готова для чтения. Модель асинхронного ввода-вывода Асинхронный ввод-вывод был введен в редакции стандарта Posix.lg 1993 года (рас- ширения реального времени). Мы сообщаем ядру, что нужно начать операцию И VRP ЛОМИТЬ ГГЯГ Л ТОМ ТГОГЛЯ КГЯ ЛПРПЯГШЯ (НКЛЮИЯСТ К'ОГТИПЛНЯИИР ЛЯППЫУ ИЯ аппя
6.2. Модели ввода-вывода 185 в наш буфер) завершится. Мы не обсуждаем эту модель в этой книге, поскольку она еще не получила достаточного распространения. Ее основное отличие от мо- дели ввода-вывода, управляемого сигналом, заключается в том, что при исполь- зовании сигналов ядро сообщает нам, когда операция ввода-вывода может быть инициирована, а в случае асинхронного ввода-вывода — когда операция заверша- ется. Пример этой модели приведен на рис. 6.5. Приложение Ядро aio_read ( Системный вызов Возвращение Дейтаграмма не готова") Ожидание данных Дейтаграмма готова Процесс продолжает выполняться Копирование дейтаграммы Обработчик сигнала обрабатывает дейтаграмму Передача сигнала, указанного в функции aio_read ▼ Копирование завершено Копирование данных из ядра в буфер приложения Рис. 6.5. Модель асинхронного ввода-вывода Мы вызываем функцию aio_read (функции асинхронного ввода-вывода Posix начинаются с аю_ или 1 то ) и передаем ядру дескриптор, указатель на буфер, раз- мер буфера (те же три аргумента, что и для функции read), смещение файла (ана- логично функции 1 seek), а также указываем, как уведомить нас, когда операция полностью завершится. Этот системный вызов завершается немедленно, и наш процесс не блокируется в ожидании завершения ввода-вывода. В этом примере предполагается, что мы указали ядру сгенерировать некий сигнал, когда опера- ция завершится. Сигнал не генерируется до тех пор, пока данные не скопирова- ны в наш буфер приложения, что отличает эту модель от модели ввода-вывода, управляемого сигналом. ПРИМЕЧАНИЕ ------------------------------------------------------------ На момент написания книги только некоторые системы поддерживают асинхронный ввод-вывод стандарта Posix. 1. Например, мы не уверены, что системы поддерживают его для сокетов. Мы используем его только как пример для сравнения с моделью уп- равляемого сигналом ввода-вывода. Сравнение моделей ввода-вывода На рис. 6.6 сравнивается пять различных моделей ввода-вывода. Здесь видно глав- НОР птгггти гтр UPTTRTnPV ГГРППГЛУ ЛЛППРПРТТ D ГТРГ\ТЭГ>1Л rhoon гтглг> т/гл П Т.1/w шгпп'за rhnon XT IT MV
186 Глава 6. Мультиплексирование ввода-вывода: функции select и poll одна и та же: процесс блокируется в вызове функции recvf rom, на то время, пока данные копируются из ядра в буфер вызывающего процесса. Однако асинхрон- ный ввод-вывод в обеих фазах отличается от первых четырех моделей. Модель блокируемого ввода-вывода Модель неблокируемого ввода-вывода Модель мультиплексирования ввода-вывода Модель управляемого сигналом ввода-вывода Модель асинхронного ввода-вывода Начало Проверка Проверка Проверка Проверка Проверка Проверка Проверка Проварка Проверка Проварка И С(§ Я Начало Ожидание данных о Я О Я Дейтаграмма готова Начало Уведомление начало Я о S §8 И ГО Копирование данных из ядра в буфер приложения Завершение Завершение Завершение Уведомление Завершение Для этих моделей первая фаза различается, вторая фаза Выполнение (блокирование процесса при вызове функции recvfrom) одинакова обеих фаз Рис. 6.6. Сравнение моделей ввода-вывода Сравнение синхронного и асинхронного ввода-вывода Posix. 1 дает следующие определения этих терминов: В Операция синхронного ввода-вывода блокирует запрашивающий процесс до тех пор, пока операция ввода-вывода не завершится. & Операция асинхронного ввода-вывода не вызывает блокирования запраши- вающего процесса. Используя эти определения, можно сказать, что первые четыре модели ввода- вывода — блокируемая, неблокируемая, модель мультиплексирования ввода-вы- вода и модель управляемого сигналом ввода-вывода — являются синхронными, поскольку фактическая операция ввода-вывода (функция recvfrom) блокирует процесс. Только модель асинхронного ввода-вывода соответствует определению асинхронного ввода-вывода.
6.3. Функция select 187 6.3. Функция select Эта функция позволяет процессу сообщить ядру, что необходимо подождать, пока не произойдет одно из некоторого множества событий, и вывести процесс из со- стояния ожидания, только когда произойдет одно или несколько таких событий или когда пройдет заданное количество времени. Например, мы можем вызвать функцию sei ect и сообщить ядру, что возвра- щать управление нужно только когда наступит любое из следующих событий: % любой дескриптор из набора {1,4, 5} готов для чтения; ** любой дескриптор из набора {2, 7} готов для записи; любой дескриптор из набора {1,4} вызывает исключение, требующее обработки; 3s истекает 10,2 секунды. Таким образом, мы сообщаем ядру, какие дескрипторы нас интересуют (гото- вые для чтения, готовые для записи или требующие обработки исключения) и как долго нужно ждать. Интересующие нас дескрипторы не ограничиваются сокета- ми: любой дескриптор можно проверить с помощью функции sei ect. ПРИМЕЧАНИЕ ------------------------------------------------------------- Беркли-реализации всегда допускали мультиплексирование ввода-вывода с любыми дескрипторами. Система SVR3 изначально ограничивала мультиплексирование вво- да-вывода дескрипторами, которые являлись потоковыми устройствами (см. главу 33), но это ограничение было снято в S VR4. include <sys/select.h> include <sys/time h> int seiect(int maxfdpl. fd_set *readset. fd_set *writeset. fdset *exceptset. const struct timeval *timeout). Возвращает положительное число- счетчик готовых дескрипторов 0 в случае тайм-аута. -1 в случае ошибки Описание этой функции мы начнем с последнего аргумента, который сообща- ет ядру, сколько ждать, пока один из заданных дескрипторов не будет готов. Струк- тура timeval задает число секунд и микросекунд: struct timeval { long tv_sec. /* секунды */ long tv_usec, /* микросекунды */ С помощью этого аргумента можно реализовать три сценария. 1. Ждать вечно: завершать работу, только когда один из заданных дескрипторов готов для ввода-вывода. Для этого нужно определить аргумент структуры timeval как пустой указатель. 2. Ждать в течение определенного времение: завершение будет происходить, когда один из заданных дескрипторов готов для ввода-вывода, но период ожи- дания ограничивается количеством секунд и микросекунд, заданным в структу- ре timeval, на которую указывает аргумент timeout. 3. Не ждать вообще: завершение происходит сразу же после проверки дескрип- торов. Это называется опросом (polling). Аргумент timeout должен указывать
188 Глава 6. Мультиплексирование ввода-вывода: функции select и poll на структуру timeval, а значение таймера (число секунд и микросекунд, задан- ных этой структурой) должно быть нулевым. Ожидание в первых двух случаях обычно прерывается, когда процесс пере- хватывает сигнал и возвращается из обработчика сигнала. ПРИМЕЧАНИЕ ----------------------------------------------------------- Ядра реализаций, происходящих от Беркли, никогда автоматически не перезапускают функцию select [105, с. 527], в то время как ядра SVR4 перезапускают, если задан флаг SA_RESTART при установке обработчика сигнала. Это значит, что в целях переноси- мости мы должны быть готовы к тому, что функция select возвратит ошибку EINTR, если мы перехватываем сигналы. Хотя структура timeval позволяет нам задавать значение с точностью до мик- росекунд, реальная точность, поддерживаемая ядром, часто значительно ниже. Например, многие ядра Unix округляют значение тайм-аута до серий по 10 мик- росекунд. Присутствует также и некоторая скрытая задержка, то есть между ис- течением времени таймера и моментом, когда ядро запустит данный процесс, про- ходит некоторое время. Квалификатор const аргумента timeout означает, что данный аргумент не из- меняется функцией sei ect при ее возвращении. Например, если мы зададим пре- дел времени, равный 10 секундам, и функция sei ect возвратит управление до ис- течения этого времени с одним или более готовых дескрипторов или ошибкой EINTR, то структура timeval не изменится, то есть при завершении функции значе- ние тайм-аута не станет равно числу секунд, оставшихся от исходных 10. Чтобы узнать количество неизрасходованных секунд, следует определить системное время до вызова функции sei ect, а когда она завершится, определить его еще раз и вычесть первое значение из второго. ПРИМЕЧАНИЕ ----------------------------------------------------------- В современных системах Linux структура timeval изменяема Следовательно, в целях переносимости будем считать, что структура timeval по возвращении становится нео- пределенной, и будем инициализировать ее перед каждым вызовом функции select. В Posix.lg определяется квалификатор const. Три средних аргумента, readset, writeset и exceptset, определяют дескрипто- ры, которые ядро должно проверить на возможность чтения и записи и на нали- чие исключений (exceptions). В настоящее время поддерживается только два ис- ключения: 1. На сокет приходят внеполосные данные. Более подробно мы опишем этот слу- чай в главе 21. 2. Присутствие информации об управлении состоянием (control status informa- tion), которая должна быть считана с управляющего (master side) псевдотер- минала, помещенного в режим пакетной обработки. Псевдотерминалы в дан- ном томе не рассматриваются. Проблема в том, как задать одно или несколько значений дескрипторов для каждого из трех аргументов. Функция sei ect использует наборы дескрипторов, обычно это массив целых чисел, где каждый бит в каждом целом числе соответ- ствует дескриптору. Например, при использовании 32-разрядных целых чисел
6,3. Функция select 189 первый элемент массива (целое число) соответствует дескрипторам от 0 до 31, второй элемент — дескрипторам от 32 до 63 и т. д. Детали реализации не влияют на приложение и скрыты в типе данных df_set и следующих четырех макросах: void FD_ZERO(fd_set *fdset) void FD_SET(int fd. fd_set * fdset). void FD CLRdnt flfd fd_set *fdset) int FD ISSETCint fd. fd_set *fdset). /* сбрасываем все биты в fdset */ /* устанавливаем бит для fd в fdset */ /* сбрасываем бит для fd в fdset */ /* установлен ли бит для fd в fdset 7 */ Мы размещаем в памяти набор дескрипторов типа f d_set, с помощью этих мак- росов устанавливаем и проверяем биты в наборе, а также можем присвоить его (как значение) другому набору дескрипторов с помощью оператора присваива- ния языка С. ПРИМЕЧАНИЕ ---------------------------------------------------------- Описываемый памп массив целых чисел, использующий по одному биту для каждого дескриптора, — эго только одни из возможных способов реализации функции select Тем не менее является обычной практикой ссылаться па отдельные дескрипторы в на- боре дескрипторов как па биты — например, так: «установить биг для прослушиваемо- го дескриптора в наборе для чтения» В разделе 6.10 мы увидим, чго функция poll использует совершенно дру! ос представ- ление’ массив структур переменной длины, по одной структуре для каждого дескриптора Например, чтобы определить переменную типа fd set и затем установить биты для дескрипторов 1,4 и 5, мы пишем: fd_set rset. FD_ZEROC&rset). FD_SET(1 &rset). FD_SET(4 &rset). FD_SET(5 &rset) /* инициализируем набор все биты сброшены */ /* устанавливаем бит для fd 1 */ /* устанавливаем бит для fd 4 */ /* устанавливаем бит для fd 5 */ Важно инициализировать набор, так как если набор будет создан в виде авто- матической переменной и не проинпциализирован, результат может оказаться непредсказуемым. Любой из трех средних аргументов функции select — readset, writeset или exceptset — может быть задан как пустой указатель, если нас не интересует опре- деляемое им условие. На самом деле, если все три указателя пустые, мы просто получаем таймер большей точности, чем обычная функция Unix sleep (которая дает точность около секунды). Функция pol 1 обеспечивает аналогичную функ- циональность. На рис. С.9 и С. 10 [93] показана функция sleep us, реализованная с помощью функций sei ect и pol 1, которая находится в состоянии ожидания в те- чение времени, измеряемого микросекундами. Аргумент maxfdpl задает число проверяемых дескрипторов. Его значение на единицу больше максимального номера проверяемого дескриптора (поскольку его имя maxfdpl). Проверяются дескрипторы 0, 1, 2 и далее до maxfdpl-1 включи- тельно. Константа FD_SETSIZE, определяемая при подключении заголовочного файла <sys/select h>, является максимальным числом дескрипторов для типа данных fd set. Ее значение часто равно 1024, но некоторые программы используют даже большее количество дескрипторов. Аргумент maxfdpl заставляет нас вычислять наибольший интересующий нас дескриптор и затем сообщать ядру его значение.
190 Глава 6. Мультиплексирование ввода-вывода: функции select и poll Например, в предыдущем коде, который включает дескрипторы 1,4 и 5, значение аргумента maxfdpl равно 6. Причина, по которой это 6, а не 5, в том, что мы задаем количество дескрипторов, а не наибольшее значение, а нумерация дескрипторов начинается с нуля. ПРИМЕЧАНИЕ--------------------------------------------------------- Зачем нужно было включать этот аргумент и вычислять его значение? Причина в том, что он повышает эффективность работы ядра. Хотя каждый набор типа fd_set может содержать множество дескрипторов (обычно до 1024), реальное количество дескрип- торов, используемое типичным процессом, значительно меньше. Эффективность воз- растает за счет того, что не копируются ненужные части набора дескрипторов между ядром и процессом, и не требуется проверять биты, которые всегда являются нулевы- ми (см. раздел 16.13 [105]). Функция sei ect изменяет наборы дескрипторов, на которые указывают аргу- менты readset, wn teset и exceptset. Эти три аргумента являются аргументами типа «значение-результат». Когда мы вызываем функцию, мы задаем значения инте- ресующих нас дескрипторов, и по ее завершении результат указывает, какие де- скрипторы готовы. Проверить после завершения определенный дескриптор из структуры fd_set можно с помощью макроса FD_ISSET. Для дескриптора, не гото- вого при завершении функции, соответствующий бит в наборе дескрипторов бу- дет сброшен. Поэтому мы устанавливаем все интересующие нас биты во всех на- борах дескрипторов каждый раз, когда вызываем функцию sei ect. ПРИМЕЧАНИЕ -------------------------------------------------------- Две наиболее общих ошибки программирования при использовании функции select — это забыть добавить единицу к наибольшему номеру дескриптора и забыть, что набо- ры дсскрпп горов имеют тип «значение-результат». Вторая ошибка приводит к тому, что функция select вызывается с нулевым битом в наборе дескрипторов, когда мы ду- маем, что он установлен в единицу. Автор лично потерял два часа, оглаживая пример для этой книги, так как забыл прибавить единицу к первому аргументу функции. Возвращаемое этой функцией значение указывает общее число готовых де- скрипторов во всех наборах дескрипторов. Если значение таймера истекает до того, как какой-нибудь из дескрипторов оказывается готов, возвращается нуле- вое значение. Возвращаемое значение -1 указывает на ошибку (которая может произойти если, например, выполнение функции прервано перехваченным сиг- налом). ПРИМЕЧАНИЕ--------------------------------------------------------- В более ранних реализациях SVR4 функция select содержала ошибку если один и тот же бит находился в нескольких наборах дескрипторов — допустим, дескриптор был готов и для чтения, и для записи, — он учитывался только один раз. В современных реализациях эта ошибка исправлена. При каких условиях дескриптор становится готовым? Мы говорили об ожидании готовности дескриптора для ввода-вывода (чтения или записи^ или возникновения исключительной ruTv^imn тпебчтнтей обпабот-
6.3. Функция select 191 ки (внеполосные данные). В то время как готовность к чтению и записи очевидна для файловых дескрипторов, в случае дескрипторов сокетов следует более вни- мательно изучить те условия, при которых функция sei ect сообщает, что сокет готов (рис. 16.52 [105]). 1. Сокет готов для чтения, если выполнено одно из следующих условий: 1. Число байтов данных в приемном буфере сокета больше или равно теку- щему значению минимального количества данных (low water-mark) для приемного буфера сокета. Операция считывания данных из сокета не бло- кируется и возвратит значение, большее нуля (то есть данные, готовые для чтения). Мы можем задать значение минимального количества данных (low-water mark) с помощью параметра сокета SO_RCVLOWAT. По умолчанию для сокетов TCP и UDP это значение равно 1. 2. На противоположном конце соединение закрывается (нами получен сег- мент FIN). Операция считывания данных из сокета не блокируется и воз- вратит нуль (то есть признак конца файла). 3. Сокет является прослушиваемым, и число установленных соединений не- нулевое. Функция accept на прослушиваемом сокете обычно не блокиру- ется, хотя в разделе 15.6 мы описываем ситуацию, в которой функция accept может оказаться блокируемой. 4. Ошибка сокета, ожидающая обработки. Операция чтения на сокете не бло- кируется и возвратит ошибку (-1) со значением переменной errno, указыва- ющим на конкретное условие ошибки. Эти ошибки, ожидающие обработки, мож- но также получить, вызвав функцию getsockopt с параметром SO_ERROR, после чего состояние ошибки будет сброшено. 2. Сокет готов для записи, если выполнено одно из следующих условий: 1. Количество байтов доступного пространства в буфере отправки сокета боль- ше или равно текущему значению минимального количества данных для буфера отправки сокета и либо сокет является присоединенным, либо со- кету не требуется соединения (например, сокет UDP). Это значит, что если мы отключим блокировку для сокета (см. главу 15), операция записи не заблокирует сокет и возвратит положительное значение (например, число байтов, принятых транспортным уровнем). Устанавливать минимальное количество данных мы можем с помощью параметра сокета SO_SNDLDWAT. По умолчанию это значение равно 2048 для сокетов TCP и UDP. 2. Получатель, которому отправляются данные, закрывает соединение. Опе- рация записи в сокет сгенерирует сигнал SIGPIPE (см. раздел 5.12). 3. Ошибка сокета, ожидающая обработки. Операция записи в сокет не бло- кируется и возвратит ошибку (-1) со значением переменной errno, указыва- ющей на конкретное условие ошибки. Эти ошибки, ожидающие обработки, ч можно также получить и сбросить, вызвав функцию getsockopt с парамет- ром сокета SO_ERROR. 3. Исключительная ситуация, требующая обработки, может возникнуть на со- кете в том случае, если приняты внеполосные данные либо если отметка вне- полосных данных в принимаемом потоке еще не достигнута. (Внеполосные данные описываются в главе 21.1
192 Глава 6. Мультиплексирование ввода-вывода: функции select и poll ПРИМЕЧАНИЕ --------------------------------------------------------------- Наши определения «готов для чтения» и «готов для записи» взяты непосредственно из макроопределений ядра soreadable и sowntable (которые описываются на с. 530-531 [105]). Аналогично, наше определение «исключительной ситуации» взято из функции soo select, которая описана там же. Обратите внимание, что когда происходит ошибка на сокете, функция select отмечает его готовым как для чтения, так и для записи. Значения минимального количества даных (low-water mark) для приема и от- правки позволяют приложению контролировать, сколько данных должно быть доступно для чтения или сколько места должно быть доступно для записи перед тем, как функция sei ect сообщит, что сокет готов для чтения или записи. Напри- мер, если мы знаем, что наше приложение не может сделать ничего полезного, пока не будет получено как минимум 64 байта данных, мы можем установить зна- чение минимального количества данных равным 64, чтобы функция select не вывела нас из состояния ожидания, если для чтения готово менее 64 байт. Пока значение минимального количества данных для отправки в сокете UDP меньше, чем буфер отправки сокета (а такое отношение между ними всегда уста- навливается по умолчанию), сокет UDP всегда готов для записи, поскольку со- единения не требуется. В табл. 6.1 суммируются описанные выше условия, при которых сокет стано- вится готовым для вызова функции select. Таблица 6.1. Условия, при которых функция select сообщает, что сокет готов для чтения или для записи либо необходима обработка исключительной ситуации Условие Сокет готов для Сокет готов для Исключительная чтения записи ситуация Данные для чтения • Считывающая половина • соединения закрыта Для прослушиваемого • • сокета готово новое соединение Пространство, доступное • для записи Записывающая половина • • соединения закрыта Ошибка, ожидающая обработки Внеполосные данные TCP • Максимальное число дескрипторов для функции select Ранее мы сказали, что большинство приложений не используют много дескрип- торов. Например, редко можно найти приложение, использующее сотни дескрип- торов. Но такие приложения существуют, и часто они используют функцию sei ect
6.4. Функция str cli (продолжение) 193 для мультиплексирования дескрипторов. Когда функция select была создана, операционные системы обычно имели ограничение на максимальное число де- скрипторов для каждого процесса (этот предел в реализации 4.2BSD состав- лял 31), и функция select просто использовала тот же предел. Но современные версии Unix допускают неограниченное число дескрипторов для каждого про- цесса (часто оно ограничивается только количеством памяти и административ- ными правилами), поэтому возникает вопрос: как же теперь работает функция select? Многие реализации имеют объявления, аналогичные приведенному ниже, которое взято из заголовочного файла 4.4BSD <sys/types. h>: /* Значение FO_SETSIZE может быть определено пользователем, но заданное здесь по умолчанию является достаточным в большинстве случаев. */ #Tfndef FD SETSIZE #define FD_SETSIZE 256 #endif Исходя из этого комментария, можно подумать, что если перед подключени- ем этого заголовочного файла присвоить FD_SETSIZE значение, превышающее 256, то увеличится размер наборов дескрипторов, используемых функцией select. К сожалению, обычно это не действует. ПРИМЕЧАНИЕ ---------------------------------------------------------- Чтобы понять, в чем дело, обратите внимание, что на рис. 16.53 [105] объявляются три набора дескрипторов внутри ядра, а в качестве верхнего предела используется опреде- ленное в ядре значение FD_SETSIZE. Единственный способ увеличить размер набо- ров дескрипторов — это увеличить значение FD_SETSIZE и затем перекомпилировать ядро. Изменения значения без перекомпиляции ядра недостаточно. Некоторые производители изменяют свои реализации функции select, с тем чтобы позволить процессу задавать значение FD SETSIZE, превышающее значение по умолчанию. BSD/OS также изменила реализацию ядра, чтобы допустить боль- шие наборы дескрипторов, кроме того, в ней добавлено четыре новых макроопре- деления FD_xrx для динамического размещения больших наборов дескрипторов в памяти и для работы с ними. Однако с точки зрения переносимости не стоит злоупотреблять использованием больших наборов дескрипторов. 6.4. Функция str_cli (продолжение) Теперь мы можем переписать нашу функцию str_cli, представленную в разде- ле 5.5 (на этот раз используя функцию select), таким образом, чтобы мы получа- ли уведомление, как только завершится процесс сервера. Проблема с предыду- щей версией состояла в том, что процесс мог оказаться заблокированным в вызове функции fgets, когда что-то происходило на сокете. Наша новая версия этой функ- ции вместо этого блокируется в вызове функции select, ожидая готовности для чтения либо стандартного потока ввода, либо сокета. На рис. 6.7 показаны раз- личные условия, обрабатываемые с помощью вызова функции select.
194 Глава 6, Мультиплексирование ввода-вывода: функции select и poll Клиент Данные или EOF (конец файла) Данные Рис. 6.7. Условия, обрабатываемые функцией select в вызове функции str_cli Сокет обрабатывает три условия: 1 . Если протокол TCP собеседника отправляет данные, сокет становится гото- вым для чтения, и функция read возвращает положительное значение (то есть число байтов данных). 2 . Если протокол TCP собеседника отправляет сегмент FIN (процесс заверша- ется), сокет становится готовым для чтения, и функция read возвращает нуль (признак конца файла). 3 . Если TCP собеседника отправляет RST (узел вышел из строя и перезагрузил- ся), сокет становится готовым для чтения, и функция read возвращает -1, а пе- ременная errno содержит код соответствующей ошибки. В листинге 6.11 представлен исходный код этой версии функции. Листинг 6.1. Реализация функции str_cli с использованием функции select (усовершенствованный вариант находится в листинге 6.2) //select/strcliseiectOl с 1 include "unp h' 2 void 3 str_cli(FILE *fp int sockfd) 4 { 5 int maxfdpl. 6 fd_set rset. 7 char sendline[MAXLINE], recvl4ne[MAXLINE]: 8 FD_ZERO(&rset) 9 for (. ) { 10 FD_SET(fi leno(fp) &rset) 11 FD_SET(sockfd &rset) 12 maxfdpl = max(fileno(fp) sockfd) + 1 13 Select(maxfdpl &rset. NULL NULL. NULL). 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу httpy/ www piter com/download
6.5. Пакетный ввод 195 14 if (FD_ISSET(sockfd. &rset)) { /* сокет готов для чтения */ 15 if (Readline(sockfd. recvline MAXLINE) == 0) 16 err_quit("str_cli server terminated prematurely") 17 FputsIrecvline stdout) 18 } 19 if (FD_ISSET(fileno(fp) Xrset)) { /* входное устройство готово для чтения*/ 20 if (Fgets(sendline MAXLINE fp) == NULL) , 21 return. /* все сделано */ 22 Writen(sockfd, sendline strlen(sendline)) 23 } 24 } 25 } Вызов функции select 8-13 Нам нужен только один набор дескрипторов — для проверки готовности соке- та для чтения. Этот набор дескрипторов инициализируется макросом FD_ZERD, после чего с помощью макроса FD_SET устанавливаются два бита: бит, соответ- ствующий указателю файла fp стандартного потока ввода-вывода, и бит, соответ- ствующий дескриптору сокета sockfd. Функция fileno преобразует указатель файла стандартного потока ввода-вывода в соответствующий ему дескриптор. Функция select (а также pol 1) работает только с дескрипторами. Функция sei ect вызывается после определения максимального из двух де- скрипторов. В этом вызове указатель на набор дескрипторов для записи и указа- тель на набор дескрипторов с исключениями являются пустыми. Последний ар- гумент (ограничение по времени) также является пустым указателем, поскольку мы хотим, чтобы процесс был блокирован, пока не будут готовы данные для чте- ния. Обработка сокета, готового для чтения 14-18 Если по завершении функции select сокет готов для чтения, отраженная стро- ка считывается функцией readl те и выводится функцией fputs. Обработка ввода, допускающего возможность чтения 19-23 Если стандартный поток ввода готов для чтения, строка считывается функци- ей fgets и записывается в сокет с помощью функции wnten. Обратите внимание, что используются те же четыре функции ввода-вывода, что и в листинге 5 5: fgets, writen, readline и fputs, — но порядок их следования внутри функции str_cl 1 изменился. Раньше выполнение функции str_cl 1 опре- делялось функцией fgets, а теперь ее место заняла select. С помощью всего не- скольких дополнительных строк кода (сравните листинги 61 и 5 4) мы значи- тельно увеличили устойчивость клиента. 6.5. Пакетный ввод К сожалению, наша функция str_cl 1 все еще не вполне корректна. Сначала вер- немся к ее исходной версии, приведенной в листинге 5.5. Эта функция работает в режиме остановки и ожидания (stop-and-wait mode), что удобно для интерак- тивного использования: функция отправляет строку серверу и затем ждет его
196 Глава 6. Мультиплексирование ввода-вывода, функции select и poll ответа. Время ожидания складывается из одного периода обращения (RTT) и вре- мени обработки сервером (которое близко к нулю в случае простого эхо-серве- ра). Следовательно, мы можем предположить, сколько времени займет отраже- ние данного числа строк, если мы знаем время обращения (RTT) между клиентом и сервером. Измерить RTT позволяет утилита Ping. Если мы измерим с ее помощью вре- мя обращения к conmx com с нашего узла solans, то средний период RTT после 30 измерений будет равен 175 миллисекундам. На с. 89 [94] показано, что это спра- ведливо для дейтаграммы IP длиной 84 байта. Если мы возьмем первые 2000 строк файла termcap Solaris 2.5, то итоговый размер файла будет равен 98 349 байт, то есть в среднем 49 байт на строку. Если мы добавим размеры заголовка IP (20 байт) и заголовка TCP (20 байт), то средний сегмент TCP будет составлять 89 байт, почти как размер пакета утилиты Ping. Следовательно, мы можем предположить, что общее время составит около 350 секунд для 2000 строк (2000x0,175 с). Если мы запустим наш эхо-клиент TCP из главы 5, действительное время получится около 354 секунд, что очень близко к нашей оценке. Рис. 6.8. Временная диаграмма режима остановки и ожидания: интерактивный ввод
6.5. Пакетный ввод 197 Если считать, что сеть между клиентом и сервером является двусторонним каналом, когда запросы идут от клиента серверу, а ответы в обратном направле- нии, то получится изображенный на рис. 6.8 режим остановки и ожидания. Запрос отправляется клиентом в нулевой момент времени, и мы предполага- ем, что время обращения RTT равно 8 условным единицам. Ответ, отправлен- ный в момент времени 4, доходит до клиента в момент времени 7. Мы также счи- таем, что время обработки сервером нулевое и что размер запроса равен размеру ответа. Мы показываем только пакеты данных между клиентом и сервером, игно- рируя подтверждения TCP, которые также передаются по сети. Но поскольку между отправкой пакета и его приходом на другой конец кана- ла существует задержка и канал является двусторонним, в этом примере мы ис- пользуем только восьмую часть вместимости канала. Режим остановки и ожида- ния удобен для интерактивного ввода, но поскольку наш клиент считывает данные из стандартного потока ввода и записывает в стандартный поток вывода, а пере- направление ввода и вывода выполнить в интерпретаторе команд крайне просто, мы легко можем запустить наш клиент в пакетном режиме. Однако когда мы пе- ренаправляем ввод и вывод, получающийся файл вывода всегда меньше файла ввода (а для эхо-сервера требуется их идентичность). Чтобы понять происходящее, обратите внимание, что в пакетном режиме мы отправляем запросы так быстро, как их может принять сеть. Сервер обрабатыва- ет их и отправляет обратно ответы с той же скоростью. Это приводит к тому, что в момент времени 7 канал целиком заполнен, как показано на рис. 6.9. Время 7: Запрею 8 Запрос 7 Запрос 6 Запрос 5 Ответ 1 Ответ 2 Ответ 3 Ответ 4 Время 8: Запрос 9 Запрос 8 Запрос 7 Запрос 6 Ответ 2 Ответ 3 Ответ 4 Ответ 5 Рис. 6.9. Заполнение канала между клиентом и сервером: пакетный режим Предполагается, что после отправки первого запроса мы немедленно посыла- ем другой запрос и т. д. Также предполагается, что мы можем отправлять запро- сы с той скоростью, с какой сеть способна их принимать, и обрабатывать ответы так быстро, как сеть их поставляет. ПРИМЕЧАНИЕ------------------------------------------------------------ Существуют различные нюансы, имеющие отношение к передаче большого количе- ства данных TCP (bulk data flow), которые мы здесь игнорируем. К ним относятся ал- горитм медленного запуска (slow start algorithm), ограничивающий скорость, с кото- рой данные отправляются на новое или незанятое соединение, и возвращаемые сегменты АСК. Все эти вопросы рассматриваются в главе 20 [94]. Чтобы увидеть, в чем заключается проблема с нашей функцией str_c1 т, пред- ставленной в листинге 6.1, будем считать, что файл ввода содержит только девять
198 Глава 6. Мультиплексирование ввода-вывода: функции select и poll строк. Последняя строка отправляется в момент времени 8, как показано на рис. 6.9. Но мы не можем закрыть соединение после записи этого запроса, по- скольку в канале еще есть другие запросы и ответы. Причина возникновения про- блемы кроется в нашем способе обработки конца файла при вводе, когда процесс возвращается в функцию mai п, которая затем завершается. Но в пакетном режиме конец файла при вводе не означает, что мы закончили читать из сокета, — в нем могут оставаться запросы к серверу или ответы от сервера. Нам нужен способ закрыть одну половину соединения TCP. Другими слова- ми, мы хотим отправить серверу сегмент FIN, тем самым сообщая ему, что закон- чили отправку данных, но оставляем дескриптор сокета открытым для чтения. Это делается с помощью функции shutdown, которая описывается в следующем разделе. 6.6. Функция shutdown Обычный способ завершить сетевое соединение — вызвать функцию cl ose. Но у функции cl ose есть два ограничения, которых лишена функция shutdown: 1. Функция close постепенно уменьшает счетчик ссылок дескриптора и закры- вает сокет, только если счетчик доходит до нуля. Мы рассматривали это в раз- деле 4.8. Используя функцию shutdown, мы можем инициировать обычную последовательность завершения соединения TCP (четыре сегмента, начина- ющихся с FIN, на рис. 2.5) независимо от значения счетчика ссылок. 2. Функция close завершает оба направления передачи данных — и чтение, и за- пись. Поскольку соединение TCP является двусторонним, возможны ситуа- Клиент write write shutdown Сервер ПодтверадениепР^ Функция read возвращает значение больше О Функция read возвращает значение больше О Функция read возвращает значение О Функция read возвращает значение больше О Функция read возвращает значение больше О Функция read возвращает значение О write write close Рис. 6.10. Вызов функции shutdown для закрытия половины соединения TCP
6,7. Функция str ch (еще раз) 199 ции, когда нам понадобится сообщить другому концу соединения, что мы за- кончили отправку, даже если на том конце соединения имеются данные для отправки нам. Это случай, рассмотренный в предыдущем разделе при описа- нии работы нашей функции str_c1 i в пакетном режиме. На рис. 6.10 показаны типичные вызовы функций в этом сценарии. #include <sys/socket h> int shutdown(int sockfd. int howto). Возвращает 0 в случае успешного выполнения -1 в случае ошибки Действие функции зависит от значения аргумента howto. SHUT_RD. Закрывается считывающая половина соединения- из сокета больше нельзя считывать данные, и все данные, находящиеся в данный момент в бу- фере приема сокета, сбрасываются. Процесс больше не может выполнять функ- ции чтения из сокета. Любые данные для сокета TCP, полученные после вызова функции shutdown с этим аргументом, подтверждаются и «молча» игнорируются. ПРИМЕЧАНИЕ ------------------------------------------------------------- По умолчанию все, что записывается в маршрутизирующий сокет (см. главу 17), воз- вращается как возможный ввод на все маршрутизирующие сокеты узла. Некоторые программы вызывают функцию shutdown со вторым аргументом SHUT_RD, чтобы предотвратить получение подобной копии Другой способ избежать копирования — отключить параметр сокета SO_USELOOPBACK. ь SHUT_WR. Закрывается записывающая половина соединения. В случае TCP это называется половинным закрытием (см. раздел 18.5 [94]). Все данные, нахо- дящиеся в данный момент в буфере отправки сокета, будут отправлены, а за- тем будет выполнена обычная последовательность действий по завершению соединения TCP. Как мы отмечали ранее, закрытие записывающей половины соединения выполняется независимо от того, является ли значение в счетчи- ке ссылок дескриптора сокета положительным или нет. Процесс теряет воз- можность записывать данные в сокет. SHUT_RDWR. Закрываются и читающая, и записывающая половины соединения. Это эквивалентно двум вызовам функции shutdown: сначала с аргументом SHUT_RD, затем — с аргументом SHUT_WR. В табл. 7.3 приведены все возможные сценарии, доступные процессу при вы- зове функций shutdown и close. Действие функции close зависит от значения па- раметра сокета SO_L INGER. ПРИМЕЧАНИЕ ------------------------------------------------------------- Три константы SHUT_ххх введены впервые в Posix 1g Типичные значения аргумента howto, с которыми вы встретитесь, — это 0 (закрытие читающей половины), 1 (закры- тие записывающей половины) и 2 (закрытие обеих половин). 6.7. Функция str_cli (еще раз) В листинге 6.2 представлена наша обновленная (и корректная) функция str_cl i. В этой версии используются функции sei ect и shutdown. Первая уведомляет нас
200 Глава 6. Мультиплексирование ввода-вывода: функции select и poll о том, когда сервер закрывает свой конец соединения, а вторая позволяет коррект- но обрабатывать пакетный ввод. Листинг 6.2. Функция str_cli, использующая функцию select, которая корректно обрабатывает конец файла //seiect/strcliseiect02 c 1 include 'unp h' 2 void 3 str_cli(FILE *fp int sockfd) 4 { 5 int maxfdpl, stdineof 6 fd_set rset, 7 char sendline[MAXLINE] recvline[MAXLINEJ: 8 stdineof = 0 9 FD_ZERO(&rset). 10 for (, ) { 11 if (stdineof == 0) 12 FD_SET(fileno(fp). &rset), 13 FD_SET(sockfd &rset) 14 maxfdpl = max(fileno(fp) sockfd) + 1 15 Seiect(maxfdpl &rset NULL NULL, NULL) г 16 if (FD_ISSET(sockfd &rset)) { /* сокет готов для чтения */ 17 if (Readlme(sockfd. recvline, MAXLINE) == 0) { 18 if (stdineof == 1) 19 return, /* нормальное завершение */ 20 else 21 err quit("str_cli server terminated prematurely”). 22 } 23 Fputs(recvline. stdout), 24 } 25 if (FD_ISSET(fileno(fp) &rset)) { /* устройство ввода готово для чтения */ 26 if (Fgets(sendlme MAXLINE fp) == NULL) { 27 stdineof = 1, 28 Shutdown!sockfd, SHUT_WR) /* отправляем сегмент FIN */ 29 FD_CLR(fileno(fp). &rset) 30 continue. 31 } 32 Writen(sockfd. sendbne. strlen(sendline)) 33 } 34 } 35 } >-8 stdi neof — это новый флаг, инициализируемый нулем. Пока этот флаг равен нулю, мы будем проверять готовность стандартного потока ввода к чтению с по- мощью функции select. 5-24 Если мы считываем на сокете признак конца файла, когда нам уже встретился ранее признак конца файла в стандартном потоке ввода, это является нормаль- ным завершением и функция возвращает управление. Но если конец файла в стан- дартном потоке ввода еще не встречался, это означает, что процесс сервера завер- шился преждевременно. 5-33 Когда нам встречается признак конца файла на стандартном устройстве ввода, наш новый флаг stdi neof устанавливается в единицу и мы вызываем функцию shutdown со вторым аргументом SHUT_WR для отправки сегмента FIN.
6.8. Эхо-сервер TCP (продолжение) 201 Если мы измерим время работы нашего клиента TCP, использующего функ- цию str_c] 1, показанную в листинге 6.2, с тем же файлом из 2000 строк, это время составит 12,3 секунды, что почти в 30 раз быстрее, чем при использовании вер- сии этой функции, работающей в режиме остановки и ожидания. Мы еще не завершили написание нашей функции str_cli: в разделе 15.2 мы разработаем ее версию с использованием неблокируемого ввода-вывода, а в раз- деле 23 3 — версию, работающую с программными потоками. 6.8. Эхо-сервер TCP (продолжение) Вернемся к нашему эхо-серверу TCP из разделов 5.2 и 5.3. Перепишем сервер как одиночный процесс, который будет использовать функцию sei ect для обра- ботки любого числа клиентов, вместо того чтобы порождать с помощью функции fork по одному дочернему процессу для каждого клиента. Перед тем как предста- вить этот код, взглянем на структуры данных, используемые для отслеживания клиентов. На рис. 6.11 показано состояние сервера до того как первый клиент установил соединение. Сервер Рис. 6.11. Сервер TCP до того, как первый клиент установил соединение У сервера имеется одиночный прослушиваемый дескриптор, показанный на рисунке точкой. Сервер обслуживает только набор дескрипторов для чтения, который мы по- казываем на рис. 6.12. Предполагается, что сервер запускается в приоритетном (foreground) режиме, а дескрипторы 0,1 и 2 соответствуют стандартным потокам ввода, вывода и ошибок. Следовательно, первым доступным для прослушивае- мого сокета дескриптором является дескриптор3. Массив целых чисел client содержит дескрипторы присоединенного сокета для каждого клиента. Все эле- менты этого массива инициализированы значением -1. client[] fd 0 fd 1 fd 2 fd 3__________- rset: | 0 | 0 | 0 [ 1 | [0] d И] -1 [2] -1 maxfd+l=4 [FD_SETSIZE-1 ] -1 Рис. 6.12. Структуры данных для сервера TCP с одним прослушиваемым сокетом
202 Глава 6. Мультиплексирование ввода-вывода, функции select и poll Единственная ненулевая запись в наборе дескрипторов — это запись для про- слушиваемого сокета, и поэтому первый аргумент функции select будет равен 4. Когда первый клиент устанавливает соединение с нашим сервером, прослу- шиваемый дескриптор становится доступным для чтения и сервер вызывает функ- цию accept. Новый присоединенный дескриптор, возвращаемый функцией accept, будет иметь номер 4, если выполняются приведенные выше предположения. На рис. 6.13 показано соединение клиента с сервером. Рис. 6.13. Сервер TCP после того, как первый клиент устанавливает соединение Теперь наш сервер должен запомнить новый присоединенный сокет в своем массиве client, и присоединенный сокет должен быть добавлен в набор дескрип- торов Изменившиеся структуры данных показаны на рис. 6.14. client [] : [0] [1] [2] [FD_SETSIZE-1] 4 -1 -1 fd 0 fd 1 fd2 fd3 fd4 set:| 0 | 0 | 0 | 1 | 1 | maxfd+l=5 Рис. 6.14. Структуры данных после того, как установлено соединение с первым клиентом Через некоторое время второй клиент устанавливает соединение, и мы полу- чаем сценарий, показанный на рис. 6 15. Рис. 6.15. Сервер TCP после того, как установлено соединение со вторым клиентом
6.8. Эхо-сервер TCP (продолжение) 203 Новый присоединенный сокет (который имеет номер 5) должен быть разме- щен в памяти, в результате чего структуры данных меняются так, как показано на рис. 6.16. Рис. 6.16. Структуры данных после того, как установлено соединение со вторым клиентом fd 0 fd 1 fd 2 fd 3 fd 4 fd 5 rset:|0|o|0|l|l|l| maxfd+l=6 Далее мы предположим, что первый клиент завершает свое соединение. TCP клиента отправляет сегмент FIN, превращая тем самым дескриптор номер 4 на стороне сервера в готовый для чтения. Когда наш сервер считывает этот присо- единенный сокет, функция read! 1 пе возвращает нуль. Затем мы закрываем сокет, и соответственно изменяются наши структуры данных. Значение cl 1 ent[0] уста- навливается в -1, а дескриптор 4 в наборе дескрипторов устанавливается в нуль. Это показано на рис. 6.17. Обратите внимание, что значение переменной maxfd не изменяется. client [] : [2] -1 fd 0 fd 1 fd 2 fd 3 fd 4 fd 5 | 0 | 0 | 0 | 1 | 0 | 1 | maxfd+l=6 [FD_SETSIZE-1] -1 Рис. 6.17. Структуры данных после того, как первый клиент разрывает соединение Итак, по мере того как приходят клиенты, мы записываем дескриптор их при- соединенного сокета в первый свободный элемент массива client (то есть в пер- вый элемент со значением -1). Следует также добавить присоединенный сокет в набор дескрипторов для чтения. Переменная maxi — это наибольший использу- емый в данный момент индекс в массиве cl i ent, а переменная maxfd (плюс один) — это текущее значение первого аргумента функции sei ect. Единственным ограни- чением на количество обслуживаемых сервером клиентов является минималь- ное из двух значений: FD_SETSIZE и максимального числа дескрипторов, которое допускается для данного процесса ядром (о чем мы говорили в конце раздела 6.3). В листинге 6.3 показана первая половина этой версии сервера. Листинг 6.3. Сервер TCP, использующий одиночный процесс и функцию select: инициализация //tcpcbserv/tcpservselectOl с 1 #include "unp h” , л продолжение &
204 Глава 6. Мультиплексирование ввода-вывода: функции select и poll Листинг 6.3 (продолжение) 2 int 3 main(int argc. char **argv) 4 { 5 int 1 maxi, maxfd. listenfd connfd. sockfd. 6 int nready client[FD_SETSIZE], 7 ssize_t n 8 fd_set rset, all set 9 Char lineEMAXLINE] 10 socklen_t clilen 11 struct sockaddr_in cliaddr. servaddr, 12 listenfd = Socket (AFJNET. SDCK_STREAM, 0): 13 bzero(&servaddr. sizeof(servaddr)). 14 servaddr sin_family - AF_INET, 15 servaddr sin_addr s_addr = htonl(INADDR_ANY). 16 servaddr sin_port = htons(SERV_PDRT). 17 BindClistenfd (SA *) &servaddr, sizeof(servaddr)) 18 Listen(listenfd LISTENQ) 19 maxfd = listenfd /* инициализация */ 20 maxi = -1 /* индекс в массиве client[] */ 21 for (i = 0 i < FD_SETSIZE i++) 22 client[i] = -1. /* -1 означает свободный элемент */ 23 FD_ZERD(&allset). 24 FD_SET{listenfd. &allset), Создание прослушиваемого сокета и инициализация функции select L2-24 Этапы создания прослушиваемого сокета те же, что и раньше: вызов функций socket, bind и listen. Мы инициализируем структуры данных при том условии, что единственный дескриптор, который мы с помощью функции sei ect выберем, изначально является прослушиваемым сокетом. Вторая половина функции main показана в листинге 6.4. Листинг 6.4. Сервер TCP, использующий одиночный процесс и функцию select: цикл //tcpcliserv/tcpservselectOl с 25 for (..) { 26 rset = all set /* присваивание значения структуре */ 27 nready = SelectCmaxfd + 1. &rset. NULL NULL. NULL). 28 if (FD_ISSET(listenfd. &rset)) { /* соединение с новым клиентом *7 29 clilen = sizeof(cliaddr), 30 connfd = Accept(listenfd. (SA *) &cliaddr. &clilen). 31 for (i = 0 i < FD_SETSIZE. i++) 32 if (clientEi] < 0) { 33 clientEi] = connfd. /* сохраняем дескриптор */ 34 break. 35 }
6.8. Эхо-сервер TCP (продолжение) 205 36 if Ci “ FD_SETSIZE) 37 err_quit("too many clients"), 38 FD_SET(connfd. 8alIset). /* добавление нового дескриптора к набору */ 39 if (connfd > maxfd) 40 maxfd = connfd /* для функции select */ 41 if (i > maxi) 42 maxi =i. /* максимальный индекс в массиве client[] */ 43 if (--nready <= 0) 44 continue. /* больше нет дескрипторов готовых для чтения */ 45 } 46 for (i = 0, 1 <- maxi. i++) { /* проверяем все клиенты на наличие данных */ 47 if ( (sockfd = clientEi]) < 0) 48 continue. 49 if (FD_ISSET(sockfd &rset)) { 50 if ( (n = Readline(sockfd. line. MAXLINE)) — 0) { 51 /* соединение закрыто клиентом */ 52 Close(sockfd). 53 FD_CLR(sockfd &allset) 54 clientEi] = -1. 55 } else 56 Writen(sockfd. line, n) 57 if (--nready <= 0) 58 break. /* больше нет дескрипторов готовых для чтения */ 59 } 60 } 61 } 62 } Блокирование в функции select 26-27 Функция sel ect ждет, пока не будет установлено новое клиентское соединение или на существующем соединении не прибудут данные, сегмент FIN или сегмент RST. Принятие новых соединений с помощью функции accept 28-45 Если прослушиваемый сокет готов для чтения, новое соединение установлено. Мы вызываем функцию accept, изменяя тем самым наши структуры данных. Для записи присоединенного сокета мы используем первый незадействованный эле- мент массива cl i ent. Число готовых дескрипторов уменьшается, и если оно равно нулю, мы можем не выполнять следующий цикл for. Это позволяет нам исполь- зовать значение, возвращаемое функцией select, чтобы избежать проверки не готовых дескрипторов. Проверка существующих соединений 46-60 Каждое существующее клиентское соединение проверяется на предмет того, содержится ли его дескриптор в наборе дескрипторов, возвращаемом функцией sel ect. Если да, то от клиента считывается строка и отражается обратно клиенту. Если клиент закрывает соединение, функция readl те возвращает нуль и наши структуры данных соответствующим образом изменяются.
206 Глава 6. Мультиплексирование ввода-вывода: функции select и poll Мы не уменьшаем значение переменной шахт, но могли бы проверять эту воз- можность каждый раз, когда клиент закрывает свое соединение. Этот сервер сложнее, чем сервер, показанный в листингах 5.1 и 5.2, но он по- зволяет избежать затрат на создание нового процесса для каждого клиента, что является хорошим примером использования функции sei ect. Тем не менее в раз- деле 15.6 мы опишем проблему, связанную с этим сервером, которая, однако, лег- ко устраняется, если сделать прослушиваемый сокет неблокируемым, а затем проверить и проигнорировать несколько ошибок из функции accept. Атака типа «отказ в обслуживании» К сожалению, функционирование только что описанного сервера вызывает про- блемы. Посмотрим, что произойдет, если некий клиент-злоумышленник соеди- нится с сервером, отправит 1 байт данных (отличный от разделителя строк) и вой- дет в состояние ожидания. Сервер вызовет функцию readl i пе, которая прочитает одиночный байт данных от клиента и заблокируется в следующем вызове функ- ции read, ожидая следующих данных от клиента. Сервер блокируется (вернее, «подвешивается») этим клиентом и не может предоставить обслуживание ника- ким другим клиентам (ни новым клиентским соединениям, ни данным существу- ющих клиентов), пока упомянутый клиент-злоумышленник не отправит символ перевода строки или не завершит свой процесс. Дело в том, что обрабатывая множество клиентов, сервер никогда не должен блокироваться в вызове функции, относящейся к одному клиенту. В противном случае можно «подвесить» сервер, что приведет к отказу в обслуживании для всех остальных клиентов. Это называется атакой типа «отказ в обслуживании» (DoS attack — Denial of Service). Такая атака воздействует на сервер, делая невозмож- ным обслуживание нормальных клиентов. Обезопасить себя от подобных атак позволяют следующие решения: использовать неблокируемый ввод-вывод (см. гла- ву 15), предоставлять каждому клиенту обслуживание отдельным потоком (на- пример, для каждого клиента порождать процесс или поток) или установить тайм- аут для ввода-вывода (см. раздел 13.2). 6.9. Функция pselect Функция psel ect была введена в Posix.lg. #include <sys/select h> #i nc 1 tide <signal h> #include <time h> int pselect(int maxfdpl, fd_set ★readset. fd_set *writeset fd_set *exceptset. const struct timespec *timeout. const sigset_t *sigmask). Возвращает количество готовых дескрипторов. О в случае тайм-аута -1 в случае опивки Функция psel ect имеет два отличия от обычной функции sei ect: 1. Функция psel ect использует структуру timespec, нововведение стандарта ре- ального времени Posix. 1b, вместо структуры timeval. struct timespec { time_t tv_sec, /* секунды */
6,9. Функция pselect 207 long tv_nsec. /* наносекунды */ }. Эти структуры отличаются вторыми элементами: элемент tv_nsec новой струк- туры задает наносекунды, в то время как элемент tv_usec прежней структуры задает микросекунды. 2. В функции psel ect добавляется шестой аргумент — указатель на маску сигна- лов. Это позволяет программе отключить доставку ряда сигналов, проверить некоторые глобальные переменные, установленные обработчиками этих от- ключенных сигналов, а затем вызвать функцию psel ect, сообщив ей, что нуж- но переустановить маску сигналов. В отношении второго пункта рассмотрим следующий пример (описанный нас. 308-309 [93]). Обработчик сигнала нашей программы для сигнала SIGINT просто устанавливает глобальную переменную i ntr_f 1 ад и возвращает управле- ние. Если наш процесс блокирован в вызове функции select, возвращение из об- работчика сигнала заставляет функцию завершить работу, присвоив еггпо значе- ние EINTR. Но когда вызывается функция sel ect, код выглядит следующим образом: if (wtr_flag) handle_intr(). /* обработка этого сигнала */ Tf ( (nready = selectC )) < 0) { if (еггпо == EINTR) { if (intr_flag) handle_intr(). } } Проблема заключается в том, что если в течение промежутка времени между проверкой переменной intr_flag и вызовом функции select приходит сигнал, он будет потерян в том случае, если функция sel ect заблокирует процесс навсегда. С помощью функции pselect мы можем переписать этот пример так, чтобы он работал более надежно: sigset_t newmask, oldmask zeromask. sigemptysetf&zeromask), sigemptyset(&newmask). sigaddset(&newmask. SIGINT), sigprocmask(SIG_BLOCK. &newmask, &oldmask): /* блокирование сигнала SIGINT */ if (intr_flag) handle_intr(). /* обработка этого сигнала */ if ( (nready = pselect( ... , &zeromask)) < 0) { if (errno “ EINTR) { if (intr_flag) handleji ntr(). } } Перед проверкой переменной intr_flag мы блокируем сигнал SIGINT, Когда вызывается функция psel ect, она заменяет маску сигналов процесса пустым на- бором (zeromask), а затем проверяет дескрипторы, возможно, переходя в состоя- ние ожидания. Но когда функция pselect возвращает управление, маске сигна-
208 Глава 6. Мультиплексирование ввода-вывода: функции select и poll лов процесса присваивается то значение, которое предшествовало вызову функ- ции pselect (то есть сигнал SIGI NT блокируется). Мы поговорим о функции psel ect более подробно и приведем ее пример в раз- деле 18.5. Функцию pselect мы используем в листинге 18.3, а в листинге 18.4 по- казываем простую, хотя и некорректную реализацию этой функции. ПРИМЕЧАНИЕ --------------------------------------------------------- Есть одно незначительное отличие между двумя функциями select. Первый элемент структуры timeval является целым числом типа long со знаком, в то время как первый элемент структуры timspec имеет тип time_t. Число типа long со знаком в первой функ- ции также должно было относиться к типу time_t, но мы не меняли его тип, чтобы не разрушать существующего кода. Однако в новой функции это можно было бы сделать. 6.10. Функция poll Функция pol 1 появилась впервые в SVR3, и изначально ее применение ограни- чивалось потоковыми устройствами (streams devices) (см. главу 33). В SVR4 это ограничение было снято, что позволило функции ро 11 работать с любыми дескрип- торами. Функция pol 1 предоставляет функциональность, аналогичную функции sei ect, но позволяет получать дополнительную информацию при работе с пото- ковыми устройствами. #include <pol1 h> int pollfstruct pollfd *fdarray, unsigned long nfds. int timeout). Возвращает- количество готовых дескрипторов. О в случае тайм-аута. -1 в случае ошибки Первый аргумент — это указатель на первый элемент массива структур. Каж- дый элемент массива — это структура pol 1 fd, задающая условия, проверяемые для данного дескриптора fd. struct pollfd { int fd. short events. short revents. } /* дескриптор, который нужно проверить */ /* события на дескрипторе, которые нас интересуют */ /* события, произошедшие на дескрипторе fd */ Проверяемые условия задаются элементом events, и состояние этого дескрип- тора функция возвращает в соответствующем элементе revents. (Наличие двух переменных для каждого дескриптора, одна из которых — значение, а вторая — результат, дает возможность обойтись без аргументов типа «значение-результат». Вспомните, что три средних аргумента функции select имеют тип «значение-ре- зультат».) Каждый из двух элементов состоит из одного или более битов, задаю- щих определенное условие. В табл. 6.2 перечислены константы, используемые для задания флага events и для проверки флага revents. Мы разделили эту таблицу на три части: первые четыре константы относятся ко вводу, следующие три — к выводу, а последние три — к ошибкам. Обратите внимание, что последние три константы не могут устанавливаться в элементе events, но всегда возвращаются в revents, когда выполняется соответствующее условие. Существует три класса данных, различаемых функцией pol 1: обычные, при- оритетные и данные с высоким приоритетом. Эти термины берут начало в реа- лизациях. основанных на потоках ('пис 3.3 51 * ’ •
6.10. Функция poll 209 Таблица 6.2. Различные значения флагов events и revents для функции poll Константа На входе (events) На выходе (revents) Описание POLLIN • • Можно считывать обычные или приоритетные данные POLLRDNORM • • Можно считывать обычные данные POLLRDBAND • • Можно считывать ♦ приоритетные данные POLLPRI • • - Можно считывать данные с высоким приоритетом POLLOUT • • Можно записывать обычные данные POLLWRNORM • • Можно записывать обычные данные POLLWRBAND • • Можно записывать приоритетные данные POLLERR POLLHUP POLLNVAL • Произошла ошибка • Произошел разрыв соединения • Дескриптор не соответствует открытому файлу ПРИМЕЧАНИЕ--------------------------------------------------------- Константа POLLIN может быть задана путем логического сложения констант POL- LRDNORM и POLLRDBAND. Константа POLLIN существовала еще в реализациях SVR3, которые предшествовали полосам приоритета в SVR4, то есть эта константа су- ществует в целях обратной совместимости. Аналогично, константа POLLOUT эквива- лентна POLLWRNORM, и первая из них предшествовала второй. Для сокетов TCP и UDP при описанных условиях функция pol 1 возвращает указанный флаг revent. К сожалению, в определении функции poll стандарта Posix. 1g имеется множество слабых мест (дополнительных возможностей воз- вратить то же условие): & Все регулярные данные TCP и все данные UDP считаются обычными. ': Внеполосные данные TCP (см. главу 21) считаются приоритетными. * Когда считывающая половина соединения TCP закрывается (например, если получен сегмент FIN), это также считается равнозначным обычным данным, и последующая операция чтения возвратит нуль. i' Наличие ошибки для соединения TCP может расцениваться либо как обыч- ные данные, либо как ошибка (POLLERR). В любом случае последующая функ- ция read возвращает -1, что сопровождается установкой переменной еггпо в со- ответствующее значение. Так обрабатываются такие условия, как получение RST или тайм-аута. Информация о доступности нового соединения на прослушиваемом сокете может считаться либо обычными, либо приоритетными данными. В больший-
210 Глава 6. Мультиплексирование ввода-вывода: функции select и poll Число элементов в массиве структур задается аргументом nfds. ПРИМЕЧАНИЕ ------------------------------------------------------- Исторически этот аргумент имел тип long без знака, что является некоторым излише- ством. Достаточно будет типа int без знака. В Unix 98 для этого аргумента определяет- ся новый тип — ngfds t. Аргумент 11 meout определяет, как долго функция находится в ожидании перед завершением. Положительным значением задается количество миллисекунд — время ожидания. В табл. 6.3 показаны возможные значения аргумента timeout. Таблица 6.3. Значения аргумента timeout для функции poll Значение аргумента timeout Описание INFTIM Ждать вечно 0 Возвращать управление немедленно, без блокирования >0 Ждать в течение указанного числа миллисекунд Константа INFTIM определена как отрицательное значение. Если таймер в дан- ной системе не обеспечивает точность порядка миллисекунд, значение округля- ется в большую сторону до ближайшего поддерживаемого значения. ПРИМЕЧАНИЕ---------------------------------------------------------- Posix.lg требует, чтобы константа INFTIM была определена в заголовочном фай- ле <poll.h>, но многие системы все еще определяют ее в заголовочном файле <sys/ stropts.h>. Как и в случае функции select, любой тайм-аут, установленный для функции poll, огра- ничивается снизу разрешающей способностью часов в данной реализации (обычно 10 миллисекунд). Функция pol 1 возвращает -1, если произошла ошибка, 0 — если нет готовых дескрипторов до истечения времени таймера; иначе возвращается число дескрип- торов с ненулевым элементом revents. Вспомните наши рассуждения в конце раздела 6.3 относительно константы FD_SETSIZE и максимального числа дескрипторов в наборе в сравнении с макси- мальным числом дескрипторов для процесса. У нас не возникает подобных про- блем с функцией ро 11, поскольку вызывающий процесс отвечает за размещение массива структур pol 1 fd в памяти и за последующее сообщение ядру числа эле- ментов в массиве. Не существует типа данных фиксированного размера, анало- гичного fd_set, о котором знает ядро. ПРИМЕЧАНИЕ --------------------------------------------------------- Posix. 1g требует наличия и функции select, и функции poll. Но если сравнивать их с точ- ки зрения переносимости, то функцию select в настоящее время поддерживает больше систем, чем функцию poll. Posix.lg определяет также функцию pselect — усовершен- ствованную версию функции select, которая обеспечивает возможность блокирования сигналов и предоставляет лучшую разрешающую способность по времени, а для функ- ции poll ничего подобного в Posix.lg нет.
6.11. Эхо-сервер TCP (еще раз) 211 6.11. Эхо-сервер TCP (еще раз) Теперь мы изменим наш эхо-сервер TCP из раздела 6.8, используя вместо функ- ции sei ect функцию pol 1. В предыдущей версии сервера, работая с функцией sei ect, мы должны были выделять массив client вместе с набором дескрипторов rset (см. рис. 6.12). С помощью функции pol 1 мы разместим в памяти массив струк- тур pol 1 fd. В нем же мы будем хранить и информацию о клиенте, не создавая для нее другой массив. Элемент fd этого массива мы обрабатываем тем же способом, которым обрабатывали массив cl lent (см. рис. 6.12): значение -1 говорит о том, что элемент не используется, а любое другое значение является значением де- скриптора. Вспомните из предыдущего раздела, что любой элемент в массиве структур pol 1 fd, передаваемый функции pol 1 с отрицательным значением элемента fd, просто игнорируется. В листинге 6.5 показана первая часть кода нашего сервера. Листинг 6.5. Первая часть сервера TCP, использующего функцию poll //tcpcliserv/tcpservpol101 с 1 #include "unp h" 2 ^include <limits h> /* для OPEN_MAX */ 3 int 4 main(int argc. char **argv) 5 { 6 int i maxi, listenfd. connfd. sockfd. 7 int nready 8 ssize_t n. 9 char line[MAXLINE]. 10 socklen_t clilen. 11 struct pollfd client[OPEN_MAX], 12 struct sockaddr_in cliaddr. servaddr. 13 listenfd - Socket(AF_INET. SOCK_STREAM. 0); 14 bzero(&servaddr. sizeof(servaddr)). 15 servaddr sin_family - AF_INET, 16 servaddr sin_addr s_addr - htonl(INADDR_ANY); 17 servaddr sin_port - htons(SERV_PORT). 18 Bind(listenfd. (SA *) &servaddr. sizeof(servaddr)). 19 ListenOistenfd. LISTEMQ). 20 client[0] fd - listenfd. 21 client[0] events - POLLRDNORM. 22 for (i - 1. i < OPEN_MAX, i++) 23 client[i].fd - -1. /* -1 означает, что элемент свободен */ 24 maxi - 0. /* максимальный индекс массива client[] */ Размещение массива структур pollfd в памяти И Мы объявляем элементы OPENJ4AX в нашем массиве структур pol 1 fd. Определе- ние максимального числа дескрипторов, которые могут быть одновременно от- крыты для процесса, выполняется непросто. Мы снова столкнемся с этой про-
212 Глава 6. Мультиплексирование ввода-вывода: функции select и poll блемой в листинге 12.1. Один из способов ее решения — вызвать функцию sysconf Posix. 1 с аргументом _SC_OPEN_MAX [93, с. 42-44], а затем динамически выделять в памяти место для массива соответствующего размера. Но одним из возвращае- мых функцией sysconf значений может быть indeterminate (неопределенность), говорящее о том, что нам нужно самим задавать значение. Здесь мы используем только константу OPEN_MAX стандарта Posix. 1. Инициализация 20-24 Мы используем первый элемент в массиве client для прослушиваемого сокета и присваиваем дескрипторам для оставшихся элементов -1. Мы также задаем в ка- честве аргумента функции pol 1 событие POLLRDNORM, чтобы получить уведомление от этой функции в том случае, когда новое соединение будет готово к приему. Переменная maxi содержит максимальный индекс массива client, используемый в настоящий момент. Вторая часть нашей функции приведена в листинге 6.6. Листинг 6.6. Вторая часть сервера TCP, использующего функцию poll //tcpcliserv/tcpservpol101 с 25 for (..) { 26 nready = Poll(client maxi + 1 INFTIM) 27 if (cllentEOJ revents & POLLRDNORM) { /* новое соединение с клиентом */ 28 clilen = sizeof(cliaddr). 29 connfd = Accept(listenfd (SA*) &cliaddr. &clilen). 30 for d = 1. 1 < OPEN_MAX. i++) 31 if (client[i] fd < 0) { 32 clientEi] fd = connfd. /* сохраняем дескриптор */ 33 break. 34 } 35 if (1 == OPEN_MAX) 36 err_quit("too many clients”). 37 client[i] events = POLLRDNORM 38 if (i > maxi) 39 maxi = i. /* максимальный индекс в массиве client!] */ 40 if (--nready <= 0) 41 continue. /* больше нет дескрипторов, готовых для чтения */ 42 } 43 for (i=l.i <= maxi i++) { /* проверяем все клиенты на наличие данных *7 44 if ( (sockfd = clientli] fd) < 0) 45 continue. 46 if (clientEi] revents & (POLLRDNORM | POLLERR)) { 47 if ( (n - readline(sockfd line. MAXLINE)) < 0) { 48 if (errno == ECONNRESET) { 49 /* соединение переустановлено клиентом */ 50 Close(sockfd). 51 client[i] fd - -1 52 } else 53 err_sys('readline error”), 54 ) else if (n -== 0) 1
6.12. Резюме 213 56 Close(sockfd). 57 clientCi] fd = -1. 58 } else 59 Writer,(sockfd. line n), 60 if (--nready <= 0) 61 break, /* больше нет дескрипторов, готовых для чтения */ 62 } 63 } 64 } 65 } Вызов функции poll, проверка нового соединения 26-42 Мы вызываем функцию pol 1 для ожидания нового соединения либо данных на существующем соединении. Когда новое соединение принято, мы находим пер- вый свободный элемент в массиве cl i ent — это первый элемент с отрицательным дескриптором. Обратите внимание, что мы начинаем поиск с индекса 1, поскольку элемент с 11 ent [ 0 ] используется для прослушиваемого сокета. Когда свободный эле- мент найден, мы сохраняем дескриптор и устанавливаем событие PDLLRDNORM. Проверка данных на существующем соединении 43-63 Два события, которые нас интересуют, — это POLLRDNORM и POLLERR. Второй флаг в элементе event мы не устанавливали, поскольку этот флаг возвращается всегда, если соответствующее условие выполнено. Причина, по которой мы проверяем событие POLLERR, в том, что некоторые реализации возвращают это событие, когда приходит сегмент RST, другие же в такой ситуации возвращают событие POLLRDNORM. В любом случае мы вызываем функцию readl i пе, и если произошла ошибка, эта функция возвратит ее. Когда существующее соединение завершается клиентом, мы просто присваиваем элементу fd значение -1. 6.12. Резюме В Unix существует пять различных моделей ввода-вывода: <• блокируемый ввод-вывод; неблокируемый ввод-вывод; ® мультиплексирование ввода-вывода; * управляемый сигналом ввод-вывод; асинхронный ввод-вывод. По умолчанию используется блокируемый ввод-вывод, и этот вариант встре- чается наиболее часто. Неблокируемый ввод-вывод и управляемый сигналом ввод-вывод мы рассмотрим в последующих главах. В этой главе мы рассмотрели мультиплексирование ввода-вывода. Асинхронный ввод-вывод определяется в стандарте Posix. 1, но поддерживающих его реализаций не так много. Наиболее часто используемой функцией для мультиплексирования ввода- вывода является функция sel ect. Мы сообщаем этой функции, какие дескрипто- ры нас интересуют (для чтения, записи или условия ошибки), а также передаем
214 Глава 6. Мультиплексирование ввода-вывода: функции select и poll ченное на единицу). Большинство вызовов функции select определяют количе- ство дескрипторов, готовых для чтения, и, как мы отметили, единственное усло- вие исключения при работе с сокетами — это прибытие внеполосных данных (см. главу 21). Поскольку функция select позволяет ограничить время блокиро- вания функции, мы используем зто свойство в листинге 13.3 для ограничения по времени операции ввода. Используя эхо-клиент в пакетном режиме с помощью функции sei ect, мы выяснили, что даже если обнаружен конец ввода, данные все еще могут находиться в канале на пути к серверу или от сервера. Обработка этого сценария требует применения функции shutdown, которая позволяет воспользоваться таким свойством TCP, как возможность половинного закрытия соединения (half- close feature). Posix. 1g определяет новую функцию psel ect, повышающую точность таймера с микросекунд до наносекунд, которой передается новый аргумент — указатель на набор сигналов. Это позволяет избежать ситуации гонок (race condition) при перехвате сигналов, о которой мы поговорим более подробно в разделе 18.5. Функция pol 1 из System V предоставляет функциональность, аналогичную функции sei ect. Кроме того, она обеспечивает дополнительную информацию при работе с потоковыми устройствами. Posix 1g требует наличия и функции sei ect, и функции pol 1, но первая используется чаще. Упражнения 1. Мы говорили, что набор дескрипторов можно присвоить другому набору де- скрипторов, используя оператор присваивания языка С. Как это сделать, если набор дескрипторов является массивом целых чисел? {Подсказка: посмотри- те на свой системный заголовочный файл <sys/select h> или <sys/types h>.) 2. Описывая в разделе 6.3 условия, при которых функция sei ect сообщает, что дескриптор готов для записи, мы указали, что сокет должен быть неблокируе- мым, для того чтобы операция записи возвратила положительное значение. Почему? 3. Что произойдет в листинге 6.1, если мы поставим слово el se перед i f в стро- ке 19? 4. В листинге 6.3 добавьте необходимый код, чтобы позволить серверу исполь- зовать максимальное число дескрипторов, допустимое ядром. {Подсказка: рас- смотрите функцию setrl innt.) 5. Посмотрите, что происходит, если в качестве второго аргумента функции shutdown передается SHUT_RD. Возьмите за основу код клиента TCP, представ- ленный в листинге 5.3, и выполните следующие изменения: вместо номера порта SERV_PORT задайте порт 19 (служба chargen, см. табл. 2.1), а также заме- ните вызов функции str_cl 1 вызовом функции pause. Запустите программу, задав IP-адрес локального узла, на котором выполняется сервер chargen. Про- смотрите пакеты с помощью такой программы, как, например, tcpdump (см. раз- дел В.5). Что происходит?
Упражнения 215 6. Почему приложение должно вызывать функцию shutdown с аргументом SHUT_RDWR, вместо того чтобы просто вызвать функцию cl ose? 7. Что происходит в листинге 6.4, когда клиент отправляет RST для завершения соединения? 8. Перепишите код, показанный в листинге 6.5, чтобы вызывать функцию sysconf для определения максимального числа дескрипторов и размещения соответ- ствующего массива cl lent в памяти.
ГЛАВА 7 Параметры сокетов 7.1. Введение Существуют различные способы получения и установки параметров сокетов: функции getsockopt и setsockopt; . функция fent 1; функция lOCtl. Эту главу мы начнем с описания функций getsockopt и setsockopt. Далее мы приведем пример, в котором выводятся заданные по умолчанию значения пара- метров, а затем дадим подробное описание всех параметров сокетов. Мы разде- лили описание параметров на следующие категории: общие, IPv4, IPv6 и TCP. При первом прочтении главы можно пропустить подробное описание парамет- ров и при необходимости прочесть отдельные разделы, на которые даны ссылки. Отдельные параметры подробно описываются в дальнейших главах, например параметры многоадресной передачи IPv4 и IPv6 мы обсуждаем в разделе 19.5. Мы также рассмотрим функцию fcntl, поскольку она реализует предусмот- ренные стандартом Posix возможности отключить для сокета блокировку ввода- вывода, включить управление сигналами, а также установить владельца сокета. Функцию 1 octi мы опишем в главе 16. 7.2. Функции getsockopt и setsockopt Эти две функции применяются только к сокетам. #wclude <sys/socket h> int getsockopt (int sockfd int level, int optname. void *optval. socklen_t ★optlen') int setsockoptdnt sockfd. int level, int optname const void *optval. socklen_t optlen) Переменная sockfd должна ссылаться на открытый дескриптор сокета. Пере- менная 1 evel определяет, каким кодом должен интерпретироваться параметр: общими программами обработки сокетов или зависящими от протокола програм- мами (например, IPv4, IPv6 или TCP). optval — это указатель на переменную, из которой извлекается новое значение параметра с помощью функции setsockopt или в которой сохраняется текущее значение параметра с помощью функции getsockopt. Размер этой переменной за- дается последним аргументом. Для функции setsockopt тип этого аргумента — «значение», а для функции getsockopt — «значение-результат».
7.2, Функции getsockopt и setsockopt 217 В табл. 7.1 приведены параметры, которые могут запрашиваться функцией getsockopt или устанавливаться функцией setsockopt. В колонке «Тип данных» приводится тип данных того, на что указывает указатель optval для каждого пара- метра. Две фигурные скобки мы используем для того, чтобы обозначить структу- ру, — например, linger {} обозначает struct linger. Существует два основных типа параметров: двоичные параметры, включаю- щие или отключающие определенное свойство (флаги), и параметры, получаю- щие и возвращающие специфичные значения, которые мы можем либо задавать, либо проверять (значения). В колонке «Флаг» определяется, относится ли пара- метр к флагам. Для флагов при вызове функции getsockopt *optval является це- лым числом. Возвращаемое значение *optval нулевое, если параметр отключен, и ненулевое, если параметр включен. Аналогично, функция setsockopt требует не- нулевого значения *optval для включения параметра, и нулевого значения — для его выключения. Если в колонке «Флаг» не содержится символа «•», то параметр используется для передачи значения заданного типа между пользовательским процессом и системой. В последующих разделах этой главы приводятся дополнительные подробно- сти о параметрах сокетов. Таблица 7.1. Параметры сокетов для функций getsockopt и setsockopt level optname get set Описание Флаг Тип данных SOL.SOCKET SO_BROADCAST • • Позволяет посылать • int широковещательные дейтаграммы SODEBUG • • Разрешает отладку • int SO_DONTROUTE • • Обходит таблицу маршрутизации • int SO_ERROR • Получает ошибку, ожидающую обра- ботки, и возвращает int значение параметра в исходное состояние SO_KEEPALIVE • • Периодически про- веряет, находится ли соединение в рабочем • int СОСТОЯНИИ SO_LINGER • • Задерживает закрытие hnger{} сокета, если имеются данные для отправки SOOOBINLINE • • Оставляет полученные • int внеполосные данные вместе с обычными данными (inline) SORCVBUF • • Размер приемного буфера int SO_SNDBUF • • Размер буфера отправки int SO_RCVLOWAT • • Минимальное количе- int ство данных для при- емного буфера сокета продолжение &
218 Глава7. Параметрысокетов Таблица 7.1 (продолжение) level optname get set Описание Флаг Тип данных SOSNDLOWAT • • Минимальное коли- mt чество данных для буфера отправки сокета SO_RCVTIMEO • • Тайм-аут при получении timeval{) SO_SNDTIMEO • • Тайм-аут при отправке timeval{) SO_REUSEADDR • • Допускает повторное • int использование ло- кального адреса SO_REUSEPORT • • Допускает повторное • int использование ло- кального адреса SO_TYPE • Возвращает тип int сокета SO_USELOOPBACK • • Маршрутизирующий сокет получает копию того, что он отправляет • int IPPROTOJP IP_HDRINCL • • Включается 1Р-заго- • int ЛОВОК IP_OPTIONS • • В заголовке IPv4 уста- см текст навливаются пара- метры IP IP_RECVDSTADDR • • Возвращает 1Р-адрес получателя • int IP_RECVIF • • Возвращает индекс интерфейса, на кото- • int ром принимается дейтаграмма UDP IP_TOS • • Тип сервиса и приоритет int IP_TTL • • Время жизни int IP_MULTICAST_IF • • Задает интерфейс для исходящих дейтаграмм in_addr{} IP_MULTICAST_TTL • • Задает TTL для исхо- дящих дейтаграмм u_char IP_MULTICAST_ • • Разрешает или отме- u_char LOOP няет отправку копии дейтаграммы на тот узел, откуда она была послана (loopback) IP ADD • Включение в i руппу ip mreq() MEMBERSHIP многоадресной передачи IP DROP • Отключение от группы ip mreq{) MEMBERSHIP многоадресной передачи IPPROTO ICMP6 FILTER • • Указывает тип сооб- icmp6_filter{} ICMPV6 щения ICMPv6, кото- рое передается процессу
7.2 Функции getsockopt и setsockopt 219 level optname get set Описание Флаг Тип данных IPPROTOJPV6 IPV6_ADDRFORM • IPV6_CHECKSUM • IPV6_DSTOPTS • IPV6_HOPLIMIT • IPV6_HOPOPTS ' • IPV6_NEXTHOP IPV6_PKTINFO • IPV6_PKTOPTIONS • IPV6_RTHDR • IPV6_UNICAS_ THOPS IPV6_MULTICAST_ • IF IPV6_MULTICAST_ • HOPS IPV6_MULTICAST_ • LOOP IPV6_ADD_ MEMBERSHIP IPV6_DROP_ MEMBERSHIP IPPROTO_TCP TCP_KEEPALIVE • TCP_MAXRT • Меняет формат адреса сокета Отступ поля контроль- ной суммы для символь- ных (неструктурирован- ных) сокетов Получение параметров • получателя Получение предельно- • го количества транзит- ных узлов для однона- правленной передачи (unicast hop limit) Получение парамет- • ров транзитных узлов (hop-by-hop options) Задает следующий • транзитный адрес Получает информа- • цию о пакете Задает параметры пакета Получает маршрут • Предел количества транзитных узлов, задаваемый по умолчанию Задает интерфейс для исходящих дейтаграмм Задает предельное количество транзит- ных узлов для исхо- дящих широковеща- тельных сообщений Разрешает или отме- • няет отправку копии дейтаграммы на тот узел, откуда она была послана (loopback) Включение в группу многоадресной передачи Отключение от группы многоадресной передачи Время простоя в секун- дах до проверки Максимальное время для повторной передачи TCP int int int int mt sockaddr{} int См текст int int in6_addr{} u_int u_int ipv6_mreq{) ipv6_mreq{) int mt продолжение &
220 Глава 7. Параметры сокетов Таблица 7.1 (продолжение} level optname get set Описание Флаг Тип данных TCPMAXSEG • • Максимальный размер int сегмента TCP TCP_NODELAY • • Отключает алгоритм • int Нагла TCPSTDURG • • Интерпретация сроч- • int ного указателя 7.3. Проверка наличия параметра и получение значения по умолчанию Напишем программу, которая проверяет, поддерживается ли большинство пара- метров, представленных в табл. 7.1, и если да, то выводит их значения, заданные по умолчанию. В листинге 7.11 содержатся объявления нашей программы. Листинг 7.1. Объявления для нашей программы, проверяющей параметры сокетов //sockopt/checkopts с 1 include "unp h" 2 include <netinet/tcp h> /* определения констант TCP_xxx */ 3 union val { 4 int i_val. 5 long l_val. 6 char c_val[10]. 7 struct linger 1inger_val; 8 struct timeval timeval_val: 9 } val. 10 static char *sock_ str_flag(union val *, int); 11 static char *sock_ _str_i nt (union val *. int): 12 static char *sock_ str_linger(umon val *, int); 13 static char *sock_ str_timeval(union val *. int): 14 struct sock opts { 15 char *opt_str. 16 int opt_level: 17 int opt name. 18 char *(*opt_val_ str)(umon val *. int): 19 } sock opts[] = { 20 "SO_BROADCAST" SOL_SOCKET, SO_BROADCAST. sock_str_flag. 21 "S0_DEBUG". SOL_SOCKET. SO-DEBUG. sock_str_flag. 22 "SO_DONTROUTE" SOL_SOCKET. SO_DDNTROUTE. sock_str_flag. 23 "SO_ERROR". SOL_SOCKET. SO_ERROR. sock_str_int. 24 "SO_KEEPALIVE" SOL_SOCKET. SO_KEEPALIVE. sock_str_flag. 25 "SO_LINGER". SOL_SOCKET. SO_LINGER. sock_str_linger 26 "SO_OOBINLINE" SOL_SOCKET. SO-OOBINLINE. sock_str_flag. 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http.// www.piter com /download
7.3. Проверка наличия параметра и получение значения по умолчанию 221 27 "SO_RCVBUF". SOL_SOCKET. SO_RCVBUF, sock_str_int. 28 "SO_SNDBUF". SOL_SOCKET. SO_SNDBUF, sock_str_int. 29 ”SO_RCVLOWAT". SOL_SOCKET. SO_RCVLOWAT. sock_str_int. 30 "SO_SNDLOWAT". SOL_SOCKET. SO_SNDLOWAT. sock_str_int. 31 "SO_RCVTIMEO", SOL_SOCKET. SO-RCVTIMEO sock_str_timeval 32 "SO_SNDTIMEO", SOL_SOCKET. SO_SNDTIMEO, sock_str_timeval 33 "SO REUSEADDR". SOL_SOCKET. SO_REUSEADDR. sockjtr Jlag. 34 #ifdef SO REUSEPORT 35 "SO REUSEPORT". SOL_SOCKET. SO_REUSEPORT. sock_str_flag. 36 #else 37 "SO REUSEPORT". 0. 0. NULL. 38 #endif 39 "SO TYPE". SOL_SOCKET. SO_TYPE. sock_str_int. 40 "SOJJSELOOPBACK”. SOL_SOCKET. SOJJSELOOPBACK. sock_str_flag. 41 "IP_TOS". IPPROTOJP. IP_TOS. sock_str_int. 42 "IP-TTL". IPPROTOJP. IP_TTL. sock_str_int. 43 "TCP_MAXSEG". IPPROTO_TCP.TCP_MAXSEG. sock_str_int. 44 ”TCP_NODELAY". IPPROTO TCP.TCP NODELAY. sock_strjlag. 45 NULL. 0. 0. NULL 46 }. Объявление объединения возможных значений 3-9 Наше объединение val содержит по одному элементу для каждого возможного возвращаемого значения из функции getsockopt. Задание прототипов функций 10-13 Мы определяем прототипы для четырех функций, которые вызываются д^р вывода значения данного параметра сокета. Задание структуры и инициализация массива 14-46 Наша структура sock opts содержит всю информацию, которая необходима, чтобы вызвать функцию getsockopt для каждого из параметров сокета и вывести его текущее значение. Последний элемент, opt_val_str, является указателем на одну из четырех функций, которые выводят значение параметра. Мы размещаем в памяти и инициализируем массив этих структур, каждый элемент которого со- ответствует одному параметру сокета. ПРИМЕЧАНИЕ----------------------------------------------------------- Не все реализации поддерживают полный набор параметров сокетов. Чтобы опреде- лить, поддерживается ли данный параметр, следует использовать #ifdef или #if defined, как показано для параметра SO REUSEPORT. Для полноты картины требуется обра- ботать подобным образом все параметры, но мы пренебрегаем этим, потому что #ifdef только удлиняет показанный код и не влияет на суть дела. В листинге 7.2 показана наша функция тэт п. Листинг 7.2. Функция main для проверки параметров сокетов //sockopt/checkopts с 47 int 48 maw(-int argc. char **argv) 49 { 50 int fd. len. . продолжение -1У
222 Глава 7. Параметры сокетов Листинг 7.2 (продолжение) 51 struct sock_opts *ptr 52 fd = Socket(AF_INET SOCK_STREAM. 0) 53 for (ptr - sock_opts ptr->opt_str '= NULL; ptr++) { 54 printfCis " ptr->opt_str). 55 if (ptr->opt_val_str == NULL) 56 printf(“(undefined)\en") 57 else { 58 len = sizeof(val) 59 if (getsockopt(fd ptr->opt_level. ptr->opt_name. 60 &val. &len) — -1) { 61 err_ret(’’getsockopt error"). 62 } else { 63 printf(“default = is\en”. (*ptr->opt_val_str) (Sval. len)); 64 } 65 } 66 } 67 exit(0). 68 } Создание сокета TCP, перебор всех параметров 52-56 Мы создаем сокет TCP, а затем перебираем все элементы нашего массива. Если указатель opt_val_str пустой, то параметр не определен реализацией (что, как мы показали, возможно для SO_REUSEPORT). Вызов функции getsockopt 57-61 Мы вызываем функцию getsockopt, но не завершаем ее выполнение, если воз- вращается ошибка. Многие реализации определеяют имена некоторых парамет- ров сокетов, даже если не поддерживают эти параметры. Неподдерживаемые па- раметры выдают ошибку ENOPROTOOPT. Вывод значения параметра по умолчанию 62-63 Если функция getsockopt успешно завершается, мы вызываем нашу функцию для преобразования значения параметра в строку и выводим эту строку. В листинге 7.1 мы показали четыре прототипа функций, по одному для каж- дого типа возвращаемого значения параметра В листинге 7.3 показана одна из этих функций, sock_str_flag, которая выводит значение параметра, являющегося флагом. Другие три функции аналогичны этой. Листинг 7.3. Функция sock_str_flag: преобразование флага в строку //sockopt/checkopts с 69 static char strres[128] 70 static char * 71 sock_str_flag(umon val *ptr int len) 72 { 73 if (len •= sizeof(int)) 74 snprintf(strres. sizeof(strres) "size (id) not Sizeof(int)". len); 75 else
7.4. Состояния сокетов 223 76 77 78 snprintf(strnes. sizeof(stnnes). ”«s". (ptr->i_val — 0) 7 "off" "on”). retunn(strres). 79 } 73-78 Вспомните, что последний аргумент функции getsockopt — это аргумент типа «значение-результат». Первое, что мы проверяем, — это то, что размер значения, возвращаемого функцией getsockopt, совпадает с предполагаемым. В зависимос- ти от того, является ли значение флага нулевым или нет, возвращается строка off или on. Выполнение этой программы под AIX 4.2 дает следующий вывод: alx checkopts SO_BROADCAST default - off SO_DEBUG default - off SO_DONTROUTE default = off SO_ERROR default = 0 SO_KEEPALIVE default = off SO_LINGER default - l_onoff = 0 1_1inger = 0 SO_OOBINLINE default = off SO_RCVBUF default - 16384 SO_SNDBUF default = 16384 SO_RCVLOWAT default - 1 SO_SNDLOWAT default = 4096 SO_RCVTIMEO default = 0 sec 0 usee SO_SNDTIMEO default = 0 sec 0 usee SO_REUSEADDR default - off SO_REUSEPORT (undefined) SOJYPE default = 1 SO_USELOOPBACK default = off IP_T0S default - 0 IP_TTL default = 60 TCP_MAXSEG default - 512 TCP_NODELAY default - off Значение 1, возвращаемое для параметра SO_TYPE, для этой реализации соот- ветствует SOCK_STREAM. 7.4. Состояния сокетов Для некоторых параметров сокетов время их установки или получения зависит некоторым образом от состояния сокета Ниже мы обсудим эту зависимость для тех параметров, к которым это относится. Следующие параметры сокетов наследуются присоединенным сокетом TCP от прослушиваемого сокета [105, с. 462-463]: SO_DEBUG, SO_DONTROUTE, SO_KEEPALIVE, SO_LINGER, SOJ3OBINLINE, SO_RCVBUF и SO_SNDBUF. Это важно для TCP, поскольку при- соединенный сокет не возвращается серверу функцией accept, пока трехэтапное рукопожатие не завершится на уровне TCP Если мы хотим убедиться при завер- шении трехэтапного рукопожатия, что один из этих параметров установлен для присоединенного сокета, нам следует установить этот параметр для прослушива- емого сокета.
224 Глава 7. Параметры сокетов 7.5. Общие параметры сокетов Мы начнем с обсуждения общих параметров сокетов. Эти параметры не зависят от протокола (то есть они управляются не зависящим от протокола кодом внутри ядра, а не отдельным модулем протокола, такого как IPv4), но некоторые из них применяются только к определенным типам сокетов. Например, несмотря на то что параметр сокета SO_BROADCAST называется общим, он применяется только к со- кетам дейтаграмм. Параметр сокета SO.BROADCAST Этот параметр управляет возможностью отправки широковещательных сообще- ний. Широковещательная передача поддерживается только для сокетов дейта- грамм и только в сетях, поддерживающих концепцию широковещательных сооб- щений (Ethernet, Token Ring и т. д.). Широковещательная передача в соединении типа «точка-точка» неосуществима. Более подробно о широковещательной пере- даче мы поговорим в главе 18. Поскольку перед отправкой широковещательной дейтаграммы приложение должно установить этот параметр сокета, оно не сможет отправить широковеща- тельное сообщение, если это не предполагалось заранее. Например, приложение UDP может принять IP-адрес получателя в качестве аргумента командной стро- ки, но оно может и не предполагать, что пользователь вводит широковещатель- ный адрес. Проверку того, является или нет данный адрес широковещательным адресом, осуществляет не приложение, а ядро: если адрес получателя является широковещательным адресом и данный параметр сокета не установлен, возвра- тится ошибка EACCESS [105, с. 233]. Параметр сокета SO_DEBUG Этот параметр поддерживается только протоколом TCP. При подключении к со- кету TCP ядро отслеживает подробную информацию обо всех пакетах, отправ- ленных или полученных протоколом TCP для сокета. Они хранятся в кольцевом буфере (circular buffer) внутри ядра, который можно проверить с помощью про- граммы trpt. На с. 916-920 [105] приводится более подробная информация и при- мер использования этого параметра. Параметр сокета SO_DONTROUTE Этот параметр указывает, что исходящие пакеты должны миновать обычные ме- ханизмы маршрутизации соответствующего протокола. Например, в IPv4 пакет направляется на соответствующий локальный интерфейс, который задается ад- ресом получателя, а именно сетевым адресом и маской подсети. Если локальный интерфейс не может быть определен по адресу получателя (например, получате- лем не является другой конец соединения типа «точка-точка» или он не находит- ся в той же сети), возвращается ошибка ENETUNREACH. Эквивалент этого параметра можно также применять к индивидуальным дей- таграммам, используя флаг MSG_DONTROUTE с функциями send, sendto или sendmsg.
7.5. Общие параметры сокетов 225 Этот параметр часто используется демонами маршрутизации (routed и gated) для того, чтобы миновать таблицу маршрутизации (в случае, если таблица марш- рутизации неверна) и заставить пакет отправиться на определенный интерфейс. Параметр сокета SO_ERROR Когда на сокете происходит ошибка, модуль протокола в ядре, происходящем от Беркли, присваивает переменной so_error для этого сокета одно из стандартных значений Unix Еххх. Это так называемая ошибка, требующая обработки (pending error) для данного сокета. Процесс может быть немедленно оповещен об ошибке одним из двух способов: 1. Если процесс блокируется в вызове функции select (см. раздел 6.3), ожидая готовности данного сокета к чтению или записи, функция sei ect возвращает управление и уведомляет процесс о соответствующем состоянии готовности. 2. Если процесс использует управляемый сигналом ввод-вывод (см. главу 22), для него или для группы таких процессов генерируется сигнал SIGIO. Процесс может получить значение переменной so error, указав параметр со- кета SO ERROR. Целое значение, возвращаемое функцией getsockopt, является ко- дом ошибки, требующей обработки. Затем значение переменной so_error сбрасы- вается ядром в 0 [105, с. 547]. Если процесс вызывает функцию read и возвращаемых данных нет, а значение so_error ненулевое, то функция read возвращает -1с errno, которой присвоено значение переменной so_error [105, с. 516]. Это значение so_error затем сбрасы- вается в 0. Если в очереди для сокета есть данные, эти данные возвращаются функ- цией read вместо кода ошибки. Если значение so_error ненулевое, то при вызове процессом функции wn te возвращается -1 с errno, равной значению переменной so error [105, с. 495], а значение so error сбрасывается в 0. ПРИМЕЧАНИЕ----------------------------------------------------- В коде, показанном па с. 495 [105], есть ошибка: so error не сбрасывается в 0. Она была выявлена в реализации BSD/OS. Всегда, когда для сокета возвращается ошибка, тре- бующая обработки, so error должна быть сброшена в 0. Здесь вы впервые встречаетесь с параметром сокета, который можно полу- чить, по нельзя установить. Параметр сокета SO_KEEPALIVE Когда параметр SOKEEPALIVE установлен для сокета TCP и в течение 2 часов не происходит обмена данными по сокету в любом направлении, TCP автомати- чески посылает собеседнику проверочное сообщение (keepalive probe). Это сооб- щение — сегмент TCP, на который собеседник должен ответить. Далее события могут развиваться по одному из трех сценариев. 1. Собеседник отвечает, присылая ожидаемый сегмент АСК. Приложение не получает уведомления (поскольку все в порядке). TCP снова отправит одно проверочное сообщение еще через 2 часа отсутствия активности в этом соеди- нении.
226 Глава 7. Параметры сокетов 2. Собеседник отвечает, присылая сегмент RST, который сообщает локальному TCP, что узел собеседника вышел из строя и перезагрузился. Ошибка сокета, требующая обработки, устанавливается равной ECONNRESET, и сокет закрывается. 3. На проверочное сообщение не приходит ответ от собеседника. Код TCP, про- исходящий от Беркли, отправляет восемь дополнительных проверочных со- общений с интервалом в 75 секунд, пытаясь выявить ошибку. TCP прекратит попытки, если ответа не последует в течение И минут и 15 секунд после от- правки первого сообщения. Если на все проверочные сообщения TCP не при- ходит ответа, то ошибка сокета, требующая обработки, устанавливается в ЕТIMEDOUT, и сокет закрывается. Если же сокет получает ошибку ICMP (Internet Control Message Protocol — протокол управляющих сообщений Интернета) в ответ на одно из проверочных сообщений, то возвращается одна из соответ- ствующих ошибок (табл. А.З и А.4), но сокет также закрывается. Типичная ошибка ICMP в этом сценарии — Host unreachable (Узел недоступен) — ука- зывает на то, что узел собеседника не вышел из строя, а только является не- доступным. При этом ошибка, ожидающая обработки, устанавливается в EHOSTUNREACH. В главе 23 [94] и нас. 828-831 [105] содержатся дополнительные подробно- сти об этом параметре. Без сомнения, наиболее типичный вопрос, касающийся этого параметра, в том, могут ли изменяться временные параметры (обычно нас интересует возможность сокращения двухчасового периода неактивности). В разделе 7.9 мы описываем новый параметр Posix.lg TCP KEEPALIVE, но он не реализован достаточно широко. В приложении Е [94] обсуждается изменение временных параметров для различ- ных ядер. Необходимо учитывать, что большинство ядер обрабатывают эти пара- метры индивидуально для каждого ядра, но не для каждого сокета, и поэтому изменение периода неактивности, например, с 2 часов на 15 минут повлияет на все сокеты узла, для которых включен параметр SO_KEEPALIVE. Назначение этого параметра — обнаружение сбоя на узле собеседника. Если процесс собеседника выходит из строя, его TCP отправит через соединение сег- мент FIN, который мы сможем легко обнаружить с помощью функции sei ect (по- этому мы использовали функцию select в разделе 6.4). Также нужно понимать, что если на проверочное сообщение не приходит ответа (сценарий 3), то это не обязательно означает, что на узле сервера произошел сбой, и существует вероят- ность, что TCP закроет действующее соединение. Если, например, промежуточ- ный маршрутизатор вышел из строя на 15 минут, то эти 15 минут полностью пе- рекрывают период отправки проверочных сообщений от нашего узла, равный И минутам и 15 секундам. Этот параметр обычно используется серверами, хотя его могут использовать и клиенты. Серверы используют его, поскольку большую часть своего времени они проводят в блокированном состоянии, ожидая ввода по соединению TCP, то есть в ожидании запроса клиента. Но если узел клиента выходит из строя, про- цесс сервера никогда не узнает об этом, и сервер будет продолжать ждать ввода данных, которые никогда не придут. Это называется наполовину открытым со- единением {half-open connection). Данный параметр позволяет обнаружить напо- ловину открытые соединения и завершить их.
7.5. Общие параметры сокетов 227 ПРИМЕЧАНИЕ----------------------------------------------------------- Большинство серверов Rlogin и Telnet устанавливают этот параметр, чтобы завершить соединение в том случае, если интерактивный клиеп г дает отбой по телефонной линии или, например, выключает терминал без завершения сеанса. Некоторые серверы, особенно серверы FTP, предоставляют приложению тайм-аут, часто до нескольких минут. Эго выполняется самим приложением, обычно при вызове функции read, когда счит ывает ся следующая команда клиента. Э гот тайм-аут не влия- ет па данный параметр соке га. В табл. 7.2 суммируются различные методы, применяемые для обнаружения того, что происходит на другом конце соединения TCP. Когда мы говорим «использование функции select для проверки готовности к чтению», мы имеем в виду вызов функции select для проверки, готов ли сокет для чтения. Таблица 7.2. Методы определения различных условий TCP Сценарий Процесс собеседника выходит из строя Узел собеседника выходит из строя Узел собеседника недоступен Наш TCP актив- TCP собеседника посылает По истечении времени По ист еченпп времени ио посылает сегмент FIN, что мы можем ожидания TCP возвра- ожидания TCP возвра- данные сразу же обнаружить, щается ошибка щается ошибка f используя функцию select для проверки ютовиости к чтению Ес чи TCP посы- лает вт ороп cei мент, TCP собеседника посылает в от- вет сегмеш RST Если TCP посылает еще один сегмент, наш TCP посылает сигнал SIGPIPE ETIMEDOUT ETIMEDOUT Наш TCP актив- TCP собеседника посылает Мы больше не получаем Мы больше пе получаем но принимает данные сегмент FIN, который мы прочитаем как признак конца файла (возможно, преждевременный) никаких данных никаких данных i Соединение не- TCP собеседника посылает По истечении 2 часов По истечении 2 часов акт явно, посы- сегмент FIN, который мы отсутствия активности отсутствия активности лается пробиып можем сразу же обпару- отсылается 9 сообщении отсылается 9 сообщений пакет жить, используя функцию select для проверки готовности к чтению для проверки наличия связи с собеседником, азатем возвращается ошибка ETIMEDOUT для проверки наличия связи с собеседником, а затем возвращается ошибка ETIMEDOUT Соединение не- активно, не посылается про- верочное сооб- щение TCP собеседника посылает сегмент FIN, который мы можем сразу же обнару- жить, используя функцию select для проверки ютовности к чтению Ничего не происходит Ничего не происходит Параметр сокета SOJJNGER Этот параметр определяет, как работает функция close, если протокол ориенти- рован на установление соединения (например, TCP, но пе UDP). По умолчанию
228 Глава 7. Параметры сокетов функция close возвращает управление немедленно, но если в отправляющем бу- фере сокета остаются какие-либо данные, система попытается доставить данные собеседнику. Параметр сокета SO_LINGER позволяет нам изменять поведение, используемое по умолчанию. Для этого необходимо, чтобы между пользовательским процес- сом и ядром передавалась следующая структура, определяемая в заголовочном файле <sys/ socket h>: struct linger { int l_onoff /* 0=off. ненулевое значение=оп */ int 1_1inger. /* время ожидания, в Posix 1g измеряется в секундах */ }• Вызов функции setsockopt приводит к одному из трех следующих сценариев в зависимости от значений двух элементов структуры 11 nger. 1. Если l onoff имеет нулевое значение, параметр выключается. Значение l_linger игнорируется и применяется ранее рассмотренный заданный по умолчанию сценарий TCP: функция close немедленно завершается. 2. Если значение l onoff ненулевое, а 1_1 inger равно нулю, TCP сбрасывает со- единение, когда оно закрывается [105, с. 1019-1020], то есть TCP игнорирует все данные, остающиеся в буфере отправки сокета, и отправляет собеседнику сегмент RST, а не обычную последовательность завершения соединения, со- стоящую из четырех пакетов (см. раздел 2.5). Пример мы покажем в листин- ге 15.14. При этом не наступает состояние TCP TIME_WAIT, по остается возможность создания другого воплощения (incarnation) этого соединения в течение 2MSL секунд (удвоенное максимальное время жизни сегмента). Оставшиеся старые дублированные сегменты из только что завершенного со- единения будут доставлены новому воплощению некорректно (см. раздел 2.6). ПРИМЕЧАНИЕ ————---------------------------------------------------------- Некоторые реализации, особенно Solaris 2.x, где х меньше либо равно 5, не реализуют это свойство параметра SO LINGER. Отдельные выступления в Usenet звучат в защиту использования этой возможности, поскольку она позволяет избежать состояния TIME_WAIT и снова запусти i ь прослу- шивающий сервер, даже если соединения все еще используются с известным портом сервера. Этого не нужно делать, поскольку это может привести к искажению данных, как показано в RFC 1337 [10]. Вместо этого перед вызовом функции bind на стороне сервера всегда нужно использовать параметр сокета SO_REUSEADDR, как показано ниже. Состояние TIME WAIT — наш друг, так как оно предназначено для того, чтобы помочь нам дождаться, когда истечет время жизни в сети старых дублированных сег- ментов. Вместо того чтобы пытаться избежать этого состояния, следует понять его на- значение (см. раздел 2.6). 3. Если оба значения — l onoff и 1_11 nger — ненулевые, то при закрытии сокета ядро будет ждать (linger) [105, с. 472], то есть если в буфере отправки сокета еще имеются какие-либо данные, процесс входит в состояние ожидания до тех пор, пока либо все данные не будут отправлены и подтверждены другим концом TCP, либо не истечет время ожидания. Если сокет был установлен как неблокируемый (см. главу 15), он не будет ждать завершения выполне- ния функции cl ose, даже если время задержки ненулевое.
7,5. Общие параметры сокетов 229 При использовании этого свойства параметра SOL INGER приложению важно проверить значение, возвращаемое функцией close. Если время ожидания исте- чет до того, как оставшиеся данные будут отправлены и подтверждены, функция close возвратит ошибку EWOULDBLOCK, и все данные, оставшиеся в буфере отправки сокета, будут сброшены. ПРИМЕЧАНИЕ----------------------------------------------------------- К сожалению, интерпретация ненулевого элемента l_linger в третьем случае зависит от реализации. В 4.4BSD единицей измерения является одна сотая секунды, a Posix зада- ет в качестве единиц измерения секунды. Другой проблемой, существующей у реали- заций, происходящих от Беркли, является то, что элемент llingcr (тина mt) копирует- ся в переменную ядра (so linger), которая представляет собой 16-разрядиое целое число, что ограничивает время задержки до 327,67 секунды. Теперь нам нужно точно определить, когда завершается функция close на со- кете в различных сценариях, которые мы рассмотрели. Предполагается, что кли- ент записывает данные в сокет и вызывает функцию close. На рис. 7.1 показана ситуация по умолчанию. Клиент Сервер write close Завершение функции close Данные, построенные в очередь согласно TCP Приложение считывает данные из очереди и сегмент FIN close Рис. 7.1. Действие функции close, заданное по умолчанию: немедленное завершение Мы предполагаем, что когда приходят данные клиента, сервер временно за- нят. Поэтому данные добавляются в приемный буфер сокета его протоколом TCP. Аналогично следующий сегмент (сегмент FIN клиента) также добавляется к при- емному буферу сокета (каким бы образом реализация ни сохраняла сегмент FIN). Но по умолчанию клиентская функция cl ose сразу же завершается. Как мы пока- зываем в этом сценарии, клиентская функция close может завершиться перед тем, как сервер прочитает оставшиеся данные в приемном буфере его сокета. Если узел сервера выйдет из строя перед тем, как приложение-сервер считает остав- шиеся данные, клиентское приложение никогда об этом не узнает. Клиент может установить параметр сокета SO_LINGER, задав некоторое поло- жительное время задержки. Когда это происходит, клиентская функция cl ose не
230 Глава 7. Параметры сокетов завершается до тех пор, пока все данные клиента и его сегмент FIN не будут под- тверждены протоколом TCP сервера. Мы показываем это на рис. 7.2. Но у нас остается та же проблема, что и на рис. 7.1: если на узле сервера происходит сбой до того как приложение-сервер считает оставшиеся данные, клиентское прило- жение никогда не узнает об этом. Основным принципом такого взаимодействия является то, что успешное за- вершение функции close с установленным параметром сокета SO_LINGER говорит нам лишь о том, что данные, которые мы отправили (и наш сегмент FIN) под- тверждены протоколом TCP собеседника. Но это не говорит нам, прочитало ли данные приложение собеседника. Если мы не установим параметр сокета SO LINGER, мы не будем знать, подтвердил ли другой конец TCP отправленные ему данные. Рис. 7.2. Функция close с установленным параметром сокета SO_LINGER и I linger, имеющим положительное значение Чтобы узнать, что сервер прочитал данные клиента, клиент может вызвать функцию shutdown (со вторым аргументом SHUT_WR) вместо функции cl ose и ждать, когда собеседник закроет с помощью функции cl ose свой конец соединения. Этот сценарий показан на рис. 7.3. Сравнивая этот рисунок с рис. 7.1 и 7.2, мы видим, что когда мы закрываем наш конец соединения, то в зависимости от вызванной функции (close или shut- down) и от того, установлен или нет параметр сокета SO LINGER, завершение может произойти в один из трех различных моментов времени: 1. Функция close завершается немедленно, без всякого ожидания (сценарий, заданный по умолчанию, см. рис. 7.1). 2. Функция cl ose задерживается до тех пор, пока не будет получен сегмент АСК, подтверждающий получение сервером сегмента FIN от клиента (рис. 7.2). 3. Функция shutdown, за которой следует функция read, ждет, когда мы получим сегмент FIN собеседника (в данном случае сервера) (рис. 7.3). Другой способ узнать, что приложение-собеседник прочитало наши данные, — использовать подтвепждение на иповне ппиложения. или АСК ппиложения.
7.5. Общие параметры сокетов 231 Рис. 7.3. Использование функции shutdown для проверки того, что собеседник получил наши данные Рис. 7.4. АСК поиложения
232 Глава 7. Параметры сокетов Например, клиент отправляет данные серверу и затем вызывает функцию read для одного байта данных: char ack WnteCsockfd data mbytes) /* данные от клиента к серверу */ n = ReadCsockfd &ack 1) /* ожидание подтверждения на уровне приложения */ Сервер читает данные от клиента и затем отправляет ему 1-байтовый сегмент — подтверждение на уровне приложения: nbytes = ReadCsockfd buff sizeof(buff)) /* данные от клиента */ /* сервер проверяет, правильное ли количество данных он получил от клиента */ WnteCsockfd "1) /* сегмент АСК сервера возвращается клиенту */ Таким образом, мы имеем гарантию, что на момент завершения функции read на стороне клиента процесс сервера прочитал данные, которые мы отправили. (При этом предполагается, что либо сервер знает, сколько данных отправляет клиент, либо существует некоторый заданный приложением маркер конца запи- си, который мы здесь не показываем.) В данном случае сегмент АСК на уровне приложения представляет собой нулевой байт, но содержимое этого байта мож- но использовать для передачи от сервера к клиенту сообщений о других услови- ях. На рис. 7 4 показан возможный обмен пакетами. В табл. 7.3 описаны два возможных вызова функции shutdown и три возмож- ных вызова функции cl ose, а также их влияние на сокет TCP. Таблица 7.3. Итоговая таблица сценариев функции shutdown и параметров сокета SO_LINGER Функция Описание shutdown, SHUTRD Через сокет больше нельзя принимать данные, процесс может по-прежнему отправлять данные через этот сокет, приемный буфер сокета сбрасывается, все данные, получаемые в дальнейшем, игнорируются протоколом TCP (см упражнение 6 5), не влияет на буфер отправки сокета shutdown, < S1IUTWR Через сокет больше нельзя отправлять данные, процесс может по-прежнему получать данные через этот сокет, содержимое буфера отправки сокета отсылается на другой конец соединения, затем выполняется обычная последовательность действий По завершению соединения TCP (FIN), не влияет на приемный буфер сокета close, lonoff - 0 (по умолчанию) Через сокет больше нельзя отправлят ь и получать данные, содержимое буфера отправки сокета отсылается на другой конец соединения Если счетчик ссылок дескриптора становится нулевым, то следом за отправкои данных из буфера отправки сокета выполняется нормальная последовательность завершения соединения TCP (FIN), данные из приемною буфера сокета сбрасываются close, l onoff = 1 Ihnger ” 0 Через сокет больше нельзя отправлять н получать данные Если счетчик ссылок дескриптора становится нулевым, то на другой конец соединения посылается сегмент RST, соединение переходит в состояние в CLOSED (минуя состояние TIME WAIT), данные из буфера отправки и приемною буфера сокета сбрасываются close, l onoff = 1 Ihnger 1= 0 Через сокет больше нельзя отправлять и получать данные, содержимое буфера отправки сокета отсылается па другой конец соединения Если счетчик ссылок дескриптора становится нулевым, то следом за отправкои данных из буфера отправки сокета выполняется нормальная последовательность завершения соединения TCP (FIN), данные из приемною буфера сокета сбрасываются, и если время задержки истекает, прежде чем оставшиеся в буфере данные будут посланы и будет подтвержден их прием, функция close возвратит ошибку EWOULDBLOCK
7.5. Общие параметры сокетов 233 Параметр сокета SO_OOBINLINE Когда установлен этот параметр, внеполосные данные помещаются в очередь нор- мального ввода (то есть вместе с обычными данными (inline)). Когда это проис- ходит, флаг MSG_OOB не может быть использован для чтения полученных внепо- лосных данных. Более подробно внеполосные данные мы рассмотрим в главе 21. Параметры сокета SO_RECVBUF и SO_SNDBUF У каждого сокета имеются буфер отправки и приемный буфер (буфер приема). Мы изобразили действие буферов отправки TCP и UDP па рис. 2.11 и 2.12. Приемные буферы используются в TCP и UDP для хранения полученных данных, пока они не будут считаны приложением. В случае TCP доступное про- странство в приемном буфере сокета — это окно, размер которого TCP сообщает другому концу соединения. Приемный буфер сокета TCP не может переполнить- ся, поскольку собеседнику не разрешается отправлять данные, размер которых превышает размер окна. Таким образом, в TCP осуществляется управление по- током, и если собеседник игнорирует объявленное окно и отправляет данные, пре- вышающие его размер, принимающий TCP игнорирует эти данные. Однако в слу- чае UDP дейтаграмма, не подходящая для приемного буфера соке га, игнорируется. Вспомните, что в UDP отсутствует управление потоком более быстрый отпра- витель легко подавит более медленного получателя, заставляя UDP получателя игнорировать дейтаграммы, как мы покажем в разделе 8.13. Указанные в заголовке раздела параметры позволяют нам изменять размеры буферов, заданные по умолчанию. Значения по умолчанию сильно отличаются в за- висимости от реализации Более ранние реализации, происходящие от Беркли, по умолчанию имели размеры буферов отправки и приема 4096 байт, а более но- вые системы используют буферы больших размеров, от 8192 до 61 440 байт. Раз- мер буфера отправки UDP по умолчанию часто составляет около 9000 байт, если узел поддерживает NFS; а размер приемного буфера UDP — около 40 000 байт. При установке размера приемного буфера сокета TCP важен порядок вызова функций, поскольку в данном случае присутствует параметр масштабирования окна TCP (см раздел 2.5). При установлении соединения обе стороны обменива- ются сегментами SYN, в которых может содержаться этот параметр Для клиента это означает, что параметр сокета SO_RECVBUF должен быть установлен перед вызо- вом функции connect. Для сервера это означает, что данный параметр должен быть установлен для прослушиваемого сокета перед вызовом функции listen. Уста- новка этого параметра для присоединенного сокета никак не повлияет на пара- метр масштабирования окна, поскольку функция accept не возвращает управле- ние процессу, пока не завершится трехэтапное рукопожатие TCP. Поэтому данный параметр должен быть установлен для прослушиваемого сокета. (Размеры буфе- ров сокета всегда наследуются от прослушиваемого сокета создаваемым присо- единенным сокетом, [105, с 462-463]. Размеры буферов сокета TCP должны быть как минимум втрое меньше MSS (максимальный размер сегмента) для соединения Если мы имеем дело с направ- ленной передачей данных, такой как передача файла в одном направлении, то говоря «размеры буферов сокета», мы подразумеваем буфер отправки сокета на отправляющем узле или приемный буфер сокета на принимающем узле. В случае
234 Глава 7. Параметры сокетов двусторонней передачи данных мы имеем в виду оба размера буферов на обоих узлах. С типичным размером буфера 8192 байта или больше и типичным MSS, равным 512 или 1460 байтам, это требование обычно выполняется. Проблемы были замечены в сетях с большими MTU (максимальная единица передачи), ко- торые предоставляют MSS больше обычного (например, в сетях ATM с MTU, равной 9188 байтов, как показано в [20]). Размеры буфера сокета TCP должны быть также четное число раз кратны раз- меру MSS для соединения. Некоторые реализации выполняют это требование для приложения, округляя размеры в сторону большего размера буфера сокета после установления соединения [105, с. 902]. Это другая причина, по которой сле- дует задавать эти два параметра сокета перед установлением соединения. Напри- мер, если использовать размеры, заданные по умолчанию в 4.4BSD (8192 байта), и считать, что используется Ethernet с размером MSS, равным 1460 байтам, то при устанавлении соединения размеры обоих буферов сокета будут округляться до 8760 байт (6x1460). Другое соображение относительно установки размеров буфера сокета связа- но с производительностью. На рис. 7.5 показано соединение TCP между двумя конечными точками (которое мы называем каналом) с вместимостью, допускаю- щей передачу восьми сегментов. Клиент Запрос 8 Запрос 7 Запрос 6 Запрос 5 | Подтверждение 1 | Подтверждение 2 |подтверждение 3 |Подтверждение 4 Сервер Рис. 7.5. Соединение TCP (канал), вмещающее восемь сегментов Мы показываем четыре сегмента данных вверху и четыре сегмента АСК вни- зу. Даже если в канале только четыре сегмента данных, у клиента должен быть буфер отправки, вмещающий минимум восемь сегментов, потому что TCP кли- ента должен хранить копию каждого сегмента, пока не получен сегмент АСК от сервера. ПРИМЕЧАНИЕ ------------------------------------------------------- Здесь мы игнорируем некоторые подробности. Прежде всего, алгоритм медленно- го запуска TCP ограничивает скорость, с которой сегменты начинают отправляться по соединению, которое до этого было неактивным. Далее, TCP часто подтверждает каж- дый второй сегмент, а не каждый сегмент, как мы это показываем. Все эти подробности описаны в главах 20 и 24 [94]. Нам необходимо понять принцип функционирования двустороннего канала и узнать, что такое его вместимость и как опа влияет на размеры буферов сокетов на обоих концах соединения. Вместимость капала характеризуется произведени- ем пропускной способности на задержку (bandwidth-delay product). Мы будем вычислять ее, умножая пропускную способность канала (в битах в секунду) на период обращения (RTT, round-trip time) (в секундах) и преобразуя результат из битов в байты. RTT легко измеряется с помощью утилиты Ping. Пропускная спо- собность — это значение, соответствующее наиболее медленной связи между дву- мя конечными точками; предполагается, что это значение каким-то образом опре- делено. Е1апример, линия Т1 (1 536 000 бит/с) с RTT 60 миллисекунд дает
7.5. Общие параметры сокетов 235 произведение пропускной способности на задержку, равное 11 520 байт. Если раз- меры буфера сокета меньше указанного, канал не будет заполнен и производи- тельность окажется ниже предполагаемой. Большие буферы сокетов требуются, когда повышается пропускная способность (например, для линии ТЗ, где она равна 45 Мбит/с) или когда увеличивается RTT (например, спутниковые каналы связи с RTT около 500 миллисекунд). Когда произведение пропускной способности на задержку превосходит максимальный нормальный размер окна TCP (65 535 байт), обоим концам соединения требуются также параметры TCP для канала с повы- шенной вместимостью (long fat pipe), о которых мы упоминали в разделе 2.5. ПРИМЕЧАНИЕ --------------------------------------------------------- В большинстве реализаций размеры буферов отправки и приема ограничиваются не- которым предельным значением. В более ранних реализациях, происходящих от Беркли, верхний предел был около 52 000 байт, но в новых реализациях предел но умолчанию равен 256 000 байт или больше, и обычно админис 1ратор имеет возможность увеличи- вать его. К сожалению, не существует простого способа, с помощью которого приложе- ние могло бы узнать этот предел. Posix.l определяет функцию fpathconf, поддерживае- мую большинством реализаций, a Posix.lg определяет новую константу _PC_SOCK_ MAXBUF, являющуюся максимальным размером буферов сокета, которую можно ис- пользовать в качестве второго аргумента этой функции. Параметры сокета SO_RCVLOWAT и SO_SNDLOWAT Каждый сокет характеризуется также минимальным количеством данных (low- water mark) для буферов приема и отправки. Эти значения используются функ- цией select, как мы показали в разделе 6.3. Указанные параметры сокета позво- ляют нам изменять эти два значения. Минимальное количество данных — это количество данных, которые должны находиться в приемном буфере сокета, чтобы функция select возвратила ответ «Сокет готов для чтения». По умолчанию это значение равно 1 для сокетов TCP и UDP. Минимальный объем для буфера отправки — это количество свободного пространства, которое должно быть в буфере отправки сокета, чтобы функция sel ect возвратила «Сокет готов для записи». Для сокетов TCP по умолчанию оно обычно равно 2048. С UDP это значение используется так, как мы показали в раз- деле 6.3, но поскольку число байтов доступного пространства в буфере отправки для сокета UDP никогда не изменяется (так как UDP пе хранит копии дейта- грамм, отправленных приложением), сокет UDP всегда готов для записи, пока раз- мер буфера отправки сокета UDP больше минимального объема. Вспомните рис. 2.12: UDP не имеет буфера отправки, у пего есть только размер буфера отправки. ПРИМЕЧАНИЕ-------------------------------------------------- Posix.lg не требует поддержки этих двух параметров сокетов. Параметры сокета SO_RCVTIMEO и SO_SNDTIMEO Эти два параметра сокета позволяют нам устанавливать тайм-аут при получении и отправке через сокет. Обратите внимание, что аргумент двух функций sockopt —
236 Глава 7. Параметры сокетов это указатель на структуру timeval, ту же, которую использует функция select (см. рис. 6.3). Это позволяет использовать для задания тайм-аута секунды и мил- лисекунды. Мы отключаем тайм-аут, установив его значение в 0 секунд и 0 мил- лисекунд. Оба тайм-аута по умолчанию отключены. Тайм-аут приема влияет на пять функций ввода: read, readv, recv, recvfrom и recvmsg. Тайм-аут отправки влияет на пять функций вывода: write, writev, send, sendto и sendmsg. Более подробно о тайм-аутах сокета мы поговорим в разделе 13.2. ПРИМЕЧАНИЕ ------------------------------------------------------------ Эти два параметра сокета и понятие наследуемых тайм-аутов получения и отправки сокетов были добавлены в реализации 4.3BSD Reno. Posix.lg не требует поддержки этих параметров сокета. В реализациях, происходящих от Беркли, эти два значения реализуют таймер от- сутствия активности, а не абсолютный таймер системного вызова чтения или записи. На с. 496 и 516 [105] об этом рассказывается более подробно. Параметры сокета SO_REUSEADDR и SO_REUSEPORT Параметр сокета SO_REUSEADDR служит для четырех целей. 1. Параметр SO REUSEADDR позволяет прослушивающему серверу запуститься и с помощью функции bi nd связаться со своим заранее известным портом, даже если существуют ранее установленные соединения, использующие этот порт в качестве своего локального порта. Эта ситуация обычно возникает следую- щим образом: 1. Запускается прослушивающий сервер. 2. От клиента приходит запрос на соединение, и для обработки этого клиента генерируется дочерний процесс. 3. Прослушивающий сервер завершает работу, но дочерний процесс продол- жает обслуживание клиента на существующем соединении. 4. Прослушивающий сервер перезапускается. По умолчанию, когда прослушивающий сервер перезапускается при помощи вызова функций socket, bi nd и 11 sten, вызов функции bi nd оказывается неудач- ным, потому что прослушивающий сервер пытается связаться с портом, кото- рый является частью существующего соединения (обрабатываемого ранее со- зданным дочерним процессом). Но если сервер устанавливает параметр сокета SO REUSEADDR между вызовами функций socket и bind, последняя выполнится успешно. Все серверы TCP должны задавать этот параметр сокета, чтобы по- зволить перезапускать сервер в этой ситуации. ПРИМЕЧАНИЕ ----------------------------------------------------- Этот сценарии порождает одни из наиболее часто задаваемых вопросов в Usenet. 2. Параметр SO_REUSEADDR позволяет множеству экземпляров одного и того же сервера запускаться на одном и том же порте, так как все экземпляры связы-
7.5. Общие параметры сокетов 237 ваются с различными локальными IP-адресами. Это типичная ситуация для узла, на котором размещается несколько серверов HTTP, использующих тех- нологию альтернативных IP-адресов, или псевдонимов (IP alias technique) (см. раздел А.4). Допустим, первичный IP-адрес локального узла — 198.69.10.2, но он имеет два альтернативных адреса — 198.69.10.128 и 198.69.10.129. За- пускаются три сервера HTTP. Первый сервер с помощью функции bind свя- жется с локальным IP-адресом 198.69.10.128 и локальным портом 80 (заранее известный порт HTTP). Второй сервер с помощью функции bind свяжется с ло- кальным IP-адресом 198.69.10.129 и локальным портом 80. Но второй вызов функции bind не будет успешным, пока не будет установлен параметр S0_ REUSEADDR перед обращением к ней. Третий сервер вызовет функцию bi nd с уни- версальным адресом в качестве локального IP-адреса и локальным портом 80. И снова требуется параметр SO REUSEADDR, для того чтобы последний вызов ока- зался успешным. Если считать, что установлен параметр SO_REUSEADDR и запу- щены три сервера, то входящие запросы TCP на соединение с IP-адресом по- лучателя 198.69.10.128 и портом получателя 80 доставляются на первый сервер, входящие запросы па соединение с IP-адресом получателя 198.69.10.129 и пор- том получателя 80 — на второй сервер, а все остальные входящие запросы TCP на соединение с портом получателя 80 доставляются на третий сервер. Послед- ний сервер обрабатывает запросы, предназначенные адресу 198.69.10.2, в до- полнение к другим альтернативным IP-адресам, для которых этот узел может быть сконфигурирован. Символы подстановки означают в данном случае «все, для чего не нашлось более точного совпадения». Заметим, что этот сценарий, допускающий множество серверов для данной службы, обрабатывается авто- матически, если сервер всегда устанавливает параметр сокета SO REUSEADDR (как мы рекомендуем). TCP не дает нам возможности запустить множество серверов, которые с по- мощью функции bi nd связываются с одним и тем же IP-адресом и одним и тем же портом: это случай полностью дублированного связывания {completely du- plicate binding), то есть мы не можем запустить один сервер, связывающийся с адресом 198.69.10.2 и портом 80, и другой сервер, также связывающийся с адресом 198.69.10.2 и портом 80, даже если для второго сервера мы устано- вим параметр SO_REUSEADDR. 3. Параметр SD_REUSEADDR позволяет одиночному процессу связывать один и тот же порт со множеством сокетов, так как при каждом связывании задается уни- кальный IP-адрес. Это обычное явление для серверов UDP, так как им необ- ходимо знать IP-адрес получателя запросов клиента в системах, не поддержи- вающих параметр сокета I P_RECVSTADDR. Мы разработаем пример с использованием этой технологии в разделе 19.11. Эта технология обычно не применяется с сер- верами TCP, поскольку сервер TCP всегда может определить IP-адрес полу- чателя при помощи вызова функции getsockname, после того как соединение установлено. 4. Параметр SO_REUSEADDR допускает полностью дублированное связывание: свя- зывание с помощью функции bind с IP-адресом и портом, когда тот же IP- адрес и тот же порт уже связаны с другим сокетом. Обычно это свойство
238 Глава 7 Параметры сокетов доступно только в системах с поддержкой многоадресной передачи без поддержки параметра сокета SO_REUSEPORT (который мы опишем чуть ниже) и только для сокетов UDP (многоадресная передача не работает с TCP) Это свойство применяется при многоадресной передаче для многократного выполнения одного и того же приложения на одном и том же узле Когда при- ходит дейтаграмма UDP для одного из многократно связанных сокетов, дей- ствует следующее правило если дейтаграмма предназначена либо для широ- ковещательного адреса, либо для адреса многоадресной передачи, то одна копия дейтаграммы доставляется каждому сокету Но если дейтаграмма предназна- чена для адреса направленной передачи, то дейтаграмма доставляется только на один сокет Какой сокет получит дейтаграмму, если в случае направленной передачи существует множество сокетов, соответствующих дейтаграмме, — зависит от реализации На с Ш-11§ [105] об этом свойстве рассказывается более подробно О широковещательной и многоадресной передачах мы пого- ворим соответственно в главах 18 и 19 В упражнениях 7 5 и 7 6 показаны примеры использования этого параметра сокета В 4 4BSD при добавлении поддержки многоадресной передачи был введен параметр сокета SO REUSEPORT Вместо того чтобы перегружать параметр SO_REUSEADDR семантикой многоадресной передачи, допускающей полностью дублированное связывание, был введен новый параметр сокета, обладающий следующей семан- тикой 1 Этот параметр допускает полностью дублированное связывание, но только если каждый сокет, который хочет связаться с тем же IP-адресом и портом, задает этот параметр сокета 2 Параметр SO_REUSEADDR считается эквивалентным параметру SO_REUSEPORT, если связываемый IP-адрес является адресом многоадресной передачи [105, с 731] Проблема с этим параметром сокета заключается в том, что не все системы его поддерживают В системах без поддержки этого параметра, но с поддержкой мио гоадресной передачи его функции выполняется параметр SO_REUSEADDR, допуска- ющий полностью дублированное связывание, когда оно имеет смысл (то есть когда имеется сервер UDP, который может быть запущен много раз на одном и том же узле в одно и то же время, предполагающий получать либо широковещательные дейтаграммы, либо дейтаграммы многоадресной передачи) Обобщить обсуждение этих параметров сокета можно с помошью следующих рекомендаций 1 Устанавливайте параметр SO_REUSEADDR перед вызовом функции bind на всех серверах TCP 2 При создании приложения многоадресной передачи, которое может быть за- пущено несколько раз на одном и том же узле в одно и то же время, устанавли- вайте параметр SO_REUSEADDR и связывайтесь с адресом многоадресной переда- чи, используемым в качестве локальною IP-адреса Более подробно об этих параметрах сокета рассказывается в главе 22 [105]
7 5 Общие параметры сокетов 239 Существует потенциальная проблема безопасности, связанная с использова- нием параметра SD_REUSEADDR Если существует сокет, связанный, скажем, с уни- версальным адресом и портом 5555, то задав параметр SO REUSEADDR, мы можем связать этот порт с другим IP-адресом, например с основным (primary) 1Р-адре- сом узла Любые приходящие дейтаграммы, предназначенные для порта 5555 и IP- адреса, который мы связали с нашим сокетом, доставляются на наш сокет, а не на другой сокет, связанный с универсальным адресом Это могут быть сегменты SYN TCP или дейтаграммы UDP (В упражнении 11 3 показано это свойство для UDP ) Для большинства известных служб, таких как HTTP, FTP и Telnet, это не состав- ляет проблемы, поскольку все эти серверы связываются с зарезервированным портом Следовательно, любой процесс, запущенный позже и пытающийся свя- заться с более специфичным экземпляром этого порта (то есть пытающийся за- владеть портом), требует прав привилегированного пользователя Однако NFS (Network File System — сетевая файловая система) может вызвать проблемы, по- скольку ее стандартный порт (2049) не зарезервирован ПРИМЕЧАНИЕ ------------------------------------------------------------------ Одна из сопутствующих проблем API сокетов в том, что установка пары сокетов вы- полняется с помощью двух вызовов функции (bind и connect) вместо одного В [100] предлагается одиночная функция, разрешающая эту проблему int bind_connect_listenCint sockfd const struct sockaddr *laddr int laddrlen const struct sockaddr *faddr int faddrlen int listen) Аргуметнт laddr задает локальный IP-адрес и локальный порт, apiyMeiiT faddr — уда- ленный IP-адрес и удаленный порт, аргумент listen задает клиент (0) или сервер (зна- чение ненулевое, то же, что и аргумент backlog функции listen) В гаком случае функ- ция bind могла бы быть библиотечной функцией, вызывающей эту функцию с пустым указателем faddr и пулевым faddrlen, а функция connect — библиотечной функцией, вызывающей эту функцию с пустым указателем laddr и нулевым laddrlen Существует несколько приложении, особенно FTP, которым необходимо задавать и локальную пару, и удаленную пару, которые могут вызывать bind_connect_listen непосредственно При наличии подобной функции отпадает необходимость в параметре SO REUSEADDR, в отличие от серверов UDP, которым явно необходимо допускать полностью дублиро- ванное связывание с одним и тем же IP-адресом и портом Другое преимущество этой новой функции в том, что сервер TCP может офапичить себя обслуживанием запро- сов па соединения, приходящих от одного определенного IP адреса и порта Это опре- деляется в RFC 793 [83], но невозможно с существующими API сокетов Аналогичное предложение о функции set addresses было сделано в 1993 году рабочей группой Posix 1003 12 Кейта Склоуэра (Keith Sklower) Однако предложение было от- клонено Параметр сокета SO_TYPE Этот параметр возвращает тип сокета Возвращаемое целое число — это значе- ние, такое как SOCK_STREAM или SDCK_DGRAM Этот параметр обычно используется процессом, наследующим сокет при запуске н 1 <
240 Глава 7. Параметры сокетов Параметр сокета SO USELOOPBACK Этот параметр применяется только к сокетам в маршрутизирующем домене (AF_ ROUTE). По умолчанию он включен на этих сокетах (единственный из параметров S0_xrx, по умолчанию включенный). Когда этот параметр включается, сокет по- лучает копию всего, что отправляется на сокет. ПРИМЕЧАНИЕ-------------------------------------------- Другой способ отключить получение этих циклических копий — вызвать функ- цию shutdown со вторым аргументом SHUT RD. 7.6. Параметры сокетов IPv4 Эти параметры сокетов обрабатываются IPv4, и для них аргумент level равен IPPROTO IP. Обсуждение пяти параметров сокетов многоадресной передачи мы отложим до раздела 19.5. ПРИМЕЧАНИЕ---------------------------------------------- Все параметры сокетов, которые мы описываем в этом разделе, определяются стандар- том Posix. 1g, за исключением параметра IP RECVIF. Параметр сокета IP_HRDINCL Если этот параметр задан для символьного сокета IP (см. главу 25), нам следует создать наш собственный заголовок IP для всех дейтаграмм, которые мы отправ- ляем через символьный сокет. Обычно ядро создает заголовок IP для дейтаграмм, отправляемых через символьный сокет, но существует ряд приложений (особен- но Traceroute), создающих свой собственный заголовок IP, заменяющий значе- ния, которые IP поместил бы в определенные поля заголовка. Когда установлен этот параметр, мы создаем полный заголовок IP со следую- щими исключениями: u IP всегда вычисляет и хранит контрольную сумму заголовка IP. Я Если мы устанавливаем поле идентификации IP в 0, ядро устанавливает это поле. Если IP-адрес отправителя (source address) — I NADDR_ANY, IP устанавливает его равным основному IP-адресу исходящего интерфейса. Как устанавливать параметры IP, зависит от реализации. Некоторые реализа- ции принимают любые параметры IP, установленные с использованием пара- метра сокета IP_OPTIONS, и добавляют их к создаваемому нами заголовку, в то время как другие требуют, чтобы наш заголовок также содержал все необхо- димые параметры IP. Пример использования этого параметра показан в разделе 26.6. На с. 1056-1057 [105] предоставлена дополнительная подробная информация об этом параметре. Параметр сокета IP_OPTIONS Установка этого параметра позволяет нам задавать параметры IP в заголовке IPv4. Это требует точного знания формата параметров IP в заголовке IP.
7.6. Параметры сокетов IPv4 241 Мы рассмотрим этот параметр в контексте маршрутизации от отправителя IPv4 в разделе 24.3. Параметр сокета IP_RECVDSTADDR Этот параметр сокета заставляет функцию recvmsg возвращать IP-адрес получа- теля в получаемой дейтаграмме UDP в качестве вспомогательных данных. При- мер использования этого параметра мы приводим в разделе 20.2. Параметр сокета IP_RECVIF Этот параметр сокета заставляет функцию recvmsg возвращать индекс интерфей- са, на котором принимается дейтаграмма UDP, в качестве вспомогательных дан- ных. Пример использования этого параметра мы приводим в разделе 20.2. ПРИМЕЧАНИЕ------------------------------------------------- Это новый параметр, разработанный Биллом Феннером (Bill Fenner) для FreeBSD и NetBSD (DARTNet) [28]. DARTNet — это экспериментальная исследовательская сеть, используемая для тестирования новых протоколов и приложений. Данный пара- метр сокета предполагалось использовать в 4.4BSD, но он так и пе вошел в эту реализа- цию. Автор взял реализацию FreeBSD и добавил ее в BSD/OS 3.0. Параметр сокета IP_TOS Этот параметр позволяет нам устанавливать поле тип службы (тип сервиса) (TOS, type-of-service) (рис. А.1) в заголовке IP для сокета TCP или UDP. Если мы вы- зываем для этого сокета функцию getsockopt, возвращается текущее значение, которое будет помещено в поле TOS заголовка IP (по умолчанию значение нуле- вое). Не существует способа извлечь это значение из полученной дейтаграммы IP. Мы можем присвоить TOS одну из констант, показанных в табл. 7.4, которые определяются в заголовочном файле <neti net/i р h>. Таблица 7.4. Константы типов сервиса IPv4 Константа Описание IPTOS_I.OWDEI.A1' Минимизирует задержку IPTOSJTHROUGHPUT Доводит производительность до максимума IPTOSRELIABLITY Доводит надежность до максимума IPTOS-LOWCOST Минимизирует стоимость Документ RFC 1349 [2] содержит подробное описание поля TOS и способов установки этого поля для стандартных приложений Интернета. Например, Telnet и Rlogin должны задавать IPTOS_LOWDELAY, в то время как при передаче данных FTP следует задавать IPTOS_THROUGHPUT. Параметр сокета IP_TTL С помощью этого параметра мы можем устанавливать и получать заданное по умолчанию значение TTL (time-to-live field — поле времени жизни, рис. А.1), ко- торое система будет использовать для данного сокета. В системе 4.4BSD, напри-
242 Глава 7. Параметры сокетов мер, значение TTL по умолчанию для сокетов TCP и UDP равно 64 (оно опреде- ляется в RFC 1700), а для символьных сокетов — 255. Как и в случае поля TOS, вызов функции getsockopt возвращает значение поля по умолчанию, которое сис- тема будет использовать в исходящих дейтаграммах, и не существует способа определить это значение по полученной дейтаграмме. Мы устанавливаем этот параметр сокета в нашей программе Traceroute в листинге 25.14. 7.7. Параметр сокета ICMPv6 Этот параметр сокета обрабатывается ICMPv6 и имеет аргумент level, равный IPPR0T0_ICMPV6. Параметр сокета ICMP6_FILTER Этот параметр позволяет нам получать и устанавливать структуру icmp6_filter, которая определяет, какие из 256 возможных типов сообщений ICMPv6 переда- ются для обработки на символьный сокет. Мы обсудим этот параметр в разде- ле 25.4. 7.8. Параметры сокетов IPv6 Эти параметры сокетов обрабатываются IPv6 и имеют аргумент level, равный IPPR0T0_IPV6. Мы отложим обсуждение пяти параметров сокетов многоадресной передачи до раздела 19.5. Отметим, что многие из этих параметров используют вспомогательные данные с функцией recvmsg, и мы покажем это в разделе 13.6. Все параметры сокетов IPv6 определены в RFC 2133 [32] и в [96]. ПРИМЕЧАНИЕ --------------------------------------------- В Posix.lg ничего не говорится об IPv6. Параметр сокета IPv6_ADDRFORM Этот параметр позволяет сокету быть преобразованным из IPv4 в IPv6 или на- оборот. Его мы описываем в разделе 10.5. Параметр сокета IPv6_CHECKSUM Этот параметр сокета задает байтовое смещение поля контрольной суммы внут- ри данных пользователя. Если значение неотрицательное, ядро, во-первых, вы- числяет и хранит контрольную сумму для всех исходящих пакетов и, во-вторых, проверяет полученную контрольную сумму на вводе, игнорируя пакеты с невер- ной контрольной суммой. Этот параметр влияет на символьные сокеты IPv6, от- личные от символьных сокетов ICMPv6. (Ядро всегда вычисляет и хранит конт- рольную сумму для символьных сокетов ICMPv6.) Если задано значение -1 (значение по умолчанию), ядро не будет вычислять и хранить контрольную сум- му для исходящих пакетов на этом символьном сокете и не будет проверять кон- трольную сумму для получаемых пакетов.
7.8. Параметры сокетов IPv6 243 ПРИМЕЧАНИЕ---------------------------------------------------------- Все протоколы, использующие IPv6, должны иметь контрольную сумму в своих соб- ственных заголовках. Эти контрольные суммы включают исевдозаголовок (RFC 1883 [25]), куда входит IPvG-адрес отправителя как часть контрольной суммы (что отлича- ет IPv6 от всех остальных протоколов, которые обычно реализуются с использованием символьного сокета с IPv4). Ядро не заставляет приложение использовать символь- ный сокет для выбора адреса отправителя, а делает это самостоятельно и затем вычис- ляет и сохраняет контрольную сумму, включающую псевдозаголовок IPv6. Параметр сокета IPv6_DSTOPTS Установка этого параметра означает, что любые полученные IPv6-параметры получателя должны быть возвращены в качестве вспомогательных данных функ- цией recvmsg. По умолчанию параметр отключен. Мы опишем функции, исполь- зуемые для создания и обработки этих параметров, в разделе 24.5. Параметр сокета IPv6_HOPLIMIT Установка этого параметра определяет, что полученное поле с предельным коли- чеством транзитных узлов1 (hop limit field) должно быть возвращено в качестве вспомогательных данных функцией recvmsg. По умолчанию параметр отключен. Мы опишем функции, используемые для создания и обработки этого параметра, в разделе 20.8. ПРИМЕЧАНИЕ----------------------------------------------- В IPv4 пс существует способа определить значение получаемого поля времени жизни. Параметр сокета IPv6_HOPOPTS Установка этого параметра означает, что любые полученные параметры транзит- ных узлов (hop-by-hop options) IPv6 должны быть возвращены в качестве вспо- могательных данных функцией recvmsg. По умолчанию параметр отключен. Мы опишем функции, используемые для создания и обработки этого параметра, в раз- деле 24.5. IPv6_NEXTHOP Это не параметр сокета, а тип объекта вспомогательных данных, который можно задать в функции sendmsg. Объект задает адрес следующего транзитного узла для дейтаграммы в качестве структуры адреса сокета. Более подробно об этом свой- стве мы поговорим в разделе 20.8. Параметр сокета IPv6_PKTINFO Установка этого параметра означает, что два фрагмента информации о получен- ной дейтаграмме IPv6 — IPv6-a/ipec получателя и индекс принимающего интер- 1 Используются также термины «поле количества переходов», «поле ограничения пересылок» и др. — Примеч перев.
244 Глава 7. Параметры сокетов фейса — должны быть возвращены в качестве вспомогательных данных функци- ей recvmsg. Мы опишем этот параметр в разделе 20.8. Параметр сокета IPv6_PKTOPTIONS Большинство параметров сокетов IPv6 рассчитаны па сокет UDP, где информа- ция передается между ядром и приложением через вспомогательные данные в функциях recvmsg и sendmsg. Сокет TCP получает и хранит эти значения с по- мощью параметра IPV6_PKT0PTI0NS. Буфер, на который указывают функции getsockopt и setsockopt, содержит ту же информацию, которая передавалась бы как вспомогательные данные в функ- циях recvmsg или sendmsg. Этот параметр сокета мы обсудим в разделе 24.7. Параметр сокета IPv6_RTHDR Установка этого параметра означает, что получаемый заголовок маршрутизации IPv6 должен быть возвращен в качестве вспомогательных данных функцией recvmsg. По умолчанию этот параметр отключен. Мы опишем функции, которые используются для создания и обработки этих параметров, в разделе 24.6. Параметр сокета IPv6_UNICAST_HOPS Этот параметр аналогичен параметру сокета IPv4 IP_TTL. Он определяет предель- ное количество транзитных узлов, заданное по умолчанию для исходящих дей- таграмм, отправляемых через этот сокет. При получении значения этого пара- метра сокета возвращается предельное количество транзитных узлов, которые ядро будет использовать для сокета. Чтобы определить действительное значение предельного количества транзитных узлов из полученной дейтаграммы IPv6, тре- буется использовать параметр сокета IPV6_H0PLIMIT. Мы устанавливаем этот па- эаметр сокета в нашей программе Traceroute в листинге 25.14. 7.9. Параметры сокетов TCP Существует пять параметров сокетов TCP, однако три из них введены в Posix.lg г не получили широкого применения. Мы присваиваем аргументу 1 evel значе- ше IPPROTO_TCP. Параметр сокета TCP_KEEPALIVE Этот параметр появился только в Posix.lg. Он задает время простоя в секундах [ля соединения, по истечении которого TCP начнет отправлять сообщения для [роверки работоспособности собеседника (keepalive probes). Значение по умол- [анию должно быть как минимум 7200 секунд, то есть 2 часа. Этот параметр дей- твует, только если включен параметр сокета SO_KEEPALIVE. Параметр сокета TCP_MAXRT Этот параметр появился только в Posix. 1g. Он задает количество времени в се- ундах, отсчитываемое с момента начала повторной передачи данных TCP, по [стечении которого соединение разрывается. Есди этот парамтер нулевой, ис-
7.9. Параметры сокетов TCP 245 пользуется значение, заданное системой по умолчанию. Значение -1 позволяет неограниченную продолжительность передачи. Если же задано положительное значение, оно может быть округлено в большую сторону к ближайшему по вели- чине времени повторной передачи для данной реализации. Параметр сокета TCP_MAXSEG Этот параметр сокета позволяет нам получать пли устанавливать максимальный размер сегмента {maximum segment size, MSS) для соединения TCP. Возвращае- мое значение — это количество данных, которые наш TCP будет отправлять на другой конец соединения. Часто это значение равно MSS, анонсируемому дру- гим концом соединения в его сегменте SYN, если наш TCP не выбирает меньшее значение, чем объявленный MSS собеседника. Если это значение получено до того как сокет присоединился, возвращаемым значением будет значение по умол- чанию, которое используется в том случае, когда параметр MSS не получен с дру- гого конца соединения. Также помните о том, что значение меньше возвращаемого действительно может использоваться для соединения, если, например, задейству- ется параметр отметки времени (timestamp option), поскольку в каждом сегменте он занимает 12 байт области, отведенной под параметры TCP. Максимальное количество данных, которые TCP отправляет в каждом сег- менте, также может изменяться за время существования соединения, если TCP поддерживает определение транспортной MTU. Если маршрут к собеседнику изменяется, это значение может увеличиваться или уменьшаться. В табл. 7.1 мы отметили, что этот параметр сокета может быть также установ- лен приложением. До 4.4BSD это было невозможно: этот параметр был доступен только для чтения. 4.4BSD позволяет приложению только лишь уменьшать это значение, но мы не можем его увеличивать [105, с. 1023J. Поскольку этот пара- метр управляет количеством данных, которые TCP посылает в каждом сегменте, имеет смысл запретить приложению увеличивать значение. После установления соединения это значение задается величиной MSS, которую объявил собеседник, и мы пе можем превысить его. Однако наш TCP всегда может отправить меньше данных, чем было анонсировано собеседником. Параметр сокета TCPJNODELAY Если этот параметр установлен, он отключает алгоритм Нагла {Nagle algorithm) (см. раздел 19.4 [94] и с. 858-859 [105]). По умолчанию этот алгоритм включен. Назначение алгоритма Нагла — сократить число небольших пакетов в глобаль- ной сети. Согласно этому алгоритму, если у данного соединения имеются непод- твержденные (outstanding) данные (то есть данные, которые отправил наш TCP и подтверждения которых он ждет), то небольшие пакеты не будут отправляться через соединение до тех пор, пока существующие данные не будут подтвержде- ны. Под «небольшим» пакетом понимается любой пакет, меньший MSS. TCP будет по возможности всегда отправлять пакеты нормального размера. Таким образом, назначение алгоритма Нагла — не допустить, чтобы у соединения было множество небольших пакетов, ожидающих подтверждения. Два типичных генератора небольших пакетов — клиенты Rlogin и Telnet, по- скольку обычно они посылают каждое нажатие клавиши в отдельном пакете.
246 Глава 7 Параметры сокетов В быстрой локальной сети мы обычно не замечаем действия алгоритма Нагла с этими клиентами, потому что время, требуемое для подтверждения небольшо- го пакета, составляет несколько миллисекунд — намного меньше, чем промежу- ток между вводом двух последовательных символов Но в глобальной сети, где для подтверждения небольшого пакета может потребоваться секунда, мы можем заметить задержку в отражении символов, и эта задержка часто увеличивается при включении алгоритма Нагла Рассмотрим следующий пример Мы вводим строку из шести символов hello1 либо клиенту Rlogin, либо клиенту Telnet, промежуток между вводом символов составляет точно 250 миллисекунд Время обращения к серверу (RTT) составля- ет 600 миллисекунд, и сервер немедленно отправляет обратно отражение симво- ла Мы считаем, что сегмент АСК, подтверждающий получение клиентского сим- вола, отправляется обратно клиенту с отражением символа, а сегменты АСК, которые клиент отправляет для подтверждения приема отраженного сервером символа, мы игнорируем (Мы поговорим о задержанных сегментах АСК чуть ниже ) Считая, что алгоритм Нагла отключен, получаем 12 пакетов, изображен- ных на рис 7 6 Рис. 7.6. Шесть символов, отраженных сервером при отключенном алгоритме Нагла Каждый символ отправляется в индивидуальном пакете сегменты данных слева направо, а сегменты АСК справа налево Но если алгоритм Нагла включен (по умолчанию), у нас имеется 8 пакетов, показанных на рис 7 7 Первый символ посылается как пакет, но следующие два символа не отправляются, поскольку у соединения есть небольшой пакет, ожи- дающий подтверждения Эти пакеты отправляются, когда прошло 600 миллисе- кунд, то есть когда прибывает сегмент АСК, подтверждающий прием первого пакета, вместе с отражением первого символа Пока второй пакет не будет под- твержден сегментом АСК в момент времени 1200, не будет отправлено ни одного небольшого пакета
7 9 Параметры сокетов TCP 247 Рис. 7.7. Пакеты, отправляемые при включенном алгоритме Нагла Алгоритм Нагла часто взаимодействует с дру! им алгоритмом TCP — алгорит мом задержанного сегмента АСК {delayed АСК) Этот алгоритм заставляет TCP не отправлять сегмент АСК сразу же при получении данных — вместо этого TCP ждет в течение небольшого количества времени (типичное значение 50-200 мил- лисекунд) и только после этого отправляет сегмент АСК Здесь делается расчет на то, что в течение этого непродолжительного времени появятся данные для от- правки собеседнику, и сегмент АСК может быть вложен в пакет с этими данны ми Таким образом, можно будет сэкономить на одном сегменте TCP Это обыч- ный случай с клиентами Rlogin и Telnet, поэтому сегмент АСК клиентского символа вкладывается в отражение символа сервером Проблема возникает с другими клиентами, серверы которых не генерируют трафика в обратном направлении, в который может быть вложен сегмент АСК Эти клиенты могут обнаруживать значительные задержки, поскольку TCP кли- ента не будет посылать никаких данных серверу, пока не истечет время таймера для задержанных сегментов АСК сервера Таким клиентам нужен способ отклю- чения алгоритма Нагла Осуществить эго позволяет параметр TCP_NODELAY Другой тип клиента, для которого нежелательно использование алгоритма Нагла и задержанных АСК TCP, — это клиент, отправляющий одиночный логи- ческий запрос своему серверу небольшими порциями Например, будем считать, что клиент отправляет своему серверу 400-байтовыи запрос, состоящий из 4 байт, задающих тип запроса, за которыми следует 396 байт данных Если клиент вы- полняет функцию write, отправляя 4 байта, и затем функцию write, отправляя остальные 396 байт, вторая часть не будет отправлена со стороны клиента, пока TCP сервера не подтвердит получение первых 4 бант Кроме того, поскольку сер- вер не может работать с 4 байтами данных, пока не получит оставшиеся 396 байт,
248 Глава 7. Параметры сокетов TCP сервера задержит сегмент АСК, подтверждающий получение 4 байт данных (то есть не будет данных от сервера клиенту, в которые можно вложить сегмент АСК). Есть три способа решить проблему с таким клиентом. 1. Использовать функцию writev вместо двух вызовов функции write. Один вы- зов функции writev приводит к отправке только одного сегмента TCP в нашем примере. Это предпочтительное решение. 2. Скопировать 4 байта данных и 396 байт данных в один буфер и вызвать один раз функцию wri te для этого буфера. 3. Установить параметр сокета TCP_NODELAY и продолжать вызывать функцию write дважды. Это наименее желательное решение. Упражнения 7.8 и 7.9 продолжают этот пример. Параметр сокета TCP_STDURG Этот параметр появился в Posix.lg и влияет на интерпретацию срочного указате- ля (с которым мы встретимся при обсуждении внеполосных данных в главе 21). Есть две интерпретации срочного указателя TCP [94, с. 292-293]. По умолчанию срочный указатель указывает на байт данных, следующий за байтом, отправлен- ным с флагом MSG_OOB. Так большинство приложений взаимодействует сегодня. Е1о если этот параметр сокета является ненулевым, то срочный указатель будет указывать на байт данных, отправленный с фла! ом MSG OOB. ПРИМЕЧАНИЕ------------------------------------------------------- Этот параметр сокета никогда не требуется устанавливать, и остается неясным, почему -гл Posix.lg определяет его. 1 I 7.10. Функция fcntl Сокращение fcntl означает «управление файлами» (file control). Эта функция выполняет различные операции управления дескрипторами. Перед описанием этой функции и ее влияния на сокет нам нужно составить некоторое более общее представление о ее возможностях. В табл. 7.5 приводятся различные операции, выполняемые функциями fcntl и iocti и маршрутизирующими сокетами. Таблица 7.5. Операции функций fcntl и ioctl и маршрутизирующих сокетов Операция fcntl ioctl Маршрутизиру- ющий сокет Posix.lg Установка сокета для неблокнрусмого ввода-вывода FSETFL, ONONBLOCK FIONBIO fcntl Установка сокета для ввода-вывода, управля- емого сигналом FSETFL, OASYNC FIOASYNC fcntl Установка владельца сокета FSETOWN SIOCSPGRP или FIOSETOWN fcntl Получение владельца сокета FGETOWN SIOCGPGRP или FJOGETOWN fcntl
7.10. Функция fcntl 249 Операция fcntl ioctl Маршрутизиру- ющий сокет Posix.lg Получение текущего количества байтов в приемном буфере сокета FIONREAD Проверка, находится ди процесс на отметке внеполосных данных SIOCATMARK sockatmark Получение списка интерфейсов SIOCGIFCONF Sysctl Операции интерфейсов SIOC|GS|IFxix Кэш-онсрации ARP SIOCrARP RTMui Операции таблицы маршрутизации SIOCojcRT RTM_.wr Первые шесть операций могут применяться к сокетам любым процессом, в то время как большинство из последних четырех (интерфейс, ARP и таблица марш- рутизации) выполняются администрирующими программами, такими как т fconf i g и route. О различных операциях функции 1 octi мы поговорим подробнее в гла- ве 16, а о маршрутизирующих сокетах — в главе 17. Существует множество способов выполнения первых четырех операций, но, как указано в последней колонке, стандарт Posix. 1g определяет, что функция fcntl является предпочтительным способом. Отметим также, что Posix. 1g предлагает функцию sockatmark (см. раздел 21.3) как наиболее предпочтительный способ те- стирования на предмет пребывания процесса на отметке внеполосных данных. Оставшиеся операции с пустой последней колонкой не стандартизованы Posix. ПРИМЕЧАНИЕ--------------------------------------------------------- Отмс'1 им также, что первые две операции, устанавливающие сокет для иеблокируемо- го ввода-вывода и для ввода-вывода, управляемого сигналом, традиционно применя- лись с использованием команд FNDELAY и FASYNC функции fcntl. Posix определяет константы О ххх. Функция fcntl предоставляет следующие возможности, относящиеся к сете- вому программированию: Неблокируемый ввод-вывод. Мы можем установить флаг состояния файла O_NONBLOCK, используя команду F_SETFL для установки неблокируемого сокета. Неблокируемый ввод-вывод мы описываем в главе 15. Управляемый сигналом ввод-вывод. Мы можем установить флаг состояния файла O_ASYNC, используя команду F_SETFL, которая вызывает генерацию сиг- нала SIGI0, когда состояние сокета изменяется. Мы рассмотрим это в главе 22. ПРИМЕЧАНИЕ -------------------------------------------------------- Этот флаг введен в Posix.lg. Команда F SETOWN позволяет нам указать, что владелец сокета (идентификатор процесса или идентификатор группы процессов) должен получать сигналы SIGIO и SIGURG. Первый сигнал генерируется, когда для сокета включается управ- ляемый сигналом ввод-вывод (см. главу 22), второй — когда для сокета при-
250 Глава 7. Параметры сокетов ходят новые внеполосные (out-of-band data) данные (см. главу 21). Команда F GETOWN возвращает текущего владельца сокета. ПРИМЕЧАНИЕ ---------------------------------------------------------------------- Термин «владелец сокета» введен в Posix. 1g. Исторически реализации, происходящие от Беркли, называли его «идентификатор группы процессов сокета», потому что пере- менная, хранящая этот идентификатор, — это элемент so_pgid структуры socket [105, с. 438]. include <fcntl h> wt fcntKint fd. int cmd. /* mt ang */ ). Возвращает в случае успешного выполнения результат зависит от аргумента cmd. -1 в случае ошибки Каждый дескриптор (включая сокет) имеет набор флагов, задающих статус файла, которые можно получить с помощью команды F^GETFL и установить с по- мощью команды F_SETFL. На сокет влияют два следующих флага: O_NONBLOCK неблокируемый ввод-вывод O_ASYNC ввод-вывод, управляемый сигналом Позже мы опишем оба эти флага подробнее. Отметим, что типичный код, ко- торый устанавливает неблокируемый ввод-вывод с использованием функции fent 1, выглядит следующим образом: int flags. /* Делаем сокет неблокируемым */ if ( (flags = fcntUfd. F GETFL. 0)) < 0) err_sys("F_GETFL error"). flags |= O_NONBLOCK, if (fcntUfd. F_SETFL. flags) < 0) err_sys("F_SETFL error") Учтите, что вам может встретиться код, который просто устанавливает жела- емый флаг: /* Неправильный способ сделать сокет неблокируемым */ if (fcntUfd. F_SETFL. OJOBLOCK) < 0) err_sys("F_SETFL error”). Хотя при этом и устанавливается флаг отключения блокировки, также снима- ются все остальные флаги состояния файла. Единственный корректный способ установить один из этих флагов состояния файла — получить текущие флаги, с помощью операции логического ИЛИ добавить новый флаг, а затем установить флаги. Следующий код сбрасывает флаг отключения блокировки в предположении, что переменная fl ags была задана с помощью вызова функции fсn11, показанного ранее: flags &= ~O_NONBLOCK. if (fcntUfd. F_SETFL. flags) < 0) err_sys("F_SETFL error”). Два сигнала, SIGIO и SIGURG, отличаются от других сигналов тем, что они гене- рируются для сокета только если сокету был присвоен владелец с помощью ко-
7.11. Резюме 251 манды F_SETOWN. Целое значение аргумента arg для команды F_SETOWN может быть либо положительным, задающим идентификатор процесса, получающего сигнал, либо отрицательным, абсолютное значение которого — это идентификатор груп- пы процессов, получающей сигнал. Команда F_GETOWN возвращает владельца соке- та, так как возвращаемое значение функции fcntl — либо идентификатор про- цесса (положительное возвращаемое значение), либо идентификатор группы процессов (отрицательное значение, отличное от -1). Разница между заданием процесса и группы процессов, получающих сигнал, в том, что в первом случае сигнал будет получен только одиночным процессом, в то время как во втором случае его получают все процессы в группе. ПРИМЕЧАНИЕ ----------------------------------------------------- В S VR4 допускается задавать владельца сокета как идентификатор процесса, но не как идентификатор группы процессов. Когда создается новый сокет с помощью функции socket, у него нет владель- ца. Но когда новый сокет создается из прослушиваемого сокета, владелец сокета наследуется присоединенным сокетом от прослушиваемого (как и многие пара- метры сокетов [105, с. 462-463]. 7.11. Резюме Параметры сокетов лежат в широком диапазоне от очень общих (SO_ERROR) до очень специфичных (параметры заголовка IP). Наиболее общеупотребительные пара- метры сокетов, которые нам могут встретиться, — это SO_KEEPALIVE, SO_RCVBUF, SO_SNDBUF и SO_REUSEADDR. Последний должен всегда задаваться для сервера TCP до того, как сервер вызовет функцию Ьт nd (см. листинг 11.4). Параметр SO_BROADCAST и десять параметров сокетов многоадресной передачи предназначены только для приложений, передающих соответственно широковещательные или многоадрес- ные сообщения. Параметр сокета SO KEEPALIVE устанавливается многими серверами TCP и ав- томатически закрывает наполовину открытое соединение. Замечательное свой- ство этого параметра в том, что он обрабатывается на уровне TCP, не требуя на уровне приложения наличия таймера, измеряющего период отсутствия активно- сти. Однако недостаток этого параметра в том, что он не видит разницы между выходом собеседника из строя и временной потерей соединения с ним. Параметр сокета SO_LINGER расширяет наши возможности в отношении конт- роля над функцией close — мы можем отложить ее завершение на некоторое вре- мя. Кроме того, этот параметр позволяет нам отправить сегмент RST вместо обыч- ной последовательности из четырех пакетов, завершающих соединение TCP. Следует соблюдать осторожность при отправке сегментов RST, поскольку в этом случае не наступает состояние TCP TIME_WAIT. Существуют случаи, когда этот параметр сокета не обеспечивает необходимой нам информации, и тогда требует- ся сегмент АСК уровня приложения. У каждого сокета TCP имеются буфер отправки и буфер приема, а у*каждого сокета UDP есть буфер приема. Параметры сокета SO_SNDBUF и SO_RCVBUF позволя- ют нам изменять размеры этих буферов. Основное применение эти функции
252 Глава 7. Параметры сокетов находят при передаче большого количества данных по каналам с повышенной вместимостью (long fat pipe), которые представляют собой соединения TCP либо с широкой полосой пропускания, либо с большой задержкой, часто с использова- нием расширений из RFC 1323. Сокеты UDP, наоборот, могут стремиться увели- чить размер приемного буфера, чтобы позволить ядру установить в очередь боль- ше дейтаграмм, если приложение занято. Упражнения 1. Напишите программу, которая выводит заданные по умолчанию размеры бу- феров отправки и приема TCP и UDP, и запустите ее в системе, к которой у вас имеется доступ. 2. Измените листинг 1.1 следующим образом. Перед вызовом функции connect вызовите функцию getsockopt, чтобы получить размер приемного буфера со- кета и MSS. Выведите оба значения. После успешного завершения функции извлеките значения тех же двух параметров сокета и выведите их. Измени- лись ли значения? Почему? Запустите программу, соединяющуюся с серве- ром в вашей локальной сети, и программу, соединяющуюся с сервером в уда- ленной сети. Изменяется ли MSS? Почему? Запустите также программу на разных узлах, к которым у вас есть доступ. 3. Запустите наш сервер TCP, приведенный в листингах 5.1 и 5.2, и наш клиент из листингов 5.3 и 5.4. Измените функцию main клиента, чтобы установить параметр сокета SOL INGER перед вызовом функции exit, задав lonoff равным 1, а 11 inger — равным 0. Запустите сервер, а затем запустите клиент. Введите строку или две на стороне клиента для проверки работоспособности, а затем завершите работу клиента, введя символ конца файла. Что происходит? Пос- ле завершения работы клиента запустите программу netstat на узле клиента и посмотрите, проходит ли сокет через состояние TIME_WAIT. 4. Будем считать, что два клиента TCP запускаются одновременно. Оба уста- навливают параметр сокета SO_REUSEADDR и затем с помощью функции bind связываются с одним и тем же локальным IP-адресом и одним и тем же ло- кальным портом (допустим, 1500). Но один из клиентов соединяется с по- мощью функции connect с адресом 198.69.10.2, порт 7000, а второй — с адресом 198.69.10.2 (тот же IP-адрес сбеседника), порт 8000. Опишите возникающую ситуацию гонок (race condition). 5. Получите исходный код для примеров в этой книге (см. предисловие) и от- компилируйте программу sock (см. раздел В.З). Сначала классифицируйте свой узел как узел, не поддерживающий многоадресную передачу, затем — как под- держивающий многоадресную передачу, но не поддерживающий параметр , SO_REUSEPORT, и наконец как узел, поддерживающий многоадресную передачу с предоставлением параметра SO_REUSEPORT. Постарайтесь запустить несколь- ко экземпляров программы sock в качестве сервера TCP (параметр -s команд- ной строки) на одном и том же порте, связывая универсальный адрес, один из адресов интерфейсов вашего узла и адрес закольцовки (loopback address). Нужно ли вам задавать параметр SO_REUSEADDR (параметр -А командной стро-
Упражнения 253 ки)? Используйте программу netstat для просмотра прослушиваемых со- кетов. 6. Продолжайте предыдущий пример, но запустите сервер UDP (параметр -и командной строки) и попытайтесь запустить два экземпляра, связанные с од- ними и теми же локальным IP-адресом и портом. Если ваша реализация под- держивает параметр SO REUSEPDRT, попытайтесь использовать ее (параметр -Т командной строки). 7. Многие версии утилиты Ping имеют флаг -d, задающий параметр сокета SO_DEBUG. В чем его назначение? 8. Продолжая пример в конце нашего обсуждения параметра сокета TCP NODELAY, предположим, что клиент выполняет две операции записи с помощью функ- ции write: первую для 4 байт данных и вторую для 396 байт. Также будем счи- тать, что время задержки АСК — 100 миллисекунд, период RTT между клиен- том и сервером равен 100 миллисекундам, а время обработки сервером каждого клиентского запроса — 50 миллисекунд. Нарисуйте временную диаграмму, показывающую взаимодействие алгоритма Нагла с задержанными сегмента- ми АСК. 9. Снова выполните предыдущее упражнение, считая, что установлен параметр сокета TCP_NODELAY. 10. Снова выполните упражнение 8, считая, что процесс вызывает функцию wn tev один раз для обоих буферов — и для 4-байтового, и для 396-байтового. 11. Прочтите RFC 1122 [9], чтобы определить рекомендуемый интервал для за- держанных сегментов АСК. 12. В какой из версий наш сервер тратит больше времени — в листинге 5.1 или 5 2? Что происходит, если сервер устанавливает параметр сокета S0_KEEPALIVE, через соединение не происходит обмена данными, узел клиента выходит из строя и не перезагружается? 13. В какой из версий наш клиент тратит больше времени — в листинге 5.3 или 5.4? Что происходит, если клиент устанавливает параметр сокета SO KEEPALIVE, через соединение не происходит обмена данными и узел сервера выходит из строя и не перезагружается? 14. В какой из версий наш клиент тратит больше времени — в листинге 5.3 или 6.2? Что происходит, если клиент устанавливает параметр сокета SO_KEEPALI VE, через соединение не происходит обмена данными и узел сервера выходит из строя и не перезагружается? 15. Будем считать, что и клиент, и сервер устанавливают параметр сокета SO_KEEPALIVE. Между собеседниками поддерживается соединение, по через это соединение не происходит обмена данными между приложениями. Когда про- ходят условленные 2 часа и требуется проверить наличие связи, сколькими сегментами TCP обмениваются собеседники? 16. Почти все реализации определяют константу SO_ACCEPTON в заголовочном фай- ле <sys/ socket h>, но мы не описывали этот параметр. Прочтите [59], чтобы понять, зачем этот параметр существует.
ГЛАВА 8 Основные сведения о сокетах UDP 8.1. Введение Существуют определенные фундаментальные различия между приложениями, использующими TCP и UDP. Они возникают из-за различий в двух транспортных уровнях: UDP является ненадежным протоколом дейтаграмм, не ориентирован- ным на установление соединения, что значительно отличает его от ориентиро- ванного на установление соединения надежного потока байтов, предоставляемо- го TCP. Тем не менее есть случаи, когда имеет смысл использовать UDP вместо TCP. Подобные случаи мы рассматриваем в разделе 20 4. Некоторые популяр- ные приложения построены с использованием UDP, например DNS (Domain Name System — система доменных имен), NFS (сетевая файловая система — Net- work File System) и SNMP (Simple Network Management Protocol — простой про- токол управления сетью). Сервер UDP Рис. 8.1. Функции сокета для модели клиент-севрер UDP
8.2. Функции recvfrom и sendto 255 На рис. 8.1 показаны вызовы функций для типичной схемы клиент-сервер UDP. Клиент не устанавливает соединения с сервером. Вместо этого клиент лишь отправляет серверу дейтаграмму, используя функцию sendto (она описывается в следующем разделе), которой нужно задать адрес получателя (сервера) в каче- стве аргумента. Аналогично сервер не устанавливает соединения с клиентом. Вместо этого сервер лишь вызывает функцию recvfrom, которая ждет, когда при- дут данные от какого-либо клиента. Функция recvfrom возвращает адрес клиента (для данного протокола) вместе с дейтаграммой, и таким образом сервер может отправить ответ именно тому клиенту, который прислал дейтаграмму. Рисунок 8.1 иллюстрирует временную диаграмму типичного сценария, имею- щего место при обмене UDP-дейтаграммами между клиентом и сервером. Мы можем сравнить этот пример с типичным обменом по протоколу TCP, изобра- женным на рис. 4.1. В этой главе мы опишем новые функции, применяемые с сокетами UDP, — recvfrom и sendto, и переделаем нашу модель клиент-сервер для применения UDP. Кроме того, мы рассмотрим использование функции connect с сокетом UDP и кон- цепцию асинхронных ошибок. 8.2. Функции recvfrom и sendto Эти две функции аналогичны стандартным функциям read и write, но треоуют трех дополнительных аргументов. #include <sys/socket h> ssize_t recvfrom(int sockfd void *buff size_t nbytes int flags struct sockaddr *from. socklen_t *addrlen) ssize_t sendto(int sockfd const void *buff, size_t nbytes int flags const struct sockaddr *to socklen_t addrlen) Обе функции возвращают количество записанных или прочитанных байтов в случае успешного выполнения -1 в случае ошибки Первые три аргумента, sockfd, buff и nbytes, идентичны первым трем аргумен- там функций read и write: дескриптор, указатель на буфер, из которого произво- дится чтение или в который происходит запись, и число байтов для чтения или записи. Мы расскажем об аргументе flags в главе 13, где мы рассматриваем функции recv, send, recvmsg и sendmsg, поскольку сейчас в нашем простом примере они не нужны. Пока мы всегда будем устанавливать аргумент fl ags в нуль. Аргумент to для функции sendto — это структура адреса сокета, содержащая адрес протокола (например, IP-адрес и номер порта) места, куда отправляются данные. Размер этой структуры адреса сокета задается аргументом addrl еп. Функ- ция recvfrom заполняет структуру адреса сокета, на которую указывает аргумент from, записывая в нее адрес протокола места, откуда отправлена дейтаграмма. Число байтов, хранящихся в структуре адреса сокета, также возвращается вызы- вающему процессу в целом числе, на которое указывает аргумент addrlen. Обра- тите внимание, что последний аргумент функции sendto является целочислен- ным значением, в то время как последний аргумент функции recvfrom — это указатель на целое значение (аргумент типа «значение-результат»!
256 Глава 8. Основные сведения о сокетах UDP Последние два аргумента функции recvfrom аналогичны двум последним ар- гументам функции accept: содержимое структуры адреса сокета по завершении сообщает нам, кто отправил дейтаграмму (в случае UDP) или кто инициировал соединение (в случае TCP). Последние два аргумента функции sendto аналогич- ны двум последним аргументам функции connect: мы заполняем структуру адре- са сокета адресом протокола того места, в которое отправляется дейтаграмма (в случае UDP) или с которым будет устанавливаться соединение (в случае TCP). Обе функции возвращают в качестве значения функции длину данных, кото- рые были прочитаны или записаны. При типичном использовании функции recvfrom с протоколом дейтаграмм возвращаемое значение — это объем пользова- тельских данных в полученной дейтаграмме. Запись дейтаграммы с нулевой длиной является нормальной. В случае UDP при этом возвращается дейтаграмма IP, содержащая заголовок IP (обычно 20 байт для IPv4 или 40 байт для IPv6), 8-байтовый заголовок UDP и никаких данных. Это также означает, что возвращемое из функции recvfrom пулевое значение вполне приемлемо для протокола дейтаграмм: оно не является признаком того, что собе- седник закрыл соединение, как это происходит при возвращении нулевого значе- ния из функции read на сокете TCP. Поскольку протокол UDP не ориентирован па установление соединения, то в нем и не существует такого события, как за- крытие соединения. Если аргумент from функции recvfrom является пустым указателем, то соот- ветствующий аргумент длины (addrlen) также должен быть пустым указателем, и это означает, что нас не интересует адрес протокола того, кто отправляет нам данные. И функция recvfrom, и функция sendto могут использоваться с TCP, хотя обычно в этом нет необходимости. ПРИМЕЧАНИЕ ------------------------------------------------------------ Т/ТСР (TCP для транзакций) использует функцию sendto, как мы показываем в раз- деле 13.9. 8.3. Эхо-сервер UDP: функция main Теперь мы переделаем нашу простую модель клиент-сервер из главы 5, исполь- зуя UDP. Диаграмма вызовов функций в программах наших клиента и сервера UDP показана на рис. 8.1. На рис. 8.2 представлены используемые функции. В лис- тинге 8.11 показана функция сервера main. stdin stdout Рис. 8.2. Простая модель клиент-сервер, использующая UDP 1 Вес исходные коды программ, опубликованные в этой книге, вы можые найти ПО адресуhttpcjV piter com/dounload.
8.4. Эхо-сервер: функция dg echo 257 Листинг 8.1. Эхо-сервер UDP //udpcliserv/udpservOl с 1 include "unp h" 2 int 3 maindnt argc, char **argv) 4 { 5 int sockfd. 6 struct sockaddr_in servaddr. cliaddr; 7 sockfd = Socket(AF_INET. SOCK_DGRAM. 0); 8 bzero(&servaddr. sizeof(servaddr)). 9 servaddr sin_family = AF_INET. 10 servaddr sin_addr.s_addr = htonl(INADDR_ANY). 11 servaddr.sin_port = htons(SERV_PORT); 12 Bindlsockfd. (SA *) &servaddr, sizeof(servaddr)). 13 dg_echo(sockfd. (SA *) &cliaddr. sizeof(cliaddr)); 14 } Создание сокета UDP, связывание с заранее известным портом при помощи функции bind -12 Мы создаем сокет UDP, задавая в качестве второго аргумента функции socket значение SOCK DGRAM (сокет дейтаграмм в протоколе IPv4). Как и в примере серве- ра TCP, адрес IPv4 для функции bi nd задается как INADDR_ANY, а заранее известный номер порта сервера — это константа SERV_PORT из заголовка unp h. 13 Затем вызывается функция dg_echo для обработки клиентского запроса серве- ром. 8.4. Эхо-сервер: функция dg_echo В листинге 8.2 показана функция dg_echo. Листинг 8.2. Функция dg_echo: отражение строк на сокете дейтаграмм //lib/dg_echo с 1 #include "unp h" 2 void 3 dg_echo(int sockfd. SA *pcliaddr. socklen_t clilen) 4 { 5 int n. 6 socklen_t len. 7 char mesg [MAXLINE]. 8 for (..) { 9 len = clilen, 10 n = Recvfrom(sockfd. mesg. MAXLINE. 0. pcliaddr. &len). 11 Sendtotsockfd, mesg. n. 0. pcliaddr len) 12 } 13 }
258 Глава 8. Основные сведения о сокетах UDP Чтение дейтаграммы, отражение отправителю 3-12 Эта функция является простым циклом, в котором очередная дейтаграмма, при- ходящая на порт сервера, читается функцией recvfrom и с помощью функции sendto отправляется обратно. Несмотря на простоту этой функции, нужно учесть ряд важных деталей. Во- первых, эта функция никогда не завершается. Поскольку UDP — это протокол, не ориентированный на установление соединения, в нем не существует никаких аналогов признака конца файла, используемого в TCP. Во-вторых, эта функция позволяет создать последовательный сервер, а не па- раллельный, который мы получали в случае TCP. Поскольку нет вызова функ- ции fork, один процесс сервера выполняет обрабатку всех клиентов. В общем слу- чае большинство серверов TCP являются параллельными, а большинство серверов UDP — последовательными. Для сокета на уровне UDP происходит неявная буферизация дейтаграмм в ви- де очереди. Действительно, у каждого сокета UDP имеется буфер приема, и каждая дейтаграмма, приходящая на этот сокет, помещается в его буфер приема. Когда процесс вызывает функцию recvfrom, очередная дейтаграмма из буфера возвра- щается процессу в порядке FIFO (First In, First Out — первым пришел, первым обслужен). Таким образом, если множество дейтаграмм приходит на сокет до того, как процесс может прочитать данные, уже установленные в очередь для сокета, то приходящие дейтаграммы просто добавляются в буфер приема сокета. Но этот буфер имеет ограниченный размер. Мы обсуждали этот размер и способы его уве- личения с помощью параметра сокета SO_RCVBUF в разделе 7.5. На рис. 8.3 показано обобщение нашей модели TCP клиент-сервер из главы 5, когда два клиента устанавливают соединения с сервером. Рис. 8.3. Обобщение модели TCP клиент-сервер с двумя клиентами Здесь имеется два присоединенных сокета, и каждый из присоединенных со- кетов на узле сервера имеет свой собственный буфер приема. На рис. 8.4 показан случай, когда два клиента отправляют дейтаграммы сер- веру UDP. Существует только один процесс сервера, и у него имеется один сокет, на ко- торый сервер получает все приходящие дейтаграммы и с которого отправляет все ответы. У этого сокета имеется буфер приема, в который помещаются все прихо- дящие дейтаграммы.
8.5. Эхо-клиент UDP: функция main 259 Рис. 8.4. Обобщение модели UDP клиент-сервер с двумя клиентами Функция шатл в листинге 8.1 является зависящей от протокола (она создает сокет протокола AF INET, а затем выделяет и инициализирует структуру адреса сокета IPv4), но функция dg_echo от протокола не зависит. Причина, по которой функция dg_echo не зависит от протокола, заключается в том, что вызывающий процесс (в нашем случае функция main) должен разместить в памяти структуру адреса сокета корректного размера, и указатель на эту структуру вместе с ее раз- мером передаются в качестве аргументов функции dg_echo. Функция dg_echo ни- когда не углубляется в эту специфичную для протокола структуру: она просто передает указатель на структуру функций recvfrom и sendto. Функция recvfrom заполняет эту структуру, вписывая в нее IP-адрес и номер порта клиента, и по- скольку тот же указатель (pci iaddr) затем передается функции sendto в качестве адреса получателя, таким образом дейтаграмма отражается обратно клиенту, от- правившему дейтаграмму. 8.5. Эхо-клиент UDP: функция main Функция main клиента UDP показана в листинге 8.3. Листинг 8.3. Эхо-клиент UDP //udpcl1serv/udpcli01 с 1 #include "unp h" 2 int 3 main(int argc. char **argv) 4 { 5 int sockfd. 6 struct sockaddr_in servaddr. 7 if (argc '= 2) 8 err_quit('usage udpcli <IPaddress>”); 9 bzero(&servaddr sizeof(servaddr)). 10 servaddr sin_family = AF_INET, 11 servaddr sin_port = htons(SERV_PORT) 12 Inet_pton(AF_INET. argv[l] &servaddr sin_addr), 13 sockfd = Socket(AFJNET. SOCK_DGRAM 0).
260 Глава 8. Основные сведения о сокетах UDP Листинг8.3. (продолжение) 14 dg_cli(stdin. sockfd, (SA *) &servaddr sizeof(servaddr)). 15 exit(O). 16 } Заполнение структуры адреса сокета адресом сервера 12 Структура адреса сокета IPv4 заполняется IP-адресом и номером порта серве- ра. Эта структура будет передана функции dg cl 1. Она определяет, куда отправ- лять дейтаграммы. 1-14 Создается сокет UDP и вызывается функция dg cl i. 8.6. Эхо-клиент UDP: функция dg.cli В листинге 8.4 показана функция dg_cl i, которая выполняет большую часть ра- боты на стороне клиента. Листинг 8.4. Функция dg_ch: цикл обработки клиента //11b/dg_cl1 с 1 #1 пс 1 tide "unp h" 2 void 3 dg_cli(FILE *fp. int sockfd const SA *pservaddr socklent servlen) 4 { 5 int n. 6 char sendline[MAXLINE]. recvline[MAXLINE + 1]. 7 while (Fgets(sendline. MAXLINE, fp) '= NULL) { 8 Sendtotsockfd sendline, strlen(sendline). 0. pservaddr. servlen); 9 n = Recvfromtsockfd. recvline. MAXLINE. 0 NULL. NULL). 10 recvline[n] =0. /* завершающий нуль */ 11 Fputs(recvline. stdout). 12 } 13 } 12 В цикле обработки на стороне клиента имеется четыре шага: чтение строки из стандартного потока ввода при помощи функции fgets, отправка строки серверу с помощью функции sendto, чтение отраженного ответа сервера с помощью функ- ции recvfrom и помещение отраженной строки в стандартный поток вывода с по- мощью функции fputs. Наш клиент не запрашивал у ядра присваивания динамически назначаемого порта своему сокету (тогда как для клиента TCP это имело место при вызове функции connect). В случае сокета UDP при первом вызове функции sendto ядро выбирает динамически назначаемый порт, если с этим сокетом еще не был связан никакой локальный порт. Как и в случае TCP, клиент может вызвать функцию bi nd явно, но это делается редко. Обратите внимание, что при вызове функции recvfrom в качестве пятого и ше- стого аргументов задаются пустые указатели. Таким образом мы сообщаем ядру,
8.8. Проверка полученного ответа 261 что мы не заинтересованы в том, чтобы знать, кто отправил ответ. Существует риск, что любой процесс, находящийся как на том же узле, так и на любом дру- гом, может отправить на IP-адрес и порт клиента дейтаграмму, которая будет прочитана клиентом, предполагающим, что это ответ сервера. Эту ситуацию мы рассмотрим в разделе 8.8. Как и в случае функции сервера dg_echo, функция клиента dg_cl 1 является не зависящей от протокола, но функция main клиента зависит от протокола. Функ- ция main размещает в памяти и инициализирует структуру адреса сокета, относя- щегося к определенному типу протокола, а затем передает функции da_с 11 указа- тель на структуру вместе с ее размером. 8.7. Потерянные дейтаграммы Наш пример клиента и сервера UDP ненадежен. Если дейтаграмма клиента по- теряна (допустим, она проигнорирована неким маршрутизатором между клиен- том и сервером), клиент навсегда заблокируется в своем вызове функции recvfrom внутри функции dg cl 1, ожидая ответа сервера, который никогда не придет. Ана- логично если дейтаграмма клиента приходит к серверу, но ответ сервера потерян, клиент навсегда заблокируется в своем вызове функции recvfrom. Единственный способ предотвратить эту ситуацию — поместить тайм-аут в клиентский вызов функции recvfrom. Мы рассмотрим это в разделе 13.2. Простое помещение тайм-аута в вызов функции recvfrom — еще не полное ре- шение. Например, если заданное время ожидания истекло, а ответ не получен, мы не можем сказать точно, в чем дело — или наша дейтаграмма не дошла до сер- вера, или же ответ сервера не пришел обратно. Если бы запрос клиента содержал требование типа «перевести определенное количество денег со счета А на счет Б» (в отличие от случая с нашим простым эхо-сервером), то тогда между потерей запроса и ответа существовала бы большая разница. Более подробно о добавле- нии надежности в модель клиент-сервер UDP мы расскажем в разделе 20.5. 8.8. Проверка полученного ответа В конце раздела 8.6 мы упомянули, что любой процесс, который знает номер ди- намически назначаемого порта клиента, может отправлять дейтаграммы нашему клиенту, и они будут перемешаны с нормальными ответами сервера. Все, что мы можем сделать, — это изменить вызов функции recvfrom, представленный в лис- тинге 8.4, так, чтобы она возвращала IP-адрес и порт отправителя ответа, и игно- рировать любые дейтаграммы, приходящие не от того сервера, которому мы от- правляем дейтаграмму. Однако здесь есть несколько ловушек, как мы дальше увидим. Сначала мы изменяем функцию клиента main (см. листинг 8.3), написанную для стандартного эхо-сервера (см. табл. 2.1). Мы просто заменяем присваивание servaddr sin_port = htons(SERV_PORT). присваиванием servaddr sinport = htons(7).
262 Глава 8. Основные сведения о сокетах UDP Теперь мы можем использовать с нашим клиентом любой узел, на котором работает стандартный эхо-сервер. Затем мы переписываем функцию dg cl i, с тем чтобы она размещала в памяти другую структуру адреса сокета для хранения структуры, возвращаемой функ- цией recvfrom. Мы показываем ее в листинге 8.5. Листинг 8.5. Версия функции dg_cli, проверяющая возвращаемый адрес сокета //udpcl1serv/dgcliaddr с 1 #include "unp h" 2 void 3 dg_cli(FILE *fp. int sockfd. const SA *pservaddr socklen_t servlen) 4 { 5 int n. 6 char sendline[MAXLINE], recvline[MAXLINE + 1]. 7 socklen t len, 8 struct sockaddr *preply_addr. 9 preply_addr = Malloc(servlen). 10 while (Fgets(sendlme. MAXLINE. Ip) '= NULL) { 11 Sendto(sockfd sendlme, strlen(sendline). 0. pservaddr, servlen): 12 len = servlen. 13 n = Recvfrom!sockfd. recvline. MAXLINE. 0 preply_addr &len) 14 if (len l= servlen || memcmp(pservaddr. preply_addr len) '= 0) { 15 printfCreply from & (ignored)\en" 16 Sock_ntop(preply_addr. len)) 17 continue 18 } 19 recvline[n] = 0. /* завершающий нуль */ 20 Fputs(recvline stdout). 21 } 22 } Размещение другой структуры адреса сокета в памяти 9 Мы размещаем в памяти другую структуру адреса сокета при помощи функции mal 1 ос. Обратите внимание, что функция dg_cl i все еще является не зависящей от протокола. Поскольку нам не важно, с каким типом структуры адреса сокета мы имеем дело, мы используем в вызове функции mal 1 ос только ее размер. Сравнение возвращаемых адресов 2-13 В вызове функции recvfrom мы сообщаем ядру, что нужно возвратить адрес от- правителя дейтаграммы. Сначала мы сравниваем длину, возвращаемую функци- ей recvfrom в аргументе типа «значение-результат», азатем сравниваем сами струк- туры адреса сокета при помощи функции memcmp. Новая версия нашего клиента работает замечательно, если сервер находится на узле с одним-единственным IP-адресом. Но эта программа может не сработать, если сервер имеет несколько сетевых интерфейсов (multihomed server). Запуска- ем эту программу на нашем узле bsdi, у которого имеется два интерфейса и два 1Р-адреса:
8.8. Проверка полученного ответа 263 solans % host bsdi bsdi kohala com has address 206 62 226 35 bsdi kohala com has address 206 62 226 66 solans X udpcli02 206.62.226.66 hello reply from 206 62 226 35 7 (ignored) goodbye reply from 206 62 226 35 7 (ignored) По рис. 1.7 видно, что мы задали IP-адрес из другой подсети. ПРИМЕЧАНИЕ----------------------------------------------------------------- Обычно это допустимо. Большинство реализаций IP принимают приходящую IP-дей- таграмму, предназначенную для любого из IP-адресов узла, независимо от интерфей- са, на который она приходит [105, с. 217-219]. Документ RFC 1122 [9] называет это моделью системы с гибкой привязкой (weak end system model) Если система должна реализовать то, что в этом документе называется моделью системы с жесткой привяз- кой (strong end system model), она принимает приходящую дейтаграмму только если дейтаграмма приходит на тот интерфейс, которому она адресована. IP-адрес, возвращаемый функцией recvfrom (IP-адрес отправителя дейтаграм- мы UDP), не является IP-адресом, на который мы посылали дейтаграмму. Когда сервер отправляет свой ответ, IP-адрес получателя — это адрес 206.62.226.33. Функция маршрутизации внутри ядра на узле bsdi выбирает адрес 206.62.226.35 в качестве исходящего интерфейса. Поскольку сервер не связал IP-адрес со сво- им сокетом (сервер связал со своим сокетом универсальный адрес, что мы можем проверить, запустив программу netstat на узле bsdi), ядро выбирает адрес отпра- вителя дейтаграммы IP. Этим адресом становится первичный IP-адрес исходя- щего интерфейса [105, с. 232-233]. Кроме того, это первичный IP-адрес интер- фейса, и поэтому если мы отправляем дейтаграмму не на первичный IP-адрес интерфейса (то есть на альтернативное имя, псевдоним), то наша проверка, пока- занная в листинге 8.5, также окажется неудачной. Одним из решений будет проверка клиентом доменного имени отвечающего узла вместо его IP-адреса. Для этого имя сервера ищется в DNS (см. главу 9) на основе IP-адреса, возвращаемого функцией recvfrom. Другое решение — сделать так, чтобы сервер UDP создал по одному сокету для каждого IP-адреса, сконфи- гурированного на узле, связал с помощью функции bind этот IP-адрес с сокетом, вызвал функцию select для каждого из всех этих сокетов (ожидая, когда какой- либо из них станет готов для чтения), а затем ответил с сокета, готового для чте- ния. Поскольку сокет, используемый для ответа, связан с IP-адресом, который являлся адресом получателя клиентского запроса (иначе дейтаграмма не была бы доставлена на сокет), мы можем быть уверены, что адреса отправителя ответа и получателя запроса совпадают. Мы показываем эти примеры в разделах 19.11 и 20.6. ПРИМЕЧАНИЕ----------------------------------------------------------------- В системе Solaris с несколькими сетевыми интерфейсами IP-адрес отправителя ответа сервера — это IP-адрес получателя клиентского запроса. Сценарий, описанный в дан- ном разделе, относится к реализациям, происходящим от Беркли, которые выбирают IP-адрес отправителя, основываясь на исходящем интерфейсе.
264 Глава 8. Основные сведения о сокетах UDP 8.9. Запуск клиента без запуска сервера Следующий сценарий, который мы рассмотрим, — это запуск клиента без запус- ка сервера. Если мы сделаем так и введем одну строку на стороне клиента, ничего не будет происходить. Клиент навсегда блокируется в своем вызове функции recvfrom, ожидая ответа сервера, который никогда не придет. Но в данном приме- ре это не имеет значения, поскольку сейчас мы стремимся глубже понять прото- колы и выяснить, что происходит с нашим сетевым приложением. Сначала мы запускаем программу tcpdump на узле bsdi, а затем запускаем кли- ент на том же узле, задав в качестве узла сервера sol an s. Затем мы вводим одну строку, но эта строка не отражается сервером. bsdi % udpcliOl 206.62.226.33 hello, world мы вводим эту строку но ничего не получаем в ответ В листинге 8.6 показан вывод программы tcpdump. Листинг 8.6. Вывод программы tcpdump, когда процесс сервера не запускается на узле сервера 01 0 0 arp who-has solans tell bsdi 02 0 002526 (0 0025) arp reply solans is-at 8 0 20 78 еЗ e3 03 0 002932 (0 0004) bsdi 1105 > solans 9877 udp 13 04 0 006932 (0 0040) solans > bsdi icmp solans udp port 9877 unreachable В первую очередь, мы замечаем, что запрос и ответ ARP получены до того как узел клиента смог отправить дейтаграмму UDP узлу сервера. (Мы оставили этот обмен в выводе программы, чтобы еще раз подчеркнуть, что до отправки IP-дей- таграммы всегда следуют отправка запроса и получение ответа по протоколу ARP.) В строке 3 мы видим, что дейтаграмма клиента отправлена, но узел сервера отвечает в строке 4 сообщением ICMP о недоступности порта. (Длина 13 вклю- чает 12 символов плюс символ новой строки.) Однако эта ошибка ICMP не воз- вращается клиентскому процессу по причинам, которые мы кратко перечислим чуть ниже. Вместо этого клиент навсегда блокируется в вызове функции recvfrom в листинге 8.4. Мы также видим, что в ICMPv6 имеется ошибка, связанная с не- доступностью порта, аналогичная ошибке ICMPv4 (см. табл. А.З и А.4), поэтому результаты, представленные здесь, аналогичны результатам для IPv6. Эта ошибка ICMP является асинхронной ошибкой. Ошибка была вызвана функцией sendto, но функция sendto завершилась нормально. Вспомните из раз- дела 2.9, что нормальное возвращение из операции вывода UDP означает только то, что дейтаграмма была добавлена к очереди вывода канального уровня. Ошиб- ка ICMP не возвращается, пока не пройдет определенное количество времени (4 миллисекунды для листинга 8.6), поэтому она и называется асинхронной. Основное правило состоит в том, что асинхронные ошибки не возвращаются для сокета UDP, если сокет не был присоединен. Мы показываем, как вызвать функцию connect для сокета UDP, в разделе 8.11. Не все понимают, почему было принято это решение, когда сокеты были впервые реализованы. (Соображения о реализациях обсуждаются на с. 748-749 [105].) Рассмотрим клиент UDP, после- довательно отправляющий три дейтаграммы трем различным серверам (то есть на три различных IP-адреса) через один сокет UDP. Клиент входит в цикл, вы-
8.10. Итоговый пример клиент-сервера UDP 265 зывающий функцию recvfrom для чтения ответов. Две дейтаграммы доставляют- ся корректно (то есть сервер был запущен на двух из трех узлов), но на третьем узле не был запущен сервер, и третий узел отвечает сообщением ICMP о недо- ступности порта. Это сообщение об ошибке ICMP содержит IP-заголовок и UDP- заголовок дейтаграммы, вызвавшей ошибку. (Сообщения об ошибках ICMPv-4 и IC Pv6 всегда содержат заголовок IP и весь заголовок UDP или часть заголовка TCP, чтобы дать возможность получателю сообщения определить, какой сокет вызвал ошибку. Это показано на рис. 25.5 и 25.6.) Клиент, отправивший три дей- таграммы, должен знать получателя дейтаграммы, вызвавшей ошибку, чтобы точ- но определить, какая из трех дейтаграмм вызвала ошибку. Но как ядро может сообщить эту информацию процессу? Единственное, что может возвратить функ- ция recvfrom, — это значение переменной еггпо. Но функция recvfrom не может вернуть в ошибке IP-адрес и номер порта получателя UDP-дейтаграммы. Следо- вательно, было принято решение, что эти асинхронные ошибки возвращаются процессу, только если процесс присоединил сокет UDP лишь к одному опреде- ленному собеседнику. ПРИМЕЧАНИЕ ----------------------------------------------------------- Linux возвращает большинство ошибок ICMP о недоступности порта даже для непри- соединенного сокета, поскольку параметр сокета SO DSBCOMPAT не включен. Воз- вращаются все ошибки о недоступности получателя, показанные в табл. А.З, отличные от ошибок с кодами 0,1,4, 5,11 и 12. Интерфейс XTI предоставляет способ вернуть процессу дополнительную информа- цию: это возможно благодаря функции t_rcvuderr (см. раздел 31.4). К сожалению, мно- гие реализации XTI не возвращают эту информацию. Мы вернемся к проблеме асинхронных ошибок с сокетами UDP в разделе 25 7 и пока- жем простой способ получения этих ошибок на неприсоединением сокете при помощи нашего собственного демона. 8.10. Итоговый пример клиент-сервера UDP На рис. 8.5 крупными черными точками показаны четыре значения, которые долж- ны быть заданы или выбраны, когда клиент отправляет дейтаграмму UDP. Клиент должен задать IP-адрес сервера и номер порта для вызова функции sendto. Обычно клиентский IP-адрес и номер порта автоматически выбираются ядром, хотя мы отмечали, что клиент может вызвать функцию bind. Мы также отмечали, что если эти два значения выбираются для клиента ядром, то динами- чески назначаемый порт клиента выбирается один раз — при первом вызове функции sendto — и более никогда не изменяется. Однако IP-адрес клиента может меняться для каждой дейтаграммы UDP, которую отправляет клиент, если пред- положить, что клиент не связывает с сокетом определенный IP-адрес при помо- щи функции bind. Причину объясняет рис. 8.5: если узел клиента имеет несколь- ко сетевых интерфейсов, клиент может переключаться между ними (на рис. 8.5 один адрес относится к канальному уровню, изображенному слева, другой — к изобоаженному споава). В худшем ваоианте этого спенаоия IP-алоес клиента.
266 Глава 8. Основные сведения о сокетах UDP socket () Рис. 8.5. Обобщение модели клиент-сервер UDP с точки зрения клиента выбираемый ядром на основе исходящего канального уровня, будет меняться для каждой дейтаграммы. Что произойдет, если клиент с помощью функции bi nd свяжет IP-адрес со своим сокетом, но ядро решит, что исходящая дейтаграмма должна быть отправлена с ка- кого-! о другого канального уровня? В этом случае дейтаграмма IP будет содер- жать IP-адрес отправителя, отличный от IP-адреса исходящего канального уров- ня (см. упражнение 8.6). На рис. 8.6 представлены те же четыре значения, но с точки зрения сервера. socket() recvfrom () bind() Рис. 8.6. Обобщение модели клиент-сервер UDP сточки зрения сервера У сервера TCP всегда есть простой доступ ко всем четырем фрагментам ин- формации для присоединенного сокета, и эти четыре значения остаются посто- янными в течение всего времени жизни соединения. Однако в случае соединения UDP IP-адрес получателя можно получить только с помощью установки пара- метра сокета IP_RECVDSTADDR для IPv4 или IPV6_PKTINF0 для IPv6 и последующего
8,11. Функция connect для UDP 267 вызова функции recvmsg вместо функции recvfrom. Поскольку протокол UDP не ориентирован на установление соединения, IP-адрес получателя может менять- ся для каждой дейтаграммы, отправляемой серверу. Сервер UDP может также получать дейтаграммы, предназначенные для одного из широковещательных ад- ресов узла или для адреса многоадресной передачи, что мы обсуждаем в главах 18 и 19. Мы покажем, как определить адрес получателя дейтаграммы UDP, в разде- ле 20.2, после того как опишем функцию recvmsg. Таблица 8.1. Информация, доступная серверу из приходящей дейтаграммы IP Из IP-дейтаграммы клиента ТСР-сервер UDP-сервер IP-адрес отправителя accept recvfrom Номер порта отправителя accept recvfrom IP-адрес получателя getsockname recvmsg Номер порта получателя getsockname getsockname 8.11. Функция connect для UDP В конце раздела 8.9 мы упомянули, что асинхронные ошибки не возвращаются на сокете UDP, если сокет не был присоединен. На самом деле для сокета UDP мы можем вызвать функцию connect (см. раздел 4.3). Но это не приведет ни к че- му похожему на соединение TCP: здесь не существует трехэтапного рукопожа- тия. Ядро просто записывает IP-адрес и номер порта собеседника, которые со- держатся в структуре адреса сокета, передаваемой функции connect, и немедленно возвращает управление вызывающему процессу. ПРИМЕЧАНИЕ ---------------------------------------------- Перегрузка функции connect этой новой возможностью для сокетов UDP может внес- ти путаницу. Если используется соглашение о том, что sockname — это адрес локально- го протокола, а peername — адрес удаленного протокола, то лучше бы эта функция на- зывалась setpeername. Аналогично, функции bind больше подошло бы название setsockname. Обладая этой возможностью, необходимо понимать разницу между двумя видами сокетов UDP. • Неприсоединенный (unconnected) сокет UDP, то есть сокет UDP, создаваемый по умолчанию. " Присоединенный (connected) сокет UDP, результат вызова функции connect для сокета UDP. Присоединенному сокету UDP свойственны три отличия от неприсоединен- ного сокета, который создается по умолчанию. 1. Мы больше не можем задавать IP-адрес получателя и порт для операции вы- вода. То есть мы используем вместо функции sendto функцию write или send. Все, что записывается в присоединенный сокет UDP, автоматически отправ- ляется на адрес протокола (например, IP-адрес и порт), заданный функцией connect. • -
268 Глава 8. Основные сведения о сокетах UDP ПРИМЕЧАНИЕ -------------------------------------------------------- Аналогично TCP мы можем вызвать функцию sendto для присоединенного сокета UDP, но не можем задать адрес получателя. Пятый аргумент функции sendto (указатель на структуру адреса сокета) должен быть пустым указателем, а шестой аргумент (размер структуры адреса сокета) должен быть нулевым. В стандарте Posix.lg определено, что когда пятый аргумент является пустым указателем, шестой аргумент игнорируется. 2. Вместо функции recvfrom мы используем функцию read или recv. Единствен- ные дейтаграммы, возвращаемые ядром для операции ввода через присоеди- ненный сокет UDP, — это дейтаграммы, приходящие с адреса, заданного в функции connect. Дейтаграммы, предназначенные для адреса локального про- токола присоединенного сокета UDP (например, IP-адрес и порт), но прихо- дящие с адреса протокола, отличного от того, к которому сокет был присоеди- нен с помощью функции connect, не передаются присоединенному сокету. Это ограничивает присоединенный сокет UDP, позволяя ему обмениваться дей- таграммами с одним и только одним собеседником. ПРИМЕЧАНИЕ--------------------------------------------------------- Точнее, обмен дейтаграммами происходит только с одним IP-адресом, а не с одним собеседником, поскольку это может быть IP-адрес многоадресной передачи, представ- ляющий, таким образом, группу собеседников. 3. Асинхронные ошибки возвращаются процессу только при операциях с присо- единенным сокетом UDP. В результате, как мы уже говорили, неприсоеди- ненный сокет UDP не получает никаких асинхронных ошибок. В табл. 8.2 сводятся воедино свойства, перечисленные в первом пункте, при- менительно к 4.4 BSD. Таблица 8.2. Сокеты TCP и UDP: может ли быть задан адрес протокола получателя Тип сокета write или send sendto, без указа- ния получателя sendto, с указани- ем получателя Сокет TCP Да Да EISCONN Сокет UDP, присоединенный Да Да EISCONN Сокет L7DP, неприсоединенный EDESTADDRREQ EDESTADDRREQ Да ПРИМЕЧАНИЕ------------------------------------------------------------- Posix. 1g определяет, что операция вывода, не задающая адрес получателя на пеприсо- единенном сокете UDP, должна возвращать ошибку ENOTCONN, а не EDESTADDR- REQ. Solans 2.5 допускает функцию sendto, которая задает адрес получателя для присоеди- ненного сокета UDP. Posix. 1g определяет, что в такой ситуации должна возвращаться ошибка EISCONN. На рис. 8.7 обобщается информация о присоединенном сокете UDP. Приложение вызывает функцию connect, задавая IP-адрес и номер порта собе- седника. Затем оно использует функции read и write для обмена данными с собе- седником.
8.11. Функция connect для UDP 269 n/или номера порта Рис. 8.7. Присоединенный сокет UDP Дейтаграммы, приходящие с любого другого IP-адреса или порта (который мы обозначаем как «???» на рис. 8.7), не передаются на присоединенный сокет, поскольку либо IP-адрес, либо UDP-порт отправителя не совпадает с адресом протокола, с которым сокет соединяется с помощью функции connect. Эти дей- таграммы могут быть доставлены на какой-то другой сокет UDP на узле. Если нет другого совпадающего сокета для приходящей дейтаграммы, UDP проигно- рирует ее и сгенерирует сообщение об ошибке ICMP о недоступности порта. Обобщая вышесказанное, мы можем утверждать, что клиент или сервер UDP может вызвать функцию connect, только если этот процесс использует сокет UDP для связи лишь с одним собеседником. Обычно именно клиент UDP вызывает функцию connect, но существуют приложения, в которых сервер UDP связывает- ся с одним клиентом на длительное время (например, TFTP), и в этом случае и клиент, и сервер вызывают функцию connect. DNS предоставляет другой пример, как показано на рис. 8.8. Вызов функции connect проходит успешно (клиент сконфигурирован для использования одного сервера) Вызов функции connect проходит успешно (клиент сконфигурирован для использования одного сервера) Невозможно вызвать функцию connect Невозможно вызвать функцию connect Рис. 8.8. Пример клиентов и серверов DNS и функции connect Клиент DNS может быть сконфигурирован для использования одного или более серверов, обычно с помощью перечисления IP-адресов серверов в файле /etc/resol v.conf. Если в этом файле указан только один сервер (на рисунке этот клиент изображен в крайнем слева прямоугольнике), клиент может вызвать функ- цию connect, но если перечислено множество серверов (второй справа прямоуголь- ник на рисунке), клиент не может вызвать функцию connect. Обычно сервер DNS обрабатывает также любые клиентские запросы, следовательно, серверы не мо- гут вызывать функцию connect.
270 Глава 8. Основные сведения о сокетах UDP Многократный вызов функции connect для сокета UDP Процесс с присоединенным сокетом UDP может снова вызвать функцию connect для этого сокета, чтобы задать новый IP-адрес и порт; отсоединить сокет. Первый случай, задание нового собеседника для присоединенного сокета UDP, отличается от использования функции connect с сокетом TCP: для сокета TCP функция connect может быть вызвана только один раз. Чтобы отсоединить сокет UDP, мы вызываем функцию connect, но прсваиваем элементу семейства структуры адреса сокета (si n_fami 1у для IPv4 или sin6_fann 1у для IPv6) значение AFJJNSPEC. Это может привести к ошибке EAFNOSUPPORT [105, с. 736], но это нормально. Именно процесс вызова функции connect на уже присо- единенном сокете UDP позволяет отсоединить сокет [105, с. 787-788]. ПРИМЕЧАНИЕ ------------------------------------------------------ В руководстве BSD по поводу функции connect традиционно говорилось. «Сокеты , дейтаграмм могут разрывать связь, соединяясь с недействительными адресами, таки- ми как пустые адреса». К сожалению, ни в одном руководстве не сказано, что представ- ляет собой «пустой адрес», и не упоминается, что в результате происходит ошибка (что нормально). Стандарт Posix.lg явно указывает, что семейство адресов должно быть установлено в AF_UNSPEC, но затем сообщает, что этот вызов функции connect мо- жет возвратить, а может и не возвратить ошибку EAFNOSUPPORT. Производительность Когда приложение вызывает функцию sendto на неприсоединенном сокете UDP, ядра реализаций, происходящих от Беркли, временно соединяются с сокетом, отправляют дейтаграмму и затем отсоединяются от сокета [105, с. 762-763]. Таким образом, вызов функции sendto для последовательной отправки двух дейтаграмм на неприсоединенном сокете включает следующие шесть шагов, выполняемых ядром: Я присоединение сокета; М вывод первой дейтаграммы; отсоединение сокета; * 8 присоединение сокета; ® вывод второй дейтаграммы; ?" отсоединение сокета. ПРИМЕЧАНИЕ----------------------------------------------------- Другой момент, который нужно учитывать, — количество поисков в таблице маршру- тизации. Первое временное соединение производит поиск в таблице маршрутизации IP-адреса получателя и сохраняет (кэширует) эту информацию. Второе временное соединение отмечает, что адрес получателя совпадает с кэшированным адресом из табли- цы маршрутизации (мы считаем, что обеим функциям sendto задан один и тот же получа- тель), и ему не нужно снова проводить поиск в таблице маршрутизации [105, с. 737-738].
8.12. Функция dg cli (продолжение) 271 Когда приложение знает, что оно будет отправлять множество дейтаграмм одному и тому же собеседнику, эффективнее будет использовать сокет явно. Вы- зов функции connect, за которым следуют два вызова функции write, теперь будет включать следующие шаги, выполняемые ядром: ' присоединение сокета: ’ > вывод первой дейтаграммы; < вывод второй дейтаграммы. В этом случае ядро копирует структуру адреса сокета, содержащую 1Р-адрес получателя и порт, только один раз, а при двойном вызове функции sendto копи- рование выполняется дважды. В [76] отмечается, что на временное присоедине- ние отсоединенного сокета UDP приходится примерно треть стоимости каждой передачи UDP. 8.12. Функция dg.cli (продолжение) Вернемся к функции dg_cli, показанной в листинге 8.4, и перепишем ее, с тем чтобы она вызывала функцию connect. В листинге 8.7 показана новая функция. Листинг 8.7. Функция dg_ch, вызывающая функцию connect //udpcliserv/dgcliconnect с 1 #include "unp h” 2 void t 3 dg_cli(FILE *fp, int sockfd const SA *pservaddr. socklen_t servlen) 4 { 5 int n. 6 char sendlineEMAXLINE]. recvline[MAXLINE + 1]; 7 Connect(sockfd (SA *) pservaddr, servlen). 8 while (Fgets(sendline MAXLINE, fp) '= NULL) { 9 Write(sockfd sendline. strlen(sendline)) 10 n = Read(sockfd. recvline MAXLINE) 11 recvline[n] =0. /* завершающий нуль */ 12 Fputs(recvline. stdout) 13 } 14 } Изменения по сравнению с предыдущей версией — это добавление вызова функции connect и замена вызовов функций sendto и recvfrom вызовами функций write и read. Функция dg_cli остается не зависящей от протокола, поскольку она не вникает в структуру адреса сокета, передаваемую функции connect. Наша функ- ция main клиента, показанная в листинге 8.3, остается той же. Если мы запустим программу на узле bsdi, задав IP-адрес узла solans (кото- рый не запускает наш сервер на порте 9877), мы получим следующий вывод: bsdi % udpcli'04 206.62.226.33 hello, world read error Connection refused
272 Глава 8. Основные сведения о сокетах UDP Первое, что мы замечаем — мы не получаем ошибку, когда запускаем процесс клиента. Ошибка происходит только после того, как мы отправляем серверу пер- вую дейтаграмму. Именно отправка этой дейтаграммы вызывает ошибку ICMP от узла сервера. Но когда клиент TCP вызывает функцию connect, задавая узел сервера, на котором не запущен процесс сервера, функция connect возвращает ошибку, поскольку вызов функции connect вызывает отправку первого пакета трехэтапного рукопожатия TCP, и именно этот пакет вызывает получение сег- мента RST от TCP сервера (см. раздел 4.3). В листинге 8.8 показан вывод программы tcpdump. Листинг 8.8, Вывод программы tcpdump при запуске функции dg_cli bsdi % tcpdump 01 0 0 bsdi 1318 > solans 9877 udp 13 02 0 000628 ( 0 0006) solans > bsdi icmp solans udp port 9877 unreachable В табл. А.З мы также видим, что возникшую ошибку ICMP ядро сопоставляет ошибке ECONNREFUSED, которая соответствует выводу строки сообщения Connection refused (В соединении отказано) функцией err_sys. ПРИМЕЧАНИЕ -------------------------------------------------------------- К сожалению, не все ядра возвращают сообщения ICMP присоединенному сокету UDP, как мы показали в этом разделе. Обычно ядра реализаций, происходящих от Беркли, возвращают эту ошибку, а ядра System V — не возвращают Например, если мы запус- тим тот же клиент на узле Solans 2 4 и с помощью функции connect соединимся с уз- лом, на котором не запушен наш сервер, то с помошью программы tcpdump мы сможем убедиться, что ошибка ICMP о недоступности порта возвращается узлом сервера, но вызванная клиентом функция read никогда не завершается Эта ситуация была исправ- лена в Solans 2.5. UnixWare не возвращает ошибку, в то время как AIX, Digital Unix, HP-UX и Linux возвращают. 8.13. Отсутствие управления потоком в UDP Теперь мы проверим, как влияет на работу UDP отсутствие какого-либо управ- ления потоком. Сначала мы изменим нашу функцию dg cl 1 так, чтобы она от- правляла фиксированное число дейтаграмм. Она больше не читает из стандарт- ного потока ввода. В листинге 8.9 показана новая версия функции. Эта функция отправляет серверу 2000 1400-байтовых дейтаграмм UDP. Листинг 8.9. Функция dg_cli, отсылающая фиксированное число дейтаграмм серверу //udpcl1serv/dgcli1oopl с 1 #include "unp h" 2 #define NDG 2000 3 #define DGLEN 1400 /* количество дейтаграмм для отправки */ /* длина каждой дейтаграммы */ 4 void 5 dg_cli(FILE *fp int sockfd const SA *pservaddr socklen_t servlen) 6 {
8.13. Отсутствие управления потоком в UDP 273 1 1 nt 1. 8 char sendline[DGLEN]. 9 for (i = 0, i < NDG i++) { 10 Sendtotsockfd sendline DGLEN. 0. pservaddr. servlen): И } 12 } Затем мы изменяем сервер так, чтобы он получал дейтаграммы и считал число полученных дейтаграмм. Сервер больше не отражает дейтаграммы обратно кли- енту. В листинге 8.10 показана новая функция dg_echo. Когда мы завершаем про- цесс сервера с помощью нашего ключа прерывания (SIGINT), он выводит число полученных дейтаграмм и завершается. Листинг 8.10. Функция dg_echo, считающая полученные дейтаграммы //udpcliserv/dgecholoopl с 1 include "unp h" 2 static void recvfrom_int(int) 3 static int count 4 void 5 dg_echo(int sockfd SA *pcliaddr, socklen_t clilen) 6 { 7 socklen_t len. 8 char mesgCMAXLINE], 9 SignaKSIGINT recvfrom_int): 10 for ( ) ( 11 len = clilen, 12 Recvfrom(sockfd mesg MAXLINE. 0. pcliaddr. &len); 13 count++. И } 15 } 16 static void 17 recvfrom_int(int signo) 18 { 19 printfCXenreceived 2d datagrams\en“. count): 20 exit(0), 21 } Теперь мы запускаем сервер на узле bsdi, в медленной системе 80386. Клиент мы запускаем в значительно более быстрой системе Sparc Station 4. Кроме того, мы запускаем программу netstat -s на узле сервера и до, и после запуска клиента и сервера, поскольку выводимая статистика покажет, сколько дейтаграмм мы потеряли. В листинге 8.11 показан вывод сервера. Листинг 8.11. Вывод на узле сервера bsdi X netstat -s | tail udp 80300 datagrams received 0 with incomplete header 0 with bad data length field 0 with bad checksum
274 Глава 8. Основные сведения о сокетах UDP 12 dropped due to no socket 77725 broadcast/multicast datagrams dropped due to no socket 1970 dropped due to full socket buffers 593 delivered 70592 datagrams output bsdi % udpserv06 запускаем наш сервер клиент посылает дейтаграммы после окончания работы клиента вводим наш символ прерывания bsdi % netstat -s | tail udp 82294 datagrams received 0 with incomplete header 0 with bad data length field 0 with bad checksum 12 dropped due to no socket 77725 broadcast/multicast datagrams dropped due to no socket 3882 dropped due to full socket buffers 675 delivered 70592 datagrams output Клиент отправил 2000 дейтаграмм, но приложение-сервер получило только 82 из них, что означает уровень потерь 96 %. Ни сервер, ни клиент не получают сообщения о том, что эти дейтаграммы потеряны. Как мы и говорили, UDP не имеет возможности управления потоком — он ненадежен. Как мы показали, для отправителя UDP не составляет труда переполнить буфер получателя. Если мы посмотрим на вывод программы netstat, то увидим, что общее число дейтаграмм, полученных узлом сервера (не приложением-сервером), равно 1994 (82 294-80 300). Шесть дейтаграмм не были получены интерфейсом либо пото- му, что буферы интерфейса были заполнены, либо потому, что эти дейтаграммы были проигнорированы отправляющим узлом. Счетчик dropped due to full socket buffers (отброшено из-за заполненных буферов сокета) показывает, сколько дей- таграмм было получено UDP и проигнорировано из-за того, что приемный буфер принимающего сокета был полон [105, с. 775]. Это значение равно 1912 (3882- 1970), что при добавлении к выводу счетчика дейтаграмм, полученных приложе- нием (82), дает 1994 дейтаграммы, полученные узлом. К сожалению, счетчик дейтаграмм, отброшенных из-за заполненного буфера, в программе netstat рас- пространяется на всю систему. Не существует способа определить, на какие при- ложения (например, какие порты UDP) это влияет. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Обратите внимание, что 97% всех полученных дейтаграмм UDP (77 725 из 80 300) на этом конкретном узле являются дейтаграммами широковещательной или многоадрес- ной передачи, которые затем игнорируются, поскольку нет приложения с сокетом, свя- , занным с портом получателя. Мы вернемся к этому феномену, когда будем говорить о широковещательной передаче в главе 18. Число дейтаграмм, полученных сервером в этом примере, недетерминирова- но. Оно зависит от многих факторов, таких как нагрузка сети, загруженность узла клиента и узла сервера. Если мы запустим этот пример еще пять раз, счетчик по- лученных дейтаграмм может принять значения 37,108, 30,108 и 114. Если мы запустим тот же клиент и тот же сервер, но на этот раз клиент на медленной системе 80386, а сервер на быстрой системе Sparc Station 4, никакие лейтагпаммы не потепяются.
8.13. Отсутствие управления потоком в UDP 275 solans % udpserv06 *? после окончания работы клиента вводим наш символ прерывания received 2000 datagrams Если мы запустим программу netstat -s под Solaris, формат вывода будет отличаться от классического вывода для реализаций Беркли, показанного в ли- стинге 8.11. Формат Solaris воспроизводит счетчики SNMP (Simple Network Management Protocol — простой протокола управления сетью, см. главу 25 [94]). Счетчик udpInDatagrams программы netstat указывает число дейтаграмм UDP, доставленных пользовательским процессам, — 139 до начала работы нашего кли- ента и 2139 после, то есть доставлены все 2000 дейтаграмм. Счетчик udpI nOverf 1 ows, который не является официальным счетчиком SNMP, считает число получен- ных дейтаграмм UDP, которые были проигнорированы из-за того, что в прием- ном буфере сокета не было места. Его значение нулевое и до, и после, как мы и предполагали. Приемный буфер сокета UDP Число дейтаграмм UDP, установленных в очередь UDP, для данного сокета ограничено размером его приемного буфера. Мы можем изменить его с помощью параметра сокета SO_RCVBUF, как мы показали в разделе 7.5. В BSD/OS по умолча- нию размер приемного буфера сокета UDP равен 41 600 байт, что допускает воз- можность хранения только 29 из наших 1400-байтовых дейтаграмм. Если мы уве- личим размер приемного буфера сокета, мы можем рассчитывать, что сервер получит дополнительные дейтаграммы. В листинге 8.12 представлена изменен- ная функция dg_echo из листинга 8.10, которая увеличивает размер приемного буфера сокета до 240 Кбайт. Если мы пять раз запустим этот сервер в системе 80386, а клиент — в системе Sparc Station 4, то счетчик полученных дейтаграмм будет иметь значение 115,168,179,145 и 133. Поскольку это лишь немногим луч- ше, чем в более раннем примере с размером буфера, заданным по умолчанию, ясно, что мы пока не получили решения проблемы. Листинг 8.12. Функция dg_echo, увеличивающая размер приемного буфера сокета //udpcliserv/dgecholoop2 с 1 include "unp h” 2 static void recvfrom_int(int); 3 static int count. 4 void 5 dg_echo(int sockfd SA *pcliaddr. socklen_t clilen) 6 { 7 int n. 8 socklen_t len 9 char mesg[MAXLINE], 1 10 Signal(SIGINT recvfrom_int). 11 n = 240 * 1024 12 Setsockopt(sockfd. SOL_SOCKET. SOJICVBUF, 8n. sizeof(n));
276 Глава 8. Основные сведения о сокетах UDP Листинг8.12 (продолжение) 15 Recvfrom(sockfd mesg MAXLINE, 0. pcliaddr. &len), 16 count++. 17 } 18 } 19 static void 20 recvfrom_int(int signo) 21 { 22 printf("\enreceived Jtd datagrams\en” count) 23 exit(0) 24 } ПРИМЕЧАНИЕ-------------------------------------------------------------------------- Почему мы устанавливаем размер буфера приема сокета равным 240x1024 байт в лис- тинге 8.122 Максимальный размер приемного буфера сокета в BSD/OS 2 1 по умолча- нию равен 262 144 байт (256x1024), но из-за способа размещения буфера в памяти (опи- санного в главе 2 [105]) он в действительности ограничен до 246 723 байт Многие более ( ранние системы, основанные на 4.3BSD, ограничивали размер буфера приема сокета , примерно до 52 000 байт. 8.14. Определение исходящего интерфейса для UDP С помощью присоединенного сокета UDP можно также задавать исходящий ин- терфейс, который будет использован для отправки дейтаграмм к определенному получателю. Это объясняется побочным эффектом функции connect, применен- ной к сокету UDP: ядро выбирает локальный IP-адрес (считая, что процесс еще не вызвал функцию bind для явного его задания). Локальный адрес выбирается в процессе поиска адреса получателя в таблице маршрутизации, причем берется основной IP-адрес интерфейса, с которого, согласно таблице, будут отправлять- ся дейтаграммы. В листинге 8.13 показана простая программа UDP, которая с помощью функ- ции connect соединяется с заданным IP-адресом и затем вызывает функцию get- sockname, выводя локальный IP-адрес и порт. Листинг 8.13. Программа UDP, использующая функцию connect для определения исходящего интерфейса //udpcl1serv/udpcli09 с 1 #include "unp h" 2 mt 3 mainlint argc char **argv) 4 { 5 int sockfd. 6 socklen_t len. 7 struct sockaddr in cliaddr. servaddr. 8 if (argc i- 2) 9 err_quit(“usage udpcli <IPaddress>")
8.15 Эхо-сервер TCP и UDP, использующий функцию select 277 10 sockfd = ScckeUAFJNET SOCK_DGRAM 0). 11 bzero(&servaddr sizeof(servaddr)) 12 servaddr sin_family = AF_INET 13 servaddr sin_port = htons(SERV_P0RT) 14 Inet_pton(AF_INET argv[l] &servaddr sin_addr) 15 Connect(sockfd. (SA *) &servaddr sizeof(servaddr)). 16 len = sizeof(cliaddr) 17 Getsockname(sockfd (SA *) &cliaddr &len) 18 pnntfC'local address £s\en' Sock_ntop((SA *) &cliaddr. len)): 19 exit(0) 20 } Если мы запустим программу на узле bsdi с несколькими сетевыми интерфей- сами, мы получим следующий вывод: bsdi X udpcli09 206.62.226.42 local address 206 62 226 35 1331 bsdi X udpcli09 206.62.226.65 local address 206 62 226 66 1332 bsdi % udpcli09 127.0.0.1 local address 127 0 0 1 1335 По рис. 1.7 мы видим, что когда мы запускаем программу первые два раза, ар- гументом командной строки является IP-адрес в разных сетях Ethernet. Ядро присваивает локальный IP-адрес первичному адресу интерфейса в соответству- ющей сети Ethernet, то есть, поскольку узел .42 находится в верхней (на рисун- ке) сети Ethernet, то адрес исходящего интерфейса равен .35. Узел .65 находится в нижней сети Ethernet, таким образом, исходящий интерфейс имеет адрес 66 При вызове функции connect на сокете UDP ничего не отправляется на этот узел — это полностью локальная операция, которая сохраняет IP-адрес и порт собесед- ника. Мы также видим, что вызов функции connect на неприсоединенном сокете UDP также присваивает сокету динамически назначаемый порт ПРИМЕЧАНИЕ -------------------------------------------------------------------------- К сожалению, эта технология действует не во всех реализациях, что особенно кас ается ядер, происходящих от SVR4. Например, это не работает в HP-UX, Solans 2 5 и Unix- Ware, но работает в AIX, Digital Unix, Linux и Solans 2.6. 8.15. Эхо-сервер TCP и UDP, использующий функцию select i Теперь мы объединим наш параллельный эхо-сервер TCP из главы 5 и наш по- следовательный эхо-сервер UDP из данной главы в один сервер, использующий функцию select для мультиплексирования сокетов TCP и UDP. В листинге 8 14 представлена первая часть этого сервера
278 Глава 8. Основные сведения о сокетах UDP Листинг 8.14. Первая часть эхо-сервера, обрабатывающего сокеты TCP и UDP при помощи функции select //udpcliserv/udpservselectOl с 1 include "unp h" 2 int 3 maindnt argc, char **argv) 4 { 5 int listenfd, connfd. udpfd. nready. maxfdpl; 6 char mesgEMAXLINE], 7 pid_t chi 1 dpid. 8 fd_set rset. 9 ssize_t n. 10 socklen_t len. 11 const int on = 1; 12 struct sockaddr_in cliaddr, servaddr. 13 void sig_chld(int). 14 /* создание прослушиваемого сокета TCP */ 15 listenfd = Socket(AF_INET. SOCK_STREAM, 0); 16 bzero(&servaddr, sizeof(servaddr)). 17 servaddr sin_family = AF_INET, 18 servaddr.sin_addr s_addr = htonl(INADDR_ANY). 19 servaddr sin_port - htons(SERV_PORT). 20 Setsockopt!listenfd. SOL_SOCKET. SO_REUSEADDR. &on. sizeof(on)). 21 Bind(listenfd, (SA *) &servaddr. sizeof(servaddr)). 22 Listen!listenfd. LISTENO). 23 /* создание сокета UDP *7 24 udpfd = Socket(AF_INET, SOCK_DGRAM. 0); 25 bzero(&servaddr, sizeof(servaddr)); 26 servaddr sin_family = AF_INET, 27 servaddr sin_addr s_addr = htonl(INADDR_ANY). 28 servaddr sin_port = htons(SERV_PORT). 29 Bind(udpfd, (SA *) &servaddr sizeof(servaddr)). Создание прослушиваемого сокета TCP 1-22 Создается прослушиваемый сокет TCP, который связывается с заранее извест- ным портом сервера. Мы устанавливаем параметр сокета SO_REUSEADDR в случае если на этом порте существуют соединения. Создание сокета UDP 1-29 Также создается сокет UDP и связывается с тем же портом. Даже если один и тот же порт используется для сокетов TCP и UDP, нет необходимости устанав- ливать параметр сокета SO_REUSEADDR перед этим вызовом функции bind, посколь- ку порты TCP не зависят от портов UDP. В листинге 8.15 показана вторая часть нашего сервера.
8.15. Эхо-сервер TCP и UDP, использующий функцию select 279 Листинг 8.15. Вторая половина эхо-сервера, обрабатывающего TCP и UDP при помощи функции select udpclт serv/udpservselectOl.с 30 Signal (SIGCHLD, sig_chld), /* требуется вызвать waitpidO */ 31 FD_ZERO(&rset), 32 maxfdpl = maxdistenfd. udpfd) + 1. 33 for (..) { 34 FD_SET(listenfd, &rset). 35 FD_SET(udpfd &rset), 36 if ( (nready = seiect(maxfdpl. &rset. NULL. NULL. NULL>) 37 if (errno == EINTR) 38 continue. /* назад в for() */ 39 else 40 err_sys("select error"). 41 } 42 if (FD_ISSET(listenfd. &rset)) { 43 len = sizeof(cliaddr). 44 connfd = Acceptdistenfd. (SA *) &cliaddr. &len): 45 if ( (childpid = ForkO) == 0) ( /* дочерний процесс */ 46 Closet!istenfd). /* закрывается прослушиваемый сокет */ 47 str_echo(connfd): /* обработка запроса */ 48 exit(0). 49 } 50 Close(connfd). /* родитель закрывает присоединенный сокет */ 51 } 52 if (FD_ISSET(udpfd. &rset)) { 53 len = sizeof(cliaddr). 54 n = Recvfrom(udpfd mesg. MAXLINE. 0. (SA *) &cliaddr. &len), 55 Sendto(udpfd mesg. n. 0. (SA *) &cliaddr. len) 56 } 57 } 58 } Установка обработчика сигнала SIGCHLD 30 Для сигнала SIGCHLD устанавливается обработчик, поскольку' СОейИНеНЙЯ TCP будут обрабатываться дочерним процессом. Этот обработчик сигнала мы показа- ли на рис. 5.8. Подготовка к вызову функции select 31-32 Мы инициализируем набор дескрипторов для функции select и вычисляем максимальный из двух дескрипторов, который мы будем ждать. Вызов функции select 34-41 Мы вызываем функцию select, ожидая только готовности к чтению прослуши- ваемого сокета TCP или сокета UDP. Поскольку наш обработчик сигнала si g chld может прервать вызов функции select, мы обрабатываем ошибку EINTR. Обработка нового клиентского соединения 42-51 С помощью функции accept мы принимаем новое клиентское соединение, а когда прослушиваемый сокет TCP готов для чтения, с помощью функции fork порож-
280 Глава 8. Основные сведения о сокетах UDP даем дочерний процесс и вызываем нашу функцию str_echo в дочернем процессе. Это та же последовательность шагов, которую мы выполняли в главе 5. Обработка приходящей дейтаграммы >2-57 Если сокет UDP готов для чтения, дейтаграмма пришла. Мы читаем ее с по- мощью функции recvfrom и отправляем обратно клиенту с помощью функции sendto. 8.16. Резюме Преобразовать наши эхо-клиент и эхо-сервер так, чтобы использовать UDP вместо TCP, оказалось несложно. Но при этом мы лишились множества возможностей, предоставляемых протоколом TCP: определение потерянных пакетов и повтор- ная передача, проверка, приходят ли пакеты от корректного собеседника, и т. д. Мы возвратимся к этой теме в разделе 20.5 и увидим, как можно улучшить на- дежность приложения UDP. Сокеты UDP могут генерировать асинхронные ошибки, то есть ошибки, о ко- торых сообщается спустя некоторое время, после того как пакет был отправлен. Сокеты TCP всегда сообщают приложению об этих ошибках, но в случае UDP для получения этих ошибок сокет должен быть присоединенным. В UDP отсутствует возможность управления потоком, и это очень легко про- демонстрировать. Обычно это не создает проблем, поскольку многие приложе- ния UDP построены с использованием модели «запрос-ответ» и не предназначе- ны для передачи большого количества данных. Есть еще ряд моментов, которые нужно учитывать при написании приложе- ний UDP, но мы рассмотрим их в главе 20 после описания функций интерфей- сов, широковещательной и многоадресной передачи. Упражнения 1. Допустим, у нас имеется два приложения: одно использует TCP, а другое — UDP. В приемном буфере сокета TCP находится 4096 байт данных, а в прием- ном буфере для сокета UDP находятся две дейтаграммы по 2048 байт. Прило- жение TCP вызывает функцию read с третьим аргументом 4096, а приложе- ние UDP вызывает функцию recvfrom с третьим аргументом 4096. Есть ли между этими вызовами какая-нибудь разница? 2. Что произойдет в листинге 8.2, если мы заменим последний аргумент функ- ции sendto (который мы обозначили 1 еп) аргументом с 1 т 1 еп? 3. Откомпилируйте и запустите сервер UDP из листингов 8.1 и 8.4, а затем — клиент из листингов 8.3 и 8.4 Убедитесь в том, что клиент и сервер работают вместе. 4. Запустите программу pi ng в одном окне, задав параметр -т 60 (отправка одно- го пакета каждые 60 секунд; некоторые системы используют ключ I вместо i), параметр -v (вывод всех полученных сообщений об ошибках ICMP) и задав адрес закольцовки на себя (обычно 127.0.0.1). Мы будем использовать эту про-
Упражнения 281 грамму, чтобы увидеть ошибку ICMP недоступности порта, возвращаемую узлом сервера. Затем запустите наш клиент из предыдущего упражнения в дру- гом окне, задав IP-адрес некоторого узла, на котором не запущен сервер. Что происходит? 5. Рассматривая рис. 8.3, мы сказали, что каждый присоединенный сокет TCP имеет свой собственный буфер приема. А если речь идет о прослушиваемом сокете, как вы думаете, есть ли у него свой собственный буфер приема? 6. Используйте программу sock (см. раздел В.З) и такое средство, как, например, tcpdump (см. раздел В.5), чтобы проверить утверждение раздела 8.10: если кли- ент с помощью функции bi nd связывает IP-адрес со своим сокетом, но отправ- ляет дейтаграмму, исходящую от другого интерфейса, то результирующая дей- таграмма содержит IP-адрес, который был связан с сокетом, даже если он не соответствует исходящему интерфейсу. 7. Откомпилируйте программы из раздела 8.13 и запустите клиент и сервер на различных узлах. Помещайте printf в клиент каждый раз, когда дейтаграмма записывается в сокет. Изменяет ли это процент полученных пакетов? Поче- му? Помещайте printf в сервер каждый раз, когда дейтаграмма читается из сокета. Изменяет ли это процент полученных пакетов? Почему? 8. Какова наибольшая длина, которую мы можем передать функции sendto для сокета UDP/IPv4, то есть каково наибольшее количество данных, которые мо- гут поместиться в дейтаграмму UDP/IPv4? Что изменяется в случае UDP/IPv6? Измените листинг 8.4, с тем чтобы отправить одну дейтаграмму UDP макси- мального размера, считать ее обратно и вывести число байтов, возвращаемых функцией recvfrom.
ГЛАВА 9 Элементарные преобразования имен и адресов 9.1. Введение Во всех примерах этой книги мы использовали численные адреса узлов (напри- мер, 206 6.226 33) и численные номера портов для идентификации серверов (на- пример, порт 13 для стандартного сервера времени и даты и порт 9877 для наше- го эхо-сервера). Однако по ряду соображений предпочтительнее использовать имена вместо чисел: во-первых, имена проще запоминаются, во-вторых, если чис- ленный адрес поменяется, имя можно сохранить, и в-третьих, с переходом на IPv6 численные адреса станут значительно длиннее, что увеличит вероятность ошибки при вводе адреса вручную. В этой главе описываются функции, выполняющие преобразование имен и адресов, gethostbyname и gethostbyaddr для преобразова- ния имен узлов и IP-адресов, а также getservbyname и getservbyport для преобразо- вания имен служб и номеров портов. Функции имен узлов недавно были усовершенствованы для работы с IPv6 в дополнение к IPv4, и мы также опишем эти изменения Это будет началом на- шего перехода к независимости от протокола, который мы продолжим в главе 11. В ней применяются функции, обсуждаемые в этой главе, а также разрабатывают- ся различные функции, которые помогут сделать наши приложения не завися- щими от протокола. 9.2. Система доменных имен Система доменных имен (Domain Name System, DNS) используется прежде всего для сопоставления имен узлов и IP-адресов. Имя узла может быть либо простым (simple пате), таким как solans или bsdi, либо полным доменным именем (fully qualified domain name, FQDN) — например, sol an s kohal a com. ПРИМЕЧАНИЕ ----------------------------------------- В техническом отношении FQDN может также называться абсолютным именем и долж- но оканчиваться точкой, но пользователи часто игнорируют точку в конце. В этом разделе мы рассмотрим только основы DNS, необходимые нам для се- тлолгл Tmr.rn-Unranrwiuuo Читатели ТЛНТРПРГГ|ПГ||МРГЯ боПРР ПОПГюбнЫМ ИЗЛО"
9 2. Система доменных имен 283 жением вопроса, могут обратиться к главе 14 [94] и к [ 1]. Дополнения, требуемые для IPv6, изложены в RFC 1886 [99]. Записи ресурсов Записи в DNS называются записями ресурсов (resource recodrs, RR). Нас интере- сует только несколько типов RR. А. Запись типа А сопоставляет имя узла 32-битовому адресу IPv4. Вот, напри- мер, четыре записи DNS для узла sol an s в домене koha 1 а сот, первая из кото- рых — это запись типа А: solans IN А 206 62 226 33 IN АААА 5flb dfOO сеЗе е200 0020 0800 2078 еЗеЗ IN MX 5 solans kohala com IN MX 10 mail host kohala com * АААА. Запись типа АААА, называемая «четыре А» (quad А), сопоставляет имя узла 128-разрядному адресу IPv6. Используется термин «четыре А», потому что 128-разрядный адрес в четыре раза больше 32-разрядного адреса. PTR. Запись PTR (pointer records — запись указателя ) сопоставляет IP-адре- са именам узлов. Для адреса IPv4 зарезервированы 4 байта 32-разрядного адре- са. Каждый байт преобразуется в десятичное значение ASCII (0-255), а затем добавляется in-addr эрга. Получившаяся строка используется в запросе PTR Для адреса IPv6 зарезервированы 32 4-разрядных полубайта 128-разрядного адреса. Каждый полубайт преобразуется в соответствующее шестнадцатерич- ное значение ASCII (0-9a-f) и добавляется к iрб int. Например, две записи PTR для нашего узла sol an s будут выглядеть так: 33 226 62 206 in-addr арга 3 е 3 е 8 7 0 2 0 0 8 0 0 2 0 0 0 0 2 ее Зес 0 0 f d b 1 f 5 ip6 int & MX. Запись типа MX (Mail Exchange Record) определяет, что узел выступает в роли «маршрутизирующего почтового сервера» для заданного узла. В при- веденном выше примере для узла sol an s предоставлено две записи типа MX. Первая имеет предпочтительное значение 5, вторая — 10. Когда существует множество записей типа MX, они используются в порядке предпочтения, на- чиная с наименьшего значения. ПРИМЕЧАНИЕ ------------------------------------------------------------- Мы не используем в примерах книги записей типа MX, но упоминаем о них, потому что они широко используются в реальной жизни ’< CNAME. Аббревиатура CNAME означает «каноническое имя» (canonical name). Обычно такие записи используются для присвоения имен общим службам, таким как ftp и www. При использовании имен служб вместо действительного имени узла перемещение службы на другой узел становится прозрачным (то есть незаметным для пользователя). Например, для нашего узла каноничес- кими именами могут быть следующие записи: ftp IN CNAME bsdi kohala com www IN CNAME bsdi kohala com mail host IN CNAME bsdi kohala com
284 Глава 9. Элементарные преобразования имен и адресов Сейчас прошло еще слишком мало времени с момента появления протокола IPv6, чтобы сказать, каких соглашений будут придерживаться администраторы для узлов, поддерживающих и IPv4, и IPv6. В нашем примере мы задали узлу sol an s и запись типа А, и запись типа АААА. Некоторые администраторы поме- щают все записи типа АААА в специальный поддомен, часто называемый i pv6. Например, имя узла, связанное с записью типа АААА, в этом случае будет иметь вид solans ipv6 kohala com. Иногда это делается потому, что администратор узла с двойным стеком несет ответственность не за весь домен, а только за отдельный поддомен ipv6. Вместо этого автор помещает и запись типа А, и запись типа АААА под кано- ническим именем узла (как показано ниже) и создает три записи RR. Первая за- пись RR, имя которой оканчивается на -4, содержит запись типа А; вторая, с име- нем, оканчивающимся на -6, содержит запись типа АААА; а третья запись RR, имя которой оканчивается на -611, содержит запись типа АААА с локальным в пре- делах физической подсети (link-local, см. главу 19) адресом узла (что иногда удоб- но использовать в целях отладки). Все записи для другого нашего узла будут вы- глядеть так: aix-4 IN A 206 62 226 43 aix IN A 206 62 226 43 IN MX 5 aix kohala com IN MX 10 mail host kohala com IN AAAA 5flb dfOO ce3e e200 0020 0800 5afc 2b36 aix-6 IN AAAA 5flb dfOO ce3e e200 0020 0800 5afc 2b36 aix-611 IN AAAA fe80 0800 5afc 2b36 Эта дает нам дополнительный контроль над протоколом, выбранным некото- рыми приложениями, как мы увидим в следующей главе. Распознаватели и серверы имен Организации работают с одним или большим количеством серверов имен (паше servers), при этом часто используется программа BIND (Berkeley Internet Name Domain). Приложения, такие как клиенты и серверы, которые мы создаем в этой книге, соединяются с сервером DNS при помощи вызова функций из библиоте- ки, называемой распознавателем (resolver). Обычные функции распознавателя — gethostbyname и gethostbyaddr, и обе они описаны в этой главе. Первая находит ад- рес узла по его имени, а вторая — наоборот. На рис 9.1 показано типичное расположение приложений, распознавателей и серверов имен. Код распознавателя содержится в системной библиотеке и встра- ивается в приложение, когда оно создается. Код приложения вызывает код рас- познавателя, используя обычные вызовы функций, — как правило, gethostbyname и gethostbyaddr. Код распознавателя читает свои файлы конфигурации, зависящие от систе- мы, чтобы определить расположение серверов имен организации. (Мы говорим «серверы имен», употребляя множественное число, потому что большинство орга- низаций работают с множеством серверов имен, хотя мы и показываем на рисун- ке только один локальный сервер.) Файл /etc/resol v conf обычно содержит IP- адреса локальных серверов имен.
9.3. Функция gethostbyname 285 Приложение Рис. 9.1. Типичное расположение приложений, распознавателей и серверов имен Распознаватель посылает запрос локальному серверу имен, используя UDP. Если локальный сервер имен не знает ответа, он обычно запрашивает другие сер- веры имен через Интернет, также используя UDP. Альтернативы DNS Возможно получить информацию об имени и адресе без использования DNS. Типичной альтернативой служат статические файлы со списком узлов (обычно файл /etc/hosts, как мы указываем в табл. 9.2) или информационная система сети (Network Information System, NIS). К сожалению, способ конфигурирования узла для использования различных типов службы имен зависит от реализации. Sola- ns 2.x и HP-UX 10.30 используют файл /etc/nswitch conf, Digital Unix — файл /etc/svc conf, a AIX использует файл /etc/netsvc conf. BIND 8.1 предоставляет свою собственную версию, которая называется IRS (Information Retrieval Service — служба получения информации), и использует файл /etc/1 rs conf. Если сервер имен должен применяться для поиска имен узлов, все эти системы ис- пользуют для задания IP-адресов серверов имен файл /etc/resol v conf. К счас- тью, эти различия обычно скрыты от программиста приложений, поэтому мы просто вызываем функции распознавателя, такие как gethostbyname и gethostbyaddr. 9.3. Функция gethostbyname Узлы компьютерных сетей мы обычно идентифицируем по их именам, удобным для человеческого восприятия. Но во всех примерах книги специально исполь- зовались IP-адреса вместо имен, поэтому мы точно знаем, что входит в структу- ры адресов сокетов для таких функций, как connect и sendto, и что возвращается функциями accept и recvfrom. Но большинство приложений имеют дело с имена- ми, а не с адресами. Это особенно актуально при переходе на IPv6, поскольку адреса IPv6 (шестнадцатеричные строки) значительно длиннее адресов IPv4, запи-
286 Глава 9. Элементарные преобразования имен и адресов санных в точечно-десятичном представлении. (Например, запись типа АААА и за- пись типа PRT для 1 рб л nt в предыдущем разделе показывают это со всей очевид- ностью.) Самая основная функция, выполняющая поиск имени узла, — это функция gethostbyname. При успешном выполнении она возвращает указатель на структу- ру hostent, содержащую все адреса IPv4 или все адреса IPv6 для узла: include <netdb h> struct hostent *gethostbyname(const char ★hostname'); Возвращает непустой указатель в случае успешного выполнения, -1 в случае ошибки Непустой указатель, возвращаемый этой функцией, указывает на следующую структуру hostent: struct hostent { char *h_name, /* официальное (каноническое) имя узла */ char **h_aliases: /* указатель на массив указателей на псевдонимы */ int h_addrtype, /* тип адреса узла AF_INET или AF_INET6 */ int h_length: /* длина адреса 4 или 16 */ char **h_addr_list. /* указатель на массив указателей с адресами IPv4 или IPv6 */ }. #define h_addr h_addr_list[0] /* первый адрес в списке */ В терминах DNS функция gethostbyname выполняет запрос на запись типа А или запись типа АААА. Эта функция может возвратить либо адрес IPv4, либо адрес IPv6. В табл. 9.1 мы приводим все условия, в которых данная функция воз- вращает эти два типа адресов. ПРИМЕЧАНИЕ ------------------------------------------------------------------ 1 Определение элемента h_addr необходимо только для обратной совместимости, и в но- вом коде не следует использовать элемент h addr. В 4.2BSD отсутствовал элемент h addr hst, но был указатель char *h addr, указывавший только па один 1Р-адрес. к----------- 1 h_length=4 Рис. 9.2. Структура hostent и ее одержимое
9.3. Функция gethostbyname 287 На рис. 9.2 представлено устройство структуры hostent и содержащаяся в ней информация, в предположении, что искомое имя узла имеет два альтернативных имени и три адреса IPv4. Все имена узла представляют собой строки языка С. Возвращаемое имя h_name называется каноническим именем узла. Например, в показанных в предыдущем разделе записях CNAME каноническое имя узла ftp.kohala.com будет иметь вид bsdi .kohala.com. Также если мы вызываем функ- цию gethostbyname с узла sol ari s с неполным именем, например sol ап s. то в каче- стве канонического имени возвращается полное доменное имя (FQDN) solan s. kohala.com. Когда возвращаются адреса IPv6, элементу h_addrtype структуры hostent при- сваивается значение AF_INET6, а элементу h_length —16. На рис. 9.3 показаны эти изменения, а затененными изображены поля, изменившиеся по сравнению с рис. 9.2. 1 h_length=16 Рис. 9.3. Изменения информации, возвращаемой в структуре hostent с адресом IPv6 ПРИМЕЧАНИЕ ------------------------------------------------------------------- Недавние версии функции gethostbyname, начиная с реализации BIND 4.9.2, допуска- ют, что аргумент hostname может быть записан в виде строки десятичных чисел, разде- ленных точками, то есть вызов в форме hptr = gethostbyname("206.62.226.33"); будет работать. Этот код был добавлен, поскольку клиент Rlogin принимает только имя узла, вызывая функцию gethostbyname, и не принимает точечно-десятичную запись [104]. Функция gethostbyname отличается от других функций сокетов, описанных нами, тем, что она не задает значение переменной еггпо, когда происходит ошиб- ка. Вместо этого она присваивает глобальной целочисленной переменной h errno одну из следующих констант, определяемых с помощью включения заголовка <netdb.h>: HOST_NOT_FOUND
288 Глава 9. Элементарные преобразования имен и адресов TRY_AGAIN NO_RECOVERY N0_DATA (идентично NO_ADDRESS) Ошибка NO_DATA означает, что заданное имя действительно, но у него нет либо записи типа А, либо записи типа АААА. Примером может служить имя узла, име- ющее только запись типа MX. Текущие реализации BIND предоставляют функцию hsterror, которая в каче- стве единственного аргумента получает значение h_errno и возвращает указатель const char* на описание ошибки. Некоторые примеры строк, возвращаемых этой функцией, мы увидим в следующем примере. Пример В листинге 9.11 показана простая программа, вызывающая функцию gethostbyname для любого числа аргументов командной строки и выводящая всю возвращае- мую информацию. Листинг 9.1. Вызов функции и вывод возвращаемой информации //names/hostent с 1 #include "unp h" 2 int 3 main(int argc. char **argv) 4 { 5 char *ptr **pptr. 6 char str[INET6_ADDRSTRLEN]: 7 struct hostent *hptr. 8 while (--argc > 0) { 9 ptr = *++argv. 10 if ( (hptr = gethostbyname(ptr)) = NULL) { 11 err_msg("gethostbyname error for host Xs: Xs". 12 ptr. hstrerror(h_errno)), 13 continue. 14 } 15 printf("official hostname Xs\en". hptr->h_name). 16 for (pptr = hptr->h_aliases *pptr '= NULL. pptr++) 17 printfC\etalias Xs\en", *pptr). 18 switch (hptr->h_addrtype) { 19 case AFJNET 20 tfifdef AF_INET6 21 case AFJNET6 22 fend if 23 pptr = hptr->h_addr_list. 24 for ( *pptr •= NULL. pptr++) 25 printfC\etaddress Xs\en". 26 lnet_ntop(hptr->h_addrtype. *pptr str sizeof(str))). 27 break 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter com/download. t
9.3. Функция gethostbyname 289 28 default 29 err rett"unknown address type"), 30 break, 31 } 32 } 33 exit(0). 34 } 8-14 Функция gethostbyname вызывается для каждого аргумента командной строки. 15-17 Выводится официальное имя узла, за которым идет список альтернативных имен. 20-22 Чтобы эта программа поддерживала и адреса IPv4, и адреса IPv6, мы допуска- ем, что возвращаемый адрес будет относиться либо к типу AF_INET, либо к типу AF_I NET6. Но мы не допускаем последнего, если адрес не определен (то есть если узел не поддерживает IPv6). 23-26 Переменная pptr указывает на массив указателей на индивидуальные адреса. Для каждого адреса мы вызываем функцию i net_ntop и выводим возвращаемую строку. Обратите внимание, что inet_ntop обрабатывает и адреса IPv4, и адреса IPv6, основываясь на значении первого аргумента. Также заметьте, что мы опре- делили аргумент str длины INET6_ADDRSTRLEN, что, как мы сказали в разделе 3.7, достаточно много для строки адреса IPv6 наибольшей допустимой длины В на- шем файле unp h мы определяем эту константу, даже если узел не поддерживает IPv6, поэтому мы всегда можем рассчитывать на то, что она определена (это по- зволяет нам избежать еще одного определения #i fdef в нашем коде). Сначала мы выполняем программу с именем нашего узла solans, у которого имеется только один адрес IPv4: solans % hostent solans official hostname solans kohala com address 206 62 226 33 Обратите внимание, что официальное имя узла — это FQDN. Кроме того, хотя у узла имеется адрес IPv6, возвращается только адрес IPv4. Следующим будет узел с несколькими адресами IPv4: solans % hostent gemim.tuc.noao.edu official hostname gemim tuc noao edu address 140 252 1 11 address 140 252 3 54 address 140 252 4 54 address 140 252 8 54 Далее идет имя, представленное в разделе 9.2 как имя с записью типа CNAME: Solaris % hostent www official hostname bsdi kohala com alias www kohala com address 206 62 226 35 Как мы и предполагали, официальное имя узла отличается от нашего аргу- мента командной строки. Чтобы увидеть строки ошибок, возвращаемые функцией hstrerror, мы снача- ла задаем несуществующее имя узла, а затем имя, имеющее только запись типа MX: solans Я hostent nosuchname
290 Глава 9. Элементарные преобразования имен и адресов gethostbyname error for host nosuchname Unknown host solans % hostent uunet.uu.net gethostbyname error for host uunet uu net No address associated with name 9.4. Параметр распознавателя RES_USEJNET6 Более новые реализации BIND (4.9.9 и выше) предоставляют параметр распо- знавателя, называемый RES_USE_INET6, который мы можем установить тремя раз- личными способами. Мы можем использовать этот параметр для сообщения рас- познавателю о том, чтобы функция gethostbyname возвращала адрес IPv6 вместо адреса IPv4. 1 Приложение может установить этот параметр самостоятельно, вызвав снача- ла функцию распознавателя res_imt и затем включив параметр. #include <resolv h> res_imt() _res options |“ RES_USE_INET6 Это нужно сделать перед первым вызовом функции gethostbyname или gethost- byaddr Параметр действителен только для установившего его приложения. 2 Если переменная окружения RES OPTIONS содержит строку i net6, параметр вклю- чается. Область влияния параметра зависит от области видимости перемен- ной окружения. Если мы установим ее в нашем файле prof 11 е, например (счи- тая, что это KornShell), с атрибутом export, следующим образом. export RES_0PTI0NS=inet6 то тогда она повлияет на каждую программу, которую мы запустим из нашей оболочки входа в систему. Но если мы просто установим переменную в ко- мандной строке (как мы показываем чугь ниже), то она повлияет только на эту команду. 3 Файл конфигурации распознавателя (обычно файл /etc/resol v conf) может содержать строку options inet6 Однако нужно отдавать себе отчет в том, что установка этого параметра в фай- ле конфигурации распознавателя влияет на все приложения на узле, которые вызывают функции распознавателя. Следовательно, эту технологию не нужно использовать, пока все приложения на узле не будут способны обрабатывать ад- реса IPv6, возвращаемые в структуре hostent. Первый метод устанавливает параметр для каждого приложения, второй — для каждого пользователя, третий — для каждой системы. Теперь запустим пример программы, показанной в листинге 9 1, присвоив пе- ременной окружения RES_OPTIONS значение inet6: solans % RES_0PTI0NS=inet6 hostent solans имя с записью типа АМА official hostname solans kohala com address 5flb dfOO ce3e e200 20 800 2078 еЗеЗ
9.5. Функция gethostbyname2 и поддержка IPv6 291 solans % RES_OPTIONS=inet6 hostent bsdi имя без записи АААА official hostname bsdi kohala com address ffff 206 62 226 35 address ffff 206 62 226 66 Когда мы выполняем нашу программу первый раз, она возвращает IPv6-адрес узла (вспомните его запись типа АААА в разделе 9.2). Когда программа выпол- няется второй раз, мы задаем имя узла, не имеющего записи типа АААА. Но тем не менее возвращаются адреса IPv6, точнее, IPv4-адреса, преобразованные к виду IPv6 (см. раздел А.5). Более подробно о поддержке IPv6 в распознавателе мы поговорим в двух сле- дующих разделах. 9.5. Функция gethostbyname2 и поддержка IPv6 Когда в BIND 4.9 4 была добавлена поддержка IPv6, появилась функция gethost- byname2 с двумя аргументами, позволяющими нам задать семейство адресов. #include <netdb h> struct hostent *gethostbyname2(const char *hostname int family) Возвращает непустой указатель в случае успешного выполнения в случае ошибки возвращает NULL и задает значение переменной h_errno Возвращаемое значение то же, что и у функции gethostbyname, — указатель на структуру hostent, и сама эта структура устроена так же. Логика функции зави- сит от аргумента family и параметра распознавателя RES_USE_INET6 (который мы упомянули в конце предыдущего раздела). Прежде чем рассматривать в подробностях эту функцию, следует обратиться к табл 9.1, которая характеризует действия функций gethostbyname и gethostbyname2 в зависимости от napaMeipa RE5_USE_INET6. Значения, которые могут изменяться, выделены полужирным шрифтом- параметр RES_USE_INET6 может быть включен (on) или выключен (off); ‘ <* вторым аргументом функции gethostbyname2 может быть AF_INET или AF_INET6; » распознаватель ищет записи типа А или записи типа АААА; - возвращаемые адреса могут иметь длину 4 или 16. Таблица 9.1. Функции gethostbyname и gethostbyname2 с параметром RESJJSEJNET6 Параметр RESJJSEJNET6 Параметр RESJJSEJNET6 gethostbyname(/iost) выключен включен Поиск записей типа А Если они Поиск записей типа АААА Если находятся, то возвращаются они находятся, то возвращаются адреса IPv4 (h_length = 4) адреса IPv6 (h_length = 16) В противном случае — ошибка В противном случае — поиск Это обеспечивает обратную записей типа А Если они совместимость со всеми суще- находятся, то возвращаются адреса ствующими приложениями, IPv4, преобразованные к виду IPv6 в которых используются (h lcngth = 16) В противном адреса IPv4 случае — ошибка продолжение &
292 Глава 9. Элементарные преобразования имен и адресов Таблица 9.1 {продолжение) Параметр RESJJSEJNET6 выключен Параметр RESJJSEJNET6 включен gethostbyname2(/rosZ, Поиск записей типа А. Если они Поиск записей типа А Если они AFJNET) находятся, то возвращаются адреса IPv4 (h_length - 4). В противном случае — ошибка находятся, то возвращаются адреса IPv4, преобразованные к виду IPv6 (hjcngth = 16). В противном случае — ошибка gethostbyname(/rost, Поиск записей типа АААА. Если Поиск записей тина АААА. Если AFJNET6) они находятся, то возвращаются адреса IPv6 (h length - 16). В противном случае — ошибка они находятся, то возвращаются адреса IPv6 (h length = 16) В противном случае — ошибка Действие функции gethostbyname2 заключается в следующем: >‘ Если аргумент family имеет значение AFJNET, делается запрос на записи типа А. При неудачном выполнении функция возвращает пустой указатель. При успешном выполнении тип и размер возвращаемого адреса зависят от нового параметра распознавателя RES_USE_INET6: если параметр не установлен (по умол- чанию), возвращаются адреса IPv4 и возвращаемый элемент h l ength структу- ры hostent будет равен 4. Если же параметр установлен, то возвращаются ад- реса IPv4, преобразованные к виду IPv6, и элемент h_l ength структуры hostent будет равен 16. ' Если аргумент family имеет значение AF JNET6, делается запрос на записи типа АААА. При успешном выполнении функции возвращаются адреса IPv6, а эле- мент h_l ength структуры hostent равен 16, иначе функция возвращает пустой указатель. Эта функция может использоваться, если приложение хочет осуществить по- иск одного определенного типа адресов — IPv4 или IPv6. Но для приложений более типичным является вызов функции gethostbyname, и более новые версии этой функции могут возвращать как адреса IPv4, так и адреса IPv6. Одним из способов изучения действия функции gethostbyname и параметра RES_U5E_INET6 является просмотр ее исходного кода, который представлен в лис- тинге 9.2. Листинг 9.2. Функция gethostbyname и поддержка IPv6 struct hostent * gethostbyname(const char *name) struct hostent *hp. if ( ( res options & RES_INIT) — 0 && resJnitO “ -1) { n_errno = NETDBJNTERNAL; return (NULL). } if (_res options & RESJISEJNET6) { hp = gethostbyname2(name. AF_INET6): if (hp) return (hp): }
9.6, Функция gethostbyaddr 293 return (gethostbyname2(name. AF_INET)), } Если распознаватель еще не инициализирован (флаг RES INIT не установлен), вызывается функция res_imt. Эта функция инициализации проверяет и обраба- тывает переменную окружения RESJDPTIONS. Если эта переменная содержит строку 1 net6 или если файл конфигурации распознавателя содержит строку opti ons i net6, функция res_imt устанавливает параметр RES_USE_INET6. Функция res_i mt обычно вызывается автоматически функцией gethostbyname (как показано здесь), когда она впервые вызывается приложением, или функцией gethostbyaddr. Альтерна- тивным вариантом является, как мы показали выше, вызов функции res_imt приложением, после чего явно устанавливается флаг RE5_USE_INET6. Если параметр RES_USE_INET6 не установлен, выполняется последняя строка нашей функции и вызывается функция gethostbyname2 с аргументом AF_INET, за- дающим семейство адресов. В табл. 9.1 показано, что при этом вызове ищутся только записи типа А. Это обеспечивает обратную совместимость со всеми суще- ствующими приложениями. Если параметр RES_U5E_INET6 включен, вызывается функция gethostbyname2 с аргументом семейства адресов AF_INET6 для поиска записей типа АААА (см. табл. 9.1). Если вызов успешен, функция gethostbyname завершается. Если вызов неудачен, вызывается функция gethostbyname2 с семейством адресов AF_INET для поиска записей типа А. Если этот вызов успешен, то (что неочевидно из листин- га 9.2) эти 4-байтовые значения автоматически отображаются в 16-байтовый ад- рес IPv4, преобразованный к виду IPv6. Таким образом, вызывая функцию gethostbyname при включенном параметре RES_USE_INET6, приложение тем самым сообщает распознавателю: «Мне нужны только адреса IPv6. Следует искать сначала записи типа АААА, а если ни одной не найдется, то записи типа А, и если они найдутся, возвратить эти адреса как адреса IPv4, преобразованные к виду IPv6». 9.6. Функция gethostbyaddr Функция gethostbyaddr получает в качестве аргумента двоичный IP-адрес и пы- тается найти имя узла, соответствующее этому адресу. Ее действие обратно дей- ствию функции gethostbyname. # include <netdb h> struct hostent *gethostbyaddr(const char *addr. size_t len. int family): Возвращает непустой указатель в случае успешного выполнения. -1 в случае ошибки Эта функция возвращает указатель па ту же структуру hostent, которую мы описывали при рассмотрении функции gethostbyname. Обычно в этой структуре нас интересует поле h_name, каноническое имя узла. Аргумент addr не относится к типу char*, но в действительности это указатель на структуру in addr или in6_addr, содержащую адрес IPv4 или IPv6, len — это длина структуры: 4 для адресов IPv4 или 16 для адресов IPv6. Аргумент farm 1у будет иметь либо значение AF_I NET, либо AF_INET6. В терминах DNS функция gethostbyaddr запрашивает у сервера имен запись типа PTR в домене in-addr.apra для адреса IPv4 или запись типа PTR в домене 1 рб int для адреса IPv6.
294 Глава 9. Элементарные преобразования имен и адресов Функция gethostbyaddr и поддержка IPv6 У функции gethostbyaddr всегда был аргумент семейства адресов, поэтому когда в BIND появилась поддержка IPv6, не возникло необходимости вводить другую функцию (аналогичную gethostbyname2). Но когда аргументом является адрес IPv6, существует ряд особенностей. Поэтому необходимо выполнить три следующих проверки в указанном порядке: 1. Если аргумент farm 1у — AF_INET6, len — 16 и адрес является адресом IPv4, пре- образованным к виду IPv6, то младшие 32 бита адреса (часть IPv4) ищутся в домене in-addr арга. 2. Если аргумент family — AF_INET6, len — 16 и адрес является адресом IPv6, со- вместимым с IPv6, то младшие 32 бита адреса (часть IPv4) ищутся в домене in-addr арга. 3 Если мы искали адрес IPv4 (аргумент farm 1 у был равен AF_I NET или имел место один из перечисленных выше случаев) и установлен параметр распознавате- ля RES_U5E_INET6, то возвращаемый адрес (копия аргумента addr) преобразует- ся из IPv4 к виду IPv6: h_addrtype — эго AF INET6, а аргумент len равен 16. Третья проверка обычно имеет небольшое значение, поскольку немногие при- ложения проверяют IP-адрес, возвращаемый функцией gethostbyaddr, так как это только копия аргумента. Обычно приложения вызывают эту функцию для про- верки элемента h_name возвращаемой структуры hostent (а также, возможно, аль- тернативных имен). 9.7. Функция uname Функция uname возвращает имя текущего узла. Эта функция не является частью библиотеки распознавателя, но мы рассматриваем ее здесь, поскольку она часто используется вместе с функцией gethostbyname для определения IP-адресов ло- кальных узлов. #include <sys/utsname h> int uname(struct utsname ★name') Возвращает неотрицательное значение в случае успешного выполнения. -1 в случае ошибки Эта функция заполняет структуру utsname, адрес которой передается вызыва- ющим: # define _UTS_NAMESIZE 16 # define _UTS_NDDESIZE 256 struct char char char char char I utsname { sysname[_UTS_NAMESIZE] nodename[_UTS_NODESIZE] release[_UTS_NAMESIZE]. version[_UTS_NAMESIZE], machine[_UTS_NAMESIZE] /* имя данной операционной системы */ /* имя данного узла */ /* наименование версии ОС*/ /* наименование модификации версии ОС*/ /* тип аппаратного устройства */ К сожалению, в Posix. 1 лишь определены имена пяти элементов показанной структуры и сказано, что каждый массив является массивом символов, который
9 8. Функция gethostname 295 оканчивается пустым байтом. Ничего не говорится ни о размере каждого масси- ва, ни об их содержимом. Размеры, представленные нами, взяты из 4.4BSD. Дру- гие операционные системы используют другие размеры. Наиболее важное упущение с точки зрения сетевого программирования ка- сается определения размера и содержимого массива nodename. Некоторые систе- мы хранят в этом массиве только имя узла (например, germ m ), в то время как другие хранят FQDN (например, делит tuc noao edu). В некоторых операцион- ных системах, таких как Solaris 2.x, массив может содержать либо одно, либо дру- гое — в зависимости от того, как администратором нас гроена операционная система. Пример: определение IP-адресов локального узла Чтобы определить IP-адреса локального узла, мы вызываем функцию uname для получения имени узла, а затем — функцию gethostbyname для получения всех его IP-адресов. Функция my_addrs, показанная в листинге 9.3, выполняет перечислен- ные задачи. Листинг 9.3. Функция, возвращающая все IP-адреса узла //lib/my_addrs с 1 include 'unp h" 2 #include <sys/utsname h> 3 char ** 4 my_addrs(int *addrtype) 5 { 6 struct hostent *hptr. 7 struct utsname myname. 8 if (uname(&myname) < 0) 9 return (NULL). 10 if ( (hptr = gethostbyname(myname nodename)) == NULL) 11 return (NULL) 12 *addrtype = hptr->h_addrtype 13 return (hptr->h_addr_list) 14 } Возвращаемое значение функции — это элемент h_addr_l i st структуры hostent, массив указателей на IP-адреса. Мы также возвращаем семейство адресов через аргумент-указатель. Другой способ определить IP-адреса локального узла — воспользоваться ко- мандой SIOCGIFCONF функции i octi. Мы рассмотрим этот случай в главе 16. 9.8. Функция gethostname Функция gethostname также возвращает имя текущего узла, include <unistd h> int gethostname(char *name sizet namelen) Возвращает 0 в случае успешного выполнения -1 в случае ошибки
296 Глава 9. Элементарныепреобразования имен и адресов name — указатель на то место, где хранится имя узла, a name] еп — размер этого мас- сива. Если в массиве есть место, то в конце стоит символ конца строки (нуль). Максимальный размер имени узла задается константой MAXHOSTNAMELEN, которая определяется во включаемом заголовочном файле <sys/param h>. ПРИМЕЧАНИЕ ------------------------------------------------------------ Исторически функция uname была определена в System V, а функция gethostname — в Беркли. Стандарт Posix.l задает uname, но Unix 98 требует наличия обеих функций. 9.9. Функции getservbyname и getservbyport Службы, как и узлы, также часто идентифицируются по именам. Используя в на- шем коде имя службы вместо номера порта, когда сопоставление имени службы номеру порта содержится в некотором файле (обычно в файле /etc/services), мы получаем следующее преимущество. Если этой службе будет назначен другой номер порта, то нам будет достаточно изменить одну строку в файле /etc/servi ces, вместо того чтобы перекомпилировать все приложения. Следующая функция, getservbyname, ищет службу по ее заданному имени. #include <netdb h> struct servent *getservbyname(const char *servname const char *protoname') Возвращает непустой указатель в случае успешного выполнения. NULL в случае ошибки Функция возвращает указатель на следующую структуру: struct servent { char *s_name char **s_aliases. int s_port. char *s_proto } /* официальное имя службы */ /* список псевдонимов */ /* номер порта, записанный в сетевом порядке байтов */ /* протокол который нужно использовать */ Имя службы должно быть задано в servname. Если протокол также задан (то есть если protoname — непустой указатель), то в структуре должен быть указан совпадающий протокол. Некоторые службы Интернета позволяют использовать и TCP, и UDP (например, DNS и все службы, представленные в табл. 2.1), в то время как другие поддерживают только один протокол (протоколу FTP требует- ся TCP). Если аргумент protoname не задан и служба поддерживает множество протоколов, то возвращаемый номер порта зависит от реализации. Обычно это не имеет значения, поскольку службы, поддерживающие множество протоколов, как правило, используют один и тот же номер порта для протоколов TCP и UDP, но это не гарантируется. Более всего в структуре servent нас интересует поле номера порта. Поскольку номер порта возвращается в сетевом порядке байтов, мы не должны вызывать функцию htons при записи его в структуру адреса сокета. Типичные вызовы этой функции могут быть такими: struct servent *sptr.
9 9. Функции getservbyname и getservbyport 297 sptr “ getservbyname("domain”, "udp’) sptr = getservbyname!"ftp” ’top') sptr = getservbyname!"ftp”. NULL), sptr “ getservbynameC’ftp". "udp”). /* DNS с использованием UDP */ /* FTP с использованием TCP */ /* FTP с использованием TCP */ /* этот вызов приведет к ошибке */ Поскольку протоколом FTP поддерживается только TCP, второй и третий вызовы одинаковы, а четвертый вызов приводит к ошибке. Вот типичные строки из файла /etc/services: solans % grep -е ftp -е domain /etc/services\fP ftp-data 20/tcp ftp domain domain tftp 21/tcp 53/udp 53/tcp 69/udp Следующая функция, getservbyport, ищет службу по заданному номеру порта и (не обязательно) протоколу. #include <netdb h> struct servent *getservbyport!int port, const char *protname). Возвращает непустой указатель в случае успешного выполнения. NULL в случае ошибки Значение аргумента port должно быть записано в сетевом порядке байтов. Типичные примеры вызова этой функции приведены ниже: struct servent *sptr. sptr - getservbyport(htons(53) "udp"). sptr “ getservbyport(htons(2D. "tcp") sptr “ getservbyport(htons(21) NULL), sptr = getservbyport(htons(21i "udp"). /* DNS с использованием UDP *7 /* FTP с использованием TCP */ /* FTP с использованием TCP */ /* этот вызов приведет к ошибке */ Последний вызов оказывается неудачным, поскольку пет службы, использу- ющей порт 21с протоколом UDP. Помните, что некоторые номера портов используются с TCP для одной служ- бы, а с UDP для совершенно другой службы, например: solans X grep 514 /etc/services shel1 514/tcp syslog 514>udp Здесь показано, что порт 514 используется командой rsh в TCP и демоном syslog в UDP. Это свойство имеют порты 512-514. Пример: использование функций gethostbyname и getservbyname Теперь мы можем изменить код нашего TCP-клиента времени и даты, показан- ный в листинге 1.1, так, чтобы использовать функции gethostbyname и getservbyname и принимать два аргумента командной строки: имя узла и имя службы. Наша программа показана в листинге 9.4. Эта программа также демонстрирует жела- тельное поведение при установлении соединения со всеми IP-адресами сервера на узле, имеющем несколько сетевых интерфейсов: попытки продолжаются до тех пор, пока соединение не будет успешно установлено или пока не будут пере- браны все адреса.
298 Глава 9. Элементарные преобразования имен и адресов Листинг 9.4. Наш клиент времени и даты, использующий функции gethostbyname и getservbyname //names/daytimetcpclil с 1 #include "unp h" 2 int 3 main(int argc char **argv) 4 { 5 int sockfd. n 6 char recvline[MAXLINE + 1] 7 struct sockaddr_in servaddr 8 struct in_addr **pptr. 9 struct hostent *hp. 10 struct servent *sp. 11 if (argc ’= 3) 12 err_quit('usage daytimetcpclil <hostname> <service>”): 13 if ( (hp = gethostbyname(argv[l])) = NULL) 14 err_quit( 'hostname error for Xs Xs' argv[l] hstrerror(h_errno)); 15 if ( (sp = getservbyname(argv[2]. ’’tcp')) == NULL) 16 err_quit(' getservbyname error for Xs' argv[2]). 17 pptr = (struct in_addr **) hp->h_addr_list 18 for ( *pptr '= NULL. pptr++) { 19 sockfd = Socket(AF_INET SOCK_STREAM, 0). 20 bzerot&servaddr. sizeof(servaddr)) 21 servaddr sin_family « AF_INET 22 servaddr sin_port » sp->s_port 23 memcpyt&servaddr sin_addr *pptr sizeoftstruct inaddr)); 24 printft"trying Xs\en", 25 Sock_ntop((SA *) &servaddr. sizeof(servaddr))), 26 if (connect(sockfd (SA *) &servaddr. sizeof(servaddr)) — 0) 27 break /* успешное выполнение */ 28 err_ret("connect error”). 29 close(sockfd). 30 } 31 if (*pptr == NULL) 32 err_quit( unable to connect”) 33 while ( (n “ Readtsockfd recvline MAXLINE)) > 0) { 34 recvline[n] =0 7* завершающий нуль */ 35 Fputs(recvline. stdout). 36 } 37 exit(0) 3B } Вызов функций gethostbyname и getservbyname 13-16 Первый аргумент командной строки — это имя узла, передаваемое в каче- стве аргумента функции gethostbyname, а второй — имя службы, передаваемое в качестве аргумента функции getservbyname. Наш код подразумевает исполь-
9.9. Функции getservbyname и getservbyport 299 зование протокола TCP, что мы указываем во втором аргументе функции getservbyname. Перебор всех адресов 18-25 Теперь мы пишем вызовы функций socket и connect в цикле, который выполня- ется для каждого адреса сервера, пока попытка вызова функции connect не ока- жется успешной или пока не закончится список серверов. После вызова функ- ции socket мы заполняем структуру адреса сокета Интернета IP-адресом и номером порта сервера. Хотя в целях увеличения производительности мы могли бы выне- сти из цикла вызов функции bzero и последующие два присваивания и поместить их непосредственно в коде функции main, наш код легче читать в таком виде, как он представлен сейчас. Установление соединения с сервером редко вызывает за- труднения у сетевого клиента. Вызов функции connect 26-30 Вызывается функция connect, и если вызов оказывается успешным, функция break завершает цикл. Если установить соединение не удается, мы выводим ошибку и закрываем сокет. Вспомните, что дескриптор, для которого вызов функции connect оказался неудачным, нужно закрыть и больше не использовать. Завершение программы 31-32 Если цикл завершается, потому что ни один вызов функции connect не удалей, программа завершается. Чтение ответа сервера 33-37 В противном случае мы читаем ответ сервера и завершаем программу, когда сервер закрывает соединение. Если мы запустим эту программу, указав один из наших узлов, на котором работает сервер времени и даты, мы получим вполне предполагаемый вывод- sol aris % daytimetcpclil aix daytime trying 206 62 226 35 13 Thu May 22 19 28 11 1997 Но еще интереснее запустить программу на маршрутизаторе с несколькими сетевыми интерфейсами, на котором не работает сервер времени и даты: solans % daytimetcpclil gateway.tuc.noao.edu daytime trying 140 252 1 4 13 connect error Connection refused trying 140 252 101 4 13 connect error Connection refused trying 140 252 102 1 13 connect error Connection refused trying 140 252 104 1 13 connect error Connection refused trying 140 252 3 6 13 connect error Connection refused trying 140 252 4 100 13 connect error Connection refused unable to connect
300 Глава 9. Элементарные преобразования имен и адресов 9.10. Другая информация о сетях В этой главе мы сфокусировали внимание на именах узлов, IP-адресах, именах и номерах портов служб. Если же обобщить полученную информацию, мы увидим, что существует четыре типа данных (имеющих отношение к сетям), которые мо- гут понадобиться приложению: узлы, сети, протоколы и службы. В большинстве случаев происходит поиск данных, относящихся к узлам (функции gethostbyname и gethostbyaddr), реже — к службам (функции getservbyname и getservbyaddr) и еще реже — к сетям и протоколам. Все четыре типа данных могу г храниться в файле, и для каждого из четырех типов определены три функции: 1. Функция get-XXXent, читающая следующую запись в файле, при необходимо- сти открывающая файл. 2. Функция setXXXent, которая открывает файл (если он еще не открыт) и пере- ходит к началу файла. 3. Функция endXXXent, закрывающая файл. Для каждого из четырех типов данных определяется его собственная структу- ра (соответственно, структуры hostent, netent, protoent и servent), что требует вклю- чения заголовка <netdb h>. В дополнение к трем функциям — get, set и end — которые допускают последо- вательную обработку файла, для каждого из четырех типов данных предоставля- ются функции ключевого поиска, или поиска по ключу {keyed lookup). Эти функ- ции последовательно проходят файл (вызывая функцию getXXXent для чтения каждой строки файла), но вместо того чтобы возвращать каждую строку вызыва- ющему процессу, эти функции ищут элемент, совпадающий с аргументом. Имена функций поиска по ключу имеют вид getAXXby YYY. Например, две функции клю- чевого поиска для информации об узле — это функции gethostbyname (ищет эле- мент, совпадающий с именем узла) и gethostbyaddr (ищет элемент, совпадающий с IP-адресом). Таблица 9.2 обобщает эту информацию. Таблица 9.2. Четыре типа данных, относящихся к сетям Тип данных Файл Структура Функции поиска по ключу Узлы /etc/hosts hostent gethostbyaddr, gethostbyname Сеги /etc/networks netent getnetbyaddr, getnetbyname Протоколы /etc/protocols protoent getprotobyname, getprotobynumber Службы /etc/services servent getservbyname, getservbyport Как это применяется, если используется DNS? Прежде всего, с помощью DNS возможен доступ только к информации об узле и о сети. Информация о протоко- ле и службах всегда считывается из соответствующего файла. Ранее в этой главе мы отмечали (см. подраздел «Альтернативы DNS»), что в разных реализациях отличаются способы, с помощью которых администратор определяет, что имен- но использовать для получения информации об узле и сети — DNS или файл. Далее, если DNS используется для получения информации об узле и о сети, имеют смысл только функции поиска по ключу. Используя, например, функцию
Упражнения 301 gethostent, не стоит надеяться, что она выполнит последовательный перебор всех записей DNS! Если вызывается функция gethostent, она считывает только ин- формацию об узлах и не использует DNS. ПРИМЕЧАНИЕ----------------------------------------------------------- Хотя информацию о сети можно сделать доступной с помощью DNS, очень немногие пользуются этим. 11а с. 347-348 [ 1 ] рассказывается об этой возможности. Однако обыч- но администраторы создают и обслуживают файл /etc/nctworks, используемый вмес- то DNS. Программа netstat с параметром -I использует этот файл, если он есть, и выво- дит имя каждой сети. 9.11. Резюме Набор функций, вызываемых приложением для преобразования имени узла в IP- адрес и обратно, называется распознавателем. Две функции, gethostbyname и gethostbyaddr, являются типичными точками входа. С переходом на IPv6 струк- тура hostent, заполняемая с помощью этих функций, остается той же, но часть информации внутри структуры изменяется. Новая функция gethostbyname? и но- вый параметр распознавателя RES_USE_INET6 также необходимы для поддержки IPv6. Общеупотребительной функцией, манипулирующей именами служб и номе- рами портов, является функция getservbyname, которая получает в качестве аргу- мента имя службы и возвращает структуру, содержащую номер порта. Это сопо- ставление обычно содержится в текстовом файле. Существуют дополнительные функции для сопоставления имен протоколов номерам протоколов и имен се- тей — номерам сетей, но они редко используются. Альтернативой DNS, которую мы не упомянули, является непосредственный вызов функций распознавателя вместо использования функций gethostbyname и gethostbyaddr. Таким способом пользуется, например, программа sendmai 1, пред- назначенная для поиска записи типа MX, чего не может сделать функция gethostbyXXX. У функций распознавателя имена начинаются с res_. Примером такой функции является функция res_imt, которую мы описали в разделе 9.4. Описание этих функций и пример вызывающей их программы находятся в раз- деле 14книги [1]. При вводе в командной строке man resol ver должны отобразиться страницы руководства для этих функций. Тему преобразований имен и адресов мы продолжим в главе 11, когда мы по- знакомимся с не зависящим от протокола интерфейсом для функций gethostbyname и gethostbyaddr — с функциями getaddrinfo и getnameinfo. Эти две функции были разработаны для работы с IPv4 и IPv6, но сначала мы рассмотрим ряд аспектов совместимости протоколов IPv4 и IPv6 в следующей главе. Упражнения 1. Измените программу, представленную в листинге 9.1, так, чтобы для каждого возвращаемого адреса вызывалась функция gethostbyaddr, а затем выведите возвращаемое имя h_name. Сначала запустите программу, задав имя узла толь-
302 Глава 9. Элементарные преобразования имен и адресов ко с одним IP-адресом, а затем — с несколькими IP-адресами. Что происхо- дит? 2. Устраните проблему, показанную в предыдущем упражнении. 3. Измените листинг 9.3, чтобы в нем вместо функции uname вызывалась функ- ция gethostname. Напишите функцию main для вызова my_addrs и затем выведи- те 1Р-адрес. 4. Что может произойти в листинге 9.4, если мы изменим третий аргумент функ- ции memcpy (при заполнении структуры адреса сокета) и он станет равен hp-> hjength? (Подсказка: Подумайте, что произойдет, если мы установим пара- метр RES_OPTIONS=i net6 при выполнении программы и зададим имя узла, имею- щего адрес IPv6.) 5. Запустите программу, показанную в листинге 9.4, задав имя службы chargen. 6. Запустите программу, показанную в листинге 9.4, задав IP-адрес в точечно- десятичной записи в качестве имени узла. Допускает ли это ваш распознава- тель? Измените листинг 9.4, чтобы разрешить IP-адрес в виде строки деся- тичных чисел с точками в качестве имени узла и строку с десятичным номером порта в качестве имени службы. В каком порядке должно выполняться тести- рование IP-адреса для строки в точечно-десятичной записи и для имени? 7. Измените программу в листинге 9.4 так, чтобы можно было работать либо с IPv4, либо с IPv6. 8. Измените программу в листинге 9.4 так, чтобы сделать запрос DNS, и сравни- те возвращаемый IP-адрес со всеми IP-адресами узла получателя, то есть вы- зовите функцию gethostbyaddr, используя IP-адрес, возвращаемый функцией recvfrom, а затем вызовите gethostbyname для поиска всех IP-адресов для узла.
ЧАСТЬ 3 ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ СОКЕТОВ
ГЛАВА 10 Совместимость IPv4 и IPv6 10.1. Введение В течение ближайших лет, возможно, произойдет постепенный переход Интер- нета с IPv4 на IPv6. Во время этого переходного периода важно, чтобы существу- ющие приложения IPv4 продолжали работать с более новыми приложениями IPv6. Например, производитель не может предложить клиент Telnet, работаю- щий только с серверами IPv6, — он должен предоставить и клиент для серверов IPv4, и клиент для серверов IPv6. Мы бы предпочли обойтись одним Telnet-кли- ентом IPv6, способным работать с серверами и IPv4, и IPv6, вместе с одним сер- вером Telnet, который работал бы с клиентами и IPv4, и IPv6. В этой главе мы увидим, как это сделать. В этой главе мы предполагаем, что на узлах работают двойные стеки протоко- лов (dual stacks), то есть набор протоколов IPv4 и набор протоколов IPv6. На рис. 2.1 представлен узел с двойным стеком. Возможно, узлы и маршрутиза- торы будут работать подобным образом в течение многих лет при переходе к IPv6. В какой-то момент многие системы смогут отключить свои стеки IPv4, но только с течением времени можно будет сказать, когда это произойдет, да и произойдет ли вообще. В этой главе мы рассмотрим, как приложения IPv4 и приложения IPv6 могут взаимодействовать друг с другом. Существует четыре комбинации клиентов и сер- веров, использующих либо IPv4, либо IPv6, что показано в табл. 10 1 Таблица 10.1. Сочетания клиентов и серверов, использующих IPv4 или IPv6 сервер IPv4 сервер IPv6 Клиент IPv4 Почти все существующие клиенты и серверы Обсуждав!ся в разделе 102 Клиент IPv6 Обсуждается в разделе 103 Простые модификации большинства существующих клиентов (например, клиент из листинга 1 1 модифицируется к виду, представленному в листинге 1 2) Мы не будем подробно рассматривать два сценария, когда клиент и сервер используют один и тот же протокол. Более интересны случаи, когда клиент и сер- вер используют разные протоколы.
10 2. Клиент IPv4, сервер IPv6 305 10.2. Клиент IPv4, сервер IPv6 Общим свойством узла с двойным стеком является то, что серверы IPv6 могут выполнять обработку как клиентов IPv4, так и клиентов IPv6. Это достигается за счет преобразования адресов IPv4 к виду IPv6 (рис. А.8). На рис. 10.1 приводит- ся подобный пример. type 86dd dport 8888 Рис. 10.1. Сервер IPv6 на узле с двойным стеком, обслуживающий клиенты IPv4 и IPv6 Слева у нас находятся клиент IPv4 и клиент IPv6. Сервер (справа) написан с использованием IPv6 и запущен на узле с двойным стеком. Сервер создал про- слушиваемый ТСР-сокет IPv6, связанный с универсальным адресом IPv6, и порт TCP 8888. Мы считаем, что клиент и сервер находятся в одной сети Ethernet. Они могут быть соединены и через маршрутизаторы, поскольку все маршрутизаторы под- держивают и IPv4, и IPv6, но это ничего не добавляет к данному обсуждению. В разделе Б.З описывается другой случай, когда клиенты и серверы IPv6 соеди- няются через маршрутизаторы, поддерживающие только IPv4. Мы считаем, что оба клиента посылают сегменты SYN для установления со- единения с сервером. Узел клиента IPv4 посылает сегмент SYN и дейтаграмму IPv4, а клиент IPv6 посылает сегмент SYN и дейтаграмму IPv6. Сегмент TCP от клиента IPv4 выглядит в сети как заголовок Ethernet, за которым идет заголовок IPv4, заголовок TCP и данные TCP. Заголовок Ethernet содержит поле типа 0x0800, которое идентифицирует кадр как кадр IPv4. Заголовок TCP содержит
306 Глава 10. Совместимость IPv4 и IPv6 порт получателя 8888 (в приложении А рассказывается более подробно о форма- тах и содержании этих заголовков). IP-адрес получателя в заголовке IPv4, кото- рый мы не показываем, — это 206.62.226.42. Сегмент TCP от клиента IPv6 выглядит в сети как заголовок Ethernet, за ко- торым следует заголовок IPv6, заголовок TCP и данные TCP. Заголовок Ethernet содержит поле типа 0x86dd, которое идентифицирует кадр как кадр IPv6. Заго- ловок TCP имеет тот же формат, что и заголовок TCP в пакете IPv4, и содержит порт получателя 8888. IP-адрес получателя в заголовке IPv6, который мы не по- казываем, будет таким: 5flb:dfOO-сеЗе:е200:20:800:2Ь37:6426. Принимающий канальный уровень просматривает поле типа Ethernet и пере- дает каждый кадр соответствующему модулю IP. Модуль IPv4 (возможно, вмес- те с модулем TCP) определяет, что сокетом получателя является сокет IPv6, и IPv4-адрес отправителя в заголовке IPv4 заменяется на эквивалентный ему ад- рес IPv4, преобразованный к виду IPv6. Этот преобразованный адрес возвраща- ется сокету IPv6 как 1Р\г6-адрес клиента, когда функция accept сервера соединяет- ся с клиентом IPv4. Все оставшиеся дейтаграммы для этого соединения являются дейтаграммами IPv4. Когда функция сервера accept соединяется с клиентом IPv6, клиентский ад- рес IPv6 остается таким же, каким был адрес отправителя в заголовке IPv6. Все оставшиеся дейтаграммы для этого соединения являются дейтаграммами IPv6. Теперь мы можем свести воедино шаги, позволяющие ТСР-клиенту IPv4 со- единяться с сервером IPv6. 1. Сервер IPv6 запускается, создает прослушиваемый сокет IPv6, и мы считаем, что с помощью функции bi nd он связывает с сокетом универсальный адрес. 2. Клиент IPv4 вызывает функцию gethostbyname и находит запись типа А для сервера (см. табл. 9.1). У узла сервера будут записи и типа А, и типа АААА, поскольку он поддерживает оба протокола, но клиент IPv4 запрашивает толь- ко запись типа А. 3. Клиент вызывает функцию connect, и клиентский узел отправляет серверу сег- мент SYN IPv4. 4. Узел сервера получает сегмент SYN IPv4, направленный прослушиваемому сокету IPv6, устанавливает флаг, указывающий, что это соединение исполь- зует адреса IPv4, преобразованные к виду IPv6, и отвечает сегментом IPv4 SYN/АСК. Когда соединение установлено, адрес, возвращаемый серверу функ- цией accept, является адресом IPv4, преобразованным к виду IPv6. 5. Все взаимодействие между клиентом и сервером происходит с использовани- ем дейтаграмм IPv4. 6. Пока сервер точно не определит, является ли данный IPv6-адрес адресом IPv4, преобразованным к виду IPv6 (с использованием макроопределения IN6_IS_ ADDR_V4MAPPED, описанного в разделе 10.4), сервер не знает, что взаимодейству- ет с клиентом IPv4. Двойной стек протоколов решает эту проблему. Анало- гично, клиент IPv4 не знает, что он взаимодействует с сервером IPv6. Главное в данном сценарии то, что узел сервера с двойным стеком имеет и ад- рес IPv4, и адрес IPv6. Этот сценарий будет работать, пока используются адреса IPv4.
10.2. Клиент IPv4, сервер IPv6 307 Сценарий работы UDP-сервера IPv6 аналогичен, но формат адреса может ме- няться для каждой дейтаграммы. Например, если сервер IPv6 получает дейта- грамму от клиента IPv4, адрес, возвращаемый функцией recvfrom, будет адресом IPv4, преобразованным к виду IPv6. Сервер отвечает на запрос клиента, вызывая функцию sendto с адресом IPv4, преобразованным к виду IPv6, в качестве адреса получателя. Формат адреса сообщает ядру, что нужно отправить клиенту дейта- грамму IPv4. Но следующей дейтаграммой, полученной сервером, может быть дейтаграмма IPv6, и функция recvfrom возвратит адрес IPv6. Если сервер отвеча- ет, ядро генерирует дейтаграмму IPv6. На рис. 10.2 показано, как обрабатывается полученная дейтаграмма IPv4 или IPv6 в зависимости от типа принимающего сокета для TCP и UDP. Предполага- ется, что это узел с двойным стеком. Сокеты IPv4 Адрес, возвращенный функцией accept или recvfrom Сокеты IPv6 AF_INET AF_INET SOCK_STREAM SOCK_STREAM sockaddr_in sockaddr_in Рис. 10.2. Обработка полученных дейтаграмм IPv4 или IPv6 в зависимости от типа принимающего сокета Если дейтаграмма IPv4 приходит на сокет IPv4, ничего особенного не проис- ходит. На рисунке изображены две стрелки, помеченные «IPv4», одна для TCP, другая для UDP. Между клиентом и сервером происходит обмен дейтаграм- мами IPv4. & Если дейтаграмма IPv6 приходит на сокет IPv6, ничего особенного не проис- ходит. На рисунке изображены две стрелки, помеченные «IPv6», одна для TCP, другая для UDP. Между клиентом и сервером происходит обмен дейтаграм- мами IPv6.
308 Глава 10. Совместимость IPv4 и IPv6 - Когда дейтаграмма IPv4 приходит на сокет IPv6, ядро возвращает соответ- ствующий адрес IPv4, преобразованный к виду IPv6, в качестве адреса, воз- вращаемого функцией accept (TCP) или recvfrom (UDP). На рисунке это показано двумя штриховыми стрелками. Такое сопоставление возможно, по- скольку адрес IPv4 можно всегда представить как адрес IPv6. Между клиен- том и сервером происходит обмен дейтаграммами IPv4. Обратное неверно: поскольку, вообще говоря, адрес IPv6 нельзя представить как адрес IPv4, на рисунке отсутствуют стрелки от протокола IPv6 к двум со- кетам IPv4. Большинство узлов с двойным стеком должны использовать следующие пра- вила обращения с прослушиваемыми сокетами: 1. Прослушиваемый сокет IPv4 может принимать соединения только от клиен- тов IPv4. 2. Если у сервера есть прослушиваемый сокет IPv6, связанный с универсальным адресом, этот сокет может принимать исходящие соединения как от клиентов IPv4, так и от клиентов IPv6. Для соединения с клиентом IPv4 локальный адрес сервера для соединения будет соответствующим адресом IPv4, преобра- зованным к виду IPv6. 3. Если у сервера есть прослушиваемый сокет IPv6, связанный с адресом IPv6, не являющимся адресом IPv4, преобразованным к виду IPv6, этот сокет мо- жет принимать исходящие соединения только от клиентов IPv6. 10.3. Клиент IPv6, сервер IPv4 Теперь мы поменяем протоколы, используемые клиентом и сервером в примере из предыдущего раздела. Сначала рассмотрим ТСР-клиент IPv6, запущенный на узле с двойным стеком протоколов. 1. Сервер IPv4 запускается на узле, поддерживающем только IPv4, и создает прослушиваемый сокет IPv4. 2. Запускается клиент IPv6 и вызывает функцию gethostbyname, запрашивая толь- ко адреса IPv6 (включает параметр RESUSEINET6). Поскольку у сервера, поддерживающего только IPv4, есть лишь записи типа А, мы видим, согласно табл. 9.1, что клиенту возвращается адрес IPv4, преобразованный к виду IPv6. 3. Клиент IPv6 вызывает функцию connect с адресом IPv4, преобразованным к ви- ду IPv6, в структуре адреса сокета IPv6. Ядро обнаруживает преобразован- ный адрес и автоматически посылает серверу сегмент SYN IPv4. 4. Сервер отвечает сегментом SYN/ACK IPv4, и устанавливается соединение, по которому происходит обмен дейтаграммами IPv4. Этот сценарий мы схематически изображаем на рис. 10.3. Если ТСР-клиент IPv4 вызывает функцию connect, задавая адрес IPv4, или если UDP-клиент IPv4 вызывает функцию sendto, задавая адрес IPv4, ничего особенного не происходит. На рисунке это изображено двумя стрелками, по- меченными «IPv4».
10.3. Клиент IPv6, сервер IPv4 309 Если ТСР-клиент IPv6 вызывает функцию connect, задавая адрес IPv6, или если UDP-клиент IPv6 вызывает функцию sendto, задавая адрес IPv6, ничего особенного не происходит. На рисунке это показано двумя стрелками, поме- ченными «IPv6». Сокеты IPv4 Сокеты IPv6 Адрес, возращенный функцией connect или sendto AF INET AF_INET Дейтаграмма IPv4 ' Дейтаграмма IPv6 Рис. 10.3. Обработка клиентских запросов в зависимости от типа адреса и типа сокета Если ТСР-клиент IPv6 вызывает функцию connect, задавая адрес IPv4, преоб- разованный к виду IPv6, или если UDP-клиент вызывает функцию sendto, за- давая адрес IPv4, преобразованный к виду IPv6, ядро обнаруживает сопостав- ленный адрес и инициирует отправку дейтаграммы IPv4 вместо дейтаграммы IPv6. На рисунке это показано двумя штриховыми стрелками. Клиент IPv4 не может задать адрес IPv6 ни функции connect, ни функции sendto, поскольку 16-байтовый адрес IPv6 не соответствует 4-байтовой структуре in_addr в структуре IPv4 sockaddr ш. Следовательно, на рисунке нет стрелок от сокетов IPv4 к протоколу IPv6. В предыдущем разделе (дейтаграмма IPv4, приходящая для сокета сервера IPv6) преобразование полученного адреса IPv4k виду IPv6 выполняется ядром, и результат прозрачно (то есть незаметно для приложения) возвращается прило- жению функцией accept или recvfrom. В этом разделе (если необходимо отпра- вить дейтаграмму IPv4 на сокете IPv6) преобразование адреса IPv4 к виду IPv6 выполняется распознавателем в соответствии с правилами, представленными в табл. 9.1, и затем преобразованный адрес прозрачно передается приложению функцией connect или sendto. >> ... . . , .
310 Глава 10 Совместимость IPv4 и IPv6 Резюме: совместимость IPv4 и IPv6 Таблица 10.2, содержащая сочетания клиентов и серверов, подводит итог обсуж- дению, проведенному в данном и предыдущем разделах. Таблица 10.2. Обобщение совместимости клиентов и серверов IPv4 и IPv6 Сервер IPv4, узел только IPv4 (только А) Сервер IPv4, узел только IPv6 (только АААА) Сервер IPv4, узел с двой- ным стеком (А и АААА) Сервер IPv6, узел с двойным стеком (А и АААА) Клиент IPv4, узел только IPv4 IPv4 Her IPv4 IPv4 Клиент IPv6, узел только IPv6 Нет IPv6 - Нет IPv6 Клиент IPv4, узел с двойным стеком IPv4 Нет IPv4 IPv4 Клиент IPv6, узел с двойным стеком IPv4 ' IPv6 Нет* IPv6 Каждая ячейка этой таблицы содержит поля «IPv4» или «IPv6» с указанием используемого протокола, если данное сочетание работает, либо «нет», если ком- бинация недопустима. Ячейка в последней строке третьей колонки отмечена звез- дочкой, поскольку совместимость зависит от адреса, выбранного клиентом. При выборе записи типа АААА отправка дейтаграммы IPv6 будет невозможна. Но выбор записи типа А, которая возвращается клиенту как адрес IPv4, преобразо- ванный к виду IPv6, приведет к отправке дейтаграммы IPv4. Хотя четверть из представленных в таблице сочетаний недопустима, в обо- зримом будущем большинство реализаций IPv6 будут использоваться на узлах с двойным стеком протоколов и будут поддерживать не только IPv6. Если мы удалим из таблицы вторую строку и вторую колонку, все записи «Нет» исчезнут и единственной проблемой останется запись, помеченная звездочкой. 10.4. Макроопределения проверки адреса IPv6 Существует небольшой класс приложений IPv6, которые должны знать, с каким собеседником они взаимодействуют (IPv4 или IPv6). Эти приложения должны знать, является ли адрес собеседника адресом IPv4, преобразованным к виду IPv6. Определены двенадцать макросов, проверяющих некоторые свойства адреса IPv6. #include <netinet/in h> int IN6_IS_ADDR_UNSPECIFIED(const struct in6_addr *aptr'). int IN6_IS_ADDR_L00PBACK(const struct in6_addr *aptr): int IN6_IS_ADDR_MULTICAST(const struct in6_addr *aptr): int IN6_IS_ADDR_LINKL0CAL(const struct in6_addr *aptr). int IN6_IS_ADDR_SITEL0CAL(const struct in6_addr *aptr) int IN6_IS_ADDR_V4MAPPED(const struct in6_addr *aptr') int IN6_IS_ADDR_V4C0MPAT(const struct in6_addr *aptr) int IN6_IS_ADDR_MC_N0DEL0CAL(const struct in6_addr ★aptr)
10 5 Параметр сокета IPv6 ADDRFORM 311 int IN6_IS_ADDR_MC_LINKL0CAL(const struct in6_addr *aptr) int IN6_IS_ADDR_MC_SITEL0CAL(const struct in6_addr *aptr), int IN6_IS_ADDR_MC_0RGLDCAL(const struct in6_addr *aptr) int IN6_IS_ADDR_MC_GLDBAL(const struct in6_addr *aptr) Все возвращают ненулевое значение если адрес IPv6 имеет указанный тип 0 в противном случае Первые семь макросов проверяют базовый тип адреса IPv6. Мы покажем раз- личные типы адресов в разделе А.5. Последние пять макросов проверяют область действия адреса многоадресной передачи IPv6 (см. раздел 19.2). Клиент IPv6 может вызвать макрос IN6_IS_ADDR_V4MAPPED для проверки адреса IPv6, возвращенного распознавателем. Сервер IPv6 может вызвать этот макрос для проверки адреса IPv6, возвращенного функцией accept или recvfrom. Как пример приложения, которому нужен этот макрос, можно привести FTP и его команду PORT. Если мы запустим FTP-клиент, зарегистрируемся на FTP- сервере и выполним команду FTP di г, FTP-клиент пошлет команду PORT FTP- серверу через управляющее соединение. Она сообщит серверу IP-адрес и порт клиента, с которым затем сервер создаст соединение. (В главе 27 [94] содержатся подробные сведения о протоколе приложения FTP ) Но FTP-клиент IPv6 дол- жен знать, с каким сервером имеет дело — IPv4 или IPv6, поскольку сервер IPv4 требует команду в формате PORT al а2. аЗ. а4 pl р2 (где первые четыре числа, каждое от 0 до 255, формируют 4-байтовый адрес IPv4, а два последних — 2-бай- товый номер порта), а серверу IPv6 необходима команда LPRT (RFC 1639 [78]), содержащая 21 число. В упражнении 10.1 приводятся примеры использования обеих команд. 10.5. Параметр сокета IPv6_ADDRFORM Параметр сокета IР v6_ADDRF0RM может менять один тип сокета на другой. При этом действуют следующие ограничения: 1. Сокет IPv4 можно всегда поменять на сокет IPv6. Любые адреса IPv4, уже связанные с сокетом, преобразуются к виду IPv4. 2. Сокет IPv6 можно поменять на сокет IPv4, только если любые адреса, уже связанные с сокетом, являются адресами IPv4, преобразованными к виду IPv6. Причиной для изменения формата адреса сокета является то, что в Unix де- скрипторы можно легко передавать между процессами. Самый типичный способ сделать это — использовать функцию fork, но мы увидим в разделе 14.7, как де- скриптор можно передать между связанными процессами, а в разделе 25.7 — меж- ду несвязанными. В качестве примера рассмотрим процесс, создающий прослушиваемый сокет IPv4 и затем принимающий соединение от клиента IPv4. Этот сервер вызывает функции fork и ехес, запуская новую программу для обработки запроса клиента. Предположим, что в этом приложении действует соглашение, по которому при- соединенный сокет передается новой программе как стандартные потоки ввода, вывода и ошибок (аналогично тому, что делает функция inetd, см. раздел 12.5). Мы можем использовать псевдокод, показанный в листинге 10 1. Единственным отличием от нашего параллельного сервера, описанного в разделе 4.8, будет дуб- лирование присоединенного сокета на согласованных дескрипторах с последую- щим вызовом функции ехес.
312 Глава 10. Совместимость IPv4 и IPv6 Но программа, выполняемая с помощью функции ехес, предполагает исполь- зование сокета IPv6. Для преобразования формата адреса сокета мы можем ис- пользовать параметр сокета IPV6_ADDRF0RM, как показано в листинге 10.21. Листинг 10.1. Сервер, принимающий входящее соединение и выполняющий новую программу int listenfd. connfd. socklen_t clilen. struct sockaddr_in serv. cli. /* структуры IPv4 */ listenfd = Socket(AF_INET, SOCK_STREAM. 0). /* сокет IPv4 */ /* вписываем в serv{} номер заранее известного порта */ * Binddistenfd Sserv. sizeof(serv)): . Listen(listenfd. LISTENQ). for ( . . ) { clilen = sizeof(cli), connfd = Accept!listenfd. &cli. &clilen) if (Fork!) == 0) { Closedistenfd). /* дочерний процесс */ ' Dup2(connfd, STDIN_FILENO). Dup2(connfd. STDOUT_FILENO). Dup2(connfd. STDERR_FILENO). H Close(connfd). Exec! ). /* начинается новая программа */ } Close(connfd). /* родитель */ } Листинг 10.2. Преобразование сокета IPv4 в сокет IPv6 int af. socklen_t clilen, struct sockaddr_in6 cli, /* IPv6 */ struct hostent *ptr. af = AFJNET6. Setsockopt(STDIN_FILENO. IPPR0TDJPV6. IPV6_ADDRF0RM. &af. sizeof(af)): clilen = sizeof(cli) Getpeername!!) Sell &clilen), ptr = gethostbyaddr(&cli sin6_addr. 16. AF_INET6), Вызов функции setsockopt изменяет формат адреса сокета от IPv4 к IPv6, а вы- зов функции getpeername возвращает адрес IPv4, преобразованный к виду IPv6, если сокет являлся сокетом IPv4. Но если эта программа выполняется с сокетом IPv6 на стандартном потоке ввода, при вызове функции setsockopt ничего не про- изойдет, так как адрес уже будет представлен в формате IPv6. Этот параметр сокета можно использовать, например, в том случае, когда про- грамма, принимающая входящее соединение IPv4, предоставляется откуда-то извне (например, у нас нет исходного кода и мы не можем изменить программу Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// v. \\ w.piter com/download
10.6. Переносимость исходного кода 313 так, чтобы она использовала IPv6, или, еще лучше, чтобы она стала пе зависящей от протокола), но наша программа, выполняемая с помощью функции ехес, обра- батывает IPv6. Если функция getsockopt вызывается для получения значения параметра I PV6_ ADDRFORM, то в зависимости от формата адреса сокета будет возвращаться либо AF_INET, либо AF_INET6. Вторым аргументом функции getsockopt или setsockopt мо- жет быть либо IPPROTO_IP, либо IPPR0T0_IPV6. 10.6. Переносимость исходного кода Большинство существующих сетевых приложений написаны для IPv4. Структу- ры sockaddr_in размещаются в памяти и заполняются, а функция socket задает AF_INET в качестве первого аргумента. При переходе от листинга 1.1 клистипгу 1.2 мы видели, что эти приложения IPv4 можно преобразовать в приложения IPv6 без особых усилий. Многие показанные нами изменения можно выполнить авто- матически, используя некоторые сценарии редактирования. Программы, более зависящие от IPv4, использующие такие свойства, как многоадресная передача, параметры IP или символьные (неструктурированные) сокеты, потребуют боль- ших усилий при преобразовании. Если мы преобразуем приложение для работы с IPv6 и распространим его ис- ходный код, нам придется думать о том, поддерживает ли принимающая система протокол IPv6. Типичный способ решения этой проблемы — применять в коде #i fdef, используя по возможности IPv6 (поскольку мы видели в этой главе, что клиент IPv6 может взаимодействовать с серверами IPv4 и наоборот). Проблема такого подхода в том, что код очень быстро становится «замусорен» директивами #i fdef, и его сложнее отслеживать и обслуживать. Наилучшим подходом будет рассмотрение перехода на IPv6 как возможности сделать программу не зависящей от протокола. Первым шагом здесь будет удале- ние вызовов функций gethostbyname и gethostbyaddr и использование функций getaddrinfo и getnameinfo, которые мы опишем в следующей главе. Эго позволит нам обращаться со структурами адресов сокетов как с непрозрачными объекта- ми, ссылаться на которые можно с помощью указателя и размера, что как раз и вы- полняют основные функции сокетов: bind, connect, recvfrom и т. д. Наши функции sock_XXX из раздела 3.8 помогут работать с ними независимо от IPv4 и IPv6. Оче- видно, эти функции содержат #i fdef для работы с IPv4 и IPv6, но если мы скроем эту зависимость от протокола в нескольких библиотечных функциях, наш код станет проще. В разделе 19.6 мы разработаем ряд функций mcast XXX, которые помогут сделать приложения многоадресной передачи не зависящими от версии протокола IP. Другой момент, который нужно учесть, — что произойдет, если мы откомпи- лируем наш исходный код в системе, поддерживающей и IPv4, и IPv6, затем рас- пространим либо исполняемый код, либо объектные файлы (но не исходный код), и кто-то запустит паше приложение в системе, пе поддерживающей IPv6. Есть вероятность, что сервер локальных имен поддерживает записи типа АААА и воз- вращает как записи типа АААА, так и записи типа А некоему собеседнику, с кото- ром пытается соединиться наше приложение. Если наше приложение, работаю- щее с IPv6, вызовет функцию socket для создания сокета IPv6, она не будет
314 Глава 10. Совместимость IPv4 и IPv6 работать, если узел не поддерживает IPv6. Мы решаем этот вопрос с помощью функций, описанных в следующей главе, игнорируя ошибку функции socket и пы- таясь использовать следующий адрес в списке, возвращаемом сервером имен. Если предположить, что у собеседника имеется запись типа А и что сервер имен воз- вращает запись типа А в дополнение к любой записи типа АААА, то сокет IPv4 успешно создастся. Этот тип функциональности имеется в библиотечной функ- ции, но не в исходном коде каждого приложения. 10.7. Резюме Сервер IPv6 на узле с двойным стеком протоколов может предоставлять сервис как клиентам IPv4, так и клиентам IPv6. Клиент IPv4 посылает серверу дейта- граммы IPv4, но стек протоколов сервера преобразует адрес клиента к виду IPv6, поскольку сервер IPv6 работает со структурами адресов сокетов IPv6. Аналогично, клиент IPv6 на узле с двойным стеком протоколов может взаи- модействовать с сервером IPv4. Распознаватель клиента возвращает адреса IPv4, преобразованные к виду IPv6, для всех записей сервера типа А, и вызов функции connect для одного из этих адресов приводит к тому, что двойной стек посылает сегмент SYN IPv4. Только отдельным специальным клиентам и серверам необхо- димо знать протокол, используемый собеседником (например, FTP), и чтобы опре- делить, что собеседник использует IPv4, можно использовать макрос IN6_IS_ ADDR_V4MAPPED. Параметр сокета IPV6_ADDRF0RM может использоваться программой, которая предполагает наличие одного типа сокета (обычно сокета IPv6). Упражнения 1. Запустите FTP-клиент IPv6 на узле с двойным стеком протоколов. Соедини- тесь с FTP-сервером IPv4, запустите команду debug, а затем команду di г. Да- лее выполните те же операции, но для сервера IPv6, и сравните команды PORT, являющиеся результатом выполнения команд di г. 2. Напишите программу, требующую ввода одного аргумента командной стро- ки, который является адресом IPv4 в точечно-десятичной записи. Создайте ТСР-сокет IPv4 и свяжите этот адрес и некоторый порт, например 8888, с со- кетом при помощи функции bind. Вызовите функцию listen, а затем pause. Напишите аналогичную программу, которая в качестве аргумента командной строки принимает шестнадцатеричную строку IPv6 и создает прослушивае- мый ТСР-сокет IPv6. Запустите программу IPv4, задав в качестве аргумента универсальный адрес. Затем перейдите в другое окно и запустите программу IPv6, задав в качестве аргумента универсальный адрес IPv6. Можете ли вы запустить программу IPv6, если программа IPv4 уже связана с этим портом? Появляется ли разница при использовании параметра сокета SGJTUSEADDR? Что будет, если вы сначала запустите программу IPv6, а затем попытаетесь запус- тить программу IPv4?
ГЛАВА 11 Дополнительные преобразования имен и адресов 11.1. Введение Функции gethostbyname и gethostbyaddr, описанные в главе 9, являются зависящи- ми от протокола. При использовании первой необходимо знать, в какой элемент структуры адреса сокета переместить результат (например, элемент sin_addr для IPv4 или элемент sin6_addr для IPv6), а при вызове последней — знать, какой из элементов содержит двоичный адрес. Эта глава начинается с описания новой функции Posix.lg getaddrinfo, которая обеспечивает независимость наших при- ложений от протокола. Функцию getnameinfo, являющуюся ее дополнением, мы рассмотрим в этой главе. Далее мы будем использовать эту функцию, а также разработаем шесть наших собственных функций, обрабатывающих типичные сценарии для клиентов и сер- веров TCP и UDP. В оставшейся части книги вместо непосредственного вызова функции getaddrinfo мы будем использовать эти функции. Функции gethostbyname и gethostbyaddr — это также замечательные примеры функций, не допускающих повторное вхождение (nonreentrant). Мы покажем, почему это так, и опишем некоторые функции, позволяющие обойти эту пробле- му. Вхождение — это проблема, к которой мы вернемся в главе 23, но мы можем описать и объяснить эту проблему' уже сейчас, так как для этого нет необходимо- сти понимать детали работы с потоками. Глава заканчивается примером полной реализации нашей функции getaddri nfo, что позволит больше узнать об этой функции — как она работает, что возвращает и как взаимодействует с IPv4 и IPv6. 11.2. Функция getaddrinfo Функция getaddrinfo скрывает все зависимости от протокола в библиотечной функции. Приложение работает только со структурами адресов сокетов, запол- няемых с помощью функции getaddrinfo. Эта функция определяется в Posix. 1g. #include <netdb h> int getaddrinfo(const char *hostname, const char *service.
316 Глава 11. Дополнительные преобразования имен и адресов const struct addrinfo khmts. struct addrinfo **result). Возвращает 0 в случае успешного выполнения ненулевое значение в случае ошибки (см табл 11 2) ПРИМЕЧАНИЕ ------------------------------------------------------------- Определение этой функции в Posix. 1g происходит от более раннего предложения Кей- та Склоуэра (Keith Sklower) для функции, называемой getconnmfo. Эта функция стала результатом обсуждений с Эриком Олменом (Eric Allman), Вилльямом Дастом (William Durst), Майклом Карелсом (Michael Karels) и Стивеном Вайсом (Steven Wise), а так- же более ранней реализации, написанной Эриком Олменом. Замечание о том, что указа- ния имени узла и имени службы достаточно для соединения с этой службой независимо от деталей протокола, было сделано Маршалом Роузом (Marshall Rose) в проекте X/Open. Через указатель resul t функция возвращает указатель на связный список струк- тур addrinfo, который задается путем включения заголовочного файла <netdb h>: struct addrinfo { int a i _flags, /* AI-PASSIVE. AI_CANONNAME */ int ai_fannly. /* AF_xxx */ int ai_socktype. /* S0CK_xxx */ int ai_protocol. /* 0 или IPPROTO-Xxx для IPv4 и IPv6 */ size_t ai_addrlen. /* длина ai_addr */ char *ai_canonname: /* указатель на каноническое имя узла */ struct sockaddr *ai_addr, /* указатель на структуру адреса сокета */ struct adorinfo *ai_next. /* указатель на следующую структуру в связном списке */ Переменная hostname — это либо имя узла, либо строка адреса (точечно-деся- тичная запись для IPv4 или шестнадцатеричная строка для IPv6). Переменная serv 1 се — это либо имя службы, либо строка, содержащая десятичный номер пор- та. (Вспомните наше решение упражнения 9.6, где мы допускаем строку с адре- сом для узла или строку с номером порта для службы.) Структура hints — это либо пустой указатель, либо указатель на структуру addri nfo, заполненную рекомендациями вызывающего процесса о типах инфор- мации, которую он хочет получить. Например, если заданная служба предостав- ляется и для TCP, и для UDP (например, служба domain, которая ссылается на сервер DNS), вызывающий процесс может присвоить элементу ai_socktype струк- туры hi nts значение SOCK DGRAM. Тогда возвращение информации будет иметь ме- сто только для дейтаграммных сокетов. Вызывающим процессом могут быть изменены значения следующих элемен- тов структуры hints: - ai_flags (AI_PASSIVE, A1_CANONNAME); ai_family (значение AF_xxx); ai_socktype (значение SDCK_xxx); ai_protocol. Флаг AI_PASSIVE указывает, что сокет будет использоваться для пассивного открытия, а флаг AI_CANDNNAME указывает функции на необходимость возвратить каноническое имя узла. Если аргументом структуры hints является пустой указатель, функция подра- зумевает нулевое значение для ai_flags, ai_socktype и ai_protocol и значение AF_UNSPEC для ai_fami 1у.
11.2. Функция getaddrinfo 317 Если функция завершается успешно (0), то в переменную, на которую указы- вает аргумент result, записывается указатель на связанный через указатель а i next список структур addri nfo. Имеется два способа возвращения множественных структур. 1. Если существует множество адресов, связанных с именем узла, то одна струк- тура возвращается для каждого адреса, который может использоваться с за- прашиваемым семейством адресов (значение ai_fann 1 у, если задано). 2. Если служба предоставляется для множества типов сокетов, то одна структура может быть возвращена для каждого типа сокета в зависимости от а i socktype. Например, если структура hints пуста и если в службе domain осуществляется поиск узла с двумя IP-адресами, возвращаются четыре структуры addri nfo: • одна для первого IP-адреса и типа сокета SOCK_STREAM; < одна для первого IP-адреса и типа сокета SOCKDGRAM; < одна для второго IP-адреса и типа сокета SOCK_STREAM; ' одна для второго IP-адреса и типа сокета SOCK_DGRAM. Мы показываем схематическое изображение этого примера па рис. 11.1. Не существует никакого гарантированного порядка структур при возвращении множества элементов. Например, мы не можем считать, что службы TCP возвра- щаются перед службами UDP. ПРИМЕЧАНИЕ ------------------------------------------------------------ Хотя это и не гарантируется, реализация должна возвращать I P-адреса в том же поряд- ке, в котором они возвращаются DNS. Например, многие серверы DNS сортируют воз- вращаемые адреса так, что если узел, отправляющий запрос, и сервер имен находятся в одной сети, то адреса в этой совместно используемой сети возвращаются первыми. Также более новые версии BIND позволяют распознавателю задавать порядок сорти- ровки адресов в файле /etc/rcsoiv.conf. Информация, возвращаемая структурами addri nfo, готова для вызова функ- ции socket и последующего вызова функций connect или sendto (для клиента) и bi nd (для сервера). Аргументы функции socket — это элементы ai family, ai socktype и а 1 _protocol. Второй и третий аргументы функций connect и bi nd — это элементы ai addr (указатель на структуру адреса сокета соответствующего типа, заполняе- мую функцией getaddrinfo) и ai addrlen (длина этой структуры адреса сокета). Если в структуре hi nts установлен флаг AI_CANONNAME, элемент ai_canonname пер- вой возвращаемой структуры указывает на каноническое имя узла. В терминах DNS это обычно полное доменное имя (FQDN). На рис. 11.1 представлена возвращаемая информация для случая, если мы выполняем следующее: struct addrinfo hints. *res. bzero(&hints. sizeof(hints)) hints ai_flags = AI_CANONNAME. hints ai_family = AF_INET. getaddrinfoCbsdi". "domain". &hints. &res). На этом рисунке все, кроме переменной res, относится к динамически вы- деляемой памяти (например, с помощью функции та 11 ос). Предполагается, что
318 Глава 11. Дополнительные преобразования имен и адресов addrinfо(} addrinfo{} 16,AF INET,53 206.62.226.35 addrxnfо{} 16,AF_INET, 53 206.62.226.35 16,AF_INET, 53 206.62.226.35 Рис. 11.1. Пример информации, возвращаемой функцией getaddrinfo
11.2. Функция getaddrinfo 319 каноническое имя узла bsdi — bsdi .kohala com, и что этот узел имеет два адреса IPv4 в DNS (см. рис. 1.7). Порт 53 предназначен для службы domain, и нужно учитывать, что этот номер порта будет представлен в структурах адресов сокетов в сетевом порядке байтов. Возвращаемые значения ai_protocol нулевые, поскольку комбинация элементов ai_fami 1у и ai_socktype полностью задает протокол для TCP и UDP. Вполне допу- стимо, если функция getaddri nfo возвратит переменную ai_protocol со значением IPPROTO_TCP для двух структур SOCK_STREAM и со значением I PPROTO_UDP для двух структур SOCK_DGRAM. В табл. 11.1 показано число структур addri nfo для каждого возвращаемого ад- реса, определяемое на основе заданного имени службы (которое может быть пред- ставлено десятичным номером порта) и рекомендации ai_socktype. Таблица 11.1. Число структур addrinfo, возвращаемых для каждого IP-адреса Элемент ai_sock- type Служба обозначена именем и предо- ставляется только TCP Служба обозначена именем и предо- ставляется только UDP Служба обозначена Служба именем и предо- ставляется TCP и UDP обозначена именем порта 0 1 1 2 2 SOCK_ STREAM 1 Ошибка 1 1 SOCK_ DGRAM Ошибка 1 1 1 Более одной структуры addri nfo возвращается для каждого IP-адреса только в том случае, когда поле ai_socktype структуры hints пусто и либо служба под- держивается TCP и UDP (как указано в файле /etc/services), либо задан номер порта для этой службы. Если бы мы рассматривали все 64 возможных варианта сочетаний входных данных для функции getaddrinfo (имеется шесть входных переменных), многие сочетания оказались бы недопустимыми, а некоторые не имели бы смысла. Вме- сто этого рассмотрим наиболее типичные случаи. Задание имени узла и службы. Это традиционный случай для клиента TCP и UDP. По завершении клиент TCP перебирает в цикле все возвращаемые IP- адреса, вызывая функции socket и connect для каждого из них, пока не устано- вится соединение или пока не будут перебраны все адреса. Мы показываем такой пример с нашей функцией tcp_connect в листинге 11.2. Для клиента UDP структура адреса сокета, заполняемая с помощью функции getaddrinfo, будет использоваться в вызове функции sendto или connect. Если клиент сообщит, что первый адрес не работает (ошибка на присоединенном сокете UDP или тайм-аут на неприсоединенном сокете), будет придпринята попытка обратиться к другому адресу. Если клиент знает, что он обрабатывает только один тип сокета (например, клиентами Telnet и FTP обрабатываются только сокеты TCP, а клиентами TFTP — только сокеты UDP), то элементу ai_socktype структуры hints долж- но быть задано соответственно либо значение SOCK_STREAM, либо значение SOCK_ DGRAM.
320 Глава 11. Дополнительные преобразования имен и адресов Типичный сервер задает службу, но не имя узла, и задает флаг AI_PASSIVE в стру- ктуре hints. Возвращаемая структура адреса сокета должна содержать IP-ад- рес, равный INADDR_ANY (для IPv4) или IN6ADDR ANY INIT (для IPv6). Сервер TCP затем вызывает функции socket, bind и listen. Если сервер хочет разместить в памяти с помощью функции mal 1 ос другую структуру адреса сокета, чтобы получить адрес клиента из функции accept, то возвращаемое значение ai_addrl еп задает требуемый для этого размер. Сервер UDP вызовет функции socket, bi nd и затем recvfrom. Если сервер хочет разместить в памяти с помощью функции mal 1 ос другую структуру адреса сокета, чтобы получить адрес клиента из функ- ции recvfrom, возвращаемое значение ai_addrlen также задает нужный размер. Как и в случае типичного клиентского кода, если сервер знает, что он обраба- тывает только один тип сокета, то элемент ai socktype структуры hi nts должен быть задан либо как SOCK STREAM, либо как SOCK DGRAM. Это позволяет избежать возвращения множества структур, с (возможно) неверным значением элемента ai_socktype. < До сих пор мы демонстрировали серверы TCP, создающие один прослушива- емый сокет, и серверы UDP, создающие один сокет дейтаграмм. Это тот вари- ант, который подразумевался в предыдущем абзаце. Альтернативным устрой- ством является сервер, который обрабатывает множество сокетов с помощью функции sei ect. В этом сценарии сервер должен последовательно перебрать все структуры из списка, возвращаемого функцией getaddrinfo, создать по од- ному сокету для каждой структуры и вызвать функцию sei ect. ПРИМЕЧАНИЕ --------------------------------------------------------- Проблема этой технологии состоит в том, ч го условие, по которому функция getaddrinfo возвращает множество структур, возникает, когда служба может обрабатываться как протоколом IPv4, так и протоколом IPv6 (табл. 11.3). Но эти два протокола не полностью независимы, как мы увидели в разделе 10.2, то есть если мы создаем про- слушиваемый сокет IPv6 для данного порта, нет необходимости создавать для пего прослушиваемый сокет IPv4, поскольку соединения, приходящие от клиентов IPv4, автоматически обрабатываются стеком протоколов и прослушиваемым сокетом IPv6. Невзирая на тот факт, что функция getaddrinfo «лучше», чем функции get- hostbyname и gethostbyaddr (помимо того, что эта функция упрощает написание кода, не зависящего от протокола, она обрабатывает и имя узла, и имя службы, и к тому же вся возвращаемая ею информация размещается в памяти динамичес- ки, а не статически), ее все же не так просто использовать, как это могло пока- заться. Проблема в том, что нам требуется разместить в памяти структуру mnts, инициализировать ее нулем, заполнить необходимые поля, вызвать функцию getaddrinfo и затем пройти весь связный список, проверяя каждый его элемент. В последующих разделах мы предоставим более простые интерфейсы для типич- ных клиентов TCP и UDP и серверов, которые будем создавать в оставшейся части книги. Функция getaddrinfo решает проблему преобразования имен узлов и имен служб в структуры адресов сокетов. В разделе 11.13 мы опишем обратную функ- цию getnameinfo, которая преобразует структуры адресов сокетов в имена узлов и имена служб. В разделе 11.16 мы покажем реализацию функций getaddrinfo, getnameinfo и freeaddrinfo.
___________________________________________11.4. Функция freeaddrmfo 321 11.3. Функция gai_strerror Ненулевые значения ошибок, возвращаемых из функции getaddrinfo, имеют на- звания и значения, показанные в табл. 11.2. Функция gai_strerror получает одно из этих значений в качестве аргумента и возвращает указатель на соответствую- щую строку с описанием ошибки. include <netdb h> char *gai_strerror(int error). Возвращает указатель на строку с описанием ошибки Таблица 11.2. Ненулевые возвращаемые значения (константы) ошибок функции getaddrinfo Константа Описание EAIADDRFAMILY Семейство адресов не поддерживается для данного имени узла EAIAGAIN Временный сбой при попытке разрешения имен EAI_BADFLAGS Недопустимое значение ai flags EAIFAIL Неисправимая ошибка при разрешении имен EAI_FAMILY Семейство aifamily пе поддерживается EAI_MEMORY Ошибка при выделении памяти EAI_NODATA С именем узла пе ассоциирован адрес EAINONAME Имя узла или имя службы неизвестны или равны NULL EAISERVICE Запрошенная служба не поддерживается для данного типа сокета aisocktype EAISOCKTYPE Тип сокета ai socktype не поддерживается EAISYSTEM Другая системная ошибка, возвращаемая в переменной еггпо 11.4. Функция freeaddrinfo Вся память, занимаемая структурами addrinfo, структурами ai_addr и строкой ai_canonname, которые возвращаются функцией getaddri nfo, динамически выделя- ется функцией ma 11 ос. Эта память освобождается при вызове функции f reeaddri nfo. include <netdb h> void freeaddrinfo(struct addrinfo *ai) Переменная ai должна указывать на первую из структур addrinfo, возвращае- мых функцией getaddri nfo. Освобождается вся область памяти, занятая структу- рами из связного списка, вместе с динамически выделенной областью памяти, содержащей данные, на которые указывают эти структуры (например, структу- ры адресов сокетов и канонические имена узлов). Предположим, что мы вызываем функцию getaddrinfo, проходим последова- тельно по всему связному списку структур addri nfo и находим нужную структуру. Если далее мы попытаемся сохранить нужную нам информацию простым копи- рованием структуры addrinfo, а затем вызовем функцию freeaddrinfo, мы полу- чим скрытую ошибку. Причина в том, что структура addri nfo сама указывает на динамически выделенный участок памяти (для структуры адреса сокета и, воз- можно, для канонического имени). Но эта область памяти, на которую указывает сохраненная нами структура, при вызове функции freeaddrinfo освобождается и может использоваться для хранения какой-либо иной информации.
322 Глава 11. Дополнительные преобразования имен и адресов ПРИМЕЧАНИЕ------------------------------------------------------- Создание копии только самой структуры addrinfo, а не структур, на которые она, в свою очередь, указывает, называется поверхностным копированием (shallow сору). Копиро- вание структуры addrinfo и всех структур, на которые она указывает, называется де- тальным копированием (deep сору). 11.5. Функция getaddrinfo: IPv6 и доменный сокет Unix Хотя стандарт Posix.lg определяет функцию getaddrinfo, в нем ничего не гово- рится об IPv6. Взаимодействие между этой функцией, распознавателем (особен- но параметром RES_USE_INET6, вспомните табл. 9.1) и IPv6 нетривиально. Следует отметить следующие моменты, прежде чем свести воедино эти виды взаимодей- ствия в табл. 11.3. - Входные данные функции getaddrinfo могут относиться к двум различным типам, которые выбираются в зависимости от того, какой тип структуры адре- са сокета вызывающий процесс хочет получить обратно и какой тип записей нужно искать в DNS. f Семейством адресов, указанным вызывающим процессом в структуре hints, задается тип структуры адреса сокета, который вызывающий процесс предпо- лагает получить. Если вызывающий процесс задает AF_INET, функция не долж- на возвращать структур sockaddr_in6, а если вызывающий процесс задает AF_ INET6, функция не должна возвращать структур sockaddr_in. S- В Posix.lg сказано, что при задании AFJJNSPEC должны возвращаться адреса, которые могут использоваться слюбым семейством протоколов, допускающим применение имени узла и имени службы. Это подразумевает, что если у узла имеются как записи типа АААА, так и записи типа А, то записи типа АААА возвращаются как структуры sockaddr_in6, а записи типа А — как структуры sockaddr_in. Нет смысла возвращать еще и записи типа А как адреса IPv4, пре- образованные к виду IPv6, в структурах sockaddr_in6, потому что при этом не возвращается никакой дополнительной информации — эти адреса уже возвра- щены в структурах sockaddr_in. $ Это утверждение Posix.lg также подразумевает, что если флаг AI_PASSIVE за- дан без имени узла, то должен быть возвращен универсальный адрес IPv6 (IN6ADDR_ANY_INIT или 0::0) в структуре sockaddr_in6 вместе с универсальным адресом IPv4 (INADDR_ANY или 0.0.0.0) в структуре sockaddr_in. Также нет смыс- ла возвращать сначала универсальный адрес IPv6, поскольку мы видели в раз- деле 10.2, что на узле с двойным стеком сокет сервера IPv6 может обрабаты- вать и клиенты IPv4, и клиенты IPv6. * Параметр распознавателя RESJJSE_INET6 вместе с какой-либо из вызываемых функций (gethostbyname или gethostbyname?) задает тип записей, поиск кото- рых ведется в DNS (тип А или тип АААА), и тип возвращаемых адресов (IPv4, IPv6 или IPv4, преобразованные к виду IPv6). Мы обобщили это в табл. 9.1. s Имя узла может также быть либо шестнадцатеричной строкой IPv6, либо стро- кой в точечно-десятичной записи IPv4. Допустимость этой строки зависит от
11.5. Функция getaddrinfo: IPv6 и доменный сокет Unix 323 семейства адресов, заданного вызывающим процессом. Шестнадцатеричная строка IPv6 неприемлема, если задано AF_INET, а строка в точечно-десятичной записи IPv4 неприемлема, если задано AF_INET6. Но если задано семейство AFJJNSPEC, то допустимы оба варианта, и при этом возвращается соответствую- щий тип структуры адреса сокета. ПРИМЕЧАНИЕ------------------------------------------------------------ Можно возразить, что если в качестве семейства протоколов задано AF_INET6, строка в точечно-десятичной записи должна возвращаться как адрес IPv4, преобразованный к виду IPv6 в структуре sockaddr_in6. Но другим способом получения этого результа- та является установка префикса строки с десятичной точкой O::ffff:. В табл. 11.3 показано, как будут обрабатываться адреса IPv4 и IPv6 функцией getaddrinfo. Колонка «Результат» отражает то, что мы хотим возвратить вызыва- ющему процессу, если входные переменные таковы, как показано в первых трех колонках. Колонка «Действия» — то, каким образом мы получаем этот результат. Код, выполняющий эти действия, мы показываем в нашей реализации функции getaddrinfo в разделе 11.16. Таблица 11.3. Функция getaddrinfo: ее действия и результаты Имя узла, указанное вызываю- щим про* цессом Семейство адресов, указанное вызываю- щим про- цессом Строка с именем узла со- держит Результат Действия Ненулевая строка с име- нем узла; ак- тивное или пассивное открытие AFUNSPEC Имя узла Шестнадца- теричная строка Строка в то- чечно-деся- тичной записи Все записи АААА возвра- щаю гея как структуры sockaddr_in6{} и все записи А возвраща- ются как структуры sockaddr_m{} Одна структура sockaddr_in6{) Одна структура sockaddrinf} Выполняется два поиска в DNS (примечание 1): gethoslbyname2(AF_INET6) с выключенным параметром RESUSEINET6 и gethostbyname2(AF_INET) с выключенным параметром RESUSEINET6 inet_pton(AF_INET6) inetpton(AFINET) AFINET6 Имя узла Шестнадца- теричная строка Все записи АААА возвра- щаются как структуры sockaddr_m6{} либо все записи А возвраща- ются как структуры sockaddr_in6{} с адресами IPv4, преобразованными к виду IPv6 Одна структура sockaddr_in6{} gethostbyname() с включен- ным параметром RES_USE_INET6 (примечание 2) inet_pton(AF_INET6) продолжение &
324 Глава 11. Дополнительные преобразования имен и адресов Таблица 11.3 (продолжение) Имя узла, Семейство Строка с Результат Действия указанное адресов, именем вызываю- указанное узла со- щим про- вызываю- держит цессом щим про- цессом Строка в то- Ошибка. Пустая стро- AFINET AFJJNSPEC чечно-деся- тичной записи Имя узла Шестнадца- теричная строка Строка в то- чечно-деся- тичной записи Неявный EAI_ADDRFAMILY Все записи А возвраща- ются как структуры sockaddr_m{} Ошибка EAIADDRFAMILY Одна структура sockaddrmf} Одна структура gethostbyname() С выклю- ченным параметром RES_USE_INET6 metpton(AFINET) mel_pton(AF INET6) ка с именем узла, пассив- ное открытие Пустая стро- AFINET6 AFINET AFUNSPEC адрес 0 0 Неявный адрес 0 0 0 0 Неявный адрес О’ 0 Неявный адрес 0 0 0 0 Неявный ад- sockaddr_in6{} и одна структура sockaddr_in{} Одна структура sockaddr_m6{} Одна структура sockaddr_in{} Одна структура mel_pton( AFINET) inet_pton(AF_INET6) metpton(AFINET) inel pton(AF INET6) ка с именем узла, актив- ное открытие AFJNET6 AFINET рес 0 1 Не- явный адрес 12700 1 Неявный адрес 0 1 Неявный ад- рес 127 0 01 sockaddr_in6{} и одна структура sockaddr_in{} Одна структура sockaddr_in6{} Одна струю ура sockaddr_m{} met_pton(AF_INET) inet_pton(AF_INET6) inetpton(AFINET) Примечание 1. Когда выполняется два поиска DNS, любой из них может ока- заться неудачным (то есть не найдется ни одной записи нужного типа для имени узла), но как минимум один поиск закончится успешно. Но если успешными ока- жутся оба поиска (у узла имеется и запись типа А, и запись типа АААА), возвра- щаются оба типа структур адреса сокета. Примечание 2. Данный поиск DNS должен выполниться успешно, иначе воз- вращается ошибка Но поскольку включен параметр RES_USE_INET6, функция get- hostbyname сначала ищет записи типа АААА, и если не находит их, то ищет функ- ции типа А (см. листинг 9.2). Установка и сброс параметра распознавателя RES_USE_INET6 со сценариями, представленными в примечаниях к таблице, нужны для осуществления требуе- мого поиска DNS, правила которого приводятся в табл 9.1. Мы отметили, что табл. 11 3 только указывает, каким образом функция getaddr- 1 nfo обрабатывает IPv4 и IPv6, то есть позволяет определить число адресов, возвра-
11.6. Функция getaddrinfo: примеры 325 щаемых вызывающему процессу. Действительное число структур addrinfo, воз- вращаемых вызывающему процессу, зависит также от заданного типа сокета и име- ни службы, как ранее было показано в табл. 11.1. В Posix.lg не сказано ничего особенного о функции getaddrinfo и доменных сокетах Unix (их мы подробно рассмотрим в главе 14). Тем не менее добавление поддержки доменных сокетов Unix в нашей реализации функции getaddri nfo и про- верка приложений с протоколами IPv4, IPv6 и доменными сокетами — это хоро- шая проверка на независимость от протоколов. В нашей реализации принимается следующее предположение: если аргумен- том имени узла для функции getaddri nfo является либо /local, либо /ишх, а аргу- ментом имени службы — полное имя (начинаегся с косой черты), то возвраща- ются структуры доменного сокета Unix. Действительные имена узлов DNS не могут содержать косой черты, и ни одно из имен существующих служб IANA не начинается с косой черты (см. упражнение 11.5). Возвращаемые структуры адре- сов сокетов содержат полное имя, готовое к вызову функции bind или connect. Если вызывающий процесс задает флаг AI_CANONNAME, имя узла (см. раздел 9.7) возвращается как каноническое имя. 11.6. Функция getaddrinfo: примеры Теперь мы покажем некоторые примеры работы функции getaddrinfo, используя тестовую программу, которая позволяет нам вводить все параметры: имя узла, имя службы, семейство адресов, тип сокета и флаги AI_CANONNAME и AI_PASSI VE. (Мы не показываем эту тестовую программу, поскольку она содержит около 350 строк малоинтересного кода. Ее можно получить тем же способом, что и прочие исход- ные коды для этой книги, см. предисловие.) Тестовая программа выдает инфор- мацию о переменном числе возвращаемых структур addri nfo, показывая аргументы вызова функции socket и адрес в каждой структуре адреса сокета. Сначала показываем тот же пример, что и на рис. 11.1: solans % testga -f inet -c -h bsdi -s domain socket(AF_INET SOCK_STREAM 0) ai_canonname = bsdi kohala com address 206 62 226 35 53 socket(AF_INET SOCK_DGRAM 0) address 206 62 226 35 53 socket(AF_INET SOCK_STREAM 0) address 206 62 226 66 53 socket(AF_INET SOCK_DGRAM 0) address 206 62 226 66 53 Параметр -f met задает семейство адресов, -с указывает, что нужно возвра- тить каноническое имя, -h bsdi задает имя узла, -s domain задает имя службы. Типичный сценарий клиента — задать семейство адресов, тип сокета (пара- метр -t), имя узла и имя службы. Следующий пример показывает это для узла с несколькими сетевыми интерфейсами с шестью адресами IPv4: solans % testga -f inet -t stream -h gateway.tuc.noao.edu -s daytime socket(AF_INET SOCK_STREAM 0)
326 Глава 11 Дополнительные преобразования имен и адресов address 140 252 101 4 13 socket (AFJ NET SOCK STREAM 0) address 140 252 102 1 13 socket(AF_INET SOCK_STREAM 0) address 140 252 104 1 13 socket (AFJ NET SOCK STREAM 0) address 140 252 3 6 13 socket(AFJNET SOCK STREAM 0) address 140 252 4 100 13 socket(AF_INET SOCK STREAM 0) address 140 252 1 4 13 Затем мы задаем наш узел al pha, у которого имеется и запись типа АААА, и за- пись типа А, не указывая семейства адресов Имя службы — ftp, которая предо- ставляется только TCP solans % testga h alpha s ftp socket(AF_INET6 SOCKJTREAM 0) address 5flb dfOO ce3e e200 20 800 2b37 6426 21 socket(AFJNET SOCK_STREAM 0) address 206 62 226 42 21 Поскольку мы не задали семейство адресов и запустили этот пример на узле, который поддерживает и IPv4, и IPv6, возвращаются две структуры одна для IPv6 и одна для IPv4 Затем мы задаем флаг AI_PASSIVE (параметр р), не указываем ни семейства адресов, ни имени узла (подразумевая универсальный адрес), задаем номер пор- та 8888 и не указываем тип сокета solans % testga р s 8888 socket(AF_INET6 SOCK_STREAM 0) address 8888 socket(AFJNET6 SOCKJJGRAM 0) address 8888 socket(AF_INET SOCKJTREAM 0) address 0000 8888 socket!AFJNET SOCKJJGRAM 0) address 0000 8888 Возвращаются четыре структуры Поскольку мы запустили эту программу на узле, поддерживающем и IPv4, и IPv6, не задав семейства адресов, функция getaddrinfo возвращает универсальный адрес IPv6 и универсальный адрес IPv4 Поскольку мы задали номер порта без типа сокета, функция getaddrinfo возвра- щает по одной структуре для каждого адреса в случае TCP и по одной структуре для каждого адреса в случае UDP Две структуры IPv6 возвращаются перед структурами IPv4, поскольку, как мы видели в главе 10, клиент или сервер IPv6 на узле с двойным стеком может взаимодействовать с собеседниками по IPv6 и по IPv4
11 7 Функция host serv 327 Чтобы проиллюстрировать использование доменных сокетов Unix, мы задаем /local в качестве имени узла и /tmp/test 1 в качестве имени службы solans % testga с р h /local s /tmp/test 1 socket(AF_LOCAL SOCK_STREAM 0) ai_canonname = solans kohala com address /tmp/test 1 socket(AF_LOCAL SOCKJJGRAM 0) address /tmp/test 1 > Поскольку мы не задаем тип сокета, возвращаются две структуры: первая для потокового сокета, вторая — для сокета дейтаграмм 11.7. Функция host_serv Наш первый интерфейс функции getaddri nfo не требует от вызывающего процес- са размещать в памяти структуру рекомендаций и заполнять ее Вместо этого ар- гументами нашей функции host_serv будут интересующие нас поля — семейство адресов и тип сокета #include unp h struct addrinfo *host_serv(const char *hostname const char *service int family int socktype} Возвращает в случае успешного выполнения указатель на структуру addrinfo NULL в случае ошибки В листинге 11 11 показан исходный код этой функции Листинг 11.1. Функция host_serv //lib/host_serv с 1 #include unp h 2 struct addrinfo * 3 host_serv(const char *host const char *serv int family 1nt socktype). 4 { 5 int n 6 struct addrinfo hints *res 7 bzero(&hints sizeof(struct addrinfo)) 8 hints ai_flags = AI_CANONNAME /* всегда возвращает каноническое имя */ 9 hints ai_family = family /* AFJJNSPEC AF_INET AF_INET6 ит Д */ 10 hints a7_socktype = socktype /* 0 SOCK_STREAM SOCK_DGRAM ит д */ 11 if ( (n = getaddrinfo(host serv &hints &res)) l= 0) 12 return (NULL) 13 return (res) /* возвращает указатель на первый элемент в связном списке */ 14 } 7-13 Функция инициализирует структуру рекомендаций (hi nts), вызывает функцию getaddrinfo и возвращает пустой указатель, если происходит ошибка Все исходные коды программ, опубликованные в этои книге вы можете найти по адресу http // www prter com/download
328 Глава 11. Дополнительные преобразования имен и адресов Мы вызываем эту функцию в листинге 15.11, когда нам нужно использовать getaddri nfo для получения информации об узле и о службе и при этом мы хотим установить соединение самостоятельно. 11.8. Функция tcp.connect Теперь мы напишем две функции, использующие функцию getaddri nfo для обра- ботки большинства сценариев клиентов и серверов TCP, которые мы создаем. Первая из этих функций, tcp_connect, выполняет обычные шаги клиента: созда- ние сокета TCP и соединение с сервером. #include 'unp h int tcp_connect(const char *hostname const char *service) Возвращает в случае успешного соединения- дескриптор присоединенного сокета, в случае ошибки не возвращается ничего В листинге 11.2 показан исходный код. Листинг 11.2. Функция tcp_connect выполнение обычных шагов клиента //lib/tcp_connect с 1 #include unp h' 2 int 3 tcp_connect(const char *host const char *serv) 4 { 5 int sockfd n 6 struct addrinfo hints *res *ressave 7 bzero(8hints sizeof(struct addrinfo)) 8 hints ai_family = AFJJNSPEC 9 hints ai_socktype = SOCK_STREAM 10 if ( (n = getaddrinfo(host serv &hints &res)) '= 0) 11 err_quit('tcp_connect error for & & &s 12 host serv gai_strerror(n)) 13 ressave = res 14 do { 15 sockfd = socket(res->ai_family res->ai_socktype res->ai_protocol) 16 if (sockfd < 0) 17 continue /* игнорируем этот адрес */ 18 if (connect(sockfd res->ai_addr res->ai_addrlen) == 0) 19 break /* успех */ 20 Close(sockfd) /* игнорируем этот адрес */ 21 } while ( (res = res->ai_next) '= NULL) 22 if (res == NULL) /* значение еггпо устанавливается при последней попытке connectО */ 23 err_sys('tcp_connect error for Bs. fcs'. host serv). 24 freeaddrinfo(ressave) 25 return (sockfd) 26 }
11.8 Функция tcp connect 329 Вызов функции getaddrinfo 7-13 Функция getaddri nfo вызывается один раз, когда мы задаем семейство адресов AFJJNSPEC и тип сокета SOCK_STREAM. Перебор всех структур addrinfo до успешного выполнения или до окончания списка 14-25 Затем пробуется каждый IP-адрес: вызываются функции socket и connect. Если выполнение функции socket неудачно, это не фа!альная ошибка, так как такое может случиться, если был возвращен адрес IPv6, а ядро узла не поддерживает IPv6 Если выполнение функции connect успешно, выполняется функция break для выхода из цикла. В противном случае, после того как перепробованы все ад- реса, цикл также завершается. Функция freeaddnnfo освобождает всю динами- чески выделенную память. Эта функция (как и другие наши функции, предоставляющие более простой интерфейс для функции getaddrinfo в следующих разделах) завершается, если либо оказывается неудачным вызов функции getaddrinfo, либо вызов функции connect не выполняется успешно Возвращение из нашей функции возможно лишь в случае успешного выполнения Было бы сложно возвратить код ошибки (одну из констант EAl_xxr), не добавляя еще одного аргумента Это значит, что наша функция-обертка тривиальна. Tcp_connect(const char *host const char *serv) { return(tcp_connect(host serv)) } Тем не менее мы по-прежнему вызываем функцию-обертку вместо функцйи tcp connect ради сохранения единообразия в оставшейся части книги ПРИМЕЧАНИЕ ------------------------------------------------------------ Проблема с возвращаемым значением заключается в том, что дескрипторы неотрица- тельные, по мы не знаем, положительны или отрицательны значения ЕА1_ххх Если бы эти значения были положительными, мы могли бы возвратить равные им по абсо- лютной величине отрицательные значения, когда вызов функции getaddrinfo окажется неудачным Но мы также должны возвратить некое другое отрицательное значение, чтобы указать, что все структуры были перепробованы безуспешно Пример: клиент времени и даты В листинге 113 показан наш клиент времени и даты из листинга 1 1, переписан- ный с использованием функции tcp_connect. Листинг 11.3. Клиент времени и даты, переписанный с использованием функции tcp_connect //names/daytimetcpcli с 1 #include unp h 2 int 3 main(int argc char **argv) 4 { 5 mt sockfd n 6 char recvlmeEMAXLINE + 1]; 7 socklen_t 1en , продолжение iP'
330 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.3 (продолжение) 8 struct sockaddr *sa, 9 if (argc '= 3) 10 err_quit(''usage daytimetcpcli <hostname/IPaddress> <service/port#>"). 11 sockfd = Tcp_connect(argv[l] argv[2J) 12 sa = Ma Hoc (MAXSOCK ADDRI 13 len = MAXSOCKADDR 14 Getpeemame(sockfd sa. &len) 15 printf("connected to fts\en". Sock_ntop_host(sa. len)), 16 while ( (n = Read(sockfd, recvline MAXLINE)) > 0) { 17 recvline[n] =0. /* завершающий нуль */ 18 Fputs(recvline. stdout). 19 } 20 exit(O). 21 } Аргументы командной строки 9-10 Теперь нам требуется второй аргумент командной строки для задания либо имени службы, либо номера порта, что позволит нашей программе соединяться с другими портами. Соединение с сервером 11 Теперь весь код сокета для этого клиента выполняется функцией tcp connect. Вывод ответа сервера 12-15 Мы вызываем функцию getpeername, чтобы получить адрес протокола сервера и вывести его. Мы делаем это для проверки протокола, используемого в приме- рах, которые скоро покажем. Обратите внимание, что функция tcp connect не возвращает размера структу- ры адреса сокета, который использовался для функции connect. Мы могли доба- вить еще один аргумент-указатель, чтобы получить это значение, но при созда- нии этой функции мы стремились добиться меньшего числа аргументов, чем у функции getaddri nfo. Поэтому мы определяем константу MAXSOCKADDR в нашем заголовке unp h так, чтобы ее размер равнялся размеру наибольшей структуры адреса сокета. Обычно это размер структуры адреса доменного сокета U nix (см. раз- дел 14.2), немного более 100 байт. Мы выделяем в памяти пространство для струк- туры указанного размера и заполняем ее с помощью функции getpeername. ПРИМЕЧАНИЕ -------------------------------------------------------- Для размещения этой структуры мы вызываем функцию malloc, вместо того чтобы раз- мещать ее следующим образом: char sockaddr[MAXSOCKADDR] Это делается для выравнивания расположения структуры в памяти. Функция malloc всегда возвращает указатель со строжайшим выравниванием, требуемым системой, в то время как для массива char может быть выделена область памяти, граница которой проходит по нечетному байту, что может вызвать проблемы для полей IP-адреса или номера порта в структуре адреса сокета. Другой способ решения потенциальной про- блемы выравнивания, состоящий в использовании объединения, был показан в лис- тинге 4.4.
11.9. Функция tcpjisten 331 Эта версия нашего клиента работает и с IPv4, и с IPv6, тогда как версия, пред- ставленная в листинге 1.1, работала только с IPv4, а версия из листинга 1.2 — толь- ко с IPv6. Сравните нашу новую версию с представленной в листинге Д.8, кото- рую мы написали, чтобы использовать функции gethostbyname и getservbyname для поддержки и IPv4, и IPv6. Сначала мы задаем имя узла, поддерживающего только IPv4: solans % daytlmetcpcll bsdi daytime connected to 206 62 226 35 Fri May 30 12 33 32 1997 Затем мы задаем имя узла, поддерживающего и IPv4, и IPv6: solans I daytimetcpcl! aix daytime connected to 5flb dfOO ce3e e200 20 800 5afc 2b36 Fri May 30 12 43 43 1997 Используется адрес IPv6, поскольку у узла имеется и запись типа АААА, и за- пись типа А. Кроме того, функция tcp_connect устанавливает семейство адресов AFJJNSPEC, поэтому, как было отмечено в табл. 11.3, сначала идет поиск записей типа АААА, и только если этот поиск неудачен, выполняется поиск записей типа А. В следующем примере мы указываем на необходимость использования имен- но адреса IPv4, задавая имя узла с суффиксом -4, что, как мы отмечали в разде- ле 9.2, в соответствии с принятым нами соглашением означает имя узла, который поддерживает только записи типа А: solans % daytlmetcpcll aix-4 daytime connected to 206 62 226 43 Fn May 30 12 43 48 1997 11.9. Функция tcpjisten Наша следующая функция, tcpjisten, выполняет обычные шаги сервера TCP: создание сокета TCP, связывание его с заранее известным портом с помощью функции bind и разрешение приема входящих запросов через соединение. В лис- тинге 11.4 представлен исходный код. #include "unp h" int tcpjisten(const char *hostname. const char *service socklenj ★lenptr'). В случае успешного выполнения возвращает дескриптор присоединенного сокета в случае ошибки не возвращает ничего Листинг 11.4. Функция tcpjisten: выполнение обычных шагов сервера TCP //11 b/tcpj i sten с 1 include ’unp h" 2 int 3 tcpjisten(const char *host. const char *serv socklenj: *addrlenp) 4 { 5 int listenfd. n. 6 const int on = 1. 7 struct addrinfo hints, *res *ressave, 8 bzero(&hints. sizeof(struct addrinfo)). 9 hints ai_flags = AI_PASSIVE. 10 hints ai_family = AFJJNSPEC. 11 hints aisocktype = SOCKJSTREAM.
332 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.4 (продолжение) 12 if ( (n = getaddnnfo(host. serv. Shints. &res)) ,= 0) 13 err_quit("tcpjisten error for fcs, &s £s". 14 host. serv. gai_strerror(n)). 15 ressave = res. 16 do { 17 listenfd = socket(res->ai_family. res->ai_socktype, res->ai_protocol). 18 if (listenfd < 0) 19 continue. /* ошибка, пробуем следующий адрес */ 20 Setsockopt(listenfd. SOLJ5OCKET. SO_REUSEADDR. Son. sizeof(on)); 21 if (bind(listenfd, res->ai_addr. res->ai_addrlen) = 0) 22 break. /* успех */ 23 Closedistenfd). /* ошибка при вызове функции bind, закрываем сокет и пробуем следующий адрес*/ 24 } while ( (res = res->ai_next) 1= NULL). 25 if (res == NULL) /* значение еггпо устанавливается при последнем вызове функции socketO или bindO */ 26 err_sys("tcpjisten error for fcs. *s“. host. serv). 27 Listendistenfd. LISTENQ). 28 if (addrlenp) 29 *addrlenp = res->ai_addrlen. /* возвращает размер адреса протокола */ 30 freeaddrinfo(ressave). 31 return (listenfd), 32 } Вызов функции getaddrinfo 8-15 Мы инициализируем структуру addrinfo с учетом следующих рекомендаций (элементов структуры hints): AI_PASSIVE, поскольку это функция для сервера, AFJJNSPEC для семейства адресов и SOCK JSTREAM. Вспомните табл. 11.3: если имя узла не задано (что вполне нормально для сервера, который хочет связать с дескрип- тором универсальный адрес), то наличие значений AJ_PASSIVE и AFJJNSPEC вызовет возвращение двух структур адреса сокета: первой для IPv6 и второй для IPv4 (в предположении, что это узел с двойным стеком). Создание сокета и связывание с адресом 16-24 Вызываются функции socket и bi nd. Если любой из вызовов окажется неудач- ным, мы просто игнорируем данную структуру addrinfo и переходим к следую- щей. Как было сказано в разделе 7.5, для сервера TCP мы всегда устанавливаем параметр сокета SCJREUSEADDR. Проверка на наличие ошибки 25-26 Если все вызовы функций socket и bind окажутся неудачными, мы сообщаем об ошибке и завершаем выполнение. Как и в случае с нашей функцией tcp_connect из предыдущего раздела, мы не пытаемся возвратить ошибку из этой функции. 27 Сокет превращается в прослушиваемый сокет с помощью функции 11 sten.
11.9. Функция tcpjisten 333 Возвращение размера структуры адреса 28-31 Если аргумент addrlепр является непустым указателем, мы возвращаем размер адресов протокола через этот указатель. Это позволяет вызывающему процессу выделять память для структуры адреса сокета, чтобы получить адрес протокола клиента из функции accept (см. также упражнение 11.1). Пример: сервер времени и даты В листинге 11.5 показан наш сервер времени и даты из листинга 4.2, переписан- ный с использованием функции tcp_l i sten. Листинг 11.5. Сервер времени и даты, переписанный с использованием функции tcpjisten //names/daytimetcpsrvl с 1 #include ''unp.h" 2 #include «time h> 3 int 4 main(int argc. char **argv) 5 { 6 int listenfd. connfd. 7 socklen_t addrlen. len. 8 char buff[MAXLINE]. ' 9 time J: ticks. 10 struct sockaddr *cliaddr. 11 if (argc '= 2) 12 err_quit("usage daytimetcpsrvl «service or port#>"): 13 listenfd = TcpJisten(NULL, argv[l], &addrlen). 14 cliaddr = Malloc(addrlen). 15 for (..) { 16 len = addrlen. 17 connfd = Accept(listenfd. cliaddr, &len). 18 printf("connection from ^s\en". Sock_ntop(cliaddr. len)): 19 ticks = time(NULL); 20 snprintf(buff. sizeof(buff). "X 24s\er\en". ctime(&ticks)). 21 Write(connfd. buff, strlen(buff)); 22 Close(connfd). 23 } 24 } Ввод имени службы или номера порта в качестве аргумента командной строки 11-12 Нам нужно использовать аргумент командной строки, чтобы задать либо имя службы, либо номер порта. Это упрощает проверку нашего сервера, поскольку связывание с портом 13 для сервера времени и даты требует прав привилегиро- ванного пользователя. Создание прослушиваемого сокета 13-14 Функция tcpjisten создает прослушиваемый сокет, и функция malloc разме- щает в памяти буфер для хранения адреса клиента.
334 Глава 11. Дополнительные преобразования имен и адресов Цикл сервера 5-23 Функция accept ждет соединения с клиентом. Мы выводим адрес клиента, вы- зывая функцию sock_ntop. В случае IPv4 или IPv6 эта функция выводит IP-адрес и номер порта. Мы могли бы использовать функцию getnameinfo (описанную в раз- деле 11 13), чтобы попытаться получить имя узла клиента, но это подразумевает запрос PTR в DNS, что может занять некоторое время, особенно если запрос PTR окажется неудачным Вразделе 148 [95] упоминается, что на занятом сервере Web почти у 25% всех клиентов, соединяющихся с этим сервером, в DNS нет записей типа PTR. Поскольку мы не хотим, чтобы наш сервер (особенно последователь- ный сервер) в течение нескольких секунд ждал запрос PTR, мы просто выводим IP-адрес и порт. Пример: сервер времени и даты с указанием протокола В листинге 115 есть небольшая проблема: первый аргумент функции tcp_l i sten — пустой указатель, объединенный с семейством адресов AFJJNSPEC, который задает функция tcp_l 1 sten, — может заставить функцию getaddri nfo возвратить структуру адреса сокета с семейством адресов, отличным от желаемого Например, первой на узле с двойным стеком будет возвращена структура адреса сокета для IPv6 (см. табл. 11.3), но, возможно, нам требуется, чтобы наш сервер обрабатывал только IPv4. У клиентов такой проблемы нет, поскольку клиент должен всегда задавать либо IP-адрес, либо имя узла Клиентские приложения обычно позволяют пользо- вателю вводить этот параметр как аргумент командной строки Это дает нам возможность задавать имя узла, связанное с определенным типом IP-адреса (вспо- мните наши имена узлов -4 и -6 в разделе 9 2), или же задавать либо строку в точеч- но-десятичной записи (для IPv4), либо шестнадцатеричную строку (для IPv6). Однако для серверов существует простая техника, позволяющая нам указать, какой именно протокол следует использовать — IPv4 или IPv6. Для этого нужно позволить пользователю ввести либо IP-адрес, либо имя узла в качестве аргу- мента командной строки и передать его функции getaddri nfo В случае IP-адреса строка точечно-десятичной записи IPv4 отличается от шестнадцатеричной стро- ки IPv6. Следующие вызовы функции inet_pton оказываются либо успешными, либо нет, как это показано в данном случае: inet_pton(AF_INET "0000 &foo) /* успешно */ inet_pton(AF_INET 0 0 &foo) /* неудачно*/ inet_pton(AFJNET6 ”0 0 0 0" &foo) /* неудачно */ inet_pton(AF_INET6 0 О' &foo) /* успешно */ Следовательно, если мы изменим наши серверы таким образом, чтобы они получали дополнительный аргумент, то при вводе % server по умолчанию мы получим IPv6 на узле с двойным стеком, но при вводе % server 0000 явно задается IPv4, а при вводе % server 0 0 явно задается IPv6. В листинге 11.6 показана окончательная версия нашего сервера времени и даты.
11.9. Функция tcpjisten 335 Листинг 11.6. Не зависящий от протокола сервер времени и даты, использующий функцию tcpjisten names/daytimetcpsrv2 с 1 #include 'unp h" 2 ^include <time h> 3 int 4 maindnt argc char **argv) 5 { 6 int listenfd connfd 7 socklen_t addrlen. len. 8 struct sockaddr *cliaddr 9 char buff[MAXLINE] 10 time J: ticks. 11 if (argc == 2) 12 listenfd = Tcpjisten(NULL argv[l] &addrlen) 13 else if (argc — 3) 14 listenfd = Tcpjisten(argv[l] argv[2] &addrlen) 15 else 16 err_quit("usage daytimetcpsrv2 [ <host> ] <service or port>"). 17 cliaddr = Malloc(addrlen) 18 for ( ) { 19 len = addrlen 20 connfd = Accept(1istenfd cliaddr. &len) 21 printf( 'connection from fts\en' Sock_ntop(cliaddr len)) 22 ticks = time(NULL) 23 snprintf(buff sizeof(buff) 'X 24s\er\en' ctime(&ticks)) 24 Write(connfd buff strlen(buff)) 25 Close(connfd) 26 } 27 } Обработка аргументов командной строки 11-16 Единственное изменение по сравнению с листингом 11 6 — это обработка аргу- ментов командной строки, позволяющая пользователю в дополнение к имени службы или порту задавать либо имя узла, либо IP-адрес для связывания с сер- вером. Сначала мы запускаем этот сервер с сокетом IPv4 и затем соединяемся с сер- вером от клиентов на двух различных узлах, расположенных в локальной подсети: solans % daytimetcpsrv2 0.0.0.0 9999 connection from 206 62 226 36 32789 connection from 206 62 226 35 1389 А теперь мы запустим сервер с сокетом IPv6: Solaris % daytnnetcpsrv2 0.:0 9999 connection from 5flb dfOO сеЗе e200 20 800 2003 f642 32799 connection from 5flb dfOO сеЗе e200 20 800 2b37 6426 1026 connection from ffff 206 62 226 36 32792 connection from ffff 206 62 226 35 1390 Первое соединение — от узла sunos5, использующего IPv6, а второе — от узла alpha, использующего IPv6. Два следующих соединения — от узлов sunos5 и bsdi,
336 Глава 11. Дополнительные преобразования имен и адресов чо они используют IPv4, а не IPv6. Мы можем определить это, потому что адреса клиента, возвращаемые функцией accept, оба являются адресами IPv4, преобра- зованными к виду IPv6. Мы только что показали, что сервер IPv6, работающий на узле с двойным сте- ком, может обрабатывать как клиенты IPv4, так и клиенты IPv6. Адреса IPv4- клиента передаются серверу IPv6 как адреса IPv4, преобразованные к виду IPv6, что мы рассматривали в разделе 10.2. Этот сервер вместе с клиентом, показанным в листинге 11.3, работают также с доменными сокетами Unix (см. главу 14), поскольку наша реализация функции getaddrinfo в разделе 11.16 поддерживает доменные сокеты Unix. Например, мы запускаем сервер следующим образом: solans % daytimetcpsrv2 /local /tmp/rendezvous где имя /tmp/rendezvous — это произвольное имя, выбираемое нами для связыва- ния на узле сервера, с которым соединяется клиент. Затем мы запускаем клиент на том же узле, задав /local в качестве имени узла и /tmp/rendezvous в качестве имени службы: solans % daytimetcpcli /local /tmp/rendezvous connected to /tmp/rendezvous Fri May 30 16 31 37 1997 11.10. Функция udp.client Наши функции, предоставляющие более простой интерфейс для функции getaddr- info, в случае UDP изменяются: в этом разделе мы представляем клиентскую функцию, создающую неприсоединенный сокет UDP, а в следующем — другую функцию, создающую присоединенный сокет UDP. ^include "unp h" int udp_client(const char *hostname, const char ★service, void **saptr socklen_t ★lenp'). Возвращает дескриптор неприсоединенного сокета в случае успешного выполнения, в случае ошибки не возвращает ничего Эта функция создает неприсоединенный сокет UDP, возвращая три элемен- та. Во-первых, возвращаемое значение функции — это дескриптор сокета. Во-вто- рых, saptr — это адрес указателя (объявляемого вызывающим процессом) на структуру адреса сокета (которая динамически размещается в памяти функцией udp_cl 1 ent), и в этой структуре функция хранит IP-адрес получателя и номер порта для будущих вызовов функции sendto. Размер этой структуры адреса сокета воз- вращается как значение переменной, на которую указывает 1 епр. Последний ар- гумент не может быть пустым указателем (как это допустимо для последнего ар- гумента функции tcp_l 1 sten), поскольку длина структуры адреса сокета требуется в любых вызовах функций sendto и recvfrom. ПРИМЕЧАНИЕ--------------------------------------------------------------- saptr должен быть объявлен как struct sockaddr **. Мы используем тип данных void **, потому что определяем другую версию этой функции для XTI в разделе 31.3, которой необходим этот аргумент, чтобы хранить адрес указателя на другой тип структуры. Это значит, что наши вызовы этой функции должны содержать приведение типа (void **).
11.10. Функция udp client 337 В листинге 11.7 показан исходный код для этой функции. Функция getaddrinfo преобразует аргументы hostname и service. Создается дей- таграммный сокет. Выделяется память для одной структуры адреса сокета, и струк- тура адреса сокета, соответствующая созданному сокету, копируется в память. Листинг 11.7. Функция udpclient: создание неприсоединенного сокета UDP //lib/udp_client с 1 #include "unp h" 2 int 3 udp_client(const char *host. const char *serv. void **saptr, socklen_t *lenp) 4 { 5 int sockfd. n. 6 struct addrinfo hints. *res, *ressave. 7 bzero(&hints. sizeof(struct addrinfo)), 8 hints a i_f airily = AFJJNSPEC. 9 hints ai_socktype = SOCKJJGRAM. 10 if ( (n = getaddrinfothost. serv. &hints. &res)) !- 0) 11 err_quit("udp_client error for 2s. 2s: 2s“, 12 host. serv. gai_strerror(n)): 13 ressave = res. 14 do { 15 sockfd = socket(res->ai_family. res->ai_socktype res->ai_protocol). 16 if (sockfd >= 0) 17 break: /* успех */ 18 } while ( (res = res->ai_next) != NULL). 19 if (res == NULL) /* значение еггпо устанавливается при последнем вызове функции socketО */ 20 err_sys("udp_client error for 2s, 2s". host. serv). 21 *saptr = Malloc(res->ai_addrlen). 22 memcpy(*saptr. res->ai_addr. res->ai_addrlen). 23 *lenp = res->ai_addrlen. 24 freeaddrinfo(ressave). 25 eturn (sockfd). 26 } Пример: не зависящий от протокола клиент времени и даты Теперь мы перепишем наш клиент времени и даты, показанный в листинге 11.3, так, чтобы в нем использовалась наша функция udp cl lent. В листинге 11.8 пред- ставлен не зависящий от протокола исходный код. Листинг 11.8. UDP-клиент времени и даты, использующий нашу функцию udp_client //names/daytimeudpclil с 1 #include "unp.h" 1Г|^ продолжение &
338 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.8 (продолжение) 3 maindnt argc. char **argv) 4 { 5 int sockfd. n. 6 char reev'd ne[MAXLINE + 1]. 7 socklen_t salen. 8 struct sockaddr *sa 9 if (argc l= 3) 10 err_quit("usage daytimeudpclil <hostname/IPaddress> <service/port#>"): 11 sockfd = Udp_client(argv[l], argv[2], (void **) &sa. &salen). 12 printfCsending to fts\en". Sock_ntop_host(sa salen)). 13 Sendto(sockfd. ””. 1. 0. sa. salen). /* посылается 1-байтовая дейтаграмма */ 14 n = Recvfrom(sockfd. recvline. MAXLINE 0. NULL. NULL). 15 recvlinetn] = 0. /* завершающий пустой байт */ 16 Fputs(recvline. stdout). 17 exit(0) 18 } 1-16 Мы вызываем нашу функцию udp_cl lent и затем выводим IP-адрес и порт сер- вера, которому мы отправим нашу дейтаграмму UDP. Мы посылаем однобайто- вую дейтаграмму и затем читаем и выводим ответ сервера. ПРИМЕЧАНИЕ----------------------------------------------------------------------- Нам нужно отправить дейтаграмму, содержащую 0 байт, поскольку ответ сервера вре- мени и даты инициируется самим получением дейтаграммы от клиента, независимо от ее длины и содержания. Но многие реализации SVR4 не допускают нулевой длины дейтаграмм UDP. Мы запускаем наш клиент, задавая имя узла с записью типа АААА и типа А. Поскольку функция getaddri nfo в первую очередь возвращает структуру с запи- сью типа АААА, создается сокет IPv6: solans % daytimeudpclil aix daytime sending to 5flb dfOO сеЗе e200 20 800 5afc-2b36 Sat May 31 08 13 34 1997 Затем мы задаем адрес того же узла в точечно-десятичной записи, в результа- те чего создается сокет IPv4: solans % daytimeudpclil 206.62.226.43 daytime sending to 206 62 226 43 Sat May 31 08 14 02 1997 11.11. Функция udp.connect Наша функция udp_connect создает присоединяющийся сокет UDP. #include "unp h" int udp_connect(const char ★hostname, const char ★service). Возвращает дескриптор присоединенного сокета в случае успешного выполнения, в случае ошибки ничего не возвращает
11.12. Функция udp server 339 В случае присоединненного сокета UDP два последних аргумента, которые требуются в функции udp_cl lent, больше не нужны. Вызывающий процесс может вызвать функцию write вместо sendto; таким образом, нашей функции не нужно возвращать структуру адреса сокета и ее длину. В листинге 11.9 представлен исходный код. Эта функция почти идентична функции tcp_connect. Однако отличие в том, что при вызове функции connect для сокета UDP ничего не отправляется собе- седнику. Если что-то не в порядке (собеседник недоступен или на заданном пор- те не запущен сервер), вызывающий процесс не обнаружит этого, пока не пошлет собеседнику дейтаграмму. Листинг 11.9. Функция udp_connect: создание присоединенного сокета UDP //lib/udp_connect с 1 #include "unp h” 2 int 3 udp_connect(const char *host. const char *serv) 4 { 5 int sockfd. n. 6 struct addrinfo hints. *res. *ressave. 7 bzero(&hints. sizeof(struct addrinfo)). 8 hints ai-family = AFJJNSPEC 9 hints ai_socktype = SOCK_DGRAM. 10 if ( (n = getaddrinfo(host serv. &hints. &res)) !- 0) 11 err_quit("udp_connect error for fts. fts fts". 12 host serv. gai_strerror(n)). 13 ressave = res 14 do { 15 sockfd = socket(res->ai_family. res->ai_socktype. res->ai_protocol): 16 if (sockfd < 0) 17 continue. /* игнорируем этот адрес */ 18 if (connect(sockfd. res->ai_addr. res->ai_addrlen) -- 0) 19 break. /* успех */ 20 Close(sockfd). /* игнорируем этот адрес */ 21 } while ( (res = res->ai_next) 1= NULL). 22 if (res == NULL) /* значение errno устанавливается при последней вызове функции connectО */ 23 err_sys(”udp_connect error for ts. fts”. host. serv). 24 freeaddrinfo(ressave). 25 return (sockfd), 26 } 11.12. Функция udp_server Наша последняя функция, предоставляющая более простой интерфейс для функ- ции getaddri nfo, — это функция udp_server. #include "unp h"
340 Глава 11. Дополнительные преобразования имен и адресов int udp_server(const char *hostname. const char *service. socklenj *lenptr) Возвращает дескриптор неприсоединенного сокета в случае успешного выполнения, в случае ошибки не возвращает ничего Аргументы функции те же, что и для функции tcpjisten: необязательный hostname, обязательный service (для связывания номера порта) и необязательный указатель на переменную, в которой возвращается размер структуры адреса со- кета. В листинге 11.10 представлен исходный код. Листинг 11.10. Функция udp_server: создание неприсоединенного сокета для сервера UDP //1ib/udp_server с 1 #include "unp h' 2 int 3 udp serverfconst char *host, const char *serv. socklenj *addrlenp) 4 { 5 int sockfd n. 6 struct addrinfo hints, *res. *ressave. 7 bzerof&hints. sizeof(struct addrinfo)), 8 hints aijlags = AIJASSIVE. 9 hints aijamily = AFJJNSPEC. 10 hints ai_socktype = SOCKJJGRAM 11 if ( (n = getaddrinfo(host, serv &hints &res)) !- 0) 12 err_quit("udp_server error for ^s. fc ^s" 13 host. serv. gai_strerror(n)). 14 ressave = res. 15 do { 16 sockfd = socket(res->ai Jamily. res->ai_socktype, res->ai_protocol), 17 if (sockfd < 0) 18 continue. /* ошибка пробуем следующий адрес */ 19 if (bind(sockfd. res->ai_addr. res->ai_addrlen) = 0) 20 break /* успех */ 21 Close(sockfd). /* ошибка при вызове функиии bind закрываем сокет и пробуем следующий адрес */ 22 } while ( (res = res->ai_next) '= NULL). 23 if (res == NULL) /* значение еггпо устанавливается при последнем вызове функции socketО or bindО */ 24 err_sys("udp_server error for 2 s 2 s” host, serv) 25 if (addrlenp) 26 *addrlenp = res->ai_addrlen /* возвращается размер адреса протокола */ 27 freeaddrinfo(ressave). 28 return (sockfd). 29 } Эта функция практически идентична функции tср 11 sten, в ней нет только вызова функции 11 sten. Мы устанавливаем семейство адресов AFJJNSPEC, но вы- зывающий процесс может использовать ту же технологию, которую мы описали
11.13. Функция getnameinfo 341 при рассмотрении листинга 11.6, чтобы потребовать использование определен- ного протокола (IPv4 или IPv6). Мы не устанавливаем параметр сокета SO REUSEADDR для сокета UDP, посколь- ку этот параметр сокета может допустить связывание множества сокетов с одним и тем же портом UDP на узлах, поддерживающих многоадресную передачу, как мы говорили в разделе 7.5. Поскольку у сокета UDP нет аналога состояния Т1МЕ_ WAIT, свойственного сокетам TCP, нет необходимости устанавливать этот пара- метр при запуске сервера. Пример: не зависящий от протокола сервер времени и даты В листинге 11.11 представлен наш сервер времени и даты, полученный путем модификации листинга 11.6 и предназначенный для использования UDP. 1 Листинг 11.11. Не зависящий от протокола UDP-сервер времени и даты //names/daytimeudpsrv2 с 1 #include "unp h” 2 include <time h> 3 int 4 main(int argc char **argv) 5 { 6 int sockfd 7 ssize_t n 8 char buff[MAXLINE] 9 time_t ticks, 10 socklen_t addrlen len 11 struct sockaddr *cliaddr. 12 if (argc == 2) 13 sockfd = Udp_server(NULL argv[l] &addrlen) 14 else if (argc == 3) 15 sockfd = Udp_server(argv[l]. argv[2] &addrlen). 16 else 17 err_quit("usage daytimeudpsrv [ <host> ] <service or port>“): 18 cliaddr = Malloc(addrlen). 19 for ( ) { 20 len = addrlen 21 n = Recvfrom(sockfd. buff. MAXLINE. 0. cliaddr. &len). 22 pnntf("datagram from £s\en". Sock_ntop(cliaddr. len)). 23 ticks = time(NULL) 24 snprintf(buff. sizeof(buff). "% 24s\er\en". ctime(&ticks)), 25 Sendto(sockfd buff, strlen(buff). 0. cliaddr. len). 26 } 27 } 11.13. Функция getnameinfo Эта функция дополняет функцию getaddri nfo: она получает адрес сокета и воз- вращает одну символьную строку с описанием узла и другую символьную строку с описанием службы. Эта функция предоставляет указанную информацию в не
342 Глава 11. Дополнительные преобразования имен и адресов зависящем от протокола виде, то есть вызывающему процессу неважно, какой тип адреса протокола содержится в структуре адреса сокета, поскольку эти подроб- ности обрабатываются функцией. include «netdb h> int getnameinfo(const struct sockaddr *sockaddr socklen_t addrlen char *host. size_t hostlen char *serv, size_t servlen int flags) Возвращает 0 в случае успешного выполнения, -1 в случае ошибки Аргумент sockaddr указывает на структуру адреса сокета, содержащую адрес протокола, преобразуемый в строку, удобную для человеческого восприятия, а ар- гумент addrl еп содержит длину этой структуры. Эта структура и ее длина обычно возвращаются любой из перечисленных ниже функций: accept, recvfrom, getsockname или getpeername. Вызывающий процесс выделяет в памяти пространство для двух строк, удоб- ных для человеческого восприятия: аргументы host и host! еп определяют строку, описывающую узел, а аргументы serv и servl еп определяют строку, которая опи- сывает службы. Если вызывающему процессу не нужна возвращаемая строка с описанием узла, задается нулевая длина этой строки (hostlen). Аналогично ну- левое значение аргумента servlen означает, что не нужно возвращать информа- цию о службе Чтобы упростить выделение памяти под массивы для хранения этих двух строк, при включении заголовочного файла <netdb h> определяются кон- станты, показанные в табл. 11.4. Таблица 11.4. Константы для размеров возвращаемых строк из функции getnameinfo Константа Описание Значение NI_MAXHOST Максимальный размер возвращаемой строки с описанием узла 1025 NI MAXSERV Максимальный размер возвращаемой строки с описанием службы 32 Разница между функциями sock ntop и getnamei nfo состоит в том, что первая не задействует DNS, а только возвращает IP-адрес и номер порта. Последняя же обычно пытается получить имя и для узла, и для службы. В табл. 11.5 показаны пять флагов, которые можно задать для изменения дей- ствия, выполняемого функцией getnamei nfo. Таблица 11.5. Флаги функции getnameinfo Константа Описание NIDGRAM Дейтаграммный сокет NINAMEREQD Возвращать ошибку, если невозможно получить имя узла по ы о адресу NINOFQDN Возвращать только ту часть FQDN, которая содержит имя узла NINUMERICHOST Возвращать численное значение адреса вместо имени узла NINUMERICSERV Возвращать номер порта вместо имени службы Флаг NI_DGRAM доЛжен быть задан, когда вызывающий процесс знает, что рабо- тает с дейтаграммным сокетом. Причина в том, что если функции getnamei nfo задать только IP-адрес и номер порта в структуре адреса сокета, она не сможет определить протокол (TCP или UDP) Существует несколько номеров пор-
11.14. Функции, допускающие повторное вхождение 343 тов, которые в случае TCP задействованы для одной службы, а в случае UDP для совершенно другой. Примером может служить порт 514, используемый службой rsh в TCP и службой sysl од в UDP. % Флаг NI_NAMEREQD приводит к возвращению ошибки, если имя узла не может быть разрешено при использовании DNS. Этот флаг может использоваться серверами, которым требуется, чтобы IP-адресу клиента было сопоставлено имя узла. Затем эти серверы получают возвращаемое имя узла, вызывают функ- цию gethostbyname и проверяют, совпадают ли результаты вызова этих двух функций хотя бы частично. Флаг NI_NOFQDN вызывает сокращение имени узла, отбрасывая все, что идет после первой точки. Например, если в структуре адреса сокета содержится IP-адрес 206.62.226.42, то функция gethostbyaddr возвратит имя al pha kohal a com. Но если в функции getnameinfo задан флаг NI_NOFQDN, она возвратит в имени узла только alpha. •* Флаг NI_NUMERICHOST сообщает функции getnamei nfo, что не нужно вызывать DNS (поскольку это занимает некоторое время). Вместо этого возвращается чи- сленное представление IP-адреса, вероятно, при помощи вызова функции 1 net_ntop. « Аналогично, флаг NI_NUMERICSERV определяет, что вместо имени службы дол- жен быть возвращен десятичный номер порта. Обычно серверы должны зада- вать этот флаг, поскольку номера портов клиента, как правило, не имеют со- ответствующего имени службы — это динамически назначаемые порты. Можно объединять несколько флагов путем логического сложения, если их сочетание имеет смысл, например NI_DGRAM и NI_NUMERICHOST, так как некоторые сочетания бессмысленны, например NI_NAMEREQD и NI_NUMERICHOST. ПРИМЕЧАНИЕ------------------------------------------------------------------ Функция getnameinfo рассматривалась в Posix.lg, но она определяется в RFC 2133. 11.14. Функции, допускающие повторное вхождение Функция gethostbyname из раздела 9.3 имеет интересную особенность, которую мы еще не рассматривали: она не допускает повторное вхождение (nonreentrant). Мы еще столкнемся с этой проблемой в главе 23, когда будем обсуджать потоки, но не менее интересно найти решение этой проблемы сейчас, без необходимости обращаться к понятию потоков. Сначала посмотрим, как эта функция работает. Если мы изучим ее исходный код (это несложно, поскольку исходный код для всей реализации BIND свобод- но доступен), то увидим, что обе функции — и gethostbyname, и gethostbyaddr — содержатся в одном файле, который имеет следующий вид: static struct hostent host. /* здесь хранится результат */ struct hostent * gethostbyname(const char *hostname) {
344 Глава 11. Дополнительные преобразования имен и адресов return(gethostbyname2(hostname family)) /*Листинг9 2*/ } struct hostent * gethostbyname2(const char *hostname int family) { /* вызов функций DNS для запроса А или АААА */ /* заполнение структуры адреса узла */ return(&host). } struct hostent * gethostbyaddr!const char *addr. size_t len int family) { /* вызов функций DNS для запроса PTR в домене in-addr.arpa */ /* заполнение структуры адреса узла */ return(&host) } Мы выделили полужирным шрифтом спецификатор класса памяти stati с ито- говой структуры, потому что основная проблема в нем. Тот факт, что эти три функ- ции используют общую переменную host, представляет другую проблему, кото- рую мы обсуждали в упражнении 9.1. (Вспомните листинг 9.2.) Функция gethostbyname2 появилась в BIND 4.9.4 с добавлением поддержки IPv6. Мы будем игнорировать тот факт, что когда мы вызываем функцию gethostbyname, задействуется функция gethostbyname2, поскольку это не относится к предмету обсуждения. Проблема повторного вхождения может возникнуть в нормальном процессе Unix, вызывающем функцию gethostbyname или gethostbyaddr и из управляющего элемента главного потока, и из обработчика сигнала. Когда вызывается обработ- чик сигнала (допустим, это сигнал SIGALRM, который генерируется раз в секунду), главный поток управляющего элемента процесса временно останавливается и вы- зывается функция обработки сигнала. Рассмотрим следующую ситуацию: main!) { struct hostent *hptr signal(SIGALRM sig_alrm). hptr = gethostbyname! ) } void signal rm!int signo) { struct hostent *hptr. hptr » gethostbyname! ):
11.14. Функции, допускающие повторное вхождение 345 Если главный поток управления в момент остановки находится в середине выполнения функции gethostbyname (допустим, функция заполнила переменную host и должна сейчас возвратить управление), а затем обработчик сигналов вы- зывает функцию gethostbyname, то поскольку в процессе существует только один экземпляр переменной host, эта переменная используется снова. При этом значе- ния переменных, вычисленные при вызове из главного потока управления, заме- няются значениями, вычисленными при вызове из обработчика сигнала. Если мы посмотрим на функции преобразования имен и адресов, представ- ленные в этой главе и в главе 9, вместе с функциями i net_XXX из главы 4, мы заметим следующее: - Функции gethostbyname, gethostbyname2, gethostbyaddr, getservbyname и getservbyport традиционно не допускают повторного вхождения, поскольку все они возвра- щают указатель на статическую структуру. Е1екоторые реализации, поддерживающие программные потоки (Solans 2.x), предоставляют версии этих четырех функций, допускающие повторное вхож- дение, с именами, оканчивающимися суффиксом _г. О них рассказывается в следующем разделе. В качестве альтернативы некоторые реализации с поддержкой программных потоков (Digital Unix 4 0 и HP_UX 10.30) предоставляют версии этих функ- ций, допускающие повторное вхождение за счет использования собственных данных программных потоков. Функции 1 net_pton и i net_ntop всегда допускают повторное вхождение. Исторически функция i net_ntoa не допускает повторное вхождение, но неко- торые реализации с поддержкой потоков предоставляют версию, допускаю- щую повторное вхождение, которая строится на основе собственных данных потоков. Функция getaddrinfo допускает повторное вхождение, только если она сама вызывает функции, допускающие повторное вхождение, то есть если она вы- зывает соответствующую версию функции gethostbyname или getservbyname для имени узла или имени службы. Одной из причин, по которым вся память для результатов ее выполнения выделяется динамически, является возможность повторного вхождения. Функция getnamei nfo допускает повторное вхождение, только если она сама вызывает такие функции, то есть если она вызывает соответствующую вер- сию функции gethostbyaddr для получения имени узла или функции getservby- port для получения имени службы. Обратите внимание, что обе результирую- щих строки (для имени узла и для имени службы) размещаются в памяти вызывающим процессом, чтобы обеспечить возможность повторного вхож- дения. Похожая проблема возникает с переменной errno. Исторически существовало по одной копии этой целочисленной переменной для каждого процесса. Если процесс выполняет системный вызов, возвращающий ошибку, то в этой перемен- ной хранится целочисленный код ошибки. Например, функция close из стандар- тной библиотеки языка С может выполнить примерно такую последовательность действий:
346 Глава 11. Дополнительные преобразования имен и адресов поместить аргумент системного вызова (целочисленный дескриптор) в регистр; & поместить значение в другой регистр, указывая, что был сделан системный вызов функции cl ose; активизировать системный вызов (переключиться на ядро со специальной инструкцией); » проверить значение регистра, чтобы увидеть, что произошла ошибка; > если ошибки нет, возвратить (0); « сохранить значение какого-то другого регистра в переменной еггпо; возвратить (-1). Прежде всего заметим, что если ошибки не происходит, значение переменной еггпо не изменяется. Поэтому мы не можем посмотреть значение этой перемен- ной, пока мы не узнаем, что произошла ошибка (обычно на это указывает возвра- щаемое функцией значение -1). Будем считать, что программа проверяет возвращаемое значение функции cl ose и затем выводит значение переменной еггпо, если произошла ошибка, как в сле- дующем примере: if (close(fd) < 0) { fprintftstderr "close error, errno = M\en' errno) exit(l). } Существует небольшой промежуток времени между сохранением кода ошиб- ки в переменной еггпо в тот момент, когда системный вызов возвращает управле- ние, и выводом этого значения программой. В течение этого промежутка другой программный поток внутри процесса (то есть обработчик сигналов) может изме- нить значение переменной еггпо. Если, например, при вызове обработчика сигна- лов главный поток управления находится между close и fprintf и обработчик сигналов делает какой-то другой системный вызов, возвращающий ошибку (до- пустим, вызывается функция write), то значение переменной еггпо, записанное при вызове функции cl ose, заменяется на значение, записанное при вызове функ- ции write. При рассмотрении этих двух проблем в связи с обработчиками сигналов од- ним из решений проблемы с функцией gethostbyname (возвращающей указатель на статическую переменную) будет не вызывать из обработчика сигнала функ- ции, которые не допускают повторное вхождение. Проблемы с переменной еггпо (одна глобальная переменная, которая может быть изменена обработчиком сиг- нала) можно избежать, перекодировав обработчик сигнала так, чтобы он сохра- нял и восстанавливал значение переменной еггпо следующим образом: void sig_alrm(int signo) { int errno_save. errno_save = errno. /* сохраняем значение этой переменной при вхождении */ if (writer ) nbytes) fprintf(stderr "write error, errno = £d\en". errno). errno “ errno_save. /* восстанавливаем значение этой переменной при завершении */ 1
____________________11.15. Функции gethostbyname_r и gethostbyaddr_r 347 В этом коде мы также вызываем функцию fpri ntf, стандартную функцию вво- да-вывода, из обработчика сигнала. Это еще одна проблема повторного вхожде- ния, поскольку многие версии функций стандартной библиотеки ввода-вывода не допускают повторного вхождения: стандартные функции ввода-вывода не должны вызываться из обработчиков сигналов. Мы вернемся к проблеме повторного вхождения в главе 23 и увидим, как про- блема с переменной еггпо решается с помощью потоков. В следующем разделе описываются некоторые версии функций имен узлов, допускающие повторное вхождение. 11.15. Функции gethostbyname.r и gethostbyaddrr Чтобы превратить функцию, не допускающую повторное вхождение, такую как gethostbyname, в повторно входимую, можно воспользоваться двумя способами. 1. Вместо заполнения и возвращения статической структуры вызывающий про- цесс размещает в памяти структуру, и функция, допускающая повторное вхож- дение, заполняет эту структуру Эта технология используется для перехода от функции gethostbyname (которая не допускает повторное вхождение) к функ- ции gethostbyname r (которая допускает повторное вхождение). Но это реше- ние усложняется, поскольку помимо того, что вызывающий процесс должен предоставить структуру hostent для заполнения, эта структура также указы- вает на другую информацию: каноническое имя, массив указателей на псевдо- нимы, строки псевдонимов, массив указателей на адреса и сами адреса (см., например, рис. 9.2). Вызывающий процесс должен предоставить один боль- шой буфер, используемый для дополнительной информации, и заполняемая структура hostent будет содержать различные указатели на этот буфер. При этом добавляется как минимум три аргумента функции: указатель на запол- няемую структуру hostent, указатель на буфер, используемый для всей про- чей информации, и размер этого буфера. Требуется также четвертый допол- нительный аргумент — указатель на целое число, в котором будет храниться код ошибки, поскольку глобальная целочисленная переменная h_errno боль- ше не может использоваться. (Глобальная целочисленная переменная h_errno создает ту же проблему повторного вхождения, что описана нами для пере- менной еггпо.) Эта технология также используется функциями getnamei nfo и i net_ntop. 2. Входящая функция вызывает функцию mall ос и динамически выделяет па- мять. Это технология, используемая функцией getaddrinfo. Проблема при та- ком подходе заключается в том, что приложение, вызывающее эту функцию, должно вызвать также функцию freeaddri nfo, чтобы освободить динамичес- кую память. Если эта функция не вызывается, происходит утечка памяти-. каждый раз, когда процесс вызывает функцию, выделяющую память, объем памяти, задействованной процессом, возрастает. Если процесс выполняется в течение длительного времени (что свойственно сетевым серверам), то по- требление памяти этим процессом с течением времени неуклонно растет. Обсудим функции Solaris 2.x, допускающие повторное вхождение, не использу- емые для сопоставления имен с адресами и наоборот (то есть для разрешения имен).
348 Глава 11. Дополнительные преобразования имен и адресов #include <netdb h> struct hostent *gethostbyname_r(const char *hostname. struct hostent *result. char *buf. int buflen int *h_ermop'). struct hostent *gethostbyaddr_r(const char *addr. int len. int type, struct hostent ★result, char *buf. int buflen. int *h_ermop'). Обе функции возвращают непустой указатель в случае успешного выполнения. NULL в случае ошибки Для каждой функции требуется четыре дополнительных аргумента. Аргумент result — это структура hostent, размещенная в памяти вызывающим процессом и заполняемая данной функцией. При успешном выполнении функции этот ука- затель также является возвращаемым значением. Аргумент but — это буфер, размещенный в памяти вызывающим процессом, a but 1 еп — его размер. Буфер будет содержать каноническое имя, массив указате- лей на псевдонимы, строки псевдонимов, массив указателей на адреса и сами ад- реса. Все указатели в структуре hostent, на которую указывает result, указывают на этот буфер. Насколько большим должен быть этот буфер? К сожалению, все, что сказано в большинстве руководств, это что-то неопределенное вроде «Буфер должен быть достаточно большим, чтобы содержать все данные, связанные с за- писью узла». Текушие реализации функции gethostbyname могут возвращать до 35 указателей на альтернативные имена (псевдонимы), до 35 указателей на адре- са и использовать буфер размером 8192 байт для хранения альтернативных имен (псевдонимов) и адресов. Поэтому буфер размером 8192 байт можно считать под- ходящим. Если происходит ошибка, код ошибки возвращается через указатель h_errnop, а не через глобальную переменную h_errno. ПРИМЕЧАНИЕ---------------------------------------------------------------------- К сожалению, проблема повторного вхождения гораздо серьезнее, чем может показаться. Во-первых, не существует стандарта относитльно повторного вхождения и функций gethostbyname и gethostbyaddr. Posix. 1g определяет обе функции, но ничего не говорит • - о безопасности в многопоточной среде. В Unix 98 сказано только, что эти две функции ‘ не обязаны быть безопасными в многопоточной среде. Во-вторых, не существует стандарта для функций г. В этом разделе (в качестве при- мера) мы привели двефункции г, предоставляемые Solaris 2.x. Но в Digital Unix и HP-UX имеются версии этих функций с другими аргументами. Первые два аргумента функ- ции gethostbyname_r такие же, как и в версии Solaris, но оставшиеся три аргумента версии Solaris объединены в новую структуру hostent_data (которая должна быть раз- мещена в памяти вызывающим процессом), а указатель на эту структуру — это третий и последний аргумент. Обычные функции gethostbyname и gethostbyaddr в Digital Unix 4.0 и в HP-UX 10.30 допускают повторное вхождение при использовании собственных . данных потоков (см. раздел 23.5). Интересный рассказ о разработке функций _r Sola- ris 2.x содержится в [60]. Наконец, хотя версия функции gethostbyname, допускающая повторное вхожде- ние, может обеспечить безопасность, когда ее одновременно вызывают несколько раз- личных потоков, это ничего не говорит нам о возможности повторного вхождения для лежащих в ее основе функций распознавателя. На момент написания этой книги функ- ции оаспозиавателя в BIND не допускают повторного вхождения.
11.16. Реализация функций getaddrinfo и getnameinfo 349 11.16. Реализация функций getaddrinfo и getnameinfo Рассмотрим реализацию функций getaddri nfo и getnamei nfo. Разработка собствен- ной реализации первой из них даст нам возможность более детально предста- вить, как она работает. Наша реализация также поддерживает доменные сокеты Unix, как мы отмечали в разделе 11.5. ПРИМЕЧАНИЕ-------------------------------------------------------- Все части кода, рассматриваемого в этом разделе, зависящие от поддержки протоколов IPv4, IPv6 или домена Unix, ограничены определениями #ifdef и #endif соответствую- щей константы: IPv4, IPv6 или UNIXDOM AIN. Это позволяет компилировать код в си- стеме, поддерживающей любые сочетания этих трех протоколов. Однако мы удалили эти выражения препроцессора из показываемого нами кода, потому что они ничего не добавляют к нашему обсуждению и делают код труднее для понимания. Заметим также, что до главы 14 мы не рассматриваем подробно доменые сокеты Unix. На рис. 11.2 показаны функции, вызываемые функцией getaddrinfo. Все они начинаются с префикса да_. Рис. 11.2. Функции, вызываемые нашей реализацией функции getaddrinfo Первый файл — это наш заголовочный файл gaijidr h, показанный в листин- ге 11.12, который включается во все наши исходные файлы. Мы включаем наш обычный заголовочный файл unp h и один дополнитель- ный заголовочный файл. Использование нашего флага AI_CLONE и нашей структу- ры search мы скоро рассмотрим. Оставшаяся часть заголовочного файла задает прототипы функций, которые мы покажем в этом разделе. Листинг 11.12. Заголовок gai_hdr.h //1ibgai/gai_hdr с 1 #include "unp h" 2 ^include «ctype h> /* isxdigitO и г д */ 3 /* следующий внутренний флаг не может перекрываться с другими флагами AI ххх */
350 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.12 (продолжение) 4 #define AI_CLONE 4 /* клонируется для других типов сокетов */ 5 struct search { 6 const char *host: /* строка с именем или адресом узла */ 7 int family, /* AF_xxx */ 8 }. 9 /* прототипы функций для наших собственных внутренних функций */ 10 int ga_aistruct(struct addrinfo ***, const struct addrinfo *, 11 const void *. int). 12 struct addrinfo *ga_clone(struct addrinfo *). 13 int ga_echeck(const char *. const char *. int int. int. int). 14 int ga_nsearch(const char *. const struct addrinfo *. struct search *). 15 int ga_port(struct addrinfo *. int. int). 16 int ga_serv(struct addrinfo *. const struct addrinfo *. const char *). 17 int ga_umx(const char *. struct addrinfo *. struct addrinfo **) 18 int gn_ipv46(char *. size_t. char *. size_t. void *. size_t. 19 int. int. int). В листинге 11.13 показана первая часть нашей функции getaddri nfo. Листинг 11.13. Функция getaddrinfo: первая часть, инициализация //libgai/getaddrinfo с 1 #include "gai_hdr h“ 2 #include <arpa/nameser h> /* необходимо для <resolv h> */ 3 #include <resolv h> /* res_imt, _res */ 4 int 5 getaddrinfo(const char *hostname. const char *servname. 6 const struct addrinfo *hintsp. struct addrinfo **result) 7 { 8 int rc. error, nsearch. 9 char **ap. *canon. 10 struct hostent *hptr. 11 struct search search[3] *sptr. 12 struct addrinfo hints *aihead, **aipnext: 13 /* 14 * Если мы встретим ошибку, нам потребуется * освободить с помощью функции freeO всю динамически * выделенную память 15 * Здесь расположен макрос для упрощения кода 16 */ 17 #define error(e) { error - (е) goto bad. } 18 aihead = NULL. /* инициализация переменных автоматического класса памяти*/ 19 aipnext = &aihead: 20 canon = NULL. 21 if (hintsp = NULL) { 22 bzero(&hints. sizeof(hints)). 23 hints ai _family = AFJJNSPEC 24 } else 25 hints = *hintsp. /* копия структуры */ 26 /* сначала проверка основных ошибок */
11.16. Реализация функций getaddrinfo и getnameinfo 351 27 if ( (rc - ga_echeck(hostname. servname. hints ai_flags, hints ai_family. 28 hints ai_socktype, hints ai_protocol)) != 0) 29 error(rc). 30 /* сначала проверка для случая домена Unix */ 31 if (hostname != NULL && 32 (strcmp(hostname, "/local") == 0 || strcmp(hostname. "/Unix”) -- 0) && 33 (servname != NULL && servname[0] =- 7')) 34 return (ga_unix(servname. &hints. result)). Задание макроса error 13-17 Более чем в двенадцати точках этой функции нам потребуется освободить всю выделенную нами память и возвратить соответствующий код, если случится ошиб- ка. Чтобы упростить код, мы задаем этот макрос, который сохраняет возвращае- мый код в переменной error и переходит к метке bad в конце функции (см. лис- тинг 11.19). Инициализация автоматических переменных 18-20 Инициализируются несколько автоматических переменных (которые создаются при входе в блок, где они объявлены, и уничтожаются при выходе из него). Мы описываем указатели a i head и aipnext в листинге 11.24. Копирование структуры hints вызывающего процесса 21-25 Если вызывающий процесс предоставляет структуру hints, мы копируем ее в на- шу собственную локальную переменную, поэтому мы можем затем изменить ее. В противном случае мы начинаем с нулевой структуры, отличной от ai family, которая инициализируется в AFJJNSPEC. Последняя обычно задается нулевой, хотя Posix. 1g этого не требует. Проверка аргументов 26-29 Мы вызываем нашу функцию ga_echeck, показанную в листинге 11.26, чтобы проверить некоторые из наших аргументов. Проверка полного имени домена Unix 30-34 Если имя узла — или /local, или /ит х, и имя службы начинается с косой черты, мы обрабатываем этот аргумент как полное имя в домене Unix. Наша функция ga_unix (см. листинг 11.23) полностью обрабатывает полное имя. Оставшаяся часть нашей функции getaddrinfo (которая продолжается в лис- тинге 11.17) работает с сокетами IPv4 и IPv6. Наша функция ga nsearch, первая часть которой представлена в листинге 11.14, вычисляет, сколько раз мы выпол- няем поиск имени узла. Если вызывающий процесс задает семейство адресов AF_INET или AF_INET6, имя узла мы ищем только один раз. Но если семейство адре- сов не задано (AFJJNSPEC), мы выполняем операцию поиска дважды: для имени узла IPv6 и для имени узла IPv4. Эту функцию мы показываем для трех случаев: нет имени узла, установлен флаг AI_PASSIVE; нет имени узла, флаг AI_PASSIVE не установлен (активный сокет); задано имя узла. Эти три случая соответствуют трем основным частям табл. 11.3.
352 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.14. Функция ga_nsearch: нет имени узла, пассивный сокет //libgai/ga_nsearch с 6 1 nt 7 ga_nsearch(const char *hostname. const struct addrinfo *hintsp 8 struct search *search) 9 { 10 int nsearch - 0 11 if (hostname == NULL || hostname[0] = '\e0') { 12 if (hintsp->ai_flags & AI_PASSIVE) { 13 /* нет имени узла, установлен флаг AIJASSIVE подразумевается связывание с помощью символов подстановки */ 14 switch (hintsp->ai_family) { 15 case AFJNET 16 search[nsearch] host ="000 0", 17 search[nsearch] family = AF_INET, 18 nsearch++ 19 break 20 case AF_INET6 21 search[nsearch] host = "0 O'. 22 search[nsearch] family = AF_INET6. 23 ‘ nsearch++ 24 break. 25 case AFJJNSPEC 26 search[nsearch] host = "0 0" /* сначала IPv6, затем IPv4 */ 27 search[nsearch] family = AFJNET6. 28 ' nsearch++. 29 search[nsearch] host = 0000". 30 search[nsearch] family = AF_INET 31 nsearch++ 32 break 33 } Отсутствие имени узла, пассивный сокет 1113 Если вызывающий процесс не указывает имя узла и задает флаг AI_PASSI VE, мы возвращаем информацию для создания одного или более пассивных сокетов, кото- рые будут связаны с универсальными адресами. Переключение (оператор switch) выполняется в зависимости от семейства адресов: сокет IPv4 связывается с адре- сом 0.0.0.0 (INADDR_ANY), а сокет IPv6 связывается с адресом 0::0 (IN6ADDR_ANY_INIT). Если задано семейство адресов AFJJNSPEC, следует возвратить информацию для создания двух сокетов: первого сокета для IPv6 и второго для IPv4. Причина, по которой порядок создания сокетов именно такой (сначала IPv6, затем IPv4), в том, что сокет на узле с двойным стеком может работать как с клиентами IPv6, так и с клиентами IPv4. Поэтому в данном случае, если вызывающий процесс создает только один сокет из возвращаемого списка структур addri nfo, зто должен быть сокет IPv6. Эта функция создает массив структур search (см. листинг 11.12), каждый эле- мент которого задает имя узла и семейство адресов. Указатель на массив струк- тур search — это последний аргумент функции. Возвращаемое значение — число этих созданных структур, и оно всегда будет равно одному или двум. Следующая часть этой функции, показанная в листинге 11.15, обрабатывает ситуацию, когда нет имени узла и флаг AI_PASS IVE не установлен. При этом подра- зумевается, что вызывающий процесс хочет создать активный сокет для локаль- ного vзлa
11.16. Реализация функций getaddrinfo и getnameinfo 353 Листинг 11.15. Функция ga nsearch: нет имени узла, не пассивный сокет //Iibgai/ga_nsearch с 34 } else { 35 /* нет имени узла и не установлен флаг AIJASSIVE соединение с локальным узлом */ 36 switch (hintsp->ai_family) { 37 case AFJNET 38 searchEnsearch] host = "localhost' . /* 127 0 0 1 */ 39 searchEnsearch] family = AFJNET. 40 nsearch++ 41 break 42 case AFJNET6 43 searchEnsearch] host = "0 1”. 44 searchEnsearch] family = AFJNET6: 45 nsearch++. 46 break. 47 case AFJJNSPEC 48 searchEnsearch] host = "0 1" /* сначала IPv6, затем IPv4 */ 49 searchEnsearch] family = AFJNET6 50 nsearch++ 51 searchEnsearch] host = ’localhost" 52 searchEnsearch] family = AFJNET. 53 nsearch++ 54 break 55 } 56 } 34 56 Для IPv4 мы считаем, что имя узла 1 оса 1 host возвратит адрес закольцовки, обыч- но 127.0.0.1. Общепринятого имени для локального узла с IPv6 не существует, поэтому мы возвращаем адрес закольцовки 0 1. В случае пассивного сокета, если не задано семейства адресов, мы возвращаем две структуры: одну для IPv6 и да- лее одну для IPv4. В листинге 11.16 показана последняя часть нашей функции — часть else ис- ходного оператора i f. Код выполняется, когда задано имя узла. Листинг 11.16. Функция ga_nsearch: задано имя узла //libgai/да jisearch с 57 } else { /* задано имя узла */ 58 switch (hintsp->aiJamily) { 59 case AFJNET 60 searchEnsearch] host = hostname 61 searchEnsearch] family = AFJNET. 62 nsearch++ 63 break. 64 case AFJNET6 65 searchEnsearch] host = hostname searchEnsearch] family - AFJNET6, и, nsearch++ 68 break 69 case AFJJNSPEC 70 searchEnsearch] host = hostname 71 searchEnsearch] family = AFJNET6 /* сначала IPv6*/ 72 nsearch++. 73 searchEnsearch] host = hostname 74 searchEnsearch] family = AFJNET /* затем IPv4 */ 75 nsearch++, 76 break. 77 }
354 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.16 (продолжение) 78 ) 79 if (nsearch < 1 || nsearch > 2) 80 err_quit("nsearch = W'. nsearch). 81 return (nsearch). 82 } 57-82 Флаг AI_PASSIVE в данном сценарии не имеет значения. Нам необходимо отыс- кать имя узла. Если вызывающий процесс создает пассивный сокет, полученная в результате структура адреса сокета будет использована в вызове функции bi nd, но если вызывающий процесс создает активный сокет, структура адреса сокета будет использована в вызове функции connect. Мы создаем одну или две структу- ры search: одну, если задано семейство адресов, и две, если оно не задано. Как и в предыдущих сценариях, возвращаются две структуры — первая для IPv6 и вто- рая для IPv4. Теперь мы возвращаемся к нашей функции getaddrinfo, которая продолжает- ся в листинге 11.17. Ее выполнение начинается с вызова функции ga_nsearch. Листинг 11.17. Функция getaddrinfo: проверка строки адреса IPv4 или IPv6 //1ibgai/getaddrinfo с 35 /* остальная часть функции для IPv4/IPv6 */ 36 nsearch - ga_nsearch(hostname. &hints. &search[0]) 37 for (sptr - &search[0]. sptr < &search[nsearch]. sptr++) { 38 /* 4check for an IPv4 dotted-decimal string */ 39 if (isdigit(sptr->host[OD) { struct in_addr inaddr. if (inet_pton(AF_INET. sptr->host. &inaddr) -= 1) { if (hints aijamily ' = AFJJNSPEC && hints aijamily AFJNET) error(EAI_ADDRFAMILY). if (sptr->family '= AFJNET) continue. /* игнорируем */ rc = ga_aistruct(&aipnext &hints. &inaddr, AFJNET): if (rc != 0) error(rc). continue. } 53 /* проверяем шестнадцатеричный адрес IPv6 */ 54 if ( (isxdigit(sptr->host[OD || sptr->host[0] — && 55 (strchr(sptr->host. '') l= NULL)) { 56 struct in6_addr m6addr. 57 if (inet_pton(AF_INET6. sptr->host. &in6addr) — 1) { 58 if (hints aijamily AFJJNSPEC && 59 hints aijamily != AFJNET6) 60 error(EAI_ADDRFAMILY). 61 If (sptr->faimly !- AFJNET6) 62 continue. /* игнорируем */ 63 rc - ga_aistruct(&aipnext, &hints. &in6addr. AFJNET6): 64 if (rc != 0) 65 error(rc). 66 continue. 67 } 68 } 41 42 43 44 45 46 47 48 49 50 51 52
11.16. Реализация функций getaddrinfo и getnameinfo 355 Вызов функции ga_nsearch 36 Мы вызываем нашу функцию ga_nsea rch, заполняющую массив search и возвра- щающую число структур в массиве — одна или две. Цикл по всем структурам search 37 Мы выполняем цикл для каждой структуры search, созданной функцией ga_ns earch. Проверка строки в точечно-десятичной записи IPv4 39-44 Если первый символ имени узла является цифрой, мы проверяем, действительно ли имя узла является строкой в точечно-десятичной записи. Для выполнения этой проверки и преобразования мы вызываем функцию i net_pton. Если функция вы- полняется успешно, но вызывающий процесс задает семейство адресов, отлич- ное от AF_INET, это приводит к ошибке. 45-46 Мы проверяем, что элемент family структуры search также равен AF_INET, хотя несовпадение в данном случае вызывает только игнорирование этой структуры. Этот сценарий может иметь место, если, например, вызывающий процесс указы- вает имя узла 192.3.4.5, но не задает никакого семейства адресов. Функция да_ nsearch создает две структуры search: одну для IPv6 и одну для IPv4. В первый раз в цикле for вызов функции i net_pton оказывается успешным, но поскольку эле- мент farm 1у структуры search — зто AF_INET6, мы хотим игнорировать эту структу- ру, а не генерировать ошибку. Создание структуры addrinfo 47-52 Наша функция ga_a i struct создает структуру addri nfo и добавляет ее к создава- емому связному списку (указатель aipnext). Проверка строки адреса IPv6 53-60 Если первый символ имени узла является либо шестнадцатеричным числом, либо двоеточием и строка содержит двоеточие, мы проверяем, является ли имя узла строкой адреса IPv6, вызывая функцию inet pton. Если она выполняется успешно, но вызывающий процесс задает семейство адресов, отличное от AF_INET6, то это ошибка. 61-62 Мы проверяем, что элемент family структуры search также равен AF_INET6, но несовпадение в данном случае вызовет только игнорирование этой структуры search. Создание структуры addrinfo 63-68 Наша функция ga_ai struct создает структуру addri nfo и добавляет ее к создава- емому связному списку. В первых двух операторах if в цикле for (см. листинг 11.17) осуществляется проверка строки в точечно-десятичной записи (адрес IPv4) или строки шестнад- цатеричных чисел (адрес IPv6). В оставшейся части цикла, показанной в листинге 11.18, ведется поиск имени узла при помощи вызова либо функции gethostbyname, либо функции gethostbyname2.
356 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.18. Функция getaddrinfo: поиск имени узла //1тЬдат/getaddrinfo с 69 /* остальная часть цикла for() для поиска имени узла */ 70 if ( (_res options & RESJNIT) — 0) 71 resinitO /* необходимо для установления _res.options */ 72 if (nsearch == 2) { 73 _res options &= ~RESJISE_INET6. 74 hptr = gethostbyname2(sptr->host sptr->famly) 75 } else { 76 if (sptr->family “ AFJNET6) 77 _res options |= RES_USE_INET6. 78 else 79 _res options &- -RES_USE_INET6. 80 hptr - gethostbyname(sptr->host). 81 } 82 If (hptr == NULL) { 83 if (nsearch = 2) 84 continue. /* если вызов неудачен, но nsearch равно двум, это не ошибка */ 85 switch (h_errno) { 86 . case HOST_NOT_FOUND 87 error(EAIJIONAME). 88 t case TRY_AGAIN 89 ' error(EAI_AGAIN). 90 case NO_RECOVERY 91 error(EAI_FAIL). 92 case N0_DATA 93 error(EAI_NODATA). 94 default 95 error(EAI_NONAME). 96 } 97 } 98 /* если семейство адресов задано проверяем нет ли расхождений */ 99 if (hints ai_famly '= AFJJNSPEC && hints ai_family '= hptr->h_addrtype) 100 error(EAI_ADDRFAMILY) 101 /* сохраняем каноническое имя */ 102 if (hostname '= NULL && hostname[0] != ’\e0' && 103 (hints ai_flags & AI_CANONNAME) && canon == NULL) { 104 if ( (canon - strdup(hptr->h_name)) = NULL) 105 error(EAI_MEMORY). 106 } 107 /* создаем одну структуру addrinfo{} для каждого возвращенного адреса */ 108 for (ар = hptr->h_addr_list. *ар '= NULL ар++) { 109 rc = ga_aistruct(&aipnext. &hints. *ар. hptr->h_addrtype). НО if (rc |= 0) 111 error(rc) 112 } ИЗ } 114 if (aihead == NULL) 115 error(EAI_NONAME) /* ничего не найдено */ Инициализация распознавателя 70-71 Мы вызываем функцию распознавателя res_init, если она не была вызвана раньше. 1
11.16. Реализация функций getaddrinfo и getnameinfo 357 Вызов функции gethostbyname2, когда поиск выполняется дважды 72-74 Если nsearch равно 2, мы проходим цикл for дважды: сначала для адреса IPv6, затем для адреса IPv4. Если аргумент имени узла имеет адрес только в одном из двух семейств адресов, мы хотим возвратить только один адрес. Например, наш узел solan s в разделе 9.2 имеет в DNS запись типа АААА и запись типа А. Прохо- дя цикл в первый раз, мы хотим найти запись типа АААА, а во второй — запись типа А. Но если имя узла имеет только запись типа А, мы не хотим обрабатывать эту запись во время первого прохождения цикла, когда элемент farm 1у структуры search равен AF_INET6, то есть поскольку мы знаем, что ищем запись типа А для этого узла, нам не нужно искать запись типа АААА с помощью функции get- hostbyname и, возможно, возвращать адрес IPv4, преобразованный к виду IPv6, соответствующий записи типа А. Глядя на табл. 9.1, мы можем сказать, что поиск записей типа А для семейства адресов AF_INET и записей типа АААА для семей- ства адресов AF_INET6 соответствует вызову функции gethostbyname2 с отключен- ным параметром RES_USE_INET6 вместо функции gethostbyname. Вызов функции gethostbyname, когда поиск выполняется однократно 75-81 Если выполняется только один поиск, мы вызываем функцию gethostbyname с установленным параметром RES_USE_INET6, если семейство адресов — AF_INET6, или с отключенным параметром, если семейство адресов — AF_INET. Например, если вызывающий процесс указывает имя узла, имеющее только запись типа А, но задает семейство адресов AF INET6, мы должны возвратить адрес IPv4, преобра- зованный к виду IPv6. Обработка неудачного вызова распознавателя 82-97 Если вызов распознавателя оказался неудачным, но nsearch равно двум, это не ошибка, поскольку один из проходов через цикл может оказаться успешным. (В конце цикла мы проверяем, что возвращается как минимум одна структура addrinfo.) Но если это был единственный вызов распознавателя, мы возвращаем ошибку, соответствующую значению h errno распознавателя. Проверка несовпадения семейства адресов 98-100 Если вызывающий процесс задает семейство адресов, но возвращаемое рас- познавателем семейство отличается от заданного, это ошибка. Сохранение канонического имени 101-106 Если вызывающий процесс задает имя узла и флаг AI_CANONNAME, мы сохраня- ем первое каноническое имя, возвращаемое распознавателем. (Вспомните лис- тинг 11.15, когда мы вызываем распознаватель для имени local host, даже если вызывающий процесс не задает имени узла.) Мы дублируем строку, возвращае- мую распознавателем, и сохраняем указатель на нее в переменной canon. Создание структуры addrinfo для каждого адреса 107-И2 Для каждого адреса, возвращаемого распознавателем в массиве h_addr_list, мы вызываем нашу функцию ga_aistruct для создания структуры addrinfo и до- бавления ее к создаваемому связному списку структур.
358 Глава 11 Дополнительные преобразования имен и адресов Проверка отсутствия совпадений 114-115 Если начало связного списка структур addrinfo по-прежнему является пус- тым указателем, это означает, что все итерации в цикле for оказались неудачными. В листинге И 19 показана последняя часть функции getaddrinfo Листинг 11.19. Функция getaddrinfo обработка имени службы //1 ibgai/getaddrinfo с 116 /* возвращается каноническое имя */ 117 if (hostname ' = NULL && hostnameLO] '= \е0 && 118 hints ai_flags & AI_CANONNAME) { 119 if (canon '= NULL) 120 aihead->ai_canonname = canon 121 else { 122 if ( (aihead->ai_canonname - strdup(search[O] host)) — NULL) 123 error(EAI_MEMORY) 124 } 125 } 126 /* теперь обрабатываем имя службы */ 127 if (servname '= NULL && servname[0] '\e0 ) { 128 if ( (rc = ga_serv(aihead &hints servname)) 0) 129 error(rc) 130 } 131 *result - aihead /* указатель на первую структуру в связном списке */ 132 return (0) 133 bad 134 freeaddrinfo(aihead) /* освобождается вся динамически выделенная память */ 135 return (error) 136 } Возвращение канонического имени 116-125 Если вызывающий процесс задает имя узла и флаг AI_CANONNAME и мы сохра- нили копию канонического имени в нашем указателе canon, этот указатель воз- вращается в элементе ai_canonname первой структуры addrinfo Если распознава- телем не найдено ни одного канонического имени (возможно, имя узла было строкой с адресом), вместо этого возвращается копия канонического имени Обработка имени службы 126-130 Если вызывающий процесс задает имя службы, оно обрабатывается при по- мощи нашей функции ga_serv Возвращение указателя на связный список 131-132 Возвращается указатель на начало созданного связного списка структур addr- 1 nfo, функция возвращает нулевое значение (что означает ее успешное выполне- ние) Возвращение ошибки 133-135 Если встречается ошибка, вызывается функция freeaddrinfo для освобожде- ния всей памяти, которая была выделена, и возвращается значение EAI_xvx Наша функция ga_serv, которая вызывалась из программы в листинге 11.19 для обработки имени службы, показана в листинге 11 20
11 16 Реализация функций getaddrinfo и getnameinfo 359 Листинг 11.20. Функция ga serv //libgai/ga_serv с 5 int 6 ga_serv(struct addrinfo *aihead const struct addrinfo *hintsp 7 const char *serv) 8 { 9 int port rc nfound 10 struct servent *sptr 11 nfound - 0 12 if (isdigitlserv[0])) { /* сначала проверяется строка с номером порта */ 13 port - htons(atoi(serv)) 14 if (hintsp->ai_socktype) { 15 /* вызывающий процесс задает тип сокета */ 16 if ( (rc = ga_port(aihead port hintsp->ai_socktype)) < 0) 17 return (EAI_MEMORY) 18 nfound += rc 19 } else { 20 /* вызывающий процесс не задает тип сокета */ 21 if ( (rc = ga_port(aihead port SOCK_STREAM)) < 0) 22 return (EAI_MEMORY) 23 nfound += rc 24 if ( (rc = ga_port(aihead port SOCK_DGRAM)) < 0) 25 return (EAI_MEMORY) 26 nfound += rc 27 } 28 } else { 29 /* пробуем имя службы TCP затем UDP */ 30 if (hintsp->ai_socktype == 0 || hintsp >ai_socktype == SOCK_STREAM) { 31 if ( (sptr = getservbyname(serv tcp )) NULL) { 32 if ( (rc = ga_port(aihead sptr->s_port SOCK_STREAM)) < 0) 33 return (EAI_MEMORY) 34 nfound += rc 35 } 36 } 37 if (hintsp->ai_socktype == 0 || hintsp >ai_socktype — SOCK_DGRAM) { 38 if ( (sptr - getservbyname!serv udp )) '= NULL) { 39 if ( (rc = ga_port(aihead sptr->s_port SOCK_DGRAM)) < 0) 40 return (EAI_MEMORY). 41 nfound += rc 42 } 43 } 44 } 45 if (nfound == 0) { 46 if (hintsp->ai_socktype — 0) 47 return (EAI_NONAME) /* все вызовы функции getservbyname!) окончились неудачно */ 48 else 49 return (EAI_SERVICE) /* эта служба не поддерживается для данного типа сокета */ 50 } 51 return (0) 52 } Проверка строки с номером порта 12-27 Если первый символ в имени службы — это цифра, мы считаем, что имя служ- бы задано через номер порта, и вызываем функцию atoi для преобразования его в двоичное число Если вызывающий процесс задает определенный тип сокета
360 Глава 11. Дополнительные преобразования имен и адресов (SOCK_STREAM или SOCKJJGRAM), то тогда наша функция ga_port вызывается один раз для этого типа сокета. Но если тип сокета не задан, наша функция ga_port вызы- вается дважды — один раз для TCP, второй для UDP. (Вспомните табл. 11.1.) Мы подсчитываем, сколько раз функция ga_port завершается успешно, и возвращаем ошибку, только если при завершении выполнения функции полученное число равно 0. Вызов функции getservbyname для TCP 28-36 Если тип сокета не задан или задан сокет TCP, вызывается функция getservbyname со вторым аргументом "tcp". Если вызов оказывается успешным, вызывается наша функция ga_port. Если этот вызов оказывается неудачным, это нормально, по- скольку имя службы может подойти для UDP. Вызов функции getservbyname для UDP 37-44 Если тип сокета не задан или задан сокет UDP, вызывается функция getserv- byname со вторым аргументом "udp". Если вызов оказывается успешным, вызыва- ется наша функция ga_port. Проверка ошибки 45-51 Если наш счетчик nfound ненулевой, функция выполнилась успешно. Иначе возвращается ошибка. Наша функция ga_port, которую мы показываем в листинге 11.21, вызывалась в листинге 11,20 каждый раз при определении номера порта. Листинг 11.21. Функция да_port //libgai/ga_port с 27 int 28 ga_port(struct addrinfo *aihead, int port, int socktype) 29 I* номер порта должен быть в сетевом порядке байтов */ 30 { 31 int nfound = 0: 32 struct addrinfo *ai. 33 for (ai = aihead. ai != NULL, ai = ai->ai_next) { 34 if (ai->ai Jlags & AIJJLONE) { 35 if (ai->ai_socktype != 0) { 36 if ( (ai = ga_clone(ai)) == NULL) 37 return (-1) /* ошибка при выделении памяти */ 38 /* ai указывает на вновь клонированный элемент, который нам нужен */ 39 } 40 } else if (ai->ai_socktype != socktype) 41 continue. /* игнорируем несовпадение типов сокетов */ 42 ai->ai_socktype = socktype. 43 switch (ai->ai_famly) { 44 case AFJNET: 45 ((struct sockaddr_in *) ai->ai_addr)->sin_port = port. 46 nfound++. 47 break. 48 case AFJNET6: 49 ((struct sockaddr_in6 *) ai->ai_addr)->sin6_port - port: 50 nfound++.
11.16. Реализация функций getaddrinfo и getnameinfo 361 51 break: 52 } 53 } 54 return (nfound); 55 } Цикл по всем структурам addrinfo 33 Мы выполняем цикл по всем структурам add г i nfo, созданным при помощи функ- ции ga_aistruct в листингах 11.17 и 11.18. Флаг AI_CLONE всегда устанавливается функцией ga_ai struct, если вызывающий процесс не задает тип сокета. Это ука- зание на то, что данная структура addrinfo, возможно, должна быть клонирована и для TCP, и для UDP. Проверка флага AI_CLONE 34-42 Если флаг AI_CLONE установлен и задан тип сокета, из данной структуры addri nfo клонируется другая структура при помощи нашей функции ga_cl one. Пример бу- дет показан далее. Установка номера порта в структуре адреса сокета 44-47 В структуре адреса сокета устанавливается номер порта, и наш счетчик nfound увеличивается на единицу. Рассмотрим следующий пример. На рис. 11.1 мы рассматривали вызов функ- ции getaddrinfo для узла с двумя IP-адресами и с именем службы domain (порт 53 и для TCP, и для UDP), тип сокета не задавался. Цикл в нашей функции getaddri nfo (см. листинг 11.18) создает две структуры addrinfo, по одной для каждого IP-ад- реса, возвращаемого функцией gethostbyname. Флаг AI_CLONE также установлен в каждой структуре, поскольку не задан тип сокета. Итоговый связный список пред- ставлен на рис. 11.3. aihead addrinfo{} addrinfo{} Рис. 11.3. Структуры addrinfo при первом вызове функции ga_port Функция ga_serv вызывается в листинге 11.19. Поскольку имя службы domain действительно и для TCP, и для UDP. функция getservbyname вызывается два раза, и поэтому функция ga_port также вызывается дважды: сначала ее последний ар- гумент равен SOCK_STREAM, а при втором вызове последний аргумент равен SOCK DGRAM. При первом вызове функция ga port начинает со связного списка, показанного
362 Глава 11. Дополнительные преобразования имен и адресов на рис. 11.3. В листинге 11.21 флаг AI_CLONE установлен для обеих структур, но тип сокета нулевой. Следовательно, все, что происходит со структурой addrinfo, когда функция ga_port вызывается в первый раз, — в структуре адреса сокета эле- мент ai_socktype принимает значение SOCK_STREAM, а номер порта — значение 53. Флаг AI_CLONE остается установленным. При этом мы получаем связный список, изображенный на рис. 11.4. aihead addrinfo{} addrinfo{} Рис. 11.4. Структуры addrinfo после первого вызова функции ga_port Но когда функция ga_port вызывается во второй раз (с последним аргументом SOCK_DGRAM), поскольку флаг AI_CLONE установлен и тип сокета ненулевой, функ- ция ga_cl one вызывается для каждой структуры addrinfo. Элемент ai_socktype каж- дой заново клонированной структуры принимает значение SOCK_DGRAM, и мы полу- чаем связный список, показанный на рис. 11.5. Наэтом рисунке вторая структура addrinfo и ее структура адреса сокета клонированы из первого набора структур, а четвертая структура addri nfo и ее структура адреса сокета клонированы из тре- тьего набора структур. aihead addrinfo{} addrinfo{} addrinfo{) addrinfo{) Рис. 11.5. Структуры addrinfo после второго вызова функции да_port В листинге 11.22 показана наша функция ga_clone, которая была вызвана в листинге 11.21 для клонирования новой структуры addrinfo и ее структуры ад- реса сокета из существующего набора структур.
11.16. Реализация функций getaddrinfo и getnameinfo 363 Листинг 11.22. Функция ga_clone //libgai/ga_clone с 5 struct addrinfo * 6 ga_clone(struct addrinfo *ai) 7 { 8 struct addrinfo *new: 9 if ( (new = callocd. sizeof(struct addrinfo))) == NULL) 10 return (NULL). 11 new->ai_next = ai->ai_next: 12 ai->ai_next = new. 13 new->ai_flags - 0: /* проверяем, что флаг AI_CLONE сброшен */ 14 new->ai _famly = ai->ai_fannly. 15 new->ai_socktype - ai->ai_socktype, 16 new->ai_protocol = ai->ai_protocol: 17 new->ai_canonname - NULL: 18 new->ai_addrlen = ai->ai_addrlen, 19 if ( (new->ai_addr = malloc(ai->ai_addrlen)) == NULL) 20 return (NULL). 21 memcpy(new->ai_addr. ai->ai_addr. ai->ai_addrlen): 22 return (new): 23 } Выделение структуры addrinfo и включение ее в связный список 9-12 В памяти размещается новая структура addrinfo, и указатель aijiext клонируе- мой структуры (то есть предыдущей структуры в списке) становится указателем на эту новую структуру. Инициализация из клонированной записи 13-22 Все поля новой структуры addrinfo копируются из клонируемой структуры, за исключением поля ai_fl ags, которое становится нулевым, и ai_canonname, которое становится пустым указателем. Указатель на заново созданную структуру — это возвращаемое значение функции. Наша функция ga um х, которую мы показываем в листинге 11.23, была вызва- на из листинга 11.13 для обработки полного имени в домене Unix. Листинг 11.23. Функция ga_unix //libgai/gajimx с 3 int 4 ga_unix(const char *path. struct addrinfo *hintsp. struct addrinfo **result) 5 { 6 int rc. 7 struct addrinfo *aihead, **aipnext. 8 aihead = NULL. 9 alpnext = &aihead. 10 if (hintsp->ai_fannly '= AFJJNSPEC && hintsp->ai_family '= AF_LOCAL) 11 return (EAI-ADDRFAMILY). 12 if (hintsp->ai_socktype ~ 0) { 13 /* если не указан тип сокета, сначала возвращаются потоковые сокеты, а затем дейтаграммные */ продолжение #
364 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.23. Функция ga_unix 14 hintsp->ai_socktype = SOCK_STREAM. 15 if ( (rc = ga_aistruct(&aipnext. hintsp. path. AF_LOCAL)) '= 0) 16 return (rc). 17 hintsp->ai_socktype = SOCK_DGRAM 18 } 19 if ( (rc = ga_aistruct(&aipnext. hintsp. path. AF_L0CAL)) '= 0) 20 return (rc). 21 if (hintsp->ai_flags & AI_CANONNAME) { 22 struct utsname myname. 23 if (uname(&myname) < 0) 24 return (EAJ_SYSTEM), 25 if ( (aihead->ai_canonname = strdup(myname.nodename)) == NULL) 26 return (EAI_MEMORY), 27 } 28 *result = aihead: /* указатель на первую структуру в связном списке */ 29 return (0). 30 } Функция ga_aistruct, создание структур 10-20 Если тип сокета не задан, мы вызываем нашу функцию ga_ai struct дважды, чтобы создать две структуры add г т nfo: один раз с типом сокета SOCK_STREAM, а второй раз — с типом сокета SOCK DGRAM. Но если вызывающий процесс задает ненулевой тип сокета, наша функция ga_ai struct вызывается только один раз, создавая одну структуру addrinfo с этим типом сокета. Возвращение канонического имени 21-27 Если вызывающим процессом был задан флаг AI CANONNAME, мы вызываем функ- цию uname, чтобы получить имя системы, и возвращаем элемент nodename (см. раз- дел 9.7) в качестве канонического имени. Значение указателей aihead и alpnext мы объясним вместе с функцией да_ ai struct, которая описывается следующей. Наша функция ga aistruct вызывается для создания структуры addrinfo в ли- стингах 11.17 и 11.18 (для адреса IPv4 или IPv6) и в листинге 11.23 (для домен- ного сокета Unix). Первую часть этой функции мы показываем в листинге 11.24. Листинг 11.24. Функция gaaistruct: первая часть //I ibgai/ga_aistruct с 5 int 6 ga_aistruct(struct addrinfo ***paipnext const struct addrinfo *hintsp, 7 const void *addr. int family) 8 { 9 struct addrinfo *ai. 10 if ( (ai = callocd. sizeof(struct addrinfo))) = NULL) 11 return (EAI_MEMORY): 12 ai->ai_next = NULL: 13 ai->ai_canonname = NULL. 14 **paipnext = ai. 15 *paipnext = &ai->ai_next. 16 if ( (ai->ai_socktype = hintsp->aisocktype) — 0)
11.16. Реализация функций getaddrinfo и getnameinfo 365 17 ai->ai_flags |- AI_CL0NE. 18 ai->ai_protocol - hintsp->ai_protocol Выделение структуры addrinfo и добавление ее к связному списку 10-15 В памяти размещается структура addrinfo и добавляется к создаваемому связ- ному списку. Для построения связного списка используются два указателя: ai head и aipnext. Оба были размещены в памяти и проипициалпзированы в листинге 11.13 для сокета IPv4 или IPv6 и в листинге 11.24 для сокета домена Unix. Указатель aihead инициализируется как пустой указатель, a a i pnext — как указатель на a i head. Они показаны на рис. 11.6. Рис. 11.6. Инициализация указателей связного списка Указатель aihead всегда указывает на первую структуру addrinfo в связном списке (следовательно, типом данных является struct addri nfo *). Указатель aipnext обычно указывает на элемент ai_next последней структуры в связном списке (по- этому его тип — struct addrinfo **). Мы употребляем по отношению к указателю aipnext слово «обычно», поскольку при инициализации он в действительности указывает на aihead, но после того, как первая структура размещена в памяти и по- мещена в список, он всегда указывает на элемент ai_next. Возвращаясь к нашей функции ga_ai struct, отметим, что после выделения новой структуры выполняются два выражения: **paipnext = ai, *paipnext = &ai->ai_next. Первое выражение устанавливает указатель a i next последней структуры в списке (или указатель a i head, если эта новая структура является первой в спис- ке) на вновь размещенную структуру, а второе — указатель aipnext на элемент aijiext вновь размещенной структуры. Дополнительный оператор разыменова- ния необходим в обоих выражениях, поскольку адрес a i pnext — это аргумент функ- ции (см. упражнение 11.4). Когда первая структура добавляется к списку, мы по- лучаем структуры данных, изображенные на рис. 11.7. aihead addrinfo_in{} Рис. 11.7. Связный список после добавления первой структуры
366 Глава 11. Дополнительные преобразования имен и адресов Когда наша функция ga_ai struct вызывается в следующий раз для размеще- ния второй структуры и добавления ее к списку, мы получаем структуры данных, изображенные на рис. 11.8. aihead addrinfo_in{) addrinfo_in{} Рис. 11.8. Связный список после добавления второй структуры NULL Установка типа сокета 16-17 Элемент ai_socktype устанавливается равным типу сокета, который задан вы- зывающим процессом, и если он нулевой, устанавливается флаг AI_CLONE. В листинге 11 25 показана вторая часть нашей функции. Оператор switch от- водит для каждого семейства адресов отдельный блок case, в котором осуществ- ляется размещение в памяти и инициализация структуры адреса сокета. Листинг 11.25. Функция ga_aistruct: вторая часть libgai/ga_aistruct с 19 switch ((ai->ai_family = family)) { 20 case AFJNET { 21 struct sockaddr_in *sinptr 22 /* размещаем структуру sockaddr_in{} и заполняем ее кроме номера порта*/ 23 if ( (sinptr - callocd sizeof(struct sockaddr_in))) — NULL) 24 return (EAI_MEMORY) 25 #lfdef HAVE_SOCKADDR_SA_LEN 26 sinptr->sin_len = sizeof(struct sockaddrjn) 27 #endif 28 sinptr->sin_fannly = AF_INET. 29 memcpy(&sinptr->sin_addr addr sizeof(struct in_addr)) 30 ai->ai_addr - (struct sockaddr *) sinptr 31 ai->ai_addrlen - sizeof(struct sockaddr_in). 32 break 33 } 34 case AFJNET6 { 35 struct sockaddr_in6 *sin6ptr 36 /* размещаем структуру sockaddr_in6{} и заполняем ее кроме номера порта */ 37 if ( (sin6ptr = callocd sizeof(struct sockaddr_in6))) “ NULL) 38 return (EAI_MEMORY) 39 #lfdef HAVE_SOCKADDR_SA_LEN 40 sin6ptr->sin6_len = sizeof(struct sockaddr_in6) 41 #endif 42 Sin6ptr->sin6_family = AF_INET6 43 memcpy(&sin6ptr >sin6_addr addr sizeof(struct in6_addr)); 44 ai >ai_addr = (struct sockaddr *) sin6ptr 45 ai->ai_addrlen = sizeof(struct sockaddr_in6) 46 break
11.16. Реализация функций getaddrinfo и getnameinfo 367 47 } 48 case AF_L0CAL { 49 struct sockaddr_un *unp 50 /* размещаем структуру sockaddr_un{} и заполняем ее */ 51 if (strlen(addr) >- sizeof(unp->sun path)) 52 return(EAI_SERVICE) 53 if ( (unp - callocd. sizeof(struct sockaddr_un))) — NULL) 54 return(EAI_MEMORY), 55 unp->sun_fann1y = AF_LOCAL 56 strcpy(unp->sun_path addr) 57 #lfdef HAVE_SOCKADDR_SA_LEN 58 unp->sun_len = SUN_LEN(unp) 59 #endif 60 ai->ai_addr = (struct sockaddr *) unp 61 ai->ai_addrlen - sizeof(struct sockaddr_un) 62 if (hintsp->ai_flags & AI_PASSIVE) 63 unlink(unp->sun_path) /* если выполняется неудачно, это не ошибка */ 64 break 65 } 66 } 67 return (0) 68 } Выделение и инициализация структуры адреса сокета IPv4 20-33 Выделяется структура sockaddr_i п, и указатель ai_addr в структуре add г т nfo уста- навливается для указания на нее. Происходит инициализация IP-адреса, семей- ства адресов и элементов длины структуры адреса сокета. Номер порта не иници- ализируется до тех пор, пока не будет вызвана функция ga_serv, которая, в свою очередь, вызывает функцию ga_port. Выделение и инициализация структуры адреса сокета IPv6 34-47 Выделяется и инициализируется структура sockaddr_in6. Выделение и инициализация структуры адреса доменного сокета Unix 48-65 Выделяется и инициализируется структура sockaddr_un. Адрес — это полное имя, и если вызывающим процессом был задан флаг AI_PASSI VЕ, мы пытаемся с помощью функции unlink отсоединить имя, чтобы не допустить возвращения ошибки при вызове функции bi nd. Но если функция unlink выполнится неудачно, это не будет ошибкой. Наша функция ga echeck, показанная в листинге 11 26, была вызвана в лис- тинге 11.13 для того, чтобы выполнить некоторую начальную проверку ошибок в аргументах вызывающего процесса Листинг 11.26. функция ga_echeck //libgai/ga_echeck с 5 int 6 ga_echeck(const char *hostname const char *servname продолжение &
368 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.26 [продолжение) 7 8 9 10 int flags, int family, int socktype int protocol) { if (flags & ~(AI_PASSIVE | AIJANONNAME)) return (EAI_BADFLAGS) /* неизвестный флаг */ 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 if (hostname == NULL || hostname[0] == '\e0’) { if (servname == NULL || servname[0] == '\e0') return (EAI NONAME) /* должно быть указано имя узла или службы */ } switch (family) { case AFJJNSPEC break case AFJNET if (socktype ’= 0 && (socktype '= SOCK STREAM && socktype i= SOCKJJGRAM && socktype l= S0CK_RAW)) return (EAI_SOCKTYPE) /* недопустимый тип сокета */ break, case AFJNET6 if (socktype '= 0 && (socktype '= SOCK STREAM && socktype SOCKJJGRAM && socktype i- SOCKJAW)) return (EAI_SOCKTYPE). /* недопустимый тип сокета */ break. case AFJOCAL if (socktype != 0 && (socktype i- SOCK STREAM && socktype '= SOCKJJGRAM)) return (EAI_SOCKTYPE). /* недопустимый тип сокета */ break, default return (EAI FAMILY). /* неизвестное семейство протоколов */ } return (0) } •-14 Проверяются флаги. Кроме того, должно быть задано либо имя узла, либо имя службы. 5-41 В зависимости от семейства адресов поддерживаются только определенные типы сокетов, при этом проверяется тип сокета. Мы не проверяем рекомендацию ai_protocol вызывающего процесса, если она есть, поскольку лишь немногие приложения задают это значение (которое стано- вится третьим аргументом функции socket). Если задается неверное сочетание, такое как тип сокета SOCK_DGRAM и протокол IPPROTO TCP, вызывающему процессу возвращается рекомендация ат protocol, как показано в листинге 11.24, а если вызывающий процесс использует это значение для вызова функции socket, воз- вращается ошибка EPROTONOSUPPORT. Мы закончили описание функции getaddri nfo, а также всех внутренних функ- ций, которые она вызывает. В листинге 11.27 представлена функция freeaddri nfo, освобождающая всю память в связном списке. Мы вызываем эту функцию в ли- стинге 11.19, если происходит ошибка, а пользователь также вызывает ее, чтобы освободить связный список структур.
11.16. Реализация функций getaddrinfo и getnameinfo 369 Листинг 11.27. Функция freeaddrinfo: первая часть //libgai/freeaddrinfo с 1 #include "gai_hdr h' 2 void 3 freeaddrinfo(struct addrinfo *aihead) 4 { 5 struct addrinfo *ai. *ainext 6 for (ai = aihead. ai l= NULL, ai = ainext) { 7 if (ai->ai_addr l= NULL) 8 free(ai->ai_addr). /* структура адреса сокета */ 9 if (ai->ai_canonname l= NULL) 10 free(ai->aijanonname). 11 ainext = ai-^aijiext. /* невозможно получить содержимое указателя avnexf’ после выполнения функции freed */ 12 free(ai). /* сама структура addrinfo{} */ 13 } 14 } 6-13 Мы перебираем связный список структур add г т nfo. Если была выделена память для структуры адреса сокета, она освобождается. Если была выделена память для строки канонического имени, она также освобождается. Наконец, освобождается память, занятая самой структурой addrinfo. Обратите внимание, что следует со- хранить содержимое указателя aijiext структуры до ее освобождения, так как мы не сможем ссылаться на эту структуру после того, как будет выполнена функция free. В листинге 11.28 показана наша реализация функции getnameinfo. Она состо- ит из оператора switch с одним блоком case для каждого семейства адресов. Листинг 11.28. Функция getnameinfo 1 //libgai/getnameinfo с 2 int 3 getnameinfo(const struct sockaddr *sa socklen_t salen 4 char *host size t hostlen, 5 char *serv, sizej servlen int flags) 6 { 7 switch (sa->sa_family) { 8 case AF_INET { 9 struct sockaddr_in *sain = (struct sockaddr_in *) sa. 10 return (gn_ipv46(host. hostlen serv servlen 11 &sain->sin_addr. sizeof(struct in_addr), 12 AFJNET. sain->sin_port, flags)) 13 } 14 case AFJNET6 { 15 struct sockaddr_in6 *sain = (struct sockaddr_in6 *) sa: » 16 return (gn_ipv46(host hostlen serv. servlen 17 ( &sain->sin6_addr. sizeof(struct in6_addr). 18 ‘ AFJNET6. sain->sin6_port, flags)). 19 > продолжение ту
370 Глава 11. Дополнительные преобразования имен и адресов Листинг 11.28 (продолжение) 20 case AF_LOCAL:{ 21 struct sockaddr_un *un - (struct sockaddr_un *) sa: 22 if (hostlen > 0) 23 snprintf(host. hostlen, "£s". "/local"): 24 if (servlen > 0) 25 snprintf(serv. servlen, ”£s", un->sun_path): 26 return (0); 27 } 28 default: 29 return (1): 30 } 31 } Обработка структур адреса сокета IPv4 и IPv6 -19 Мы вызываем нашу функцию gn_i pv46 (она показана следующей) для обработ- ки структур адресов сокетов IPv4 и IPv6. Обработка структур адреса доменного сокета Unix 0-27 Для структуры адреса доменного сокета Unix мы возвращаем имя узла /local, а в качестве имени службы возвращаем имя, связанное с сокетом. Если с сокетом не связано никакого полного имени, возвращаемое имя службы будет пустой строкой. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Мы возвращаем имя узла и имя службы, используя функцию snprintf вместо функции strncpy. Если бы мы использовали последнюю, мы могли бы написать: strncpy(host. "/local", hostlen); Хотя это гарантирует, что мы не переполним буфер вызывающего процесса, но если hostlen меньше или равно 6, буфер вызывающего процесса не будет содержать симво- ла конца строки (нуля). Но мы пишем библиотечную процедуру и всегда должны воз- вращать строку, оканчивающуюся нулем, если вызывающий процесс предполагает это. Это может вызвать проблемы для вызывающего процесса далее по ходу программы. Поэтому следует всегда писать: strncpy(host. "/local", hostlen-1). hostfhostlen-1] - '\e0'. Тогда мы сможем быть уверены, что не переполним буфер вызывающего процесса и что наш результат оканчивается нулем (то есть имеется символ конца строки). Вместо двух указанных операторов мы можем использовать snprintf. В качестве альтернативы можно определить нашу собственную библиотечную функцию, которая вызывает функцию strncpy и добавляет к результату символ конца строки (нуль), однако вызов существу- ющей функции snprintf выглядит проще. В листинге 11.29 представлена наша функция gni pv46, обрабатывающая струк- туры адреса сокета IPv4 и IPv6 для функции getnameinfo. Листинг 11.29. Функция gn_ipv46: обработка структур адреса сокета IPv4 и IPv6 //libgai/gn_ipv46 с 5 int 6 gn_ipv46(char *host. size_t hostlen. char *serv. size_t servlen, 7 void *aptr. size_t alen. int family, int port, int flags)
11.16. Реализация функций getaddrinfo и getnameinfo 371 8 { 9 char *ptr. 10 struct hostent *hptr; 11 struct servent *sptr; 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 if (hostlen > 0) { if (flags & NI_NUMERICHOST) { if (inet_ntop(family. aptr. host, hostlen) -- NULL) return (1): } else { hptr = gethostbyaddr(aptr. alen. family). if (hptr != NULL && hptr->h_name 1= NULL) { if (flags & NI_N0FQDN) { if ( (ptr - strchr(hptr->h_name. '.')) !- NULL) *ptr = 0: /* отбрасываем все после первой точки */ } snprintf(host, hostlen. "Ss". hptr->h_name); } else { if (flags & NI_NAMEREQD) return (1); if (inet_ntop(family, aptr. host, hostlen) = NULL) return (1): } } ) if (servlen > 0) { if (flags & NI_NUMERICSERV) { snprintf(serv. servlen. "£d". ntohs(port)): } else { sptr = getservbyport(port, (flags & NI_DGRAM) ? “udp" : NULL): if (sptr ' = NULL && sptr->s_name NULL) snprintf(serv. servlen. “$s". sptr->s_name). else snprintf(serv. servlen. 'W. ntohs(port)). } } return (0). Возвращение имени узла 12-23 Если задан флаг NI_NUMERICHOST, мы вызываем функцию inetjitop, чтобы возвра- тить формат представления IP-адреса; в противном случае функция gethostbyaddr ищет имя узла, соответствующее IP-адресу. Если функция gethostbyaddr выпол- няется успешно и если задан флаг NI_NOFQDN (означающий, что не требуется воз- вращать полное доменное имя), возвращается имя узла, усеченное до первой точ- ки (см. табл. 11.5). Обработка неудачного выполнения функции gethostbyaddr 24-29 Если функция gethostbyaddr выполняется неудачно (к сожалению, это обычное явление при существующем количестве неверно сконфигурированных серверов DNS в Интернете, см. раздел 14.8 [95]) и если был задан флаг NI_NAMEREQD, возвра- щается ошибка. В противном случае строка адреса, соответствующая IP-адресу, формируется функцией i net_ntop.
372 Глава 11. Дополнительные преобразования имен и адресов Возвращение строки с именем службы 2 42 Если был задан флаг NI_NUMERICSERV, возвращается десятичный номер порта. В противном случае вызывается функция getservbyport. Последний аргумент — это пустой указатель, если не задан флаг NI_DGRAM Если функция getservbyport выполняется неудачно, возвращается десятичный номер порта. 11.17. Резюме Функция getaddrinfo — это полезная функция, позволяющая нам писать код, не зависящий от протокола. Но ее непосредственное выполнение включает несколько шагов, причем для различных сценариев нужно обрабатывать повторяющиеся особенности: просматривать все возвращаемые структуры, игнорировать возвра- щение ошибки из функции socket, устанавливать параметр сокета SO_REUSEADDR для серверов TCP и т. п. Мы упрощаем все эти подробности с помощью наших функций tcp_connect, tcp_l i sten, udp_cl i ent, udp_connect и udp_server. Мы показали использование этих функций при создании версий наших клиентов и серверов TCP и UDP, не зависящих от протокола. Функции gethostbyname и gethostbyaddr также служат примером функций, которые обычно не допускают повторного вхождения. Эти функции совместно используют общую статическую структуру для записи результата, указатель на которую они обе возвращают. Мы столкнемся снова с проблемой повторного вхождения в главе 23 при описании потоков и обсудим пути ее решения. Мы рас- смотрели предоставляемые некоторыми производителями версии _г этих двух функций, что является приемлемым решением, однако требует изменения всех приложений, вызывающих эти функции. Упражнения 1. В листинге 11.4 вызывающий процесс должен передать указатель на целое число, чтобы получить размер адреса протокола. Если вызывающий процесс не делает этого (то есть передает пустой указатель в качестве последнего аргу- мента), как может вызывающий процесс получить действительный размер адресов протокола? 2 Измените листинг 11.6, чтобы вызвать функцию getnamei nfo вместо функции sock ntop. Какие флаги вы должны передать функции getnamei nfo? 3. В разделе 7.5 мы обсуждали завладение портом с помощью параметра сокета SO_REUSEADDR. Чтобы увидеть, как это происходит, создайте не зависящий от протокола сервер времени и даты UDP, показанный в листинге 11.1. Запусти- те один экземпляр сервера в одном окне, свяжите его с универсальным адре- сом и некоторым портом, который вы выберете. Запустите в другом окне кли- ент и проверьте, что этот сервер выполняет обработку клиента (отметьте вызов функции рг 1 ntf на узле сервера). Затем запустите другой экземпляр сервера в другом окне, и на этот раз свяжите его с одним из адресов направленной пере- дачи узла и тем же портом, что и первый сервер. С какой проблемой вы сразу же столкнетесь? Устраните эту проблему и перезапустите второй сервер Запустите клиент, отправьте дейтаграмму и проверьте, что второй сервер захва-
Упражнения 373 тил порт первого сервера. Если возможно, запустите второй сервер снова с учетной записью, отличной от учетной записи первого сервера, чтобы про- верить, происходит ли по-прежнему захват порта, поскольку некоторые про- изводители не допускают второго связывания, если идентификатор пользова- теля отличен от идентификатора процесса, уже связанного с портом. 4. Обсуждая листинг 11.24, мы отметили, что адрес указателя aipnext — это ар- гумент функции ga_ai struct, требующий дополнительного оператора разыме- нования при ссылке на переменную. Почему бы нам не сделагь aipnext глобаль- ной переменной, вместо того чтобы передавать ее адрес в качестве аргумента? 5. В нашем обсуждении домена Unix в конце раздела 11.5 мы отметили, что ни одно из имен служб IANA не начинается с косой черты. Содержит ли какое- нибудь из этих имен косую черту? 6. В конце раздела 2.10 мы показали два примера Telnet: сервер времени и даты и эхо-сервер. Зная, что клиент проходит через два этапа — функцию gethost- byname и функцию connect, определите, к каким этапам относятся строки выво- да клиента. 7. Функции gethostbyaddr может потребоваться длительное время (до 80 секунд) на возвращение ошибки, если для IP-адреса не может быть найдено имя узла. Напишите новую функцию getnameinfo timeo, которая получает дополнитель- ный целочисленный аргумент, задающий максимальную длительность ожи- дания ответа в секундах. Если время таймера истекает и флаг NI_NAMEREQD не задан, вызовите функцию i net_ntop и возвратите строку адреса.
ГЛАВА 12 Процессы-демоны и суперсервер inetd 12.1. Введение Демон (daemon) — это процесс, выполняющийся в фоновом режиме и не завися- щий от управления со всех терминалов. Системы Unix обычно имеют множество процессов (от 20 до 50), которые являются демонами, работают в фоновом режи- ме и выполняют различные административные задачи. Независимость от всех терминалов нужна в том случае, если демон запускает- ся с терминала (в противоположность запуску из сценария инициализации). Нам нужна возможность использовать этот терминал позже для других задач. Напри- мер, если мы запустим демон с терминала, а затем выйдем из терминала и в него войдет кто-то еще, то во время сеанса следующего пользователя не должны появ- ляться какие-либо сообщения об ошибках, связанные с нашим демоном. Анало- гично, сигналы, генерируемые клавишами терминалов (например, сигнал преры- вания), не должны влиять на какие-либо демоны, которые мы раньше запустили с терминала. Хотя наш сервер несложно запустить в фоновом режиме (заканчи- вая командную строку оболочки символом амперсанд &), нам нужно, чтобы наша программа сама автоматически помещала себя в фоновый режим независимо от терминалов. Существует несколько способов запустить демон: 1. Во время запуска системы многие демоны запускаются сценариями инициа- лизации системы. Эти сценарии часто находятся в каталоге /etc или в катало- ге, имя которого начинается с /etc/rc, но их расположение и содержание зави- сят от реализации. Такие демоны запускаются с правами привилегированного пользователя. Некоторые сетевые серверы часто запускаются из этих сценариев: суперсер- вер inetd (следующий пункт, который мы рассмотрим), сервер Web и почто- вый сервер (часто sendmai1). Демон syslogd, обсуждаемый в разделе 12.2, обыч- но запускается одним из этих сценариев. 2. Многие сетевые серверы запускаются суперсервером i netd, который мы опи- шем далее в этой главе. Сам i netd запускается в одном из сценариев на шаге 1. Суперсервер inetd прослушивает сетевые запросы (Telnet, FTP и т. д.), и ког- да приходит запрос, активизирует требуемый сервер (сервер Telnet, сервер FTP и т. д.).
12 2. Демон syslogd 375 3. За периодические процессы в системе отвечает демон cron, и программы, ко- торые он активизирует, выполняются как демоны. Сам демон cron запускает- ся на шаге 1 во время загрузки системы. 4. Если программа должна быть выполнена в определенный момент времени в бу- дущем, применяется команда at. Демон cron обычно инициирует эти програм- мы, когда приходит время их выполнения, поэтому они выполняются как де- моны. 5. Демоны можно запускать с пользовательских терминалов, как в основном, так и в фоновом режимах. Это часто выполняется при тестировании демона или перезапуске демона, завершенного по некоей причине. Поскольку у демона нет управляющего терминала, ему необходимы средства для вывода сообщений о некоторых событиях — это могут быть обычные инфор- мационные сообщения или экстренные сообщения об аварийных ситуациях, ко- торые должен обрабатывать администратор. Использование функции syslog — это стандартный путь вывода таких сообщений. Эта функция посылает сообще- ния демону syslogd. 12.2. Демон syslogd Системы Unix обычно запускают демон syslogd в одном из сценариев инициали- зации системы, и он функционирует, пока система работает. Реализации syslogd, происходящие от Беркли, выполняют при запуске следующие действия: 1. Считывается файл конфигурации, обычно /etc/syslog conf, в котором указа- но, что делать с каждым типом сообщений, получаемых демоном. Эти сообще- ния могут добавляться в некоторый файл (особой разновидностью такого файла является /dev/console, который записывает сообщение на консоль), пе- редаваться определенному пользователю (если этот пользователь вошел в си- стему) или передаваться демону syslogd на другом узле. 2. Создается доменный сокет Unix и связывается с полным именем /var/run/log (в некоторых системах /dev/log). 3. Создается сокет UDP и связывается с портом 514 (служба syslog). 4. Открывается файл (устройство) /dev/k 1 од. Любые сообщения об ошибках внут- ри ядра появляются как входные данные на этом устройстве. Демон syslogd, выполняется в бесконечном цикле, в котором вызывается функ- ция sei ect, ожидающая, когда один из трех его дескрипторов (из пунктов 2,3 и 4) станет готов для чтения. Этот демон считывает сообщение п выполняет то, что предписывает делать с этим сообщением файл конфигурации. Если демон полу- чает сигнал SIGHUP, он заново читает файл конфигурации. Мы можем отправлять сообщения о событиях для записи в журнал (log messa- ges) демону syslogd из наших демонов, создав дейтаграммный доменный сокет Unix и указывая при отправке полное имя, с которым связан демон, но более про- стым интерфейсом является функция syslog, которую мы описываем в следую- щем разделе. В качестве альтернативы мы можем создать сокет UDP п отправ- лять наши сообщения на адрес закольцовки и порт 514.
376 Глава 12. Процессы-демоны и суперсервер inetd ПРИМЕЧАНИЕ ------------------------------------------------------------ Более новые реализации отключают возможность создания сокета UDP, если опа не задана администратором, поскольку если позволить кому угодно отправлять дейта- граммы UDP на этот порт (возможно, заполняя приемный буфер его сокета), это мо- жет привести к тому, что законные сообщения пе будут получены. Между различными реализациями демона syslogd существуют отличия. Например, до- менные сокеты Unix используются Беркли-реализациями, а реализации System V ис- пользуют потоковый драйвер (streams log driver). Различные реализации, происходя- щие от Беркли, используют для доменных сокетов Unix различные полные имена. Мы можем игнорировать все эти подробности, если используем функцию syslog. 12.3. Функция syslog Поскольку у демона нет управляющего терминала, он не может просто вызвать функцию fрг 1 ntf для вывода в стандартный поток сообщений об ошибках (stderr). Обычная техника записи в журнал сообщений демона — это вызов функции sysl од. ^include <syslog h> void sys1og(int priority const char *message, ). ПРИМЕЧАНИЕ ------------------------------------------------------ ► Хотя эта функция изначально разрабатывалась для BSD, в насюящсс время она пре- доставляется большинством производителей систем Unix. В Posix о функции syslog не говорится ничего, по ее требует Unix 98. Аргумент priority — это комбинация аргументов level и faci 1 ity, которые мы показываем в табл. 12.1 и 12.2. Аргумент message аналогичен строке формата функ- ции printf с добавлением спецификации Жш, которая заменяется сообщением об ошибке, соответствующем текущему значению переменной еггпо. Символ пере- вода строки может появиться в конце строки message, но он не является обяза- тельным. Сообщения для журнала имеют значение level (уровень) от 0 до 7, что мы по- казываем в табл. 12.1. Это упорядоченные значения. Если отправитель не задает значение 1 evel, используется значение по умолчанию LOG_NOTICE. Таблица 12.1. Аргумент level журнальных сообщений Level Значение Описание LOGEMERG 0 Система нс может функционировать, экстренная ситуация (наивысший приоритет) LOGALERT 1 ' Следует немедленно принять .меры, срочная ситуация LOGCRIT 2 Критическая ситуация LOGERR 3 Состояние ошибки LOGWARNING 4 Предупреждение , LOG_NOTICE 5 Необычное, хотя и не ошибочное состояние (значение аргумента level по умолчанию) LOG_INFO 6 ' * Информационное сообщение LOG-DEBUG 7 Отладочные сообщения (низший приоритет)
12.3. Фун кция syslog 377 Сообщения также содержат аргумент facility для идентификации типа про- цесса, посылающего сообщение. Мы показываем его различные значения в табл. 12.2. Если не задано значение аргумента faci 1 ity, используется его значение по умол- чанию — LOGJJSER. Таблица 12.2. Аргумент facility журнальных сообщений facility Описание LOG_AUTH Сообщения по безопаспости/авторизации LOG_AUTHPRIV Сообщения по безопаспости/авторизации (частные) LOGCRON Демон cron LOG_DAEMON Системные демоны LOG_FTP Демон FTP LOG_KERN Сообщения ядра LOG LOCALO Локальное использование LOG LOCAL1 Локальное использование LOG_LOCAL2 Локальное использование LOG_LOCAL3 Локальное использование LOG LOCAL4 Локальное использование LOG LOCALS Локальное использование LOG LOCAL6 Локальное использование LOG_LOCAL7 Локальное использование LOG_LPR Демон принтера LOG_MAIL Почтовая система LOG_NEWS Система телеконференций LOG_SYSLOG Внутренние сообщения системы syslog LOG USER Сообщения пользовательского уровня (значение аргумента facility по умолчанию) LOG_UUCP Система UUCP Например, демон может сделать следующий вызов, когда вызов функции rename неожиданно оказывается неудачным: syslog(LOG_INFO|LOG_LOCAL2. "renameUs. £s) W. filel file2) Назначение аргументов faci I ity и level в том, чтобы все сообщения, которые посылаются процессами определенного типа (то есть с одним значением аргу- мента facility), могли обрабатываться одинаково в файле /etc/syslog.conf или чтобы все сообщения одного уровня (с одинаковым значением аргумента level) обрабатывались одинаково. Например, файл конфигурации может содержать строки kern.* /dev/console local? debug /var/log/cisco log для указания, что все сообщения ядра направляются на консоль, а сообщения относительно отладки со значением аргумента faci I ity, равным local?, добавля- ются в файл/var/log/cisco log. Когда приложение впервые вызывает функцию sys I од, она создает дейтаграмм- ный доменный сокет Unix и затем вызывает функцию connect для сокета с зара- нее известным полным именем, которое создано демоном syslogd (например, /var/run/log). Этот сокет остается открытым, пока процесс не завершится. Дру- гим вариантом является вызов процессом Функций onenlоа и clnsel оо.
378 Глава 12. Процессы-демоны и суперсервер inetd include <syslog h> void openlog(const char *ident. int options, int facility'). void closelog(void). Функция openlog может быть вызвана перед первым вызовом функции syslog, а функция cl osel од — когда приложение закончит отправлять сообщения в журнал. Аргумент ident — это строка, которая будет добавлена в начало каждого жур- нального сообщения функцией syslog. Часто это имя программы. Аргумент opt 1 ons может принимать одно из перечисленных в табл. 12.3 значе- ний. Обычно он формируется путем применения операции логического ИЛИ к константам из табл. 12.3. Таблица 12.3. Аргумент options (параметр) для функции openlog Параметр Описание LOG_CONS Выводить журнал на консоль, если невозможно послать сообщение демону syslogd LOGNDELAY Не откладывать создание сокета, открыть его сейчас LOGPERROR Записывать сообщение в stderr, а также посылать его демону syslogd LOG_PID Включать идентификатор процесса (PID) в каждую запись журнала Обычно доменный сокет Unix не создается при вызове функции openl од. Вме- сто этого сокет открывается при первом вызове функции syslog. Параметр LOG_ NDELAY указывает, что сокет должен создаваться при вызове функции openlog. Аргумент fact 11 ty функции openl од задает значение fact 1i ty, используемое по умолчанию для любого последующего вызова функции syslog, при котором не задается аргумент facility. Некоторые демоны вызывают функцию openl од и за- дают значение аргумента facility (которое обычно не изменяется для данного демона) и затем в каждом вызове функции sysi од задают только аргумент 1 evel (поскольку 1 evel может изменяться в зависимости от ошибки). Сообщения для записи в журнал могут также генерироваться командой 1 ogger. Это может использоваться в сценариях интерпретатора команд, например для отправки сообщений демону sys 1 ogd. 12.4. Функция daemonjnit В листинге 12.1* показана функция, называемая daemon_init, которую мы мож^м вызвать (обычно с сервера), чтобы придать процессу свойства демона. Листинг 12.1. Функция daemonjnit: придание процессу свойств демона //daemon_imt с 1 #include "unp.h" 2 #include <syslog h> 3 #define MAXFD 64 4 extern int daemon_proc; /* определено в error c */ 1 Все исходные коды программ, опубликованные в этой книге, ВЦ можете Н^йр! Г|О адресу ЬЦру/ www.piter.com/download.
12.4. Функция daemonjnit 379 5 void 6 daemon_i mt (const char *pname, int facility) 7 { 8 mt i. 9 pid_t pid. 10 if ( (pid = Fork») != 0) 11 exit(0). /* завершение работы родителя */ 12 /* первый дочерний процесс продолжает работу */ 13 setsidO, /* становится главным в сеансе */ 14 Signal(SIGHUP. SIGJGN). 15 if ( (pid = ForkO) != 0) 16 exit(0); /* первый дочерний процесс завершается-*^ 17 18 /* второй дочерний процесс продолжает работу */ daemon_proc =1. /* для наших функций егг_ХХХ() */ 19 chdirC/"). /* изменение рабочего каталога */ 20 umask(0); /* маска режима создания файла сбрасывается q 0 */ 21 for (i = 0. i < MAXFD. i++) 22 close(i); 23 openlog(pname. LOG—PID. facility). 24 } Вызов функции fork 10-11 Сначала мы вызываем функцию fork, после чего родительский процесс завер- шается, а дочерний продолжается. Если процесс был запущен из интерпретатора команд в фоновом режиме, то когда родительский процесс завершается, оболоч- ка считает, что команда выполнена. Это автоматически запускает дочерний про- цесс в фоновом режиме. Дочерний процесс наследует идентификатор группы процессов от родительского процесса, но получает свой собственный идентифи- катор процесса. Это гарантирует, что дочерний процесс не является главным в группе процессов, что требуется для следующего вызова функции sets id. Вызов функции setsid 12-13 Функция setsid — это функция Posix. 1, создающая новый сеанс. (Вглаве 9 [93] подробно рассказывается о взаимоотношениях процессов.) Процесс становится главным в новом сеансе, становится главным в новой группе процессов и не име- ет управляющего терминала. Игнорирование сигнала SIGHUP и новый вызов функции fork 14-16 Мы игнорируем сигнал SIGHUP и снова вызываем функцию fork. Когда эта функ- ция завершается, родительский процесс на самом деле является первым дочер- ним процессом, и он завершается, оставляя выполняться второй дочерний про- цесс. Назначение второй функции fork — гарантировать, что демон не сможет автоматически получить управляющий терминал, если потом он откроет устрой-
380 Глава 12. Процессы-демоны и суперсервер inetd ство терминала. В SVR4, когда главный процесс сеанса (Уез управляющего терми- нала открывает устройство терминала (которое в этот момент не является управ- ляющим терминалом для другого сеанса), терминал становится управляющим терминалом главного процесса сеанса. Но вызывая второй раз функцию fork, мы гарантируем, что второй дочерний процесс больше не является главным в сеансе, поэтому он не может получить управляющий терминал. Сигнал SIGHUP следует игнорировать, поскольку, когда главный процесс сеанса завершает работу (пер- вый дочерний процесс), всем процессам в сеансе (наш второй дочерний процесс) посылается сигнал SIGHUP. Установка флага для функций ошибок 7-18 Мы присваиваем глобальной переменной daemon_proc ненулевое значение. Эта внешняя переменная задается нашими функциями егг_ХХХ (см. раздел Г.4), и ее ненулевое значение сообщает этим функциям, что нужно вызвать функцию sysl од вместо функции fpnntf (которая выводит сообщение об ошибке в стандартный поток сообщений об ошибках). Это спасает нас от необходимости проходить че- рез весь наш код и вызывать одну из наших функций ошибок, если сервер не ра- ботает как демон (то есть когда мы проверяем сервер), а при работе в режиме демона заменять все вызовы на вызовы syslog. Изменение рабочего каталога и сброс всех битов в маске режима создания файла 9-20 Мы изменяем рабочий каталог на корневой каталог, хотя у некоторых демонов могут быть причины изменить рабочий каталог на какой-либо другой. Напри- мер, демон печати может изменить его на каталог, в котором накапливается со- держимое заданий для принтера и происходит вся работа по выводу данных на печать. Если демоном сбрасывается дамп (файл core), он появляется в текущем рабочем каталоге. Другой причиной для изменения рабочего каталога является то, что демон мог быть запущен в любой файловой системе, и если он там оста- нется, эту систему нельзя будет размонтировать. Маска режима создания файла устанавливается в 0, чтобы в том случае, если демон создает свои собственные файлы, биты разрешения в унаследованной маске создания режимного кода фай- ла не повлияли на биты разрешения в новых файлах. Закрытие всех открытых дескрипторов 1-22 Мы закрываем все открытые дескрипторы, которые наследуются от процесса, выполнившего демон (обычно оболочка). Проблема состоит в определении наи- большего используемого дескриптора: в Unix нет ни одной функции, предостав- ляющей это значение. Есть способы определения максимального числа дескрип- торов, которое может открыть процесс, но даже это достаточно сложно [93, с. 43], поскольку предел может быть бесконечным Наше решение — закрыть первые 64 дескриптора, даже если большинство из них, возможно, не было открыто. Некоторые демоны открывают /dev/nul 1 для чтения и записи и подключают к нему дескрипторы стандартных потоков ввода, вывода и сообщений об ошиб- ках. Это гарантирует, что наиболее типичные дескрипторы открыты и операция чтения из любого из них возвращает 0 (конец файла), а ядро игнорирует все, что записано в любой из этих трех дескрипторов. Причина, по которой требуется от-
12.4. Функция daemonjnit 381 крыть эти дескрипторы, заключается в том, что любая библиотечная функция, вызываемая демоном и считающая, что она может читать из стандартного потока ввода или записывать либо в стандартный поток вывода, либо в стандартный по- ток сообщений об ошибках, не завершится с ошибкой. Некоторые демоны ис- пользуют альтернативный вариант — открывают файл журнала (log file), в кото- рый они будут записывать сообщения во время выполнения, и подключают к нему стандартные потоки ввода, вывода и сообщений об ошибках. Использование демона syslogd для вывода сообщений об ошибках 23 Вызывается функция openlog. Первый ее аргумент берется из вызывающего процесса и обычно является именем программы (например, argv[0]). Мы указы- ваем, что идентификатор процесса должен добавляться к каждому сообщению. Аргумент facility также задастся вызывающим процессом, и его значением мо- жет быть константа из табл 12 2 либо, если приемлемо значение по умолчанию LOGJJSER, нулевое значение. Отметим, что поскольку демон выполняется без управляющего терминала, он никогда не должен получать сигнал SIGHUP от ядра. Следовательно, многие демо- ны используют этот сигнал в качестве уведомления от администратора, что файл конфигурации демона изменился и демон должен еще раз считать файл Два дру- гих сигнала, которые демон никогда не должен получать, — это сигналы SIGI NT и IGWINCH, и они также могут использоваться для уведомления демона о некото- рых изменениях. Пример: сервер времени и даты в качестве демона В листинге 12.2 представлено изменение нашего сервера времени и даты, не за- висящего от протокола. В отличие от сервера, показанного в листинге 11.6, в нем вызывается функция daemonjnit, чтобы этот сервер мог выполняться в качестве демона. Листинг 12.2. Не зависящий от протокола сервер времени и даты, работающий в качестве демона //inetd/daytimetcpsrv2 с 1 #include "unp h' 2 #include «time h> 3 int 4 main(int argc char **argv) 5 { 6 int listenfd connfd 1 socklenj addrlen len 8 struct sockaddr *cliaddr 9 char buff[MAXLINE] 10 time_t ticks 11 aaemon_imt(argv[0] 0) 12 if (argc == 2) 13 listenfd = Tcpjisten(NULL. argv[l] &addrlen).
382 Глава 12. Процессы-демоны и суперсервер inetd Листинг 12.2 (продолжение) 14 else if (argc == 3) 15 listenfd = Tcp_listen(argv[l], argv[2], &addrlen). 16 else 17 err_quit("usage daytimetcpsrv2 [ <host> ] <service or port>"): 18 cliaddr = Malloc(addrlen). 19 for (..) { 20 len = addrlen. 21 connfd = Accept(listenfd. cliaddr, &len). 22 err_msg("connection from %s". Sockjitopfcliaddr. len)). 23 ticks = time(NULL). 24 snprintf(buff, sizeof(buff). "% 24s\er\en". ctime(&ticks)): 25 Write(connfd. buff, strlen(buff)). 26 Close(connfd). 27 } 28 } Изменений всего два: мы вызываем нашу функцию daemon_imt, как только программа запускается, а затем вызываем нашу функцию err_msg вместо printf, чтобы вывести IP-адрес и порт клиента. На самом деле, если мы хотим, чтобы наши программы могли выполняться как демоны, мы должны исключить вызов функций printf и fprintf и вместо них использовать нашу функцию err_msg. Если мы запустим эту программу на нашем узле sol ан s и затем проверим файл /var/adm/messages (куда мы отправляем все сообщения LOGJJSER) после соедине- ния с нашим узлом bsdi, мы получим: Jun 4 15 15 33 solans kohala com daytimetcpsrv2[14882] connection from ffff 206 62 226 35 3356 (Мы разбили одну длинную строку на две строки.) Дата, время и FQDN автома- тически ставятся в начале сообщения демоном syslogd. 12.5. Демон inetd В типичной системе Unix может существовать много серверов, ожидающих за- проса клиента. Примерами являются FTP, Telnet, Rlogin, TFTP и т. д. В систе- мах, предшествующих 4.3BSD, каждая из этих служб имела связанный с ней про- цесс. Этот процесс запускался во время загрузки из файла /etc/rc, и каждый процесс выполнял практически идентичные задачи запуска: создание сокета, свя- зывание при помощи функции bind заранее известного порта с сокетом, ожида- ние соединения (TCP) или получения дейтаграммы (UDP) и последующее вы- полнение функции fork. Дочерний процесс выполнял обслуживание клиента, а родительский процесс ждал, когда поступит следующий запрос клиента. Эта модель связана с двумя проблемами. 1. Все эти демоны содержали практически идентичный код запуска, направлен- ный сначала на создание сокета, а затем на превращение процесса в процесс демона (аналогично нашей функции daemon_imt). 2. Каждый демон занимал некоторое место в таблице процессов, но при этом большую часть времени находился в состоянии ожидания.
12.5. Демон inetd 383 Реализация 4.3BSD упростила ситуацию, предоставив суперсервер (superserver) Интернета — демон inetd. Этот демон может применяться серверами, использу- ющими TCP или UDP, и не поддерживает других протоколов, таких как домен- ные сокеты Unix. Демон inetd решает две вышеупомянутые проблемы. 1. Он упрощает написание процессов демонов, поскольку обрабатывает боль- шинство подробностей запуска. Таким образом устраняется необходимость вызова нашей функции daemon_imt для каждого сервера. 2. Этот демон позволяет одиночному процессу (inetd) ждать входящие клиент- ские запросы ко множеству служб (вместо одного процесса для каждой служ- бы). Это сокращает общее число процессов в системе. Процесс inetd сам становится демоном, используя технологии, которые мы изложили при описании функции daemonjmt. Затем он считывает и обрабатыва- ет файл конфигурации, обычно файл /ect/inetd conf. Этот файл задает, какие службы должен обрабатывать суперсервер, а также что нужно делать, когда при- ходит запрос к одной из этих служб. Каждая строка содержит поля, показанные в табл. 12.4. Вот несколько строк в качестве примера: ftp stream tep nowait root /usr/bin/ftpd ftpd -1 telnet stream tep nowait root /usr/bin/telnetd telnetd login stream top nowait root /usr/bin/rlogind rlogind -s tftp dgram udp wait nobody /usr/bin/tftpd tftpd -s /tftpboot Действительное имя сервера всегда передается в качестве первого аргумента программе, выполняемой с помощью функции ехес. Таблица 12.4. Поля файла inetd.conf Поле Описание service-name socket-type Protocol wait-flag login-name server-program server-program-arguments Должен быть в /etc/services stream (TCP) или drgam (UDP) Должен быть в /etc/protocols; либо tep, либо udp Обычно nowait для TCP и wait для UDP Из /etc/password; обычно root Полное имя программы для вызова ехес Аргументы программы для вызова ехес ПРИМЕЧАНИЕ ------------------------------------------------------------- Таблица и приведенные строки — это только пример. Большинство производителей добавили демону inetd свои собственные функции. Примером может служить возмож- ность обрабатывать серверы вызовов удаленных процедур (RPC) в дополнение к сер- верам TCP и U DP, а также возможность обрабатывать другие протоколы, отличные от TCP и UDP. Полное имя для функции ехес и аргументы командной строки сервера, очевидно, зависят от приложения. Взаимодействие IPv6 с файлом /etc/inetd.conf зависит от производителя. Иногда в качестве поля protocol указывается tcp6 или udp6, чтобы подчеркнуть, что для серве- ра должен быть создан сокет IPv6. Иллюстрация действий, выполняемых демоном inetd, представлена на рис. 12.1.
384 Глава 12. Процессы-демоны и суперсервер inetd Рис. 12.1. Шаги, выполняемые демоном inetd 1. При запуске демон читает файл /etc/т netd conf и создает сокет соответствую- щего типа (потоковый или дейтаграммный сокет) для всех служб, заданных в файле Максимальное число серверов, которое может обрабатывать демон inptd ча висит от максимального числа дескрипторов, которое он может со-
12 5 Демон inetd 385 здать Каждый новый сокет добавляется к набору дескрипторов, который бу- дет использован для вызова функции sel ect 2. Для сокета вызывается функция bind, задающая заранее известный порт для сервера и универсальный IP-адрес Этот номер порта TCP или UDP получа- ется при вызове функции getservbyname с полями service-name и protocol из файла конфигурации в качестве аргументов. 3. Для сокетов TCP вызывается функция listen, так что принимаются входя- щие запросы на соединение Этот шаг не выполняется для дейтаграммных со- кетов. 4 После того как созданы все сокеты, вызывается функция sel ect, ожидающая, когда какой-либо из сокетов станет готов для чтения. Вспомните из раздела 6 3, что прослушиваемый сокет TCP становится готов для чтения, когда новое со- единение готово быть принятым с помощью функции accept, а сокет UDP ста- новится готов для чтения, когда приходит дейтаграмма. Демон i netd большую часть времени блокирован в этом вызове функции sel ect, ожидая, когда сокет станет готов для чтения 5. Когда функция sel ect определяет, что сокет готов для чтения, то если этот сокет является сокетом TCP, вызывается функция accept для принятия ново- го соединения 6 Демон inetd запускает функцию fork, и дочерний процесс обрабатывает за- прос клиента. Это аналогично стандартному параллельному серверу (см раз- дел 4.8). Дочерний процесс закрывает все дескрипторы, кроме дескриптора, который он обрабатывает новый присоединенный сокет, возвращаемый функцией accept для сервера TCP, или исходный сокет UDP. Дочерний процесс трижды вызывает функцию dup2, подключая сокет к деск- рипторам 0, 1 и 2 (стандартные потоки ввода, вывода и сообщений об ошиб- ках) Исходный дескриптор сокета затем закрывается. При этом в дочернем процессе открытыми остаются только дескрипторы 0, 1 и 2. Если дочерний процесс читает из стандартного потока ввода, он читает из сокета, и все, что он записывает в стандартный поток вывода или стандартный поток сообщений об ошибках, записывается в сокет Дочерний процесс вызывает функцию getpwnam, чтобы получить значение поля login-name, заданного в файле конфигурации. Если это не поле root, дочерний процесс становится указанным пользователем при помощи функций setgid и setuid. (Поскольку процесс inetd выполняется с идентификатором пользо- вателя, равным 0, дочерний процесс наследует этот идентификатор пользова- теля при выполнении функции fork, поэтому он имеет возможность стать лю- бым пользователем по своему выбору.) Теперь дочерний процесс вызывает функцию ехес, чтобы выполнить соответ- - ствующую программу сервера (поле server program) для обработки запроса, передавая аргументы, указанные в файле конфигурации. У. Если сокет является потоковым сокетом, родительский процесс должен за- крыть присоединенный сокет (как наш стандартный параллельный сервер) Родительский процесс снова вызывает функцию sel ect, ожидая, когда следу- ющий сокет станет готов для чтения
386 Глава 12 Процессы-демоны и суперсервер inetd Чтобы рассмотреть более подробно, что происходит с дескрипторами, на рис. 12.2 показаны дескрипторы в демоне i netd в момент прихода нового запроса на соеди- нение от клиента FTP. inetd I * Порт TCP номер 21 ’ • Порт TCP номер 23 • • • < > Порт UDP номер 69 < i Порт TCP номер 21 Прослушиваемые сокеты TCP и сокеты UDP, ожидающие того момента, когда они станут готовы для чтения } Присоединенный сокет TCP, возвращенный функцией accept Рис. 12.2. Дескрипторы демона inetd в тот момент, когда приходит запрос на порт 21 TCP Запрос на соединение направляется на порт 21 TCP, но новый присоединен- ный сокет создается функцией accept. На рис. 12.3 показаны дескрипторы в дочернем процессе после вызова функ- ции fork, после того как дочерний процесс закрывает все остальные дескрипто- ры, кроме дескрипторов присоединенного сокета inetd (дочерний процесс) Соединение с клиентом *•' • Порт TCP номер 21 Выполнение серверной программы Рис. 12.3. Дескрипторы демона inetd в дочернем процессе Следующий шаг для дочернего процесса — подключение присоединенного сокета к дескрипторам 0,1 и 2 и последующее закрытие присоединенного сокета. При этом мы получаем дескрипторы, изображенные на рис. 12.4. Затем дочерний процесс вызывает функцию ехес, и, как сказано в разделе 4 7, во время выполнения функции ехес все дескрипторы обычно остаются открыты- ми, поэтому реальный сервер, на котором выполняется функция ехес, использует любой из дескрипторов 0,1 и 2 для взаимодействия с клиентом. Эти дескрипто- ры должны быть единственными открытыми на стороне сервера дескрипторами. Описанный нами сценарий относится к ситуации, при которой файл конфи- гурации задает в поле wait-flag значение nowait для сервера. Это типично для всех служб TCP и означает, что демону inetd не нужно ждать завершения его дочернего процесса, перед тем как он примет другое соединение для данной служ- бы. Если приходит другой запрос на соединение для той же службы, он возвра- щается родительскому процессу, как только тот снова вызовет функцию sei ect.
12.5 Демон inetd 387 Выполнение серверной программы Рис. 12.4. Дескрипторы демона inetd после выполнения функции dup2 Шаги 4,5 и 6, перечисленные выше, выполняются снова, и новый запрос обраба- тывается другим дочерним процессом. Задание флага wait для службы UDP изменяет шаги, выполняемые родитель- ским процессом. Флаг указывает на то, что демон inetd должен ждать заверше- ния своего дочернего процесса, прежде чем снова вызвать функцию sei ect для определения готовности этого сокета UDP для чтения. Происходят следующие изменения: 1. После выполнения функции fork в родительском процессе сохраняется иден- тификатор дочернего процесса. Это дает возможность родительскому процес- су узнать, когда завершается определенный дочерний процесс, анализируя значение, возвращаемое функцией waitpid. 2. Родительский процесс отключает способность сокета выполнять последую- щие функции sei ect, сбрасывая соответствующий бит в наборе дескрипторов с помощью макроса FD_CLR. Это значит, что дочерний процесс завладевает со- кетом до своего завершения. 3. Когда завершается дочерний процесс, родительский процесс уведомляется об этом с помощью сигнала SIGCHLD, и обработчик сигналов родительского про- цесса получает идентификатор завершающегося дочернего процесса. Он сно- ва включает функцию sei ect для соответствующего сокета, устанавливая бит для этого сокета в своем наборе дескрипторов. Причина, по которой дейтаграммный сервер должен завладевать сокетом, пока он не завершит работу, лишая тем самым демона i netd возможности выполнять функцию select на этом сокете для проверки готовности его для чтения (в ожи- дании другой дейтаграммы клиента), в том, что для сервера дейтаграмм суще- ствует только один сокет, в отличие от сервера TCP, у которого имеется прослу- шиваемый сокет и по одному присоединенному сокету для каждого клиента Если демон 1 netd не отключил чтение на сокете дейтаграмм и, допустим, родительский процесс (inetd) завершил выполнение перед дочерним, дейтаграмма от клиента все еще будет находиться в приемном буфере сокета Это приводит к тому, что функция select снова сообщает, что сокет готов для чтения, и демон inetd снова выполняет функцию fork, порождая другой (ненужный) дочерний процесс. Де- мон 1 netd должен игнорировать дейтаграммный сокет до тех пор, пока он не узна- ет, что дочерний процесс прочитал дейтаграмму из приемного буфера сокета. Демон 1 netd узнает, что дочерний процесс закончил работу с сокетом, путем по-
388 Глава 12. Процессы-демоны и суперсервер inetd лучения сигнала SIGCHLD, указывающего на то, что дочерний процесс завершился. Подобный пример мы показываем в листинге 20.5. Пять стандартных служб Интернета, описанных в табл. 2.1, обеспечиваются самим демоном inetd (см. упражнение 12.2). Поскольку функцию accept для сервера TCP вызывает демон inetd (а не сам сервер), реальный сервер, запускаемый демоном inetd, обычно вызывает функ- цию getpeername для получения IP-адреса и номера порта клиента. Вспомните рис. 4.9, где мы показывали, что после выполнения вызовов fork и ехес (что вы- полняет демон 1 netd) у реального сервера есть единственный способ получить идентификацию клиента — вызвать функцию getpeername. Демон 1 netd обычно не используется для серверов, работающих с большими объемами данных, в особенности почтовыми серверами и серверами Web. На- пример, функция sendmail обычно запускается как стандартный параллельный сервер, как мы отмечали в разделе 4.8. В этом режиме стоимость порождения про- цесса для каждого клиентского соединения равна стоимости функции fork, тогда как в случае сервера TCP, активизированного демоном i netd, — стоимости функ- ций fork и ехес. Серверы Web используют множество технологий для минимиза- ции накладных расходов при порождении процессов для обслуживания клиен- тов, как мы покажем в главе 27. 12.6. Функция daemonjnetd В листинге 12.3 показана функция daemon_i netd, которую мы можем вызвать с сер- вера, запущенного демоном inetd. Листинг 12.3. Функция daemonjnetd для придания свойств демона процессу, запущенному демоном inetd //daemon_inetd с 1 #include "unp h" 2 #include <syslog h> 3 extern int daemon_proc: /* определено в error.с-*/ 4 void 5 daemon_inetd(const char *pname. int facility) 6 { 7 daemon_proc =1. /* для наших функций errXXXO */ 8 openlog(pname. LOG_PID, facility). 9 } Эта функция тривиальна по сравнению с daemon_init, потому что все шаги выполняются демоном inetd при запуске. Все, что мы делаем — устанавливаем флаг daemon_proc для наших функций ошибок (см. табл. Г.1) и вызываем функ- цию openlод с теми же аргументами, что и при вызове функции daemon ! mt, пред- ставленной в листинге 12.1. Пример: сервер времени и даты, активизированный демоном inetd Листинг 12.4 представляет собой модификацию нашего сервера времени и даты, показанного в листинге 12.2, который может быть активизирован демоном inetd.
12.6. Функция daemon Jnetd 389 Листинг 12.4. He зависящий от протокола сервер времени и даты, который может быть активизирован демоном inetd //inetd/daytimetcpsrv3 с 1 #include "unp h" 2 include <time h> 3 int 4 maindnt argc. char **argv) 5 { 6 socklen_t len. 7 struct sockaddr *cliaddr. 8 char buff[MAXLINE], 9 time_t ticks: 10 daemon_inetd(argv[0]. 0). 11 cliaddr = Mai 1oc(MAXSOCKADDR). 12 len = MAXSOCKADDR. 13 Getpeername(0. cliaddr. &len). 14 err_msg("connection from ^s“. Sock_ntop(cliaddr. len)). 15 ticks = time(NULL). 16 snprintf(buff. sizeof(buff). "% 24s\er\en" ctime(&ticks)): 17 Write(0. buff, strlen(buff)). 18 Close(O). /* закрываем соединение TCP */ 19 exit(0). 20 } В программе сделано два важных изменения. Во-первых, исчез весь код созда- ния сокета: вызовы функций tcp_l i sten и accept. Эти шаги выполняются демо- ном inetd, и мы ссылаемся на соединение TCP, используя нулевой дескриптор (стандартный поток ввода). Во-вторых, исчез бесконечный цикл for, поскольку сервер активизируется по одному разу для каждого клиентского соединения. После предоставления сервиса клиенту сервер завершает свою работу. Вызов функции getpeername 11-14 Поскольку мы не вызываем функцию tcp l i sten, мы не знаем размера структу- ры адреса сокета, которую она возвращает, а поскольку мы не вызываем функ- цию accept, мы не знаем и адреса протокола клиента. Следовательно, мы выделя- ем буфер для структуры адреса сокета, используя нашу константу MAXSOCKADDR и вызываем функцию getpeername с нулевым дескриптором в качестве первого ар- гумента. Чтобы выполнить этот пример в нашей системе BSD/OS, сначала мы присва- иваем службе имя и порт, добавляя следующую строку в /etc/services: mydaytime 9999/tcp Затем мы добавляем строку в /etc/inetd conf: mydaytime stream tcp nowait rstevens /usr/home/rstevens/daytimetcpsrv3 daytimetcpsrv3 (Мы разбили длинную строку на более короткие.) Мы помещаем выполняемый код в заданный файл и отправляем демону inetd сигнал SIGHUP, сообщающий ему, что нужно заново считать файл конфигурации. Следующий шаг — выполнить
390 Глава 12. Процессы-демоны и суперсервер inetd программу netstat, чтобы проверить, что на порте TCP 9999 создан прослушива- емый сокет- bsdi К, netstat -па | grep 9999 tep 0 0 * 9999 * * LISTEN Затем мы запускаем сервер с другого узла: alpha X telnet bsdi 9999 Trying 206 62 226 35 Connected to bsdi Escape character is ' Thu Jun 5 11 13 50 1997 Connection closed by foreign host Файл /var/1 og/messages (в который, как указано в нашем файле /etc/sysl og conf, должны направляться наши сообщения с аргументом facility=LOG_USER) содер- жит запись: Jun 5 11 13 50 bsdi daytimetcpsrv3[28724] connection from 206 62 226 42 1042 12.7. Резюме Демоны — это процессы, выполняемые в фоновом режиме независимо от управ- ления с терминалов. Многие сетевые серверы работают как демоны. Все выход- ные данные демона обычно отправляются демону syslogd при помощи вызова функции syslog. Администратор полностью контролирует все, что происходит с этими сообщениями, основываясь на том, какой демон отправил данное сооб- щение, и насколько оно серьезно. Чтобы запустить произвольную программу и выполнять ее в качестве демона, требуется пройти несколько шагов: вызвать функцию fork для запуска в фоно- вом режиме, вызвать функцию setsid для того, чтобы создать новый сеанс Posix 1 и стать главным процессом сеанса, снова вызвать функцию fork, чтобы избежать перехода в режим управления с терминала, изменить рабочий каталог и маску режима создания файла и закрыть все ненужные файлы. Наша функция daemon_ imt выполняет все эти шаги. Многие серверы Unix запускаются демоном i netd. Он осуществляет все необ- ходимые шаги по превращению процесса в демон, и при запуске действительного сервера открывается сокет для стандартных потоков ввода, вывода и сообщений об ошибках. Это позволяет нам опустить вызовы функций socket, bind, listen и accept, поскольку все эти шаги выполняются демоном inetd. Упражнения 1 Что произойдет в листинге 12 2, если мы отложим вызов функции daemon_i n 11 до завершения обработки аргументов командной строки, и функция err_quit будет вызвана до того, как программа станет демоном? 2. Как вы думаете, какие из 10 серверов, перечисленных в табл. 2.1 (учитываются версии TCP и UDP для каждой из пяти служб, управляемых демоном inetd), реализуются с помощью вызова функции fork, а какие не требуют этой функции? 3. Что произойдет, если мы создадим сокет UDP, свяжем порт 7 с сокетом (стан- дартный эхо-сервер в табл 2 1) и отправим дейтаграмму UDP серверу chargen?
Упражнения 391 4. В руководстве Solans 2.x для демона inetd описывается флаг -t, заставляю- щий демон inetd вызывать функцию sysi од (с аргументами faci 1 ity=LOG_DAEMON и level =LOG_NOTICE) для протоколирования клиентского IP-адреса и порта лю- бой службы TCP, которые обрабатывает демон i netd Как демон i netd получа- ет эту информацию? В этом же руководстве сказано, что демон i netd не может выполнить это для сокета UDP. Почему? Есть ли способ обойти эти ограничения для служб UDP?
ГЛАВА 13 Дополнительные функции ввода-вывода 13.1. Введение Эта глава охватывает разнообразные функции и технологии, которые мы поме- щаем в общую категорию «расширенного ввода-вывода». Сначала мы описываем установку тайм-аута для операции ввода-вывода, которую можно выполнить тре- мя различными способами. Затем мы рассматриваем три варианта функций read и write: recv и send, до- пускающие четвертый аргумент, содержащий флаги, передаваемые от процесса к ядру; readv и writev, позволяющие нам задавать массив буферов для ввода или вывода; recvmsg и sendmsg, объединяющие все свойства других функций ввода- вывода и обладающие новой возможностью получения и отправки вспомогатель- ных данных. Мы также рассказываем о том, как определить, сколько данных находится в при- емном буфере сокета и как использовать с сокетами стандартную библиотеку ввода-вывода С. Эту главу мы заканчиваем кратким обзором протокола Т/ТСР — TCP для транзакций, позволяющего избежать трехэтапного рукопожатия. 13.2. Тайм-ауты сокета Существует три способа установки тайм-аута для операции ввода-вывода через сокет. 1. Вызов функции alarm, которая генерирует сигнал SIGALARM, когда истекает за- данное время. Это подразумевает обработку сигналов, которая может варьи- роваться от одной реализации к другой. К тому же такой подход может стать помехой другим существующим вызовам функции alarm в данном процессе. 2. Блокирование при ожидании ввода-вывода в функции sei ect, имеющей встро- енное ограничение времени, вместо блокирования в вызове функции read или write. 3. Использование более новых параметров сокета — SO RCVTIMEO и SO_SNDTIMEO. Проблема при использовании этого подхода заключается в том, что не все ре- ализации поддерживают эти параметры сокета. Все три технологии работают с функциями ввода и вывода (такими как read, write и их вариации, например recvfrom и sendto), но нам также хотелось бы иметь
13.2. Тайм-ауты сокета 393 технологию, работающую с функцией connect, поскольку процесс соединения TCP может занять длительное время (обычно 75 секунд). Функцию select можно использовать для установки тайм-аута функции connect, только когда сокет находится в неблокируемом режиме (который мы рассматриваем в разде- ле 15.3), а параметры сокетов, устанавливающие тайм-аут, не работают с функ- цией connect. Мы также должны отметить, что первые две технологии работают с любым дескриптором, в то время как третья технология только с дескрип- торами сокетов. Теперь мы представим примеры применения всех трех технологий. Тайм-аут для функции connect (сигнал SIGALRM) В листинге 13.11 показана наша функция connect_tn>ieo, вызывающая функцию connect с ограничением по времени, заданным вызывающим процессом. Первые три аргумента — это аргументы, которых требует функция connect, а четвертый аргумент — это длительность ожидания в секундах. Листинг 13.1. Функция connect с тайм-аутом //lib/connect_timeo с 1 #include "unp h” 2 static void connect_alarm(int). 3 int 4 connect_timeo(int sockfd. const SA *saptr. socklen t salen. int nsed) 5 { 6 Sigfunc *sigfunc. 7 int n. 8 sigfunc = Signal(SIGALRM connect_alarm) 9 if (alarm(nsec) '= 0) 10 err_msg("connect_timeo alarm was already set"); 11 if ( (n = connect(sockfd. saptr. salen)) < 0) { 12 close(sockfd). 13 if (errno == EINTR) 14 errno = ETIMEDOUT. 15 } 16 alarm(O). /* отключение alarm */ 17 Signal(SIGALRM. sigfunc) /* восстанавливаем прежний обработчик Кйгйала *f 18 return (n) 19 } 20 static void 21 connect_alarm(int signo) 22 { 23 return. /* просто прерываем connectО */ 24 } Установка обработчика сигналов 8 Для SIGALRM устанавливается обработчик сигнала. Текущий обработчик сигна- ла (если таковой имеется) сохраняется, и таким образом мы можем восстановить его в конце функции. ' Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http.// www piter com/download
394 Глава 13. Дополнительные функции ввода-вывода Установка таймера 9-10 Таймер для процесса устанавливается на время (число секунд), заданное вы- зывающим процессом. Возвращаемое значение функции al arm — это число секунд, остающихся в таймере для процесса (если он уже установлен для процесса) в на- стоящий момент или 0 (если таймер не был установлен прежде). В первом случае мы выводим сообщение с предупреждением, поскольку мы стираем предыдущую установку таймера (см. упражнение 13.2). Вызов функции connect 11-15 Вызывается функция connect, и если функция прерывается (EINTR), мы присва- иваем переменной еггпо значение ETIMEDOUT. Сокет закрывается, чтобы не допус- тить продолжения трехэтапного рукопожатия. Выключение таймера и восстановление предыдущего обработчика сигнала 16-18 Таймер при обнулении выключается, и восстанавливается предыдущий обра- ботчик сигналов (если таковой имеется). Обработка сигнала SIGALRM 20-24 Обработчик сигнала просто возвращает управление. Предполагается, что это прервет ожидание функции connect, заставив ее возвратить ошибку EINTR. Вспом- ните нашу функцию signal (см. листинг 5.5), которая не устанавливает флага SA_RESTART, когда перехватываемый сигнал — это сигнал SIGALRM. Одним из важных моментов в этом примере является то, что мы всегда можем сократить период ожидания для функции connect, используя эту технологию, но мы не можем увеличить период, заданный для ядра. В Беркли-ядре тайм-аут для функции connect обычно равен 75 секундам. Мы можем задать меньшее значение для нашей функции, допустим 10, но если мы задаем большее значение, скажем 80, тайм-аут самой функции connect все равно составит 75 секунд. Другой важный момент в данном примере — то, что мы используем возмож- ность прерывания системного вызова (connect) для того, чтобы возвратить уп- равление, прежде чем истечет время ожидания ядра. Такой подход допустим, когда мы выполняем системный вызов и можем обработать возвращение ошибки EINTR. Но в разделе 26.6 мы встретимся с библиотечной функцией, выполняющей сис- темный вызов, которая сама выполняет заново системный вызов при возвраще- нии ошибки EINTR. Мы можем продолжать работать с сигналом SIGALRM и в этом слу- чае, но в листинге 26.6 мы увидим, что нам придется воспользоваться функциями sigsetjmp и siglongjmp, поскольку библиотечная функция игнорирует ошибку EINTR. Тайм-аут для функции recvfrom (сигнал SIGALRM) В листинге 13.2 показана новая версия функции dg_cli, приведенной в листин- ге 8.4, в которую добавлен вызов функции alarm для прерывания функции recvfrom при отсутствии ответа в течение 5 секунд. Листинг 13.2. Функция dg_cli, в которой при установке тайм-аута для функции recvfrom используется функция alarm //advio/daclitimeo3 с
13.2. Тайм-ауты сокета 395 2 static void sig_alrm(int). 3 void 4 dg_cli(FILE *fp, int sockfd. const SA *pservaddr. socklen t servlen) 5 { 6 int n, 7 char sendline[MAXLINE], recvline[MAXLINE + 1J: 8 Signal(SIGALRM, sig_alrm), 9 while (Fgets(sendlme. MAXLINE, fp) '= NULL) { 10 Sendto(sockfd. sendline. strlen(sendline), 0. pservaddr. servlen). 11 alarm(5), 12 if ( (n = recvfrom(sockfd. recvline. MAXLINE 0. NULL. NULL)) < 0) { 13 if (errno == EINTR) 14 fprintf(stderr, "socket timeoutXen") 15 else 16 errsys("recvfrom error"). 17 } else { 18 alarm(O) 19 recvline[n] = 0. /* завершающий нуль */ 20 Fputs(recvline. stdout). 21 } 22 } 23 } 24 static void 25 signalrm(int signo) 26 { 27 return. /* просто прерываем recvfromO */ 28 } Обработка тайм-аута из функции recvfrom 8-22 Мы устанавливаем обработчик для сигнала SIGALRM и затем вызываем функцию alarm для 5-секундного тайм-аута при каждом вызове функции recvfrom. Если функция recvfrom прерывается нашим обработчиком сигнала, мы выводим сооб- щение об ошибке и продолжаем работу. Если получена строка от сервера, мы от- ключаем функцию al arm и выводим ответ. Обработчик сигнала SIGALRM 24-28 Наш обработчик сигналов возвращает управление, прерывая блокированную функцию recvfrom. Этот пример работает корректно, потому что каждый раз, когда мы устанав- ливаем функцию alarm, мы читаем только один ответ. В разделе 18.4 мы исполь- зуем ту же технологию, но поскольку мы читаем множество ответов для данной функции alarm, возникает ситуация гонок, которую мы должны обработать. Тайм-аут для функции recvfrom (функция select) Мы демонстрируем вторую технологию для установки тайм-аута (использова- ние функции sei ect) в листинге 13.3. Здесь показана наша функция readabl e_timeo, которая ждет, когда дескриптор станет готов для чтения, но не более заданного числа секунд.
396 Глава 13. Дополнительные функции ввода-вывода Листинг 13.3. Функция readable_timeo: ожидание, когда дескриптор станет готов для чтения //lib/readable_timeo с 1 #include "unp h" 2 int 3 readable_timeo(int fd. int sec) 4 { 5 fdset rset. 6 struct timeval tv; 7 FD_ZERO(&rset). 8 FD_SET(fd. &rset). 9 tv tvsec = sec. 10 tv tv_usec = 0; 11 return (select(fd + 1. &rset. NULL. NULL. &tv)). 12 /* > если дескриптор готов для чтения */ 13 } Подготовка аргументов для функции select 7-10 В наборе дескрипторов для чтения включается бит, соответствующий данному дескриптору. В структуре tTmeval устанавливается время (число секунд), в тече- ние которого вызывающий процесс готов ждать. Блокирование в функции select 11-12 Функция select ждет, когда дескриптор станет готов для чтения или истечет заданное время ожидания. Возвращаемое значение этой функции — это возвра- щаемое значение функции sel ect: -1 в случае ошибки; 0, если истекло время ожи- дания, и положительное значение, задающее число готовых дескрипторов, если таковые появились. Эта функция не выполняет операции чтения — она просто ждет, когда дескрип- тор будет готов к чтению. Следовательно, эту функцию можно использовать с лю- бым типом сокета — TCP или UDP. Создание аналогичной функции, называемой writable_timeo, тривиально. Эта функция ждет, когда дескриптор будет готов для записи. Мы используем эту функцию в листинге 13.4, где показана еще одна версия нашей функции dg cl 1, приведенной в листинге 8.4. Эта новая версия вызывает функцию recvfrom, только когда наша функция readable timeo возвращает поло- жительное значение. Мы не вызываем функцию recvfrom, пока функция readable_timeo не сообщит нам, что дескриптор готов для чтения. Тем самым мы гарантируем, что функция recvfrom не заблокируется. Листинг 13.4. Функция dg_cli, вызывающая функцию readable_timeo для установки тайм-аута //advio/dgclitimeol с 1 #include "unp h" 2 void 3 dg_cli(FILE *fp, int sockfd. const SA *pservaddr. socklen t servlen)
13.2. Тайм-ауты сокета 397 4 { а’ 5 int n. 6 char sendlinefMAXLINE] recvline[MAXLINE + 1]: 7 while (Fgets(sendline. MAXLINE, fp) '= NULL) { 8 Sendto(sockfd. sendline, strlen(sendline). 0. pservaddr. servlen); 9 if (Readable_timeo(sockfd. 5) = 0) { 10 fprintf(stderr, "socket timeout\en"). 11 } else { 12 n = Recvfran(sockfd. recvline. MAXLINE. 0. NULL. NULL); 13 recvline[n] = 0. /* завершающий нуль */ 14 Fputs(recvline. stdout). 15 } 16 } 17 } Тайм-аут для функции recvfrom (параметр сокета SO_RCVTIMEO) В нашем последнем примере демонстрируется применение параметра сокета SO RCVTIMEO. Мы устанавливаем этот параметр один раз для дескриптора, задавая значение тайм-аута, и этот тайм-аут затем применяется ко всем операциям чте- ния этого дескриптора. Одна из замечательных особенностей этого метода состо- ит в том, что мы устанавливаем данный параметр только один раз, тогда как пре- дыдущие два метода требовали выполнения некоторых действий перед каждой операцией, для которой мы хотели задать временной предел. Но этот параметр сокета применяется только к операциям чтения. Аналогичный параметр SO_SNDTIMEO применяется только к операциям записи, и ни один параметр сокета не может использоваться для установки тайм-аута для функции connect. Листинг 13.5. Функция dg_ch, использующая параметр сокета SO_RCVTIMEO для установки тайм-аута //advio/dgclitimeo2 с 1 #include "unp h" 2 void 3 dg_cli(FILE *fp. int sockfd. const SA *pservaddr socklen_t servlen) 4 { 5 int n. 6 char sendline[MAXLINE]. recvline[MAXLINE + 1]. 7 struct timeval tv: 8 tv tv_sec = 5. 9 tv tv_usec = 0. 10 Setsockopt(sockfd. SOL_SOCKET. SO_RCVTIMEO, &tv. sizeof(tv)); 11 while (Fgets(sendline, MAXLINE, fp) '= NULL) { 12 Sendto(sockfd. sendline, strlen(sendline). 0. pservaddr. servlen); 13 n = recvfrom(sockfd. recvline, MAXLINE. 0. NULL. NULL), 14 if (n < 0) { 15 if (errno == EWOULDBLOCK) { 16 fpmntf(stderr, "socket timeout\en"), 17 continue. 1® } else продолжение &
398 Глава 13. Дополнительные функции ввода-вывода Листинг 13.5. (продолжение) 19 err_sys("recvfrom error”): 20 } 21 recvline[n] = 0. /* завершающий нуль */ 22 Fputs(recvline. stdout): 23 } 24 } Установка параметра сокета 8-10 Четвертый аргумент функции setsockopt — это указатель на структуру timeval, в которую записывается желательное значение тайм-аута. Проверка тайм-аута 15-17 Если тайм-аут операции ввода-вывода истекает, функция (в данном случае recvfrom) возвращает ошибку EWOULDBLOCK. 13.3. Функции recv и send Эти две функции аналогичны стандартным функциям read и write, но для них требуется дополнительный аргумент. #include <sys/socket h> ssizet recv(int sockfd void *buff. sizet nbytes int flags'). ssize t send(int sockfd const void *buff size_t nbytes. int flags) Обе функции возвращают количество прочитанных или записанных байтов в случае успешного выполнения -1 в случае ошибки Первые три аргумента функций recv и send совпадают с тремя первыми аргу- ментами функций read и wri te. Аргумент fl ags либо имеет нулевое значение, либо формируется в результате применения операции логического ИЛИ к констан- там, представленным в табл. 13.1. Таблица 13.1. Аргумент flags для функций ввода-вывода flags Описание recv send MSG_DONTROUTE He искать в таблице маршрутизации • MSGDONTWAIT Только эта операция является неблокируемой • • MSGOOB Отправка или получение внеполосных данных • • г MSG_PEEK Просмотр приходящих сообщений • MSGWAITALL Ожидание всех данных • 1 MSG_DONTROUTE. Этот флаг ообщает ядру, что получатель находится в нашей сети, и поэтому не нужно выполнять поиск в таблице маршрутизации. Дополни- тельную информацию об этом свойстве мы приводим при описании парамет- ра сокета SOJDONTROUTE (см. раздел 7.5). Это свойство можно включить для од- ной операции вывода с флагом MSG DONTROUTE или для всех операций вывода данного сокета, используя указанный параметр сокета. * MSG_DONTWAIT. Этот флаг указывает, что отдельная операция ввода-вывода яв- ляется неблокируемой. Таким образом, отпадает необходимость включать флаг
13.3. Функции recv и send 399 отсутствия блокировки для сокета, выполнять операцию ввода-вывода и за- тем выключать флаг отсутствия блокировки. Неблокируемый ввод-вывод мы опишем в главе 15 вместе с включением и выключением флага отсутствия блокировки для всех операций ввода-вывода через сокет. ПРИМЕЧАНИЕ------------------------------------------------------------ Этот флаг введен в Net/З и может не поддерживаться в некоторых системах. i > MSG_OOB. С функцией send этот флаг указывает, что отправляются внеполосные данные. В случае TCP в качестве внеполосных данных должен быть отправ- лен только 1 байт, как показано в главе 21. С функцией recv этот флаг указы- вает на то, что вместо обычных данных должны читаться внеполосные дан- ные. fc MSG_PEEK. Этот флаг позволяет нам просмотреть пришедшие данные, готовые для чтения, при этом после выполнения функции recv или recvfrom данные не сбрасываются (при повторном вызове этих функций снова возвращаются уже просмотренные данные). Подробнее мы поговорим об этом в разделе 13.7. MSG_WAITALL. Этот флаг был впервые введен в 4.3BSD Reno. Он сообщает ядру, что операция чтения должна выполняться до тех пор, пока не будет прочита- но запрашиваемое количество байтов. Если система поддерживает этот флаг, мы можем опустить функцию readn (см. листинг 3.8) и заменить ее макро- определением: #define readn(fd. ptr n) recv(fd. ptr. n MSG WAITALL) Даже если мы задаем флаг MSG_WAITALL, функция может возвратить количество байтов меньше запрашиваемого в том случае, если перехватывается сигнал, или соединение завершается, или есть ошибка сокета, требующая обработки. Существуют дополнительные флаги, используемые протоколами, отличны- ми от TCP/IP. Например, транспортный уровень OSI основан на записях (а не на потоке байтов, как TCP) и для операций вывода поддерживает флаг MSG EOR, за- дающий конец логической записи. Т/ТСР (TCP для транзакций, описываемый в разделе 13.9), поддерживает новый флаг MSG EOF для объединения операции вы- вода с отправкой сегмента FIN. С аргументом fl ags связана одна фундаментальная проблема: он передается по значению и не является аргументом типа «значение-результат». Следователь- но, он может использоваться только для передачи флагов от процесса к ядру. Ядро не может передать флаги обратно процессу. Это не представляет проблемы с TCP/IP, поскольку очень редко бывает необходимо передавать флаги обратно процессу от ядра. Но когда к 4.3BSD Reno были добавлены протоколы OSI, появилась не- обходимость возвращать процессу флаг MSG EOR при операции ввода. В 4.3BSD Reno было принято решение оставить аргументы для общеупотребительных функ- ций (recv и recvfrom) как есть и изменить структуру msghdr, которая используется с функциями recvmsg и sendmsg. В разделе 13.5 мы увидим, что в эту структуру был добавлен целочисленный элемент msg_f 1 ags, и поскольку структура передается по ссылке, ядро может изменить эти флаги по завершении функции. Это значит также, что если процессу необходимо, чтобы флаги изменялись ядром, процесс должен вызвать функцию recvmsg вместо вызова функции recv или recvfrom.
400 Глава 13. Дополнительные функции ввода-вывода 13.4. Функции readv и writev Эти две функции аналогичны функциям read и write, но readv и writev позволяют использовать для чтения или записи один или более буферов с помощью одного вызова функции. Эти операции называются операциями распределяющего чте- ния {scatter read) (поскольку вводимые данные распределяются по нескольким буферам приложения) и объединяющей записи {gather write) (поскольку данные из нескольких буферов объединяется для одной операции вывода). #include <sys/uio h> ssize_t readv(int filedes. const struct icvec *iov. int lovcnt). ssize_t writevtint filedes. const struct icvec *iov. int wvcnt). Обе функции возвращают количество считанных или записанных байтов. -1 в случае ошибки Второй аргумент обеих функций — это указатель на массив структур i ovec, для определения которого требуется включить заголовочный файл <sys/uio h>: struct icvec { void *iov_base. /* начальный адрес буфера */ size_t iov_len, /* размер буфера */ }. ПРИМЕЧАНИЕ ------------------------------------------------------------ Функции readv и writev еще не стандартизованы Posix. Но структура iovec использует- ся также с функциями resvmsg и sendmsg (см. раздел 13.5) и стандартизована в Posix.lg. Типы данных элементов структуры iovec определяются Posix.lg. Вам могут встретить- ся реализации, определяющие iov base как char *, a iov_len как int. { --------------------------------------------------------------------- Существует некоторый предел числа элементов в массиве структур iovec, за- висящий от реализации. 4.4BSD, например, допускает до 1024 элементов, в то время как предел Solaris 2.5 равен 16. Posix.lg требует, чтобы константа IOV_MAX определялась включением заголовочного файла <sys/uio h> и чтобы ее значение было не менее 16. Функции readv и wri tev могут использоваться с любым дескриптором, но только не с сокетами. Кроме того, writev является атомарной операцией. Для протокола, основанного на записях, такого как UDP, один вызов функции writev генерирует одну дейтаграмму UDP. Мы отметили одно использование функции writev с параметром сокета ТСР_ NODELAY в разделе 7.9. Мы сказали, что при записи с помощью функции wri tev 4 байт и затем 396 байт может активизироваться алгоритм Нагла, и предпочтительное решение в данном случае заключается в вызове функции wri tev для двух буферов. 13.5. Функции recvmsg и sendmsg Эти две функции являются наиболее общими для всех операций ввода-вывода. Действительно, мы можем заменить все вызовы функций read, readv, recv и recvfrom вызовами функции recvmsg. Аналогично, все вызовы различных функций вывода можно заменить вызовами функции sendmsg. include <sys/socket h> ssize t recvmsg(int sockfd. struct msghdr *msg. int flags')
13.5. Функции recvmsg и sendmsg 401 ssize_t sendmsglint sockfd. struct msghdr *msg, int flags'). Обе функции возвращают количество прочитанных или записанных байтов в случае успешного выполнения. -1 в случае ошибки Большинство аргументов обеих функций скрыто в структуре msghdr: struct msghdr { void *msg_name. /* адрес протокола */ socklen_t msg_namel en. /* размер адреса протокола */ struct iovec *msg_iov; /* массив буферов */ size_t msg_iovlen. /* количество элементов в массиве msg_iov */ void *msg_control. /* вспомогательные данные, должны быть выровнены для струк- туры cmsghdr' */ socklen_t msg_control 1en. /* размер вспомогательных данных */ int }' msg_f1ags. /* флаги, возвращенные функцией recvmsgO */ ПРИМЕЧАНИЕ------------------------------------------------------------ Показанная нами структура msghdr восходит к 4.3BSD Reno и определяется Posix.lg. Некоторые системы (например, Solaris 2.5) используют более раннюю структуру msghdr, которая появилась в 4.2BSD. У более ранней структуры нет элемента msg flags, а элементы msg control и msg controllen называются msg_accrights и msg accrightslen. В этой системе поддерживается только одна форма вспомогательных данных — пере- дача дескрипторов файлов (так называемые права доступа). При появлении протоко- лов OSI в 4.3BSD Reno были добавлены новые формы вспомогательных данных, вслед- ствие чего были обобщены имена элементов структуры. Элементы msg_name и msg_namel еп используются, когда сокет не является при- соединенным (например, неприсоединенный сокет UDP). Они аналогичны пятому и шестому аргументам функций recvfrom и sendto: msg_name указывает на структуру адреса сокета, в которой вызывающий процесс хранит адрес протоко- ла получателя для функции sendmsg или функция recvmsg хранит адрес протокола отправителя. Если нет необходимости задавать адрес протокола (например, со- кет TCP или присоединенный сокет UDP), элемент msg_name должен быть пус- тым указателем. Элемент msg_namelen является аргументом типа «значение» для функции sendmsg, но для функции recvmsg это аргумент типа «значение-ре- зультат». Элементы msg i ov и msg_i ovl еп задают массив буферов ввода и вывода (массив структур iovec), аналогичный второму и третьему аргументам функций readv и writev. Элементы msg_control и msg controllen задают расположение и размер необя- зательных вспомогательных данных. Элемент msg control 1 еп — это аргумент типа «значение-результат» функции recvmsg. Вспомогательные данные мы рассматри- ваем в разделе 13.6. Работая с функциями recvmsg и sendmsg, следует учитывать различие между двумя флаговыми переменными: это аргумент flags, который передается по зна- чению, и элемент msg_f 1 ags структуры msghdr, который передается по ссылке (по- скольку функции передается адрес этой структуры). : Элемент msg_f 1 ags используется только функцией recvmsg. Когда вызывается функция recvmsg, аргумент flags копируется в элемент msg_flags [105, с. 502], и это значение используется ядром для управления приемом данных. Затем это значение обновляется в зависимости от результата функции recvmsg.
402 Глава 13. Дополнительные функции ввода-вывода Элемент msg_flags игнорируется функцией sendmsg, поскольку эта функция использует аргумент fl ags для управления выводом данных. Это значит, что если мы хотим установить флаг MSG_DONTWAIT при вызове функции sendmsg, то мы должны присвоить это значение аргументу flags, а присваивание значе- ния MSG_DONTWAIT элементу msg_f 1 ags не имеет никакого эффекта. В табл. 13.2 показано, какие флаги проверяются ядром для функций ввода и вывода и какие элементы msg_f 1 ags может возвращать функция recvmsg. Для эле- мента sendmsg msg_f 1 ags нет колонки, потому что, как мы отмечали, он не исполь- зуется. Таблица 13.2. Флаги для различных функций ввода-вывода Флаг Проверяются функ- циями send flags sendto flags sendmsg flags Проверяются функ- циями recv flags recvfrom flags recvmsg flags Возвращаются функ- цией recvmsg msgflags MSG_DONTROUTE • MSGDONTWAIT • • MSGPEEK • MSGWAITALL • MSGEOR • MSG OOB • • • MSG-BCAST • MSG_MCAST • MSG_TRUNC MSGCTRUNC • Первые четыре флага только проверяются и никогда не возвращаются, вто- рые два проверяются и возвращаются, а последние четыре флага только возвра- щаются. Следующие ниже комментарии относятся к шести флагам, возвращае- мым функцией recvmsg. < MSG_BCAST. Этот флаг введен в BSD/OS и возвращается, если дейтаграмма была получена как широковещательная дейтаграмма канального уровня или если ее IP-адрес получателя является широковещательным адресом. Этот флаг предоставляет более удачную возможность определить, что дейтаграмма UDP была отправлена на широковещательный адрес, чем параметр сокета IP_RECVDST- ADDR. MSG_MCAST. Этот флаг введен в BSD_OS и возвращается, если дейтаграмма была получена как дейтаграмма многоадресной передачи канального уровня. ‘ MSG_TRUNC. Этот флаг возвращается, если дейтаграмма была усечена: у ядра имеется больше данных для возвращения, чем позволяет пространство в па- мяти, выделенное для них процессом (сумма всех элементов iov_len). Более подробно мы рассмотрим это в разделе 20.3. > 1 MSG_CTRUNC. Этот флаг возвращается, если были усечены вспомогательные дан- ные: у ядра имеется больше вспомогательных данных для возвращения, чем позволяет выделенное для них процессом пространство в памяти (msg_con- trollenl
13.5. Функции recvmsg и sendmsg 403 " MSG_E0R. Этот флаг означает конец записи. Он сбрасывается, если возвращае- мые данные не заканчивают запись. Если же возвращаемые данные заканчи- вают логическую запись, этот флаг устанавливается. TCP не использует этот флаг, поскольку это протокол потока байтов. MSG_OOB. Этот флаг никогда не возвращается для внеполосных данных TCP. Он возвращается другими наборами протоколов (например, протоколами OSI). Реализации могут возвращать некоторые из входных аргументов fl ags в эле- менте msg_f 1 ags, поэтому мы должны проверять только те значения флагов, кото- рые нас интересуют (например, последние шесть в табл. 13.2). На рис. 13.1 представлена структура msghdr и информация, на которую она указывает. На этом рисунке отражена ситуация, предшествующая вызову функ- ции recvmsg для сокета UDP. Рис. 13.1. Структуры данных в тот момент, когда функция recvmsg вызывается для сокета UDP Для адреса протокола в памяти выделяется 16 байт, а для вспомогательных данных — 20 байт. Инициализируется массив из трех структур iovec: первая за- дает 100-байтовый буфер, вторая — 60-байтовый буфер, третья — 80-байтовый буфер. Мы также предполагаем, что был установлен параметр сокета IP_RECVSTADDR для получения IP-адреса получателя из дейтаграммы UDP. Затем будем считать, что с адреса 198.69.10.2, порт 2000, приходит 170-байто- вая дейтаграмма UDP, предназначенная для нашего сокета UDP с IP-адресом получателя 206.62.226.35. На рис. 13.2 показана вся информация, содержащаяся в структуре msghdr в момент завершения функции recvmsg. Затемненными показаны поля, изменяемые функцией recvmsg. По сравнению с рис. 13.1 на рис. 13.2 изменяется следующее: ® В буфер, на который указывает элемент msg_name, записывается структура ад- реса сокета Интернета, содержащая IP-адрес и UDP-порт отправителя, опре- ПР ПР1ШРТР nrv плпмириилм ПОитотпилАГО
404 Глава 13. Дополнительные функции ввода-вывода Рис. 13.2. Изменение рис. 13.1 при завершении функции « Обновляется аргумент msg_namelеп, имеющий тип «значение-результат». Его новым значением становится количество данных, хранящихся в msg_name. Но на самом деле его значение как перед вызовом функции recvmsg, так и при ее завершении равно 16. & Первые 100 байт данных записываются в первый буфер, следующие 60 байт — во второй буфер и последние 10 байт — в третий буфер. Последние 70 байт третьего буфера не изменяются. Возвращаемое значение функции recvmsg — это размер дейтаграммы (170). Буфер, на который указывает msg_control, заполняется как структура cmsghdr. (Более подробно о вспомогательных данных мы поговорим в разделе 13.6, а об этом параметре сокета — в разделе 20.2.) Значение cmsg_l еп равно 16, cmsg_l evel — IPPROTO_IP, cmsg_type — IP_RECVDSTADDR, а следующие 4 байта 20-байтового бу- фера содержат IP-адрес получателя из полученной дейтаграммы UDP. По- следние 4 байта 20-байтового буфера, которые мы предоставили для хране- ния вспомогательных данных, не изменяются. £> Обновляется элемент msg_control len — его новым значением становится фак- тический размер записанных вспомогательных данных. Этот аргумент также является аргументом типа «значение-результат», и его результат по заверше- нии функции равен 16. & Элемент msg_f 1 ags изменяется функцией recvmsg, но процессу никакие флаги не возвращаются. В табл. 13.3 показаны различия между рассмотренными пятью группами функ- ций ввода-вывода.
13.6. Вспомогательные данные 405 Таблица 13.3. Сравнение пяти групп функций ввода-вывода Функция Произ- Только Один бу- вольный дескрип- фер для дескрип- тор со- чтения и тор кета записи Распреде- Наличие Указание Управляю- ляющее флагов адреса щая ин- чтение, собесед- формация объеди- ника няющая запись read, write readv, writev recv, send recvfrom, sendto recvmsg, sendsg 13.6. Вспомогательные данные Вспомогательные данные (ancillary data) можно отправлять и получать, исполь- зуя элементы msg_control и msg_control len структуры msghdr с функциями sendmsg и recvmsg. Другой термин, используемый для обозначения вспомогательных дан- ных, — управляющая информация (control information). В этом разделе мы рас- сматриваем данное понятие и показываем структуру и макросы, используемые для создания и обработки вспомогательных данных. Примеры программ мы от- кладываем до следующих глав, в которых рассказывается о применении вспомо- гательных данных. В табл. 13.4 приводится обобщение различных вариантов применения вспо- могательных данных, рассматриваемых в этой книге. Таблица 13.4. Использование вспомогательных данных Протокол cmsgjevel cmsgtype Описание IPv4 IPPROTOIP IPRECDVSTADDR Получает адрес получателя с дейтаграммой U DP IPRECVII' Получает индекс интерфейса с дейтаграммой UDP IPv6 IPPROTOIPV6 IPV6DSTOPTS Задает/получает параметры получателе IPV6HOPLIMIT Задает/получает предел количества транзитных узлов IPV6HOPOPTS Задает/получает параметры для транзитных узлов IPV6NEXTHOP Задает следующий транзитный адрес IPV6PKTINFO Задает/получает информацию о пакете IPV6RTHDR Задает/получает информацию о пакете Домен Unix SOLJSOCKET SCMRIGHTS Посылает/получает дескрипторы SCMCREDS Посылает/получает данные, идентифицирующие пользователя Набор протоколов OSI также использует вспомогательные данные для раз- личных целей, которые мы не рассматриваем в этой книге.
406 Глава 13. Дополнительные функции ввода-вывода Вспомогательные данные состоят из одного или более объектов вспомогатель- ных данных (ancillary data objects), каждый из которых начинается со структуры cmsghdr, определяемой подлючением заголовочного файла <sys/socket. h>: struct cmsghdr { socklen_t cmsg_1en. /* длина в байтах, включая эту структуру */ int cmsg_level, /* исходящий протокол */ int cmsg_type. /* тип данных, специфичный для протокола */ /* далее следует массив символов без знака cmsg_data[] */ } Мы уже видели згу структуру на рис. 13.2, когда она использовалась с пара- метром сокета IP_RECVDSTARRD для возвращения IP-адреса получателя дейтаграм- мы UDP. Вспомогательные данные, на которые указывает элемент msg_control, должны быть соответствующим образом выровнены для структуры cmsghdr. Один из способов выравнивания мы показываем в листинге 14.9. На рис. 13.3 приводится пример двух объектов вспомогательных данных, со- держащихся в буфере управляющей информации. cmsghdr{} cmsghdr{} Объекты вспомогательных данных CMSGJSPACE () Объекты вспомогательных данных CMSG_SPACE () Рис. 13.3. Два объекта вспомогательных данных Элемент msg_control указывает на первый объект вспомогательных данных, а общая длина вспомогательных данных задается элементом msg_control 1 еп. Каж- дому объекту предшествует структура cmsghdr, которая описывает объект. Между элементом cmsg_type и фактическими данными может существовать заполнение, а также заполнение может быть в конце данных, перед следующим объектом вспо- могательных данных. Пять макросов CMSG_xxx, которые мы описываем чуть ниже, учитывают это возможное заполнение. ПРИМЕЧАНИЕ --------------------------------------------------------- Не все реализации поддерживают наличие нескольких объектов вспомогательных дан- ных в буфере управляющей информации.
13.6. Вспомогательные данные 407 На рис. 13.4 приводится формат структуры cmsghdr при ее использовании с до- менным сокетом Unix для передачи дескрипторов (см. раздел 14.7) или передачи данных, идентифицирующих пользователя (см. раздел 14.8). cmsghdr{} cmsghdr{} cmsg_len 16 cmsg_len 112 cmsg_level SOL_SOCKET cmsg_level SOL SOCKET cmsg_type SCM_RIGHTS cmsg_type SCM CREDS Дескриптор fcredf} Рис. 13.4. Структура cmsghdr при использовании с доменными сокетами Unix Предполагается, что каждый из трех элементов структуры cmsghdr занимает 4 байта и между структурой cmsghdr и данными нет заполнения. При передаче дескрипторов содержимое массива cmsg_data — это фактические значения дескрип- торов. На этом рисунке мы показываем только один передаваемый дескриптор, но в общем может передаваться и более одного дескриптора (тогда значение эле- мента cmsg_len будет равно 12 плюс число дескрипторов, умноженное на 4, если считать, что каждый дескриптор занимает 4 байта). Вспомогательные данные, возвращаемые функцией recvmsg, могут содержать любое число объектов вспомогательных данных. Чтобы скрыть возможное запол- нение от приложения, для упрощения обработки вспомогательных данных опреде- лены следующие пять макросов (что требует включения заголовочного файла <sys /socket h>). include <sys/socket h> include <sys/param h> /* для макроса ALIGN во многих реализациях */ struct cmsghdr *CMSG_FI RSTHDR( struct msghdr ★mhdrptr'). Возвращает указатель на первую структуру cmsghdr или NULL, если нет вспомогательицх данных struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr struct cmsghdr *crrsgptr):‘ Возвращает указатель на структуру cmsghdr или NULL, если нет больше объектов вспойоГа'- ‘ ' тельных данных unsigned char *CMSG_DATA(struct cmsghdr ★cmsgptr). Возвращает указатель на первый байт данных связанных со структурой cmsghdr unsigned int CMSG_LEN(unsigned int length) Возвращает значение, которое записывается в cmsglen unsigned int CMSG_SPACE(unsigned int length).
408 Глава 13. Дополнительные функции ввода-вывода ПРИМЕЧАНИЕ ------------------------------------------------------------------ В Posix. 1g определены первые пять макросов, а в [96] определены последние два. Эти макросы будут использованы в следующем псевдокоде: struct msghdr msg. struct cmsghdr *cmsgptr. /* заполнение структуры msg */ /* вызов recvmsgO */ for (cmsgptr = CMSG_FIRSTHDR(&msg) cmsgptr 1- NULL: cmsgptr = CMSG_NXTHDR(&msg. cmsgptr)) { if (cmsgptr->cmsg_level == && cmsgptr->cmsg_type == ) { u_char *ptr. ptr = CMSG_DATA(cmsgptr), /* обработка данных, на которые указывает ptr */ } } Макрос CMSG FIRSTFDR возвращает указатель на первый объект вспомогатель- ных данных или пустой указатель, если в структуре msghdr нет вспомогательных данных (или msg_control является пустым указателем, или cmsg_len меньше раз- мера структуры cmsghdr). Макрос CMSG_NXTHDR возвращает пустой указатель, когда в буфере управления нет другого объекта вспомогательных данных. ПРИМЕЧАНИЕ------------------------------------------------------------------- Многие существующие реализации макроса CMSG FIRSTHRD никогда не использу- ют элемент msgcontrollen и просто возвращают значение cmsgcontroL В листинге 20.2 мы проверяем значение msg controllen перед вызовом макроопределения. Разница между макросами CMSG_LEN и CMSG SPACE заключается в том, что пер- вый возвращает длину объекта вместе с дополняющими нулями (это значение хранится в cmsg l еп), а последний возвращает длину собственно объекта (это зна- чение может использоваться для динамического выделения памяти под объект). 13.7. Сколько данных находится в очереди? Иногда требуется узнать, сколько данных находится в очереди данного сокета для чтения, не считывая эти данные. Для этого имеется три способа. 1. Если нашей целью не является блокирование в ядре (поскольку мы можем выполнять другие задачи, пока данные для чтения еще не готовы), может ис- пользоваться неблокируемый ввод-вывод. Мы обсуждаем его в главе 15. 2. Если мы хотим проверить данные, но при этом оставить их в приемном буфере для считывания какой-либо другой частью процесса, мы можем использовать флаг MSG_PEEK (см. табл. 13.1). Если мы не уверены, что какие-либо данные го- товы для чтения, мы можем объединить этот флаг с отключением блокировки для сокета или с флагом MSG_DONTWA1T. Помните о том, что для потокового соке-
13.8. Сокеты и стандартный ввод-вывод 409 та количество данных в приемном буфере может изменяться между двумя последовательными вызовами функции recv. Например, предположим, что мы вызываем recv для сокета TCP, задавая буфер длиной 1024 и флаг MSG PEEK, и возвращаемое значение равно 100. Если затем мы снова вызовем функцию recv, возможно, возвратится более 100 байт (мы задаем длину буфера больше 100), поскольку в промежутке между двумя нашими вызовами recv могут быть получены дополнительные данные. А что произойдет в случае сокета UDP, когда в приемном буфере имеется дейтаграмма? При вызове recvfrom с фла- гом MSG_PEEK, за которым последует другой вызов без задания MSG_PEEK, возвра- щаемые значения обоих вызовов (размер дейтаграммы, ее содержимое и адрес отправителя) будут совпадать, даже если в приемный буфер сокета между дву- мя вызовами добавляются дополнительные дейтаграммы. (Мы считаем, ко- нечно, что никакой другой процесс не использует тот же дескриптор и не осу- ществляет чтение из данного сокета в это же время.) 3. Некоторые реализации поддерживают команду FIONREAD функции тocti. Тре- тий аргумент функции т oct 1 — это указатель на целое число, а возвращаемое в этом целом числе значение — это текущее число байтов в приемном буфере сокета [105, с. 553]. Это значение является общим числом установленных в очередь байтов, которое для сокета UDP включает все дейтаграммы, уста- новленные в очередь. Также помните о том, что значение, возвращаемое для сокета UDP, в Беркли-реализациях включает пространство, требуемое для структуры адреса сокета, содержащей IP-адрес отправителя и порт для каж- дой дейтаграммы (16 байт для IP4, 24 байта для IP6). 13.8. Сокеты и стандартный ввод-вывод Во всех наших примерах мы применяли то, что иногда называется вводом-выво- дом Unix: вызывали функции read и write и их разновидности (recv, send и т. д.). Эти функции работают с дескрипторами и обычно реализуются как системные вызовы внутри ядра Unix. Другой метод выполнения ввода-вывода заключается в использовании стан- дартной библиотеки ввода-вывода. Она задается стандартом ANSI С и была заду- мана как библиотека, совместимая с He-Unix-системами, поддерживающими ANSI С. Стандартная библиотека ввода-вывода обрабатывает некоторые моменты, о ко- торых мы должны заботиться сами при использовании функций ввода-вывода Unix, такие как автоматическая буферизация потоков ввода и вывода. К сожале- нию, ее обработка буферизации потока может представить новый ряд проблем, о которых следует помнить. Глава 5 [93] подробно описывает стандартную биб- лиотеку ввода-вывода, а в [79] представлена полная реализация стандартной биб- лиотеки ввода-вывода и ее обсуждение. ПРИМЕЧАНИЕ----------------------------------------------------- При обсуждении стандартной библиотеки ввода-вывода используется термин «поток» в выражениях типа «мы открываем поток ввода» или «мы очищаем поток вывода». Не путайте это с подсистемой потоков System V, которую мы обсуждаем в главе 33.
410 Глава 13. Дополнительные функции ввода-вывода Стандартная библиотека ввода-вывода может использоваться с сокетами, но есть несколько моментов, которые необходимо при этом учитывать. ‘ Стандартный поток ввода-вывода может быть создан из любого дескриптора при помощи вызова функции fdopen. Аналогично, имея стандартный поток ввода-вывода, мы можем получить соответствующий дескриптор, вызывая функцию fileno. С функцией flleno мы впервые встретились в листинге 6.1, когда мы хотели вызвать функцию sel ect для стандартного потока ввода-вы- вода. Функция select работает только с дескрипторами, поэтому нам необхо- димо было получить дескриптор для стандартного потока ввода-вывода. Эй Сокеты TCP и UDP являются двусторонними. Стандартные потоки ввода- вывода также могут быть двусторонними: мы просто открываем поток типа г+, что означает чтение-запись. Но в таком потоке за функцией вывода не мо- жет следовать функция ввода, если между ними нет вызова функции fflush, fseek, fsetpots или rewind. Аналогично, за функцией вывода не может следо- вать функция ввода, если между ними нет вызова функции fseek, fsetpots, rewi nd, в том случае, когда при вводе не получен признак конца файла. Про- блема с последними тремя функциями состоит в том, что все они вызывают функцию 1 seek, которая не работает с сокетами. ; Простейший способ обработки подобной проблемы чтения-записи — это от- крытие двух стандартных потоков ввода-вывода для данного сокета: одного для чтения и другого для записи. Пример: функция str_echo, использующая стандартный ввод-вывод Сейчас мы модифицируем наш эхо-сервер TCP (см. листинг 5.2) для использо- вания стандартного ввода-вывода вместо функций readl те и wri ten. В листин- ге 13.6 представлена версия нашей функции str_echo, использующая стандарт- ный ввод-вывод. (С этой версией связана проблема, которую мы вскоре опишем.) Листинг 13.6. Функция str_echo, переписанная с использованием стандартного ввода-вывода //advio/str_echo_stdio02 с 1 include unp h" 2 void 3 str_echo(int sockfd) 4 { 5 char line[MAXLINE] 6 FILE *fpin *fpout 7 fpin = Fdopen(sockfd "r"), 8 fpout = FdopenCsockfd ”w"). 9 for ( ) { 10 if (Fgetsdine MAXLINE fpin) == NULL) 11 return /* соединение закрывается удаленным концом */ 12 Fputsdine fpout). 13 } 14 }
13.8. Сокеты и стандартный ввод-вывод 411 Преобразование дескриптора в поток ввода и поток вывода 7-13 Функцией fdopen создаются два стандартных потока ввода-вывода: один для ввода и другой для вывода. Вызовы функций headline и writen заменены вызова- ми функций fgets и fputs. Если мы запустим наш сервер с этой версией функции str_echo и затем запус- тим наш клиент, мы увидим следующее: solans X tcpcli02 206.62.226.33 hello, world мы набираем эту строку но не получаем отражения and hi и на эту строку нет ответа hello?? и на эту строку нет ответа 'D наш символ конца файла hello world и затем выводятся три отраженные строки and hi hello” Здесь возникает проблема буферизации, поскольку сервер ничего не отража- ет, пока мы не введем наш символ конца файла. Выполняются следующие шаги: Мы набираем первую строку ввода, и она отправляется серверу. ’ Сервер читает строку с помощью функции fgets и отражает ее с помощью функции fputs. ‘ Но стандартный поток ввода-вывода сервера полностью буферизован стандар- тной библиотекой ввода-вывода. Это значит, что библиотека копирует отра- женную строку в свой стандартный буфер ввода-вывода для этого потока, но не выдает содержимое буфера в дескриптор, поскольку буфер не заполнен. Мы набираем вторую строку ввода, и она отправляется серверу. Сервер читает строку с помощью функции fgets и отражает ее с помощью функции fputs. - Снова стандартная библиотека ввода-вывода сервера только копирует строку в свой буфер, но не выдает содержимое буфера в дескриптор, поскольку он не заполнен. < По тому же сценарию вводится третья строка. Мы набираем наш символ конца файла, и функция str_cl 1 (см. листинг 6.2) вызывает функцию shutdown, посылая серверу сегмент FIN. TCP сервера получает сегмент FIN, который читает функция fgets, в резуль- тате чего функция fgets возвращает пустой указатель. “ Функция str_echo возвращает серверу функцию main (см. листинг 5.9), и до- черний процесс завершается при вызове функции exit. х Библиотечная функция exi t языка С вызывает стандартную функцию очист- ки ввода-вывода [93, с. 162-164], и буфер вывода, который был частично за- полнен нашими вызовами функции fputs, теперь выводит скопившиеся в нем данные. I Дочерний процесс сервера завершается, в результате чего закрывается его при- соединенный сокет, клиенту отсылается сегмент FIN и заканчивается после- довательность завершения соединения TCP. ‘ Наша функция str cl 1 получает и выводит той отраженных строки.
412 Глава 13. Дополнительные функции ввода-вывода Затем функция strci i получает символ конца файла на своем сокете, и кли- ент завершает свою работу. Проблема здесь заключается в том, что буферизация на стороне сервера вы- полняется автоматически стандартной библиотекой ввода-вывода. Существует три типа буферизации, выполняемой стандартной библиотекой ввода-вывода. 1. Полная буферизация (fully buffered) означает, что ввод-вывод имеет место, только когда буфер заполнен, процесс явно вызывает функцию ffl ush или про- цесс завершается посредством вызова функции exit. Обычный размер стан- дартного буфера ввода-вывода — 8192 байт. 2. Буферизация по строкам (line buffered) означает, что ввод-вывод имеет место, только когда встречается символ перевода строки, процесс вызывает функ- цию ffl ush или процесс завершается вызовом функции exi t. 3. Отсутствие буферизации (unbuffered) означает, что ввод-вывод имеет место каждый раз, когда вызывается функция стандартного ввода-вывода. Большинство реализаций Unix стандартной библиотеки ввода-вывода исполь- зуют следующие правила: Стандартный поток ошибок никогда не буферизуется. Стандартные потоки ввода и вывода полностью буферизованы, если они не подключены к терминальному устройству, в случае чего они буферизуются по строкам. Все остальные потоки полностью буферизованы, если они не подключены к терминалу, в случае чего они буферизованы по строкам. Поскольку сокет не является терминальным устройством, проблема, отмечен- ная в нашей функции str echo в листинге 13.6, заключается в том, что поток вы- вода (fpot) полностью буферизован. Есть два решения: мы можем сделать поток вывода буферизованным по строкам при помощи вызова функции setvbuf либо заставить каждую отраженную строку выводиться при помощи вызова функции ffl ush после каждого вызова функции fputs. Применение любого из этих измене- ний скорректирует поведение нашей функции str echo. ПРИМЕЧАНИЕ ----------------------------------------------------------- Другим решением будет вообще отказаться от стандартной библиотеки ввода-вывода и использовать библиотеку sfio. Этот вариант описан в [58], а исходный код является свободно доступным. Будьте осторожны — некоторые реализации стандартной библиотеки ввода-вывода все еще вызывают проблемы при работе с дескрипторами, большими 255. Эта проблема может возникнуть с сетевыми серверами, обрабатывающими множество дескрипто- ров. Проверьте определение структуры FILE в вашем заголовочном файле <stdio.h>, чтобы увидеть, к какому типу переменных относится дескриптор. 13.9. Т/ТСР: TCP для транзакций Т/ТСР — это незначительное изменение TCP, помогающее избежать трехэтап- ного рукопожатия между узлами, которые недавно взаимодействовали друг с дру- гом. Т/ТСР подробно описывается в [95], RFC 1379 [11] и RFC 1644 [13]. t
13.9. T/TCP: TCP для транзакций 413 ПРИМЕЧАНИЕ --------------------------------------------------------- Наиболее широко распространена реализация Т/ТСР, имеющаяся в FreeBSD. Т/ТСР может объединять сегмент SYN, сегмент FIN и данные в одиночном сегменте в предположении, что размер данных меньше MSS. Мы показываем это на рис. 13.5. Первый сегмент, включающий сегменты SYN, FIN и данные клиен- та, генерируется одним вызовом функции sendto. Он объединяет функциональ- ность connect, write и shutdown. Сервер выполняет обычные шаги, вызывая функ- ции socket, bi nd, 11 sten и accept. Последняя функция завершается, когда приходит клиентский сегмент. Затем сервер отправляет свой ответ с помощью функции send и закрывает сокет. Это вызывает отправку сегментов SYN, FIN и самого от- вета от сервера клиенту. Если мы сравним это с рис. 2.5, то увидим, что уменьша- ется не только количество сегментов в сети (3 для Т/ТСР, 10 для TCP и 2 для UDP), но и время, требуемое клиенту для инициирования соединения, отправки запроса и чтения ответа сервера, сокращается на один период RTT. Клиент Сервер socket sendto read (блокировка) socket, bind, listen, accept (блокировка) Завершение функции accept Завершение функции read ‘^одтвёр^^^иента !]53^!&^прИемаг Функция read считывает запрос. Сервер обрабатывает запрос. ' Функция send отправляет ответ клиенту, close Рис. 13.5. Схема минимальной транзакции Т/ТСР ПРИМЕЧАНИЕ------------------------------------------------------------ Мы игнорируем здесь некоторые подробности, которые полностью раскрыты в [95]. Например, когда клиент в первый раз общается с сервером, требуется трехэтапное ру- копожатие. Но затем в течение некоторого времени этого можно избежать, пока на обоих концах соединения сохраняется некоторая кэшированная информация о соединении, если в течение этого времени ни один из концов соединения не выйдет из строя и не перезагрузится. Три показанных сегмента формируют минимальный обмен «запрос- ответ». Дополнительные сегменты требуются, если или запрос, или ответ не помеща- ются в один сегмент. Термин «транзакция» означает запрос клиента и ответ сервера. Общие примеры транзакций — это запрос DNS и ответ сервера или запрос HTTP и от- вет сервера. Мы не говорим здесь о двухфазном протоколе с подтверждением заверше- ния транзакций.
414 Глава 13. Дополнительные функции ввода-вывода Преимуществом использования Т/ТСР в том, что он сохраняет надежность TCP (последовательные номера, тайм-ауты, повторные передачи и т. п.) в отли- чие от ненадежного варианта с применением UDP, при котором добавление на- дежности является проблемой. Кроме того, Т/ТСР поддерживает такие свойства TCP (часто отсутствующие у приложений UDP), как медленный старт и предот- вращение перегрузок (переполнения буфера). Т/ТСР требует минимальных изменений API сокетов. Следует отметить, что в системе, предоставляющей Т/ТСР, не требуется вносить изменения в прило- жения TCP, когда функции Т/ТСР не требуются. Все существующие приложе- ния TCP продолжают работать, используя API сокетов, который мы уже описы- вали. Клиент вызывает функцию sendto для объединения установления соединения с отправкой данных. Это заменяет отдельные вызовы функций connect и wri te. Адрес протокола сервера теперь передается функции sendto вместо функции connect. М Применяется новый флаг вывода MSG_EOF (см. табл. 13.1), чтобы указать, что больше никаких данных через сокет отправлено не будет. Это позволяет объ- единить операцию вывода (функция send или sendto) с функцией shutdown. Задание этого флага с функцией sendto, которая также задает адрес сервера, — это способ, с помощью которого отправляется сегмент, содержащий SYN, FIN и данные. Обратите внимание, что на рис. 13.5 сервер отправляет свой ответ, используя функцию send, а не write. Причина в том, что для отправки сегмента FIN с ответом нужно задать флаг MSG_EOF. (Не путайте этот новый флаг с суще- ствующим флагом MSG_EOR, который указывает конец записи для протоколов, ориентированных на записи.) £ Определяется новый параметр сокета TCP_NOPUSH со значением аргумента 1 evel, равным IPPROTO_TCP. Этот параметр не позволяет TCP отправлять сегмент толь- ко для того, чтобы опустошить буфер отправки сокета. Клиенты должны уста- навливать этот параметр при отправке запроса с одним вызовом функции sendto, если запрос превышает MSS, так как он может сократить число отправ- ляемых сегментов. На с. 47-49 [95] рассказывается об этом новом параметре сокета более подробно. Клиент, который хочет установить соединение с сервером и послать запрос с помощью Т/ТСР, должен вызвать функции socket, setsockopt (чтобы вклю- чить параметр сокета TCP_NOPUSH) и sendto. Если функция setsockopt выполня- ется неудачно с ошибкой ENOPROTOOPT или функция sendto выполняется неудачно с ошибкой ENOTCONN, это означает, что узел не поддерживает Т/ТСР. В этом случае клиент просто вызывает функции connect и write, за которыми, воз- можно, следует функция shutdown (если должен быть отправлен только один запрос) - Единственное изменение, которого требует сервер, — вместо функции write вызвать для отправки ответа функцию send с флагом MSG_EOF, если сервер хо- чет послать с ответом сегмент FIN. Тесты времени компиляции для Т/ТСР могут использовать #ifdef MSG_EOF. Приложение В книги [95] содержит пример кода клиента и сервера Т/ТСР.
Упражнения 415 13.10. Резюме Существует три способа установить ограничение времени для операции с сокетом: № Использовать функцию alarm и сигнал SIGALRM. Ж Задать предел времени в функции sei ect. < Использовать более новые параметры сокета SO_RCVTIMEO и SO_SNDTIMEO. Первый способ легко использовать, но он включает обработку сигналов и, как показано в разделе 18.5, может привести к ситуации гонок. Использование функ- ции sei ect означает, что блокирование происходит в этой функции (с заданным в ней пределом времени) вместо блокирования в вызове функции read, write или connect. Другая альтернатива — использование новых параметров сокета — также проста в использовании, но предоставляется не всеми реализациями. Функции recvmsg и sendmsg являются наиболее общими из пяти групп предос- тавляемых функций ввода-вывода. Они объединяют целый ряд возможностей, свойственных других функциям ввода-вывода, позволяя задавать флаг MSG xxx (как функции recv и send), возвращать или задавать адрес протокола собеседника (как функции recvfrom и sendto), использовать множество буферов (как функции readv и wri tev). Кроме того, они обеспечивают две новых возможности: возвраще- ние флагов приложению и получение или отправку вспомогательных данных. В тексте книги мы описываем десять различных форм вспомогательных дан- ных, шесть из которых появились в IPv6. Вспомогательные данные состоят из объектов вспомогательных данных. Перед каждым объектом идет структура cmsghdr, задающая его длину, уровень протокола и тип данных. Пять макросов, начинаю- щихся с префикса CMSG_, используются для создания и анализа вспомогательных данных. Сокеты могут использоваться со стандартной библиотекой ввода-вывода С, но это добавляет еще один уровень буферизации к уже имеющемуся в TCP. На самом деле недостаток понимания буферизации, выполняемой стандартной биб- лиотекой ввода-вывода, является наиболее общей проблемой при работе с этой библиотекой. Поскольку сокет не является терминальным устройством, общем решением этой потенциальной проблемы будет отключение буферизации стан- дартного потока ввода-вывода. Т/ТСР — это простое усовершенствование TCP, которое поможет избежать трехэтапного рукопожатия, допуская более быстрый ответ сервера на запрос кли- ента, если этот клиент и сервер недавно взаимодействовали. С точки зрения про- граммирования клиента преимущество Т/ТСР заключается в том, что вместо обычной последовательности вызова функций connect, write и shutdown требуется вызывать только функцию sendto. Упражнения 1. Что происходит в листинге 13.1, когда мы переустанавливаем обработчик сиг- налов, если процесс не установил обработчик для сигнала SIGALRM? 2. В листинге 13.1 мы выводим предупреждение, если у процесса уже установ- лен таймер alarm. Измените эту функцию так, чтобы новое значение alarm для процесса задавалось после выполнения connect до завершения функции.
416 Глава 13. Дополнительные функции ввода-вывода 3 Измените листинг 11.3 следующим образом: перед вызовом функции read вы- зовите функцию recv с флагом MSG_PEEK. Когда она завершится, вызовите функ- цию 1 octi с командой FIONREAD и выведите число байтов, установленных в оче- редь в буфере приема сокета. Затем вызовите функцию read для фактического чтения данных. 4. Что происходит с оставшимися в стандартном буфере ввода-вывода данны- ми, если процесс, дойдя до конца функции main, не обнаруживает там функ- ции exit? 5 Примените каждое из двух изменений, описанных после листинга 13.6, и убе- дитесь в том, что каждое из них решает проблему буферизации.
ГЛАВА 14 Доменные протоколы Unix 14.1. Введение Доменные протоколы Unix — это не набор протоколов, а способ связи клиентов и серверов на отдельном узле, использующий тот же API, который используется для клиентов и серверов на различных узлах, — сокеты или XTI. Доменные про- токолы Unix представляют альтернативу методам IPC (Interprocess Communica- tions — взаимодействие процессов), которым посвящен второй том1 этой серии, применяемым, когда клиент и сервер находятся на одном узле. Подробности дей- ствительной реализации доменных сокетов Unix в ядре, происходящем от Берк- ли, приводятся в третьей части [95]. В домене Unix предоставляются два типа сокетов: потоковые (аналогичные сокетам TCP) и дейтаграммные (аналогичные сокетам UDP). Хотя предоставля- ется также и символьный сокет, но его семантика никогда не документировалась, он не используется никакой из известных автору программ и не определяется в Posix. 1g. Доменные сокеты Unix используются по трем причинам. 1. В реализациях, происходящих от Беркли, доменные сокеты Unix часто вдвое быстрее сокета TCP, когда оба собеседника находятся на одном и том же узле [95, с. 223-224] Преимущества при этом получает одно приложение —X Win- dow System Когда клиент ХИ запускается и открывает соединение с серве- ром XI1, клиент проверяет значение переменной окружения DISPLAY, которая задает имя узла сервера, окно и экран. Если сервер находится на том же узле, что и клиент, клиент открывает потоковое соединение через доменный сокет Unix с сервером, иначе клиент открывает соединение TCP с сервером. 2. Доменные сокеты Unix используются при передаче дескрипторов между про- цессами на одном и том же узле Подобный пример мы приводим в разделе 14.7. 3. Более новые реализации доменных сокетов Unix предоставляют регистраци- онные данные клиента (идентификатор пользователя и идентификаторы груп- пы) серверу, что может служить дополнительной проверкой безопасности. Мы покажем это в разделе 14.8. Адреса протоколов, используемые для идентификации клиентов и серверов в домене Unix, — это полные имена в обычной файловой системе. Вспомните, что IPv4 использует комбинацию 32-разрядных адресов и 16-разрядных номеров У У Стивенс UNIX взаимодействие процессов — СПб Питер, 2002
418 Глава 14. Доменные протоколы Unix портов для своих адресов протоколов, а IPv6 для своих адресов протоколов ис- пользует комбинацию 128-разрядных адресов и 16-разрядных номеров портов. Эти полные имена не являются обычными именами файлов Unix: мы не можем читать из этих файлов или записывать в них. Это может делать только програм- ма, связывающая полное имя с доменным сокетом Unix. 14.2. Структура адреса доменного сокета Unix В листинге 14.1’ показана структура адреса доменного сокета Unix, задаваемая включением заголовочного файла <sys/un h>. Листинг 14.1. Структура адреса доменного сокета Unix: socladdr_un struct sockaddr_un { uint8_t sunjen. sa_family_t sun_farmly. /* AF_LOCAL */ char sun_path[104]. /* полное имя. оканчивающееся нулем */ }. ПРИМЕЧАНИЕ --------------------------------------------------------------- Более ранние реализации BSD определяли размер массива sun_path равным 108 байт, а не 104, как показано в нашем примере. В Posix.lg требуется только, чтобы его размер был как минимум 100 байт. Причина этих ограничений связана с реализацией 4.2BSD, в которой требуется, чтобы эта структура соответствовала 128-битовому mbuf (буферу памяти ядра). Полное имя, хранимое в символьном массиве sun_path, должно завершаться нулем. Имеется макрос SUN_LEN, который получает указатель на структуру sock- addr_un и возвращает длину структуры, включая число непустых байтов в полном имени. Неопределенный адрес обозначается пустой строкой, то есть элемент sun_path[O] должен быть равен нулю. Это эквивалент константы INAODR_ANY про- токола IPv4 и константы IN6ADDR_ANY_INIT протокола IPv6 для домена Unix. ПРИМЕЧАНИЕ --------------------------------------------------------------- В Posix.lg доменным протоколам Unix дали название «локального 1РС», чтобы пе вклю- чать в название зависимость от операционной системы Unix. Историческая константа AF_UNIX становится константой AF_LOCAL. Тем не менее мы будем продолжать использовать термин «домен Unix», так как он стал именем де-факто, независимо от соответствующей операционной системы. Кроме того, несмотря па попытку Posix.lg исключить зависимость от операционной системы, структура адреса сокета сохраняет суффикс un! Пример: функция bind и доменный сокет Unix Программа, показанная в листинге 14.2, создает доменный сокет Unix, с помощью функции bi nd связывает с ним полное имя и затем вызывает функцию getsockname и выводит это полное имя. 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter com/download.
14.2. Структура адреса доменного сокета Unix 419 Листинг 14.2. Связывание полного имени с доменным сокетом Unix uni xdoma i n/um xbi nd. с 1 include "unp h" 2 int 3 main(int argc char **argv) 4 { 5 int sockfd. 6 socklen_t len: 7 struct sockaddrjjn addrl, addr2: 8 if (argc l= 2) 9 err_quit("usage unixbind <pathname>"); 10 sockfd = Socket(AF_LOCAL. SOCK_STREAM. 0). 11 unlink(argv[l]). /* игнорируем возможную ошибку */ 12 bzero(&addrl. sizeof(addrl)), 13 addrl sun_farmly = AF_LOCAL. 14 strncpy(addrl sun_path. argv[l], sizeof(addrl sun_path) - 1). 15 BindCsockfd. (SA*) &addrl. SUN_LEN(&addrl)). 16 len = sizeof(addr2), 17 Getsockname(sockfd. (SA *) &addr2. &len). 18 printfC'bound name = Xs. returned len = Xd\en". addr2 sun_path. len) 19 exit(0). 20 } Удаление файла И Полное имя, которое функция bind должна связать с сокетом, — это аргумент командной строки. Если это полное имя уже существует в файловой системе, при выполнении функции bi nd возникает ошибка. Следовательно, мы вызываем функ- цию uni ink, чтобы удалить файл в том случае, если он уже существует. Если его не существует, функция uni i nk возвращает ошибку, которую мы игнорируем. Вызов функций bind и getsockname 12-18 Мы копируем аргумент командной строки, используя функцию strncpy, чтобы избежать переполнения структуры в том случае, если полное имя слишком длин- ное. Поскольку мы инициализируем структуру нулем и затем вычитаем единицу из размера массива sun_path, мы знаем, что полное имя оканчивается нулем. Да- лее вызывается функция bi nd и мы используем макрос SUNJ.EN для вычисления длины аргумента функции. Затем мы вызываем функцию getsockname, чтобы по- лучить имя, которое было только что связано с сокетом, и выводим результат. Если мы запустим программу в BSD/OS, то получим следующие результаты: bsdi X umask сначала вводим наше значение umask 0002 оно отображается в восьмеричной системе bsdi % umxbind /tmp/foo.bar bound name = /tmp/foo bar. returned len = 14 bsdi X unixbind /tmp/foo.bar снова запускаем программу bound name = /tmp/foo bar. returned len = 14 bsdi X Is -1 /tmp/foo.bar srwxrwxrwx 1 rstevens wheel 0 May 20 11 02 /tmp/foo bar
420 Глава 14 Доменные протоколы Unix bsdi % Is IF /tmp/foo bar srwxrwxrwx 1 rstevens wheel 0 May 20 11 02 /tmp/foo bar= Сначала мы вводим наше значение uniask, поскольку в Posix 1g указано, что права доступа к создаваемому объекту определяю юя этим значением Наше зна- чение 2 выключает бит, разрешающий запись в файл для прочих пользователей (этот бит называется other-write или woild-wnte) Затем мы запускаем програм- му и видим, что длина, возвращаемая функцией getsockname, равна 14 один байт для элемента sun_len, один бант для элемента sun_family и 12 байт для полного имени (исключая завершающий нуль) Это пример аргумента типа «значение- результат», значение которого при завершении функции отличается от значения при вызове функции Мы можем вывести полное имя, используя спецификатор формата its функции printf, поскольку полное имя, хранящееся в sun_path, пред- ставляет собой завершающуюся нулем строку Затем мы снова запускаем програм- му, чтобы проверить, что вызов функции uni 1 nk удаляет соответствующий файл Мы запускаем команду Is 1, чтобы увидеть биты разрешения для файла и тип файла В 4 4BSD тип файла — это сокет, что обозначается символом s Мы также замечаем, что все девять битов разрешения включены, так как 4 4BSD не изменя- ет принятые по умолчанию биты разрешения на наше значение umask Наконец, мы снова запускаем 1 s с параметром F, что заставляет 4 4BSD добавить знак ра- венства (соответствующий типу «сокег») к полному имени ПРИМЕЧАНИЕ ------------------------------------------------------------- Posix 2 ничего не зпае г о сокетах и определяет только ч го параметр -Г выводи г косую черту для катало! а звездочку для выполняемого файла и вер шкальную черту для файла типа TIFO Теперь мы запустим ту же программу в Solans 2 5 solans % umask 02 solans % umxbind /tmp/foo bar bound name = /tmp/foo bar returned len - 110 solans % umxbind /tmp/foo bar bound name = /tmp/foo bar returned len = 110 solans % Is IF /tmp/foo bar p 1 rstevens otherl 0 May 20 11 36 /tmp/foo bar| Первое огличие состоит в том, что длина, возвращаемая функцией getsockname равна 110, общему размеру структуры Solans sockaddr_un Это нормально, посколь- ку имя файла в элементе sun_path оканчивается нулем Мы также видим, что все биты разрешений файла (для чтения, записи и выполнения) по умолчанию сбро- шены Поскольку все биты разрешении сброшены, мы не можем сказа 1ь, исполь- зовалось паше значение umask или нет Наконец, Is 1 указывает, что файл с дан- ным именем имеет тип именованного канала (FIFO) — это свойственно всем доменным сокетам Unix в SVR4, и параметр F выводит вертикальную черту для обозначения FIFO 14.3. Функция socketpair Функция socketpair создает два сокета которые затем соединяются друг с дру- гом Эта функция применяется только к доменным сокетам Unix. #include <sys/socket h>
14 4 Функции сокетов 421 int socketpairdnt \f I farm ly\fP int \fItype\fP int \fIprotocol\fP int \f!sockfd[2]\fP) Возвращает ненулевое значение в случае успешного выполнения -1 в случае ошибки Аргумент farm 1у должен быть равен AF_L0CAL, а аргумент protocol должен быть нулевым Однако аргумент type может быть равен как SOCK_STREAM, так и SOCKJ3GRAM Два дескриптора сокета создаются и возвращаются как sockfd[0] и sockfd[l] ПРИМЕЧАНИЕ -------------------------------------------------------------- Эта функция аналог ична функции Unix pipe при ее вызове возвращаются два дескрип- тора, причем каждый дескриптор соединен с дру| им Действительно Беркли-реализа- ции реализуют функцию pipe, выполняя те же внутренние операции, что и функция socketpair [95, с 253-254| Два созданных сокета не имеют имен Это значит, что не было неявного вызо- ва функции bind Результат выполнения функции socketpair с аргументом type, равным SOCK_ STREAM, называется потоковым каналом (sit earn pipe) Потоковый канал является аналогом обычною канала Unix (который создается функцией pipe), но он дву- сторонний, что позволяет использовать оба дескриптора и для чтения, и для за- писи Потоковый канал, созданный функцией socketpai г, изображен на рис 14 1 ПРИМЕЧАНИЕ -------------------------------------------------------------- Posix 1 пс требует поддержки двусторонних каналов В SVR4 функция pipe возвраща- ет два двусторонних дескриптора, в то время как ядра, происходящие от Беркли, тра- диционно возвращают односторонние дескрипторы (см рис 17 31 [95]) 14.4. Функции сокетов Функции сокетов применяются к доменным сокетам Unix с учетом некоторых особенностей и ограничении Ниже мы перечисляем требования Posix, указывая, где они применимы Отметим, что на сегодняшний день не все реализации соот- ветствуют этим требованиям 1 Права доступа к файлу по умолчанию для полного имени, созданного функ- цией bind, задаются значением 0777 (чтение, запись и выполнение данного файла разрешены владельцу файла, группе пользователей, в которую он вхо- дит, и всем остальным пользователям) и могут быть изменены в соответствии с текущим значением umask 2 Имя, связанное с доменным сокетом Unix, должно быть абсолютным, а не от- носительным именем Причина, по которой нужно избегать относительного имени, в том, что в таком случае разрешение имени зависит от текущего рабо- чего каталога вызывающего процесса, то есть если сервер связывается с отно- сительным именем, клиент должен находиться в том же каталоге, что и сервер (или должен знать этот каталог), для того чтобы вызов клиентом функции connect или sendto был успешным ПРИМЕЧАНИЕ ----------------------------------------------------- В Posix 1g сказано что связывание относительного имени с доменным сокетом Unix приводит к непредсказуемым результаты
422 Глава 14. Доменные протоколы Unix 3. Полное имя, заданное в вызове функции connect, должно быть именем, в на- стоящий момент связанным с открытым доменным сокетом Unix того же типа (потоковым или дейтаграммным). Ошибка происходит в следующих случаях: если имя существует, но не является сокетом; если имя существует и является сокетом, но ни один открытый дескриптор с ним не связан; если имя суще- ствует и является открытым сокетом, но имеет неверный тип (то есть потоко- вый доменный сокет Unix не может соединиться с именем, связанным с дей- таграммным доменным сокетом Unix, и наоборот). 4. С функцией connect доменного сокета Unix связана такая же проверка прав доступа, какая имеет место при вызове функции open для доступа к файлу толь- ко на запись. 5. Потоковые доменные сокеты Unix аналогичны сокетам TCP: они предостав- ляют интерфейс байтового потока без границ записей. 6. Если при вызове функции connect для потокового доменного сокета Unix об- наруживается, что буфер прослушиваемого сокета заполнен (см. раздел 4.5), немедленно возвращается ошибка ECONNREFUSED. В этом отличие от сокета TCP: прослушиваемый сокет TCP игнорирует приходящий сегмент SYN, если оче- редь сокета заполнена, и выполняется несколько попыток отправки сегмента SYN. 7. Дейтаграммные доменные сокеты Unix аналогичны сокетам UDP: они предо- ставляют ненадежный сервис дейтаграмм, сохраняющий границы записей. 8. В отличие от сокетов UDP, при отправке дейтаграммы на неприсоединенный дейтаграммный доменный сокет Unix с сокетом не связывается полное имя. (Вспомните, что отправка дейтаграммы UDP на неприсоединенный сокет UDP заставляет динамически назначаемый порт связываться с сокетом.) Это озна- чает, что получатель дейтаграммы не будет иметь возможности отправить от- вет, если отправитель не связал со своим сокетом полное имя. Аналогично, в отличие от TCP и UDP, при вызове функции connect для дейтаграммного доменного сокета Unix с сокетом не связывается полное имя. 14.5. Клиент и сервер потокового доменного протокола Unix Теперь мы перепишем наш эхо-клиент и эхо-сервер TCP из главы 5 с использо- ванием доменных сокетов Unix. В листинге 14.3 показан сервер, который являет- ся модификацией сервера из листинга 5.9 и использует потоковый доменный протокол Unix вместо протокола TCP. Листинг 14.3. Эхо-сервер потокового доменного протокола Unix //umxdomain/unixstrservOl с 1 include "unp h' 2 int 3 main(int argc. char **argv) 4 { 5 int listenfd connfd. 6 pid_t chi 1 dpid. 7 socklen t clilen.
14,5. Клиент и сервер потокового доменного протокола Unix 423 8 struct sockaddr_un cliaddr servaddr 9 void sig_chld(int) 10 listenfd - Socket(AF_LOCAL. SOCK_STREAM 0). 11 uniink(UNIXSTR_PATH) 12 bzero(&servaddr. sizeof(servaddr)), 13 servaddr sun_fannly - AF_LOCAL 14 strcpytservaddr sun_path UNIXSTR_PATH). 15 Bind(listenfd, (SA *) Sservaddr, sizeof(servaddr)); 16 Listen(listenfd LISTENQ). 17 Signal(SIGCHLD. sig_chld). 18 for ( .) { 19 clilen = sizeof(cliaddr). 20 if ( (connfd - accept(listenfd (SA *) &cliaddr Sclilen)) < 0) { 21 if (errno -- EINTR) 22 continue /* назад в ford */ 23 else 24 err sysC’accept error") 25 } 26 if ( (childpid = ForkO) == 0) { /* дочерний процесс */ 27 Closed istenfd). /* закрывается прослушиваемый сокет */ 28 str_echo(connfd). /* обработка запроса */ 29 exit(0) 30 ) 31 Close(connfd), /* родитель закрывает присоединенный сокету*/. 32 ) 33 I 8 Теперь две структуры адреса сокета относятся к типу sockaddr_un. 10 Для создания потокового доменного сокета Unix первый аргумент функции socket должен иметь значение AF_LOCAL. 11-15 Константа UNIXSTR_PATH определяется в файле unp h как /tmp/umx/str. Сначала мы вызываем функцию unlink, чтобы удалить полное имя в случае, если оно со- хранилось после предыдущего запуска сервера, а затем инициализируем струк- туру адреса сокета перед вызовом функции bi nd. Ошибка при выполнении функ- ции unlink — это нормальное явление. Обратите внимание, что этот вызов функции bind отличается от вызова, пока- занного в листинге 14.2. Здесь мы задаем размер структуры адреса сокета (тре- тий аргумент) как общий размер структуры sockaddr_un, а не просто число байтов, занимающее полное имя. Оба значения длины приемлемы, поскольку полное имя должно оканчиваться нулем. Оставшаяся часть функции такая же, как и в листинге 5.9. Используется та же функция str_echo (см. листинг 5.2). В листинге 14.4 представлен эхо-клиент потокового доменного протокола Unix. Это модификация листинга 5.3. Листинг 14.4. Эхо-клиент потокового доменного протокола Unix unixdomain/unixstrcliOl с 1 include "unp.h" 2 int продолжение
424 Глава 14. Доменные протоколы Unix Листинг 14.4 (продолжение) 3 nain(int argc char **argv) 4 { 5 int sockfd 6 struct sockaddr_un servaddr 7 sockfd - Socket(AF_LOCAL SOCK_STREAM 0) 8 bzero(&servaddr sizeof(servaddr)) 9 servaddr sun_famly - AF_L0CAL 10 strcpy(servaddr sun_path UNIXSTR_PATH) 11 Connect(sockfd (SA *) Sservaddr sizeof(servaddr)) 12 str_cli(stdin sockfd) /* выполняет всю работу */ 13 exit(0) 14 } 6 Теперь структурой адреса сокета, которая должна содержать адрес сервера, бу- дет структура sockaddr_un. 7 Первый аргумент функции socket — AF_LOCAL. МО Код для заполнения структуры адреса сокета идентичен коду, показанному для сервера: инициализация структуры нулем, установка семейства протоколов AF_ LOCAL и копирование полного имени в элемент sun_path. 12 Функция str_cl т — та же, что и раньше (в листинге 6.2 представлена последняя разработанная нами версия). 14.6. Клиент и сервер дейтаграммного доменного протокола Unix Теперь мы перепишем наши клиент и сервер UDP из разделов 8 3 и 8 5 с исполь- зованием сокетов. В листинге 14.5 показан сервер, который является модифика- цией листинга 8.1. Листинг 14.5. Эхо-сервер дейтаграммного доменного протокола Unix umxdomain/umxdgservOl с 1 #include 'unp h” 2 int 3 main(int argc char **argv) 4 { 5 int sockfd 6 struct sockaddr_un servaddr cliaddr 7 sockfd = Socket(AF^LOCAL SOCK_DGRAM 0). 8 unlink(UhlIXDG_PATH) 9 bzero(&servaddr sizeof(servaddr)) 10 servaddr sun_famly - AFJ.OCAL 11 strcpytservaddr sun_path UNIXDG_PATH) 12 Bind(sockfd (SA *) Sservaddr sizeof(servaddr)) 13 dg_echo(sockfd (SA *) Scliaddr sizeof(cliaddr)). 14 }
14 6 Клиент и сервер дейтаграммного доменного протокола Unix 425 6 Две структуры адреса сокета относятся теперь к типу sockaddr_un. 7 Для создания дейтаграммного доменного сокета Unix первый аргумент функ- ции socket должен иметь значение AF LOCAL 8-12 Константа UNIXDG_PATH определяется в заголовочном файле unp Икак/йпр/итх dg. Сначала мы вызываем функцию unlink, чтобы удалить полное имя в случае, если оно сохранилось после предыдущего запуска сервера, а затем инициализируем структуру адреса сокета перед вызовом функции bi nd Ошибка при выполнении (Пункции unlink — это нормальное явление. 15 Используется та же функция dg_echo (см листинг 8 2) В листинге 14 6 представлен эхо-клиент дейтаграммною доменного протоко- ла Unix Эго модификация листинга 8 3 листинг 14.6. Эхо-клиент дейтаграммного доменного протокола Unix umxdomain/umxdgcliOl с 1 include unp h 2 int 3 main(int argc char **argv) 4 { 5 int sockfd 6 struct sockaddr_un cliaddr servaddr 7 sockfd = Socket(AF-LOCAL SOCK_DGRAM 0) 8 bzerot&cliaddr sizeof(cliaddr)) /* связывание сокета с адресом */ 9 cliaddr sun_famly » AF_LOCAL 10 strcpy(cliaddr sun_path tmpram(NULL)) li Bindtsockfd (SA *) Scliaddr sizeof(cliaddr)) 1? bzero(&servaddr sizeof(servaddr)) /* заполняем структуру адреса сокета сервера */ 13 servaddr sun_family - AF_LOCAL 14 strcpy(servaddr sun_path UNIXDG_PATH) 15 dg_cli(stdin sockfd. (SA *) &servaddr sizeof(servaddr)) 6 exit(0) 1/ ) 6 Структурой адреса сокета, содержащей адрес сервера, теперь будет структура sockaddr_un Мы также размещаем в памяти одну из этих структур, чтобы она со- держала адрес клиента, о чем мы расскажем далее. 7 Первый аргумент функции socket — это AF_LOCAL. 8-11 В отличие от клиента UDP, при использовании дейтаграммного доменного про- юкола Unix требуется явно связать с помощью функции bind полное имя с на- шим сокетом, чтобы сервер имел полное имя, на которое он мог бы отправить (вой ответ Мы вызываем функцию tmpnam, чтобы получить уникальное полное имя, с которым мы затем при помощи функции bi nd свяжем наш сокет Вспомни- те из раздела 14 4, что при отправке дейтаграммы на неприсоединениый дейта- граммный доменный сокет Unix не происходит неявного связывания полного име- ни с сокетом. Следовательно, если мы опустим этот шаг, вызов сервером функции recvfrom в функции dg_echo возвращает пустое полное имя, что затем приведет к ошибке, когда сервер вызовет функцию sendto
426 Глава 14, Доменные протоколы Unix 12-14 Код для заполнения структуры адреса сокета заранее известным полным име- нем идентичен коду, представленному ранее для сервера. 15 Функция dg_cl 1 остается той же, что и раньше (см. листинг 8.4). 14.7. Передача дескрипторов Когда нам требуется передать дескриптор от одного процесса другому, обычно мы выбираем одно из двух решений: 1. Дочерний процесс использует все открытые дескрипторы совместно с роди- тельским процессом после вызова функции fork. 2. Все дескрипторы обычно остаются открытыми при вызове функции ехес. В первом случае процесс открывает дескриптор, вызывает функцию fork, а за- тем родительский процесс закрывает дескриптор, позволяя дочернему процессу с ним работать. При этом открытый дескриптор передается от родительского про- цесса дочернему. Но нам также хотелось бы, чтобы у дочернего процесса была возможность открывать дескриптор и передавать его обратно родительскому про- цессу. Современные системы Unix предоставляют способ передавать любой откры- тый дескриптор от одного процесса любому другому процессу. При этом вовсе не обязательно, чтобы процессы были родственными, как родительский и дочерний процессы. Эта технология требует, чтобы мы сначала создали между двумя про- цессами доменный сокет Unix и затем использовали функцию sendmsg для отправки специального сообщения через этот доменный сокет. Ядро обрабатывает это со- общение специальным образом, передавая открытый дескриптор от отправителя получателю. ПРИМЕЧАНИЕ--------------------------------------------------------- Передача ядром 4.4BSD открытого дескриптора по доменному сокету Unix описыва- ется в главе 18 [95]. SVR4 использует другую технологию внутри ядра для передачи открытого дескрипто- ра: команды ISENDFD и I_RECVFD функции ioctl, описанные в разделе 15.5.1 [93]. Но процесс все же имеет возможность доступа к указанному свойству ядра за счет до- менного сокета Unix. В этой книге мы описываем применение доменных сокетов Unix для передачи открытых дескрипторов, поскольку это наиболее переносимая техноло- гия программирования: она работает как с Беркли-ядрами, так и с S VR4, в то время как команды I_SENDFD и I RECVFD функции ioctl работают только в SVR4. Технология 4.4BSD позволяет передавать множество дескрипторов с помощью оди- ночной функции sendmsg, в то время как технология S VR4 передает за один раз юлько один дескриптор. Во всех наших примерах один дескриптор передается за один раз. Шаги при передаче дескриптора между процессами будут такими: 1. Создание доменного сокета Unix или потокового сокета, или дейтаграммного сокета. Если целью является породить с помощью функции fork дочерний процесс, с тем чтобы дочерний процесс открыл дескриптор и передал его обратно роди- тельскому процессу, родительский процесс может вызвать функцию socketpai г для создания потокового канала, который может использоваться для переда- чи дескриптора. в ,
14,7, Передача дескрипторов 427 Если процессы не являются родственными, сервер должен создать потоковый доменный сокет Unix, связать его при помощи функции bind с полным име- нем, тем самым позволяя клиенту соединиться с этим сокетом при помощи функции connect. Затем клиент может отправить запрос серверу для открытия некоторого дескриптора, а сервер может передать дескриптор обратно через доменный сокет Unix. Как альтернатива между клиентом и сервером может также использоваться дейтаграммный доменный сокет Unix, однако преиму- щества этого способа невелики, к тому же существует возможность игнориро- вания дейтаграммы. Далее в примерах этой главы мы будем использовать по- токовый сокет между клиентом и сервером. 2. Один процесс открывает дескриптор при помощи вызова любой из функций Unix, возвращающей дескриптор, например open, pi ре, mkfi fo, socket или accept. От одного процесса к другому можно передать дескриптор любого типа, поэто- му мы называем эту технологию «передачей дескриптора», а не «передачей дескриптора файла». 3. Отправляющий процесс строит структуру msghdr (см. раздел 13.5), содержа- щую дескриптор, который нужно передать. В Posix. 1g определено, что дескрип- тор должен отправляться как вспомогательные данные (элемент msg_control структуры msghdr, см. раздел 13.6), ио более старые реализации используют элемент msg_accri ghts. Отправляющий процесс вызывает функцию sendmsg для отправки дескриптора через доменный сокет Unix, созданный на шаге 1. На этом этапе мы говорим, что дескриптор находится «в полете». Даже если от- правляющий процесс закроет дескриптор после вызова функции sendmsg, но до вызова принимающим процессом функции recvmsg, дескриптор остается открытым для принимающего процесса. Отправка дескриптора увеличивает счетчик ссылок дескриптора на единицу. 4. Принимающий процесс вызывает функцию recvmsg для получения дескрип- тора через доменный сокет Unix, созданный на шаге 1. Номер дескриптора в принимающем процессе может отличаться от номера дескриптора в отправ- ляющем процессе. Передача дескриптора — это не передача номера дескрип- тора. Этот процесс включает создание нового дескриптора в принимающем процессе, который ссылается на ту же запись таблицы файлов в ядре, что и де- скриптор, отправленный отправляющим процессом. Клиент и сервер должны располагать некоторым протоколом уровня прило- жения, с тем чтобы получатель дескриптора имел информацию о времени его появления. Если получатель вызывает функцию recvmsg, не выделив места в па- мяти для получения дескриптора, и дескриптор передается как готовый для чте- ния, то передаваемый дескриптор закрывается [105, с. 518]. Кроме того, нужно избегать установки флага MSG_PEEK в функции recvmsg, если предполагается полу- чение дескриптора, поскольку в этом случае результат непредсказуем. Пример передачи дескриптора Теперь мы представим пример передачи дескриптора. Мы напишем программу под названием mycat, которой в качестве аргумента командной строки передается полное имя файла. Эта программа открывает файл и копирует его в стандартный поток вывода. Но вместо вызова обычной функции Unix open мы вызываем нашу
428 Глава 14. Доменные протоколы Unix собственную функцию my_open. Эта функция создает потоковый канал и вызыва- ет функции fork и ехес для запуска другой программы, открывающей нужный файл. Эта программа должна затем передать дескриптор обратно родительскому процессу по потоковому каналу. На рис. 14.1 показан первый шаг: наша программа mycat после создания пото- кового канала при помощи вызова функции socketpair. Мы обозначили два де- скриптора, возвращаемых функцией socketpair, как [0] и [1]. mycat [0] [1] Рис. 14.1. Программа mycat после создания потокового канала при использовании функции socketpair Затем процесс взывает функцию fork, и дочерний процесс вызывает функцию ехес для выполнения программы openf i1 е. Родительский процесс закрывает де- скриптор [1], а дочерний процесс закрывает дескриптор [0]. (Нет разницы, на каком конце потокового канала происходит закрытие. Дочерний процесс мог бы закрыть [1], а родительский — [0].) При этом получается схема, показанная на рис. 14.2. exit (состояние выхода) mycat openfile Рис. 14.2. Программа mycat после запуска программы openfile Родительский процесс должен передать программе openfi1 е три фрагмента информации: полное имя открываемого файла, режим открытия (только чтение, чтение и запись или только запись) и номер дескриптора, соответствующий его концу потокового канала (который мы обозначили [1].) Мы выбрали такой спо- соб передачи этих трех элементов, как ввод аргументов командной строки при вызове функции ехес. Альтернативным способом будет отправка этих элементов в качестве данных по потоковому каналу. Программа отправляет обратно откры- тый дескриптор по потоковому каналу и завершается. Статус выхода программы сообщает родительскому процессу, смогли файл открыться, и если нет, то какого типа ошибка произошла. Преимущество выполнения дополнительной программы для открытия файла заключается в том, что за счет приравнивания привилегий пользователя к при-
14.7, Передача дескрипторов 429 вилегиям владельца файла мы получаем возможность открывать те файлы, кото- рые мы не имеем права открывать в обычной ситуации. Эта программа позволяет расширить концепцию обычных прав доступа Unix (пользователь, группа и все остальные) и включить любые формы проверки прав доступа. Мы начнем с нашей программы mycat, показанной в листинге 14.7. Листинг 14.7. Программа mycat: копирование файла в стандартный поток вывода //umxdomain/mycat с 1 #include "unp h” 2 int my_open(const char *. int): 3 int 4 main(int argc. char **argv) 5 { 6 int fd. n, 7 char buff[BUFFSIZE]: 8 if (argc !- 2) 9 err_quit("usage mycat <pathname>"); 10 if ( (fd - my_open(argv[l]. O_RDONLY)) < 0) 11 err_sys("cannot open is". argv[l]). 12 while ( (n = Read(fd. buff. BUFFSIZE)) > 0) 13 Write(STDOUT_FILENO, buff. n). 14 exit(0). 15 } Если мы заменим вызов функции my_open вызовом функции open, эта простая программа всего лишь скопирует файл в стандартный поток вывода. Функция my_open, показанная в листинге 14.8, должна выглядеть для вызыва- ющего процесса как обычная функция Unix open. Она получает два аргумента — полное имя и режим открытия (например, O_RDONLY обозначает, что файл досту- пен только для чтения), открывает файл и возвращает дескриптор. Листинг 14.8. Функция my_open: открытие файла и возвращение дескриптора //umxdomain/rnyopen с 1 #include "unp h" 2 int 3 my_open(const char *pathname, int mode) 4 I 5 int fd. sockfd[2], status. 6 pid_t childpid, 7 char c, argsockfd[10]. argmode[10], 8 Socketpair(AF_LOCAL. SOCK^STREAM. 0. sockfd). 9 if ( (childpid = ForkO) -= 0) { /* дочерний процесс */ 10 Close(sockfd[0]) 11 snprintf(argsockfd. sizeof(argsockfd). “!kf". sockfd[l]). 12 snprintf(argmode, sizeof(argmode) "fcd" mode). 13 execl(" /openfile", "openfile". argsockfd. pathname argmode. 14 (char *) NULL). продолжение тУ
430 Глава 14. Доменные протоколы Unix Листинг 14.8. (продолжение) 15 err_sys( 'execl error"). 16 } 17 /* родительский процесс- ожидание завершения дочернего процесса */ 18 Close(sockfd[l]) /* закрываем конец, который мы не используем */ 19 Waitpid(chiIdpid. Sstatus. 0). 20 if (WIFEXITED(status) == 0) 21 err_quit("child did not terminate"). 22 if ( (status = WEXITSTATUS(status)) - 0) 23 Read_fd(sockfd[0], &c. 1 &fd). 24 else { 25 errno = status. /* установка значения еггпо в статус дочернего процесса */ 26 fd = -1. 27 } Создание потокового канала 8 Функция socketpai г создает потоковый канал. Возвращаются два дескриптора: sockfdEO] и sockfdEl]. Это состояние, которое мы показали на рис. 14.1. Функции fork и ехес )-16 Вызывается функция fork, после чего дочерний процесс закрывает один конец потокового канала. Номер дескриптора другого конца потокового канала поме- щается в массив argsockfd, а режим открытия помещается в массив argmode. Мы вызываем функцию snprintf, поскольку аргументы функции ехес должны быть символьными строками. Выполняется программа openfile. Функция execl воз- вращает управление только в том случае, если она встретит ошибку. При удач- ном выполнении начинает выполняться функция main программы openfile. Родительский процесс в ожидании завершения дочернего процесса 17-22 Родительский процесс закрывает другой конец потокового канала и вызывает функцию waitpid для ожидания завершения дочернего процесса. Статус завер- шения дочернего процесса возвращается в переменной status, и сначала мы про- веряем, что программа завершилась нормально (то есть не была завершена из-за возникновения какого-либо сигнала). Затем макрос WEXITSTATUS преобразует ста- тус завершения в статус выхода, значение которого должно находиться между О и 255. Мы вскоре увидим, что если при открытии необходимого файла програм- мой openfi 1 е происходит ошибка, то эта программа завершается, причем статус ее завершения равен соответствующему значению переменной еггпо. Получение дескриптора 23 Наша функция read_fd, которую мы показываем в следующем листинге, полу- чает дескриптор потокового канала. Кроме получения дескриптора мы считыва- ем 1 байт данных, но ничего с этими данными не делаем. ПРИМЕЧАНИЕ ---------------------------------------------------- При отправке и получении дескриптора по потоковому каналу мы всегда отправляем как минимум 1 байт данных, даже если получатель никак эти данные не обрабатывает. Иначе получатель не сможет распознать, что значит пулевое возвращаемое значение из функции read fd: отсутствие данных (по, возможно, есть дескриптор) или конец файла.
14.7, Передача дескрипторов 431 В листинге 14.9 показана функция read_fd, вызывающая функцию recvmsg для получения данных и дескриптора через доменный сокет Unix. Первые три аргу- мента этой функции те же, что и для функции read, а четвертый (recvfd) является указателем на целое число. После выполнения этой функции recvfd будет указы- вать на полученный дескриптор. Листинг 14.9. Функция read_fd: получение данных и дескриптора //lib/read_fd с 1 include "unp h" 2 ssize_t 3 read_fd(int fd. void *ptr, size_t nbytes. int *recvfd) 4 { 5 struct msghdr msg, 6 struct lovec iov[l], 7 ssize_t n 8 int newfd. 9 #ifdef HAVE_MSGHDR_MSG_CONTROL 10 union { 11 struct cmsghdr cm. 12 char control[CMSG_SPACE(sizeof(int))]: 13 } control_un: 14 struct cmsghdr *cmptr. 15 msg msg_control = control_un control. 16 msg msg_controllen = sizeof(control_un control): 17 #else ' 18 msg msg_accrights = (caddr_t) & newfd. 19 msg msg_accrightslen = sizeof(int). 20 #endif 21 msg msg_name = NULL. 22 msg msg_namelen = 0, 23 iov[0] iov_base = ptr. 24 iov[0J iov_len = nbytes: 25 msg msgjiov = iov. 26 msg msg_iovlen = 1. 27 if ( (n = recvmsg(fd. &msg, 0)) <- 0) 28 return (n). 29 #ifdef HAVE_MSGHDR_MSG_CDNTRDL 30 if ( (cmptr = CMSG_FIRSTHDR(tosg)) != NULL && 31 mptr->cmsg_len == CMSG_LEN(sizeof(int))) { 32 if (cmptr->cmsg_level SOL_SOCKET) 33 err_quit("control level '= SOL_SOCKET"). 34 if (cmptr->cmsg_type SCM_RIGHTS) 35 err_quit("control type l= SCM_RIGHTS"), 36 *recvfd = *((int *) CMSG_DATA(cmptr)) 37 } else 38 *recvfd = -1. /* дескриптор не был передан */ 39 #else 40 if (msg msg_accrightslen == sizeof(int)) 41 *recvfd = newfd. 42 el se продолжение
432 Глава 14. Доменные протоколы Unix Листинг 14.9(продолжение) 43 *recvfd = -1. /* дескриптор не был передан */ 44 #endif 45 return (n). 46 } 9-26 Эта функция должна работать с обеими версиями функции recvmsg: с элемен- том msg_control и с элементом msg_accrights. Наш заголовочный файл config.h (см. листинг Г.2) определяет константу HAVE_MSGHDR_MSG_CONTROL, если поддержи- вается версия функции recvmsg с msg_control. Проверка выравнивания буфера msg_control 10-13 Буфер msg_control должен быть выровнен в соответствии со структурой msghdr. Просто выделить в памяти массив типа char недостаточно. Здесь мы объявляем объединение, состоящее из структуры cmsghdr и символьного массива, что гаран- тирует необходимое выравнивание массива. Возможно и другое решение — вы- звать функцию malloc, но это потребует освобождения памяти перед завершени- ем функции. 27-45 Вызывается функция recvmsg. Если возвращаются вспомогательные данные, их формат будет таким, как показано на рпс. 13.4. Мы проверяем, что длина, уро- вень и тип верны, затем получаем вновь созданный дескриптор и возвращаем его через указатель вызывающего процесса recvfd. Макрос CMSG_DATA возвращает ука- затель на элемент cmsg_data объекта вспомогательных данных как указатель на элемент типа unsigned char. Мы преобразуем его к указателю на элемент типа int и получаем целочисленный дескриптор, на который указывает этот указатель. Если поддерживается более старый элемент msg_accrights, то длина должна быть равна размеру целого числа, а вновь созданный дескриптор возвращается через указатель recvfd вызывающего процесса. В листинге 14.10 показана программа openfile. Она получает три аргумента командной строки, которые должны быть переданы, и вызывает обычную функ- цию open. Листинг 14.10. Функция openfile: открытие файла и передача дескриптора обратно //umxdomain/openfile с 1 #mclude 'unp h" 2 int 3 main(int argc. char **argv) 4 { 5 int fd 6 ssize_t n 7 if (argc '= 4) 8 err_quit('openfile <sockfd#> <f11ename> <mode>”k 9 if ( (fd = open(argv[2], atoi(argv[3]))) < 0) 10 exit((errno > 0) ? errno 255) 11 if ( (n = write_fd(atoi(argv[l]) 1. fd)) < 0) 12 exit((errno > 0) 7 errno 255) 13 exit(0). 14 )
14.7, Передача дескрипторов 433 Аргументы командной строки 7-12 Поскольку два из трех аргументов командной строки были превращены в сим- вольные строки функцией my_open, они преобразуются обратно в целые числа при помощи функции atoi. Открытие файла 9-10 Файл открывается с помощью функции open. Если встречается ошибка, статус завершения этого процесса содержит значение переменной еггпо, соответствую- щее ошибке функции open. Передача дескриптора обратно 11-12 Дескриптор передается обратно функцией wri te_fd, которую мы покажем в сле- дующем листинге. Затем этот процесс завершается, но ранее в этой главе мы ска- зали, что отправляющий процесс может закрыть переданный дескриптор (это происходит, когда мы вызываем функцию exit), поскольку ядро знает, что де- скриптор находится в состоянии передачи («в полете»), и оставляет его откры- тым для принимающего процесса. ПРИМЕЧАНИЕ---------------------------------------------------------------------- Статус выхода должен лежать в пределах от 0 до 255. Максимальное значение пере- менной еггпо — около 150. Альтернативный способ, при котором не требуется, чтобы значение переменной еггпо было меньше 256, заключается в том, чтобы передать об- ратно указание на ошибку в виде обычных данных при вызове функции sendmsg. В листинге 14.11 показана последняя функция, write_fd, вызывающая функ- цию sendmsg для отправки дескриптора (и, возможно, еще каких-либо данных, которые мы не используем) через доменный сокет Unix. Листинг 14.11. функция write_fd: передача дескриптора при помощи вызова функции sendmsg //1 ib/write_fd с 1 #include "unp h" 2 ssize_t 3 wnte_fd(int fd void *ptr. size_t nbytes. int sendfd) 4 { 5 struct msghdr msg. 6 struct iovec iov[l], 7 #ifdef HAVE_MSGHDR_MSG_CONTROL 8 union { 9 struct cmsghdr cm. 10 char control [CMSG__SPACE(sizeof(1nt))]: 11 } control jjn, 12 struct cmsghdr *cmptr. 13 msg msg_control = controljjn control 14 msg msg_controllen = sizeof(control_un control). 15 cmptr = CMSG_FIRSTHDR(&msg) 16 cmptr->cmsg_len = CMSG__LEN(sizeof(int)). 17 cmptr->cmsg_level = SOL_SOCKET.
434 Глава 14. Доменные протоколы Unix Листинг 14.11 (продолжение) 19 *(fint *) CMSG_DATA(cmptr)) = sendfd. 20 #else 21 msg msg_accrights - (caddr_t) & sendfd. 22 msg msg_accmghtslen = sizeof(int), 23 #endif 24 msg msg_name = NULL. 25 msg msg_namelen - 0. 26 wv[0] wv_base = ptr; 27 wv[0] lOV-len = nbytes; 28 msg msgjiov = iov, 29 msg msg_wvlen = 1. 30 return (sendmsg(fd. &msg, 0)). 31 } Как и в случае функции read_fg, эта функция обрабатывает либо вспомога- тельные данные, либо права доступа, которые предшествовали вспомогательным данным в более ранних реализациях. В любом случае инициализируется струк- тура msghdr и затем вызывается функция sendmsg. В разделе 25.7 мы приводим пример передачи дескриптора, в котором участву- ют неродственные (unrelated) процессы, а в разделе 27.9 — пример, где задейство- ваны родственные процессы. В них мы будем использовать функции read_fd и write_fd, которые только что описали. 14.8. Получение информации об отправителе На рис.13.4 мы показали другой тип информации, передаваемой через доменный сокет Unix в виде вспомогательных данных: информацию об отправителе, кото- рая передается с помощью структуры fcred, определяемой путем включения за- головочного файла <sys/ucred. h>. struct fcred { uid_t fc_ruid. /* действующий идентификатор пользователя */ gid_t fc_rgid, /* действующий групповой идентификатор */ char fc_login[MAXLOGNAME], /* имя setloginO*/ uid_t fc_uid. /* идентификатор пользователя */ short fc_ngroups. /* количество групп */ gid_t fc_groups[NGROUPS], /* дополнительные групповые идентификаторы */ }. #define fc_gid fc_groups[0] /* групповой идентификатор */ Обычно MAXLONGNAME и NGROUPS имеют значение 16. Значение fc_ngroups равно как минимум 1, а первым элементом массива является идентификатор группы. ПРИМЕЧАНИЕ------------------------------------------------------------------- Эту структуру мы фактически определяем как структуру ucred внутри структуры fcred с именами, определенными при помощи директивы #define, с тем чтобы получить вид единой структуры. Мы показываем «логическое» определение этой структуры. Такая возможность появилась только в BSD/OS 2.1. Мы описываем ее, несмотря па то что она не является широко распространенной, поскольку это важное, хотя и простое дополнение доменных протоколов Unix. Когда клиент и сервер связываются с помощью этих протоколов, серверу часто бывает необходим способ точ но узнать, кто является кли- ентом, чтобы убедиться, что клиент имеет право запрашивать определенный сервис.
14.8. Получение информации об отправителе 435 Указанная информация всегда доступна через доменный сокет Unix при сле- дующих условиях: Информация о пользователе отправляется в качестве вспомогательных дан- ных при пересылке через доменный сокет Unix, только если получатель вклю- чил параметр сокета LOCAL_CREDS. Значение 1 evel для этого параметра (см. раз- дел 7.2) равно нулю. ' На сокете дейтаграмм идентифицирующие данные сопровождают каждую дейтаграмму. На потоковом сокете они отправляются однократно — при пер- вой отправке данных. 1 Идентифицирующие данные не могут быть отправлены вместе с дескрипто- ром, то есть с конкретным сообщением можно отправить только один из двух типов вспомогательных данных. ПРИМЕЧАНИЕ------------------------------------------------------- Это ограничение, присущее реализации BSD/OS. В общем случае у нас должна быть возможность передавать множество типов вспомогательных данных в одном вызове функции sendmsg, поскольку каждый объект вспомогательных данных имеет свой соб- ственный тип и длину (см. рис. 13.3). ' У пользователя нет возможности подделать идентифицирующие данные. Когда вспомогательные данные отправляются через доменный сокет Unix, ядро про- веряет, что вспомогательные данные не относятся к уровню SOL_SOCKET или SCM_CREDS. Если отправитель попытается послать ложные идентифицирующие данные, ядро их проигнорирует. Один момент, о котором мы не упомянули в предыдущем разделе, заключает- ся в том, что когда дескриптор получен в системе SVR4 (при помощи функции ioctl с командой I_RECVFD), ядро также передает принимающему процессу иден- тифицирующие данные отправителя: структуру strrecvfd, содержащую вновь созданный дескриптор, фактический идентификатор пользователя и фактичес- кий идентификатор группы. Эта форма передачи идентифицирующих данных имеет место при каждой передаче дескрипторов. Кроме того, когда в SVR4 созда- ются соединения клиент-сервер с использованием модуля потоков connl d (анало- гично созданию нового сокета с помощью функции accept для доменного сокета Unix), новый дескриптор передается вместе со структурой strrecvfd, содержащей идентификацию клиента. В SVR4 у нас нет возможности доступа к этой иденти- фицирующей информации при помощи доменных сокетов Unix. (В разделе 15.5.1 [93] приводится подробное описание использования модуля потоков connl d.) Если Беркли-реализация не поддерживает новый способ передачи идентифицирую- щих данных, изложенный нами в этом разделе, то значит, для доменного сервера Unix не существует гарантированного способа получить информацию о клиенте. В разделе 15.5.2 [93] приводится подробное описание обходного пути, позволяю- щего получить эту информацию, однако идентифицирующие данные пользова- теля всегда должны предоставляться ядром. Пример В качестве примера передачи идентифицирующих данных мы изменим наш по- токовый доменный сервер Unix так, чтобы он запрашивал идентифицирующие
436 Глава 14. Доменные протоколы Unix данные клиента. В листинге 14.12 показана новая функция, read_cred, аналогич- ная функции read, но возвращающая также структуру fcred, содержащую иден- тифицирующие данные отправителя. Листинг 14.12. Функция read_cred: чтение и возвращение идентифицирующих данных отправителя //umxdomain/readcred с 1 #include "unp h” 2 #include <sys/param h> 3 ^include <sys/ucred h> 4 ssize_t 5 read_cred(int fd. void *ptr. size t nbytes. struct fcred *fcredptr) 6 { 7 struct msghdr msg. 8 struct iovec iov[l], 9 ssize_t n. 10 union { 11 struct cmsghdr cm. • 12 char control[CMSG_SPACE($1ZeQf(struct fcred))]: 13 } control_un. 14 struct cmsghdr *cmptr: 15 msg msg_control = control_un.control: 16 msg msg_controllen - sizeof(control_un.control): 17 msg msg_name = NULL. 18 msg msg_namelen = 0. 19 iov[0] iov_base - ptr. 20 iov[0] iov_len = nbytes: 21 msg msg_iov - iov 22 msg msg_iovlen = 1 23 if ( (n = recvmsg(fd. &msg, 0)) < 0) 24 return (n). 25 if (fcredptr) { 26 if (msg msg_control1en > sizeofCstruct cmsghdr)) { 27 cmptr = CMSG_FIRSTHDR(&msg). 28 if (cmptr->cmsg_len '= CMSG_LEN(sizeof(struct fcred))) 29 err_quit("control length = И” cmptr->cmsg_len). 30 if (cmptr->cmsg_level ’= SOL-SOCKET) 31 err_quit("control level '= SOL_SOCKET") 32 if (cmptr->cmsg_type l= SCM_CREDS) 33 err_quit("control type ~= SCM_CREDS"). 34 memcpyffcredptr. CMSG_DATA(cmptr) sizeofCstruct fcred)) 35 ] else 36 bzero(fcredptr sizeof(struct fcred)) /* ничего не возвращается */ 37 } 38 return (n). 39 } 4-5 Первые три аргумента идентичны аргументам функции read, а четвертый аргу- мент — это указатель на структуру fcred, которая будет заполнена. Формат воз- вращаемых вспомогательных данных показан на рис. 13.4. !6-36 Если данные были переданы, проверяются длина, уровень и тип вспомогатель- ных данных, и результирующая структура копируется обратно вызывающему
14.8. Получение информации об отправителе 437 процессу. Если никаких идентифицирующих данных не было передано, мы об- нуляем структуру. Поскольку число групп (fc_ngroups) всегда равно 1 или боль- ше, нулевое значение указывает вызывающему процессу, что ядро не возвратило никаких идентифицирующих данных. Функция mai и для нашего эхо-сервера (см. листинг 14.3) остается неизменной. В листинге 14.13 показана новая версия функции str_echo, полученная путем модификации листинга 5.2. Эта функция вызывается дочерним процессом после того, как родительский процесс принял новое клиентское соединение и вызвал функцию fork. Листинг 14.13. Функция str_echo, запрашивающая идентифицирующие данные клиента //umxdomain/strecho с 1 ^include "unp h" 2 include <sys/param h> 3 include <sys/ucred h> 4 ssizet read_cred(int void *. size_t. struct fcred *). 5 void 6 str_echo(int sockfd) 1 { 8 ssize_t n 9 const int on = 1. 10 char line[MAXLINE], 11 struct fcred cred. 12 Setsockopt(sockfd. 0. LOCAL_CREDS. Son sizeof(on)), 13 if ( (n = read_cred(sockfd. NULL. 0. Acred)) < 0) 14 err_sys("read_cred error") 15 if (cred fc_ngroups == 0) 16 printf("(no credentials returned)\en") 17 else { 18 printfC'real user ID = fcdken". cred fc_ruid). 19 printfCreal group ID = kd\en" cred fc_rgid). 20 printfClogin name = V*s\en' MAXLOGNAME cred fclogin): 21 printf("effective user ID = fcd\en" cred fc_uid) 22 printf("effective group ID = W\en". cred fc_gid), 23 printfCXd supplementary groups ”. cred fc_ngroups - 1); 24 for (n = 1 n < cred fc_ngroups, n++) /* [0] is the egid */ 25 printf(" 5&d", cred fc_groups[n]). 26 printf("\en") 27 } 28 for (. ) { 29 if ( (n = Readline(sockfd line MAXLINE)) == 0) 30 return /* соединение закрывается удаленном концом */ 31 Writen(sockfd. line n) 32 } 33 } 12 Для присоединенного сокета включается параметр сокета LOCAL_CREDS. 13-14 Наша функция read_cred вызывается впервые. Мы задаем нулевую длиру^фс как нас интересуют только вспомогательные данные. ” • ’ 17-27 Если идентифицирующие данные возвращаются, они выводятся. 28-32 Оставшаяся часть цикла не меняется. Этот код считывает строки от клиента и затем отправляет их обратно клиенту.
438 Глава 14. Доменные протоколы Unix Наш клиент, представленный в листинге 14.4, остается неизменным. Если мы запустим сервер в одном окне, а клиент в другом, то для сервера по- сле однократного выполнения клиента получим представленный ниже вывод, bsdi % unixstrserv02 real user ID = 482 real group ID = 52 login name = rstevens effective user ID = 482 effective group ID = 52 7 supplementary groups- 20 0 1 2 3 5 7 Эта информация выводится только после того, как клиент отправляет серве- ру первую строку для ее отражения, поскольку, как мы ранее отмечали, иденти- фицирующие данные отправляются ядром по потоковому сокету при первой от- правке данных через сокет (а не после установления соединения). Это отличается от технологии SVR4, о которой мы упоминали ранее, где идентифицирующие данные отправляются вместе с вновь созданным дескриптором (что будет экви- валентно завершению функции accept для доменного сокета Unix). 14.9. Резюме Доменные сокеты Unix являются альтернативой IPC, когда клиент и сервер на- ходятся на одном узле. Преимущество использования доменных сокетов Unix перед некоторой формой IPC состоит в том, что используемый API практически идентичен клиент-серверному сетевому соединению. Преимущество использо- вания доменных сокетов Unix перед TCP, когда клиент и сервер находятся на одном узле, заключается в повышенной производительности доменных сокетов Unix относительно TCP во многих реализациях. Мы изменили наш эхо-сервер и эхо-клиент TCP и UDP для использования доменных протоколов Unix, и единственным главным отличием оказалась необ- ходимость при помощи функции bi nd связывать полное имя с клиентским сокетом UDP так, чтобы серверу UDP было куда отправлять ответы. Наш код в разде- лах 14.5 и 14.6 непосредственно взаимодействует со структурой адреса доменно- го сокета Unix, но предпочтительнее использовать функции tcp_XXX" и udp_XXX из главы И, поскольку наша реализация функции getaddrinfo поддерживает до- менные сокеты Unix. Передача дескрипторов между клиентами и серверами, находящимися на од- ном узле, — это мощная технология, которая используется при работе с домен- ными сокетами Unix. Мы показали пример передачи дескриптора от дочернего процесса обратно родительскому процессу в разделе 14.7. В разделе 25.7 мы по- кажем пример, в котором клиент и сервер не будут родственными, а в разде- ле 27.9 — другой пример, когда дескриптор передается от родительского процес- са дочернему. Упражнения 1. Что произойдет, если доменный сервер Unix вызовет функцию unlink после вызова функции bind?
Упражнения 439 2. Что произойдет, если доменный сервер Unix при завершении не отсоединит с помощью функции unlink свое известное полное имя, а клиент будет пытаться с помощью функции connect соединиться с сервером через некоторое время после того, как сервер завершит работу? 3. Скомпилируйте не зависящие от протокола клиент и сервер времени и даты (см. листинги 11.8 и 11.11) и запустите их, задав доменный сокет Unix (см. раз- дел 11.6). Что происходит? Как вы можете исправить эту ситуацию? 4. Измените листинг 11.3 так, чтобы после того, как будет выведен адрес прото- кола собеседника, вызывалась бы функция sleep(5), а также чтобы вывести число байтов, возвращаемых функцией read всякий раз, когда она возвращает положительное значение. Измените листинг 11.6 так, чтобы для каждого бай- та результата, отправляемого клиенту, вызывалась функция write. (Мы об- суждаем подобные изменения в решении упражнения 1.5.) Запустите клиент и сервер на одном узле, используя TCP. Сколько байтов считывает клиент с помощью функции read? Запустите клиент и сервер на одном узле, используя доменный сокет Unix. Изменилось ли что-нибудь? Теперь для сервера вместо функции write вызовите функцию send и задайте флаг MSG EOR (чтобы выполнить это упражнение, вам нужно использовать Бер- кли-реализацию). Запустите клиент и сервер на одном узле, используя домен- ный сокет Unix. Изменилось ли что-нибудь? 5. Напишите программу, определяющую значения, показанные в табл. 4.6. Один из подходов — создать потоковый канал и затем с помощью функции fork раз- ветвить родительский и дочерний процессы. Родительский процесс входит в цикл for, увеличивая на каждом шаге значение backlog от 0 до 14. Каждый раз при прохождении цикла родительский процесс сначала записывает значе- ние backlog в потоковый канал. Дочерний процесс читает это значение, созда- ет прослушиваемый сокет, связанный с адресом закольцовки, и присваивает backlog считанное значение. Затем дочерний процесс делает запись в потоко- вый канал просто для того, чтобы сообщить родительскому процессу о своей готовности. Затем родительский процесс пытается установить как можно боль- ше соединений, задав предварительно аргумент функции alarm равным 2 се- кундам, поскольку при достижении предельного значения backlog вызов функ- ции connect заблокируется, и отправляет еще раз сегмент SYN. Дочерний процесс никогда не вызывает функцию accept, что позволяет ядру установить в очередь все соединения с родительским процессом. Когда истекает время ожидания родительского процесса (аргумент функции alarm, в данном случае 2 секунды), по счетчику цикла он может определить, какая по счету функция connect соответствует предельному значению back 1 од. Затем родительский про- цесс закрывает свои сокеты и пишет следующее новое значение в потоковый канал для дочернего процесса. Когда дочерний процесс считывает новое зна- чение, он закрывает прежний прослушиваемый сокет п создает новый, заново начиная процедуру. 6. Проверьте, вызывает ли пропуск вызова функции bind в листинге 14.6 ошиб- ку сервера.
ГЛАВА 15 Неблокируемый ввод-вывод 15.1. Введение По умолчанию сокеты блокируют выполнение процесса. Это означает, что когда мы вызываем на сокете функцию, которая не может выполниться немедленно, наш процесс переходит в «спящее» состояние и ждет, когда будет выполнено определенное условие. Мы можем разделить функции сокетов, способные вы- звать блокирование, на четыре категории. 1. Операции ввода: функции read, readv, recv, recvfrom и recvmsg. Если мы вызыва- ем одну из этих функций ввода для блокируемого сокета TCP (а по умолча- нию такой сокет является блокируемым) и в приемном буфере сокета отсут- ствуют данные, то сокет вызывает переход в спящее состояние на то время, пока не придут какие-нибудь данные. Поскольку TCP является протоколом байтового потока, из этого состояния мы выйдем, когда придет «хоть сколько- нибудь» данных: это может быть одиночный байт данных, а может быть и це- лый сегмент данных TCP. Если мы хотим ждать до тех пор, пока не будет до- ступно определенное фиксированное количество данных, мы вызываем нашу функцию readn (см. листинг 3.8) или задаем флаг MSG_WAITALL (см. табл. 13.1). Поскольку UDP является протоколом дейтаграмм, то если приемный буфер блокируемого сокета UDP пуст, мы переходим в состояние ожидания и нахо- димся в нем до тех пор, пока не придет дейтаграмма UDP. В случае неблокируемого сокета при невозможности удовлетворить условию операции ввода (как минимум 1 байт данных для сокета TCP или целая дейта- грамма для сокета UDP) возврат происходит немедленно с ошибкой EWOULDBLOCK. 2. Операции вывода: функции write, writev, send, sendto и sendmsg. В отношении сокета TCP в разделе 2.9 мы сказали, что ядро копирует данные из буфера приложения в буфер отправки сокета. Если для блокируемого сокета недоста- точно места в буфере отправки, процесс переходит в состояние ожидания до тех пор, пока место не освободится. В случае неблокируемого сокета TCP при недостатке места в буфере отправ- ки завершение происходит немедленно с ошибкой EWOULDBLOCK. Если в буфере отправки сокета есть место, возвращаемое значение будет представлять коли- чество байтов, которое ядро смогло скопировать в буфер (это называется час- тичным копированием — short count). В разделе 2.9 мы также сказали, что на самом деле буфера отправки UDP не существует. Ядро только копирует данные приложения и перемещает их вниз
15.2. Неблокируемые чтение и запись: функция str cli (продолжение) 441 по стеку, добавляя к данным заголовки UDP и IP. Следовательно, операция вывода на блокируемом сокете UDP (каким он является по умолчанию) ни- когда не заблокируется. 3. Прием входящих соединений: функция accept. Если функция accept вызыва- ется для блокируемого сокета и новое соединение недоступно, процесс пере- водится в состояние ожидания. Если функция accept вызывается для неблокируемого сокета и новое соеди- нение недоступно, возвращается ошибка EWOULDBLOCK. 4. Инициирование исходящих соединений: функция connect для TCP. (Вспом- ните, что функция connect может использоваться с UDP, но она не вызывает создания «реального» соединения — она лишь заставляет ядро хранить IP-ад- рес и номер порта собеседника.) В разделе 2.5 мы показали, что установление соединения TCP включает трехэтапное рукопожатие и что функция connect не возвращает управление, пока клиент не получит сегмент АСК пли SYN. Это значит, что функция TCP connect всегда блокирует вызывающий процесс как минимум на время обращения (RTT) к серверу. Если функция connect вызывается для неблокируемого сокета TCP и соедине- ние не может быть установлено немедленно, инициируется установление соеди- нения (например, отправляется первый пакет трехэтапного рукопожатия TCP), но возвращается ошибка EINPROGRESS. Обратите внимание, что эта ошибка отли- чается от ошибки, возвращаемой в первых трех сценариях. Также отметим, что некоторые соединения могут быть установлены немедленно, когда сервер нахо- дится на том же узле, что и клиент, поэтому даже в случае неблокируемого вызо- ва функции connect мы должны быть готовы к тому, что она успешно выполнит- ся. Пример неблокируемой функции connect мы покажем в разделе 15.3. ПРИМЕЧАНИЕ----------------------------------------------------- Традиционно System V возвращала для неблокируемой операции ввода-вывода, кото- рую невозможно выполнить, ошибку EAGAIN, в то время как Беркли-реализации воз- вращали ошибку EWOULDBLOCK. Еще больше дело запутывается тем, что согласно Posix. 1 используется EAGAIN, в то время как в Posix. 1g определено, что используется EWOULDBLOCK. К счастью, большинство систем (включая SVR4 и 4.4BSD) опре- деляют эти две ошибки с одним и тем же кодом (проверьте свой системный заголовоч- ный файл <sys/errno.h>), поэтому пе важно, какой из них использовать. В нашем тек- сте мы используем ошибку EWOULDBLOCK, как определяется в Posix.lg. В разделе 6.2 мы представили различные модели ввода-вывода и сравнили неблокируемый ввод-вывод с другими моделями. В этой главе мы покажем при- меры четырех типов операций и разработаем новый тип клиента, аналогичный web-клиенту, инициирующий одновременно множество соединений TCP при помощи неблокируемой функции connect. 15.2. Неблокируемые чтение и запись: функция str_cli (продолжение) Мы снова возвращаемся к нашей функции str_cl т, которую мы обсуждали в раз- делах 5.5 и 6.4. Последняя ее версия, задействущая функцию sel ect, продолжает использовать блокируемый ввод-вывод. Например, если в стандартном устрой-
442 Глава 15. Неблокируемый ввод-вывод стве ввода имеется некоторая строка, мы читаем ее с помощью функции fgets и затем отправляем серверу с помощью функции wri ten. Но вызов функции wri ten может вызвать блокирование процесса, если буфер отправки сокета полон. В то время как мы заблокированы в вызове функции wri ten, данные могут быть до- ступны для чтения из приемного буфера сокета. Аналогично, когда строка ввода доступна из сокета, мы можем заблокироваться в последующем вызове функции fputs, если стандартный поток вывода работает медленнее, чем сеть. Наша цель в данном разделе — создать версию этой функции, использующую неблокируе- мый ввод-вывод. Блокирование будет предотвращено, благодаря чему в это вре- мя мы сможем сделать еще что-то полезное. К сожалению, добавление неблокируемого ввода-вывода значительно услож- няет управление буфером функции, поэтому мы будем представлять функцию частями. Мы также заменим вызовы функций из стандартной библиотеки ввода- вывода на обычные read и write. Это даст возможность отказаться от функций стандартной библиотеки ввода-вывода с неблокируемыми дескрипторами, так как их применение может привести к катастрофическим последствиям. Мы работаем с двумя буферами: буфер to содержит данные, направляющиеся из стандартного потока ввода к серверу, а буфер f г — данные, приходящие от сер- вера в стандартный поток вывода. На рис. 15.1 представлена организация буфера to и указателей в буфере. stdin socket Рис. 15.1. Буфер, содержащий данные из стандартного потока ввода, идущие к сокету Указатель toi ptr указывает на следующий байт, в который данные могут быть считаны из стандартного потока ввода. Указатель tooptr указывает на следую- щий байт, который должен быть записан в сокет. Число байтов, которое может socket 1 friptr Sfг[MAXLINE] I____________________I Уже отправленные данные Данные, которые нужно отправить на стандартное устройство вывода Пространство для считывания данных из стандартного устройства ввода froptr IT stdout Рис. 15.2. Буфер, содержащий данные из сокета, идущие к стандартному
15.2. Неблокируемые чтение и запись: функция str cli (продолжение) 443 быть считано из стандартного потока ввода, равно &to[MAXLINE] минус toiptr. Как только значение tooptr достигает ton ptr, оба указателя переустанавливаются на начало буфера. На рис. 15.2 показана соответствующая организация буфера fr. В листин- ге 15.1’ представлена первая часть функции. Листинг 15.1. Функция str_cli: первая часть, инициализация и вызов функции //nonblock/strclinonb с 1 include "unp h" 2 void 3 str_cli(FILE *fp, int sockfd) 4 { 5 int maxfdpl. val. stdineof: 6 ssize_t n. nwritten, 7 fd_set rset, wset. 8 char to[MAXLINE], fr[MAXLINEJ. 9 char *toiptr. *tooptr, *friptr. *froptr; 10 val = Fcntl(sockfd. F_GETFL. 0). 11 Fcntl(sockfd. F_SETFL. val | O_NONBLOCK): 12 val = Fcntl(STDIN_FILENO. F_GETFL. 0). 13 Fcntl(STDINJILENO. F_SETFL. val | O_NONBLOCK): 14 val = Fcntl(STDOUT_FILENO. FGETFL. 0). 15 Fcntl (STDOUTJILENO. F_SETFL. val | O_NONBLOCK): 16 toiptr = tooptr = to; /* инициализация указателей буфера */ 17 friptr = froptr = fr. 18 stdineof = 0 19 maxfdpl = max(max(STDIN_FILENO. STDOUT_FILENO). sockfd) + 1: 20 for (..) { 21 FD_ZERO(&rset). 22 FD_ZERO(&wset). 23 if (stdineof == 0 && toiptr < &to[MAXLINE]) 24 FD_SET(STDIN_FILENO. &rset). /* чтение из стандартного потока ввода */ 25 if (friptr < &fr[MAXLINE]) 26 FD_SET(sockfd. &rset). /* чтение из сокета */ 27 if (tooptr != toiptr) 28 FD_SET(sockfd. &wset). /* данные для записи в сокет */ 29 if (froptr l= friptr) 30 FD_SET (STDOUTJI LENO. &wset). /* данные для записи в стандартный поток вывода */ 31 Select(maxfdpl. &rset. &wset. NULL. NULL). Установка неблокируемых дескрипторов 10-15 Все три дескриптора делаются неблокируемыми при помощи функции fcntl: сокет в направлении к серверу и от сервера, стандартный поток ввода и стандарт- ный поток вывода. 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www.piter.coin/download.
444 Глава 15. Неблокируемый ввод-вывод Инициализация указателей буфера 6 19 Инициализируются указатели в двух буферах и вычисляется максимальный дескриптор. Это значение, увеличенное на единицу, будет использоваться в каче- стве первого аргумента функции sel ect. Основной цикл: подготовка к вызову функции select 20 Как и в случае первой версии этой функции, показанной в листинге 6.2, основ- ной цикл функции содержит вызов функции select, за которой следуют отдель- ные проверки различных интересующих нас условий. Подготовка интересующих нас дескрипторов 11-30 Оба набора дескрипторов обнуляются и затем в каждом наборе включается не более двух битов. Если мы еще не прочитали конец файла из стандартного пото- ка ввода и есть место для как минимум 1 байта данных в буфере to, то в наборе флагов чтения включается бит, соответствующий стандартному потоку ввода. Если есть место для как минимум 1 байта данных в буфере f г, то в наборе флагов чтения включается бит, соответствующий сокету. Если есть данные для записи в сокет в буфере to, то в наборе флагов записи включается бит, соответствующий сокету. Наконец, если в буфере f г есть данные для отправки в стандартный поток вывода, то в наборе флагов записи включается бит, соответствующий этому стан- дартному потоку. Вызов функции select 31 Вызывается функция sel ect, ожидающая, пока одно из четырех условий не ста- нет истинным. Для этой функции мы не задаем тайм-аута. Следующая часть нашей функции показана в листинге 15.2. Этот код содер- жит первые две проверки (из четырех возможных), выполняемые после заверше- ния функции select. Листинг 15.2. Функция str_cli: вторая часть, чтение из стандартного потока ввода или сокета //nonblock/strclinonb с 32 if (FDJSSET(STDIN_FILENO. &rset)) { 33 if ( (n = read(STDIN_FILENO, toiptr. &to[MAXLINE] - toiptr)) < 0) { 34 if (errno 1= EWOULDBLOCK) 35 err_sys(”read error on stdin”). 36 } else if (n == 0) { 37 fprintf(stderr. ”^s EOF on stdinXen". gf_time()), 38 stdineof = 1, /* c stdin все сделано*/ 39 if (tooptr == toiptr) 40 Shutdown(sockfd. SHUT_WR). /* отсылаем FIN */ 41 } else { 42 fprintf(stderr, ”^s. read Xd bytes from stdinXen", gf_time(). n). 43 toiptr += n. /* только что полученное из функции read число,*/ 44 FD_SET(sockfd, &wset), /* включаем бит в наборе чтения */ 45 } 46 }
15.2. Неблокируемые чтение и запись: функция str cli (продолжение) 445 47 if (FDISSETCsockfd. &rset)) { 48 if ( (n = readtsockfd. friptr. &fr[MAXL!NE] - friptr)) < 0) { 49 if (errno != EWOULDBLOCK) 50 err_sys("read error on socket"). 51 } else if (n == 0) { 52 fprintf(stderr. ”«s EOF on socketXen". gf_time()). 53 if (stdineof) 54 return. /* нормальное завершение */ 55 else 56 err_quit("str_cli server terminated prematurely"): 57 } else { 58 fprintf(stderr, ’Is read Й bytes from socketXen" 59 gf_time(), n). 60 friptr += n. /* только что полученное из функции read число */ 61 FD_SET(STDOUT_F1LENO. &wset). /* включаем бит в наборе чтения */ 62 } 63 } Чтение из стандартного потока ввода с помощью функции read 32-33 Если стандартный поток ввода готов для чтения, мы вызываем функцию read. Третий ее аргумент — это количество свободного места в буфере to. Обработка ошибки 34-35 Если происходит ошибка EWOULDBLOCK, мы ничего нс предпринимаем. Обычно эта ситуация — когда функция select сообщает нам о том, что дескриптор готов для чтения, а функция read возвращает ошибку EWOULDBLOCK — не должна возни- кать, но тем не менее мы ее обрабатываем. Возвращение конца файла функцией read 36-40 Если функция read возвращает нуль, мы закончили со стандартным потоком ввода. Флаг stdi neof установлен. Если в буфере to больше нет данных для от- правки (tooptr равно toiptr), функция shutdown отправляет серверу сегмент FIN. Если в буфере to еще есть данные для отправки, сегмент FIN не можег быть от- правлен до тех пор, пока содержимое буфера не будет записано в сокет. ПРИМЕЧАНИЕ ------------------------------------------------- Мы выводим в стандартный поток сообщений об ошибках строку, отмечающую конец файла, вместе с текущим временем. Мы покажем, как мы используем этот вывод, после описания функции. Аналогичные вызовы функции fprintf выполняются неоднократно в процессе выполнения нашей функции. Возвращение данных функцией read 41-45 Когда функция read возвращает данные, мы увеличиваем на единицу toiptr. Мы также включаем бит, соответствующий сокету, в наборе флагов записи, что- бы позже при проверке этого бита в цикле он был включен и тем самым иниции- ровалась бы попытка записи в сокет с помощью функции write.
446 Глава 15 Неблокируемый ввод-вывод ПРИМЕЧАНИЕ --------------------------------------------------------- Это одно из непростых конструктивных решений, которые приходится принимать при написании кода У нас есть несколько альтернатив Вместо установки бита в наборе записи мы можем ничего не делать, и в этом случае функция select будет проверять возможност ь записи в сокет, кот да она будет вы твана в следующий раз Но это требует дополни гелытото прохода цикла и вызова функции select когда мы уже знаем, что у нас есть данные для записи в сокет Другой вариант — дублировать код, который запи- сывает в сокет, но это кажется расточительным, к тому же это возможный источник ошибки (в случае, если в этой части дублируемото кода есть ошибка и мы обнаружива- ем и устраняем ее только в одном месте) Наконец, мы можем создать функцию запи- сывающую в сокет, и вызывать эту функцию вместо дублирования кода, по эта функ- ция должна использовать три локальные переменные совместно с функцией str cli, что может привести к необходимости сделать >ти переменные глобальными Выбор, сделанный в пашем случае — это результат субьективнот о мнения автора относитель- но того, какой из описанных трех вариантов предпочтительнее Чтение из сокета с помощью функции read 7 63 Эти строки кода аналогичны выражению i f, только что описанному для слу- чая, когда стандартный поток ввода готов для чтения Если функция read возвра- щает ошибку EWOULDBLOCK, ничето не происходит Если мы встречаем признак конца файла, присланный сервером, это нормально в том случае, когда мы уже получи- ли признак конца файла в стандартном потоке ввода Но в противном случае это будет ошибкой, означающей преждевременное завершение работы сервера (Server terminated prematurely) Если функция read возвращает некоторые данные, f n ptr увеличивается на единицу и в наборе флагов записи включается бит для стандарт- ного потока вывода, с тем чтобы попытаться записать туда данные в следующей части функции В листинге 15 3 показана последняя часть нашей функции Листинг 15.3. функция str_ch третья часть, запись в стандартный поток вывода или сокет //nonblock/strclinonb с 64 if (FD_ISSET(STDOUT_FILENO &wset) && ((n = friptr froptr) > 0)) { 65 if ( (nwritten = write(STDOUT_FILENO froptr n)) < 0) { 66 if (errno i- EWOULDBLOCK) 67 err_sys( write error to stdout ) 68 } else { 69 fpnntf(stderr wrote M bytes to stdoutXen 70 gf_time() nwntten) 71 froptr += nwntten /* только что полученное из функции write число */ 72 if (froptr == fnptr) 73 froptr = fnptr = fr /* назад к началу буфера */ 74 } 75 } 76 if (FD_ISSET(sockfd &wset) && ((n = toiptr tooptr) > 0)) { 77 if ( (nwritten = wnte(sockfd tooptr n)) < 0) { 78 if (errno '= EWOULDBLOCK) 79 err_sys( write error to socket ) 80 } else { 81 fpnntf(stderr Is wrote Й bytes to socketXen
15 2 Неблокируемые чтение и запись функция str ch (продолжение) 447 82 gf_time() nwritten) 83 tooptr +- nwritten /* только что полученное из функции write число */ 84 if (tooptr == toiptr) { 85 toiptr = tooptr = to /* назад к началу буфера */ 86 if (stdineof) 87 Shutdown(sockfd SHUT_WR) /досылаем FIN */ 88 } 89 } 90 } 91 } 92 } Запись в стандартный поток вывода с помощью функции write 64 67 Если есть возможность записи в стандартный поток вывода и число байтов для записи больше нуля, вызывается функция write Если возвращается ошибка EWOULDBLOCK, ничего не происходит Обратите внимание, что это условие возмож- но, поскольку код в конце предыдущей части функции включает бит в наборе флагов записи для стандартного потока вывода, когда не известно, успешно вы- полнилась функция write или нет Успешное выполнение функции write 68 74 Если функция write выполняется успешно, froptr увеличивается на число за- писанных байтов Если указатель вывода стал равен указателю ввода, оба указа- теля переустанавливаются на начало буфера Запись в сокет с помощью функции write 76 90 Эта часть кода аналогична коду, только что описанному для записи в стандарт- ный поток вывода Единственное отличие состоит в том, что когда указатель вы- вода доходит до указателя ввода, не только оба указателя переустанавливаются в начало буфера, но и появляется возможность отправить серверу сегмент FIN Теперь мы проверим работу этой функции и операций неблокируемого вво- да-вывода В листинге 15 4 показана наша функция gf_time, вызываемая из функ- ции str_cl 1 Листинг 15.4. Функция gf_time возвращение указателя на строку времени //lib/gf_time с 1 include unp h 2 include <time h> 3 char * 4 gf_time(void) 5 { 6 struct timeval tv 7 static char str[30] 8 char *ptr 9 if (gettimeofdayt&tv NULL) < 0) 10 err_sys( gettimeofday error ) 11 ptr = ctime(&tv tv_sec) 12 strcpy(str &ptr[ll]) 13 /* Fn Sep 13 00 00 00 1986\en\e0 */ продолжение &
448 Глава 15. Неблокируемый ввод-вывод Листинг 15.4 (продолжение) 14 /* 0123456789012345678901234 5 */ 15 snprintf(str + 8 sizeof(str) - 8 ' Ж061сГ tv tv_usec). 16 return (str) 17 } Эта функция возвращает строку, содержащую текущее время, включая мик- росекунды, в таком формате: 12 34 56 123456 Здесь специально используется тот же формат, что и для отметок времени, которые выводятся программой tcpdump. Обратите внимание, что все вызовы функ- ции fprintf в нашей функции str__cl 1 записывают данные в стандартный поток сообщений об ошибках, позволяя нам отделить данные стандартного потока вы- вода (строки, отраженные сервером) от наших диагностических данных. Затем мы можем запустить наш клиент и функцию tcpdump, получить эти диагностичес- кие данные вместе с результатом функции tcpdump и отсортировать вместе два вида выходных данных в порядке их получения. Это позволит нам увидеть, что происходит в нашей программе, и соотнести это с действиями TCP. Например, сначала мы запускаем функцию tcpdump на нашем узле solans, со- бирая только сегменты TCP, идущие к порту 7 или от него (эхо-сервер), и сохра- няем выходные данные в файле, который называется tcdp solans % tcpdump -w tcpd tep and port 7 Затем мы запускаем клиент TCP на этом узле и задаем сервер на узле bsdi: solans К tcpch02 206.62.226.35 < 2000.lines > out 2> diag Стандартный поток ввода — это файл 2000 lines, тот же файл, что мы исполь- зовали для листинга 6.2. Стандартный поток вывода перенаправлялся в файл out, а стандартный поток сообщений об ошибках — в файл diag По завершении мы запускаем. solans % diff 2000.lines out чтобы проверить, что отраженные строки идентичны введенным строкам. Нако- нец, мы прекращаем выполнение функции tcpdump нажатием соответствующей клавиши терминала, после чего выводим записи функции tcpdump, сортируя их по времени получения вместе с данными диагностики, полученными от клиента. В листинге 15.5 показана первая часть этого результата. Листинг 15.5. Отсортированный вывод функции tcpdump и данных диагностики solans % tcpdump -г tcpd -N | sort diag - 10 18 34 486392 solans 33621 > bsdi echo S 1802738644 1802738644(0) win 8760 <mss 1460> 10 18 34 488278 bsdi echo > solans 33621 S 3212986316 3212986316(0) ack 1802738645 win 8760 <mss 1460> 10 18 34 488490 solans 33621 > bsdi echo ack 1 win 8760 10 18 34 491482 read 4096 bytes from stdin 10 18 34 518663 solans 33621 > bsdi echo P 1 1461(1460) ack 1 win 8760 10 18 34 519016 wrote 4096 bytes to socket 10 18 34 528529 bsdi echo > solans 33621 P 1 1461(1460) ack 1461 win 8760 10 18 34 528785 solans 33621 > bsdi echo 1461 2921(1460) ack 1461 win 8760 10 18 34 528900 solans 33621 > bsdi echo P 2921 4097(1176) ack 1461 win 8760 10 18 34 528958 solans 33621 > bsdi echo ack 1461 win 8760 10 18 34 536193 bsdi echo > solans 33621 1461 2921(1460) ack 4097 win 8760
15.2. Неблокируемые чтение и запись: функция str cli (продолжение) 449 10 18 34 536697 bsdi echo > solans 33621 Р 2921 3509(588) ack 4097 win 8760 10 18 34 544636 read 4096 bytes from stdin 10 18 34 568505 read 3508 bytes from socket 10 18 34 580373 solans 33621 > bsdi echo ack 3509 win 8760 10 18 34 582244 bsdi echo > solans 33621 P 3509 4097(588) ack 4097 win 8760 10 18 34 593354 wrote 3508 bytes to stdout 10 18 34 617272 solans 33621 > bsdi echo P 4097 5557(1460) ack 4097 win 8760 10 18 34 617610 solans 33621 > bsdi echo P 5557 7017(1460) ack 4097 win 8760 10 18 34 617908 solans 33621 > bsdi echo P 7017 8193(1176) ack 4097 win 8760 10 18 34 618062 wrote 4096 bytes to socket 10 18 34 623310 bsdi echo > solans 33621 ack 8193 win 8760 10 18 34 626129 bsdi echo > solans 33621 4097 5557(1460) ack 8193 win 8760 10 18 34 626339 solans 33621 > bsdi echo ack 5557 win 8760 10 18 34 626611 bsdi echo > solans 33621 P 5557 6145(588) ack 8193 win 8760 10 18 34 628396 bsdi echo > solans 33621 6145 7605(1460) ack 8193 win 8760 10 18 34 643524 read 4096 bytes from stdin 10 18 34 667305 read 2636 bytes from socket 10 18 34 670324 solans 33621 > bsdi echo ack 7605 win 8760 10 18 34 672221 bsdi echo > solans 33621 P 7605 8193(588) ack 8193 win 8760 10 18 34 691039 wrote 2636 bytes to stdout Мы разбили длинные строки на части, а также удалили записи (DF) из сегмен- тов Solans, означающие, что устанавливается бит DF (он используется для опре- деления величины транспортной MTU). Параметр -N функции tcpdump указыва- ет, что нужно вывести только часть полного доменного имени (sol ari s kohal a com), относящуюся к узлу (sol ari s). Используя этот вывод, мы можем нарисовать временную диаграмму происхо- дящих событий (рис. 15.3). На этой диаграмме представлены события в различ- ные моменты времени, причем ориентация диаграммы такова, что более поздние события расположены ниже на странице. На этом рисунке мы не показываем сегменты АСК. Также помните, что если программа выводит сообщение wrote N bytes to stdout (записано N байт в стан- дартное устройство вывода), это означает, что выполнилась функция write, воз- можно, заставившая TCP отправить один или более сегментов данных. По этому рисунку мы можем проследить динамику обмена клиент-сервер. Использование неблокируемого ввода-вывода позволяет программе использовать преимущество этой динамики, считывая или записывая данные, когда операция ввода или вывода может иметь место. Ядро сообщает нам, когда может произой- ти операция ввода-вывода, при помощи функции sei ect. Мы можем рассчитать время выполнения нашей неблокируемой версии, ис- пользуя тот же файл из 2000 строк и тот же сервер (с периодом RTT, равным 175 миллисекундам), что и в разделе 6 7. Теперь время оказалось равным 6,9 се- кунды по сравнению с 12,3 секунды в версии из раздела 6.7 Следовательно, не- блокируемый ввод-вывод сокращает общее время выполнения этого примера, в котором файл отправляется серверу. Более простая версия функции str_cli Неблокируемая версия функции str_cli, которую мы только что показали, не- тривиальна: около 135 строк кода по сравнению с 40 строками версии, использу- ющей функцию sei ect с блокируемым вводом-выводом (см. листинг 6.2), и 20 стро- ками начальной версии, работающей в режиме остановки и ожидания (см. лис-
450 Глава 15 Неблокируемый ввод-вывод Принимающий Буфер буфер fr сокета клиента Буфер to клиента Буфер отправки сокета Сервер Стандартное устройство ввода Стандартное устройство ввода Стандартное устройство ввода 3508 (чтение) 3508 w Стандартное ---- , ► устройство (запись) ввода 2636 -----► 2636 w Стандартное ---------устройство ввода Рис. 15.3. Временная диаграмма событий для примера неблокируемого ввода тинг 5 4) Мы знаем, что эффект от удлинения кода в два раза, с 20 до 40 строк, оправдывает затраченные усилия, поскольку в пакетном режиме скорость возра- стает почти в 30 раз, а применение функции select с блокируемыми дескрипто- рами осуществляется не слишком сложно Но будут ли оправданы затраченные усилия при написании приложения, использующего неблокируемый ввод-вывод, с учетом усложнения итогового кода? Нет, ответим мы Если нам необходимо использовать неблокируемый ввод-вывод, обычно бывает проще разделить приложение либо на процессы (при помощи функции fork), либо на потоки (см главу 23)
15 2 Неблокируемые чтение и запись функция str ch (продолжение) 451 В листинге 15 6 показана еще одна версия нашей функции str_cl 1, разделяе- мая на два процесса при помощи функции fork Эта функция сразу же вызывает функцию fork для разделения на родительс- кий и дочерний процессы Дочерний процесс копирует строки от сервера в стан- дартный поток вывода, а родительский процесс — из стандартного потока ввода серверу, как показано на рис 15 4 Стандартное устройство ввода Стандартное устройство вывода Рис. 15.4. Функция str_cli, использующая два процесса Мы явно отмечаем, что соединения TCP являются двусторонними и что ро- дительский и дочерний процессы совместно используют один и тот же дескрип- тор сокета родительский процесс записывает в сокет, а дочерний процесс читает из сокета Есть только один сокет, один буфер приема сокета и один буфер от- правки, но на этот сокет ссылаются два дескриптора один в родительском про- цессе и один в дочернем Листинг 15.6. Версия функции str_cli, использующая функцию fork //nonblock/strclifork с 1 include unp h 2 void 3 str_cli(FILE *fp int sockfd) 4 { 5 pid_t pid 6 char sendline[MAXLINE] recvlineFMAXLINE] 7 if ( (pid = ForkO) == 0) { /* дочернин процесс! сервер ->i(6fdput */ 8 while (Readline(sockfd recvline MAXLINE) > 0) 9 Fputs(recvline stdout) 10 kill(getppid() SIGTERM) /* в случае если родительский процесс все еще выполняется */ 11 exit(0) 12 } 13 /* родитель stdin > сервер */ 14 while (Fgets(sendlme MAXLINE fp) '= NULL) 15 Writen(sockfd sendline strlen(sendline)) 16 Shutdown(sockfd SHUT_WR) /* конец файла на stdin, посмлаен FI(j*7 17 paused 18 return 19 } Нам нужно снова вспомнить о последовательности завершения соединения Обычное завершение происходит, когда в стандартном потоке ввода встречается конец файла Родительский процесс считывает конец файла и вызывает функ-
452 Глава 15 Неблокируемый ввод-вывод цию shutdown для отправки сегмента FIN (Родительский процесс не может вы- звать функцию close, см упражнение 15 1 ) Но когда это происходит, дочерний процесс должен продолжать копировать от сервера в стандартный поток вывода, пока он не получит признак конца файла на сокете Также возможно, что процесс сервера завершится преждевременно (см раз- дел 5 12), и если это происходит, дочерний процесс считывает признак конца файла на сокете В таком случае дочерний процесс должен сообщить родительс- кому, что нужно прекратить копирование из стандартного потока ввода в сокет (см упражнение 15 2) В листинге 15 6 дочерний процесс отправляет родительс- кому процессу сигнал SIGTERM, в случае, если родительский процесс еще выполня- ется (см упражнение 15 3) Другим способом обработки этой ситуации было бы завершение дочернего процесса, и если родительский процесс все еще выполнял- ся бы к этому моменту, он получил бы сигнал SIGCHLD Родительский процесс вызывает функцию pause, когда заканчивает копирова- ние, что переводит его в состояние ожидания того момента, когда будет получен сигнал Даже если родительский процесс не перехватывает никаких сигналов, он все равно переходит в состояние ожидания до получения сигнала SIGTERM от до- чернего процесса По умолчанию действие этого сигнала — завершение процесса, что вполне устраивает нас в этом примере Родительский процесс ждет заверше- ния дочернего процесса, чтобы измерить точное время для этой версии функции str_cl 1 Обычно дочерний процесс завершается после родительского, но поскольку мы измеряем время, используя команду оболочки time, измерение заканчивает- ся, когда завершается родительский процесс Отметим простоту этой версии по сравнению с неблокируемым вводом-выво- дом, представленным ранее в этом разделе Наша неблокируемая версия управ- ляла четырьмя различными потоками ввода-вывода одновременно, и поскольку все четыре были неблокируемыми, нам пришлось иметь дело с частичным чтени- ем и частичной записью для всех четырех потоков Но в версии с функцией fork каждый процесс обрабатывает только два потока ввода-вывода, копируя один в другой В применении неблокируемого ввода-вывода не возникает необходи- мости, поскольку если есть данные для чтения из потока ввода, то в соответству- ющий поток вывода записывать нечего Сравнение времени выполнения различных версий функции str_cli Итак, мы продемонстрировали четыре различных версии функции str_cl i Для каждой версии мы покажем время, которое потребовалось для ее выполнения, в том числе и для версии, использующей программные потоки (см листинг 23 1) В каждом случае было скопировано 2000 строк от клиента Solaris 2 5 к серверу с периодом RTT, равным 175 миллисекундам В 354,0 секунд, режим остановки и ожидания (см листинг 5 4), ® 12,3 секунды, функция select и блокируемый ввод-вывод (см листинг 6 2), ж 6,9 секунд, неблокируемый ввод-вывод (см листинг 15 1), ч.' 8,7 секунд, функция fork (см листинг 15 6), * 8,5 секунд, версия с потоками (см листинг 23 1) Наша версия с неблокируемым вводом-выводом почти вдвое быстрее версии, использующей блокируемый ввод-вывод с функцией sel ect Наша простая вер-
15 3 Неблокируемая функция connect 453 сия с применением функции fork медленнее версии с неблокируемым вводом- выводом Тем не менее, учитывая сложность кода неблокируемого ввода-вывода по сравнению с кодом функции fork, мы рекомендуем более простой подход 15.3. Неблокируемая функция connect Когда сокет TCP устанавливается как неблокируемый, а затем вызывается функ- ция connect, она немедленно возвращает ошибку EINPROGRESS, однако трехэтапное рукопожатие TCP продолжается Далее мы с помощью функции select проверя- ем, успешно или нет завершилось установление соединения Неблокируемая функция connect находит применение в трех случаях 1 Трехэтапное рукопожатие может наложиться на какои-либо другой процесс Для выполнения функции connect требуется один период обращения RTT (см раздел 2 5), и это может занять от нескольких миллисекунд в локальной сети до сотен миллисекунд или нескольких секунд в глобальной сети Это вре- мя мы можем провести с пользой, выполняя какой-либо другой процесс 2 Мы можем установить множество соединений одновременно, используя эту технологию Этот способ уже стал популярен в применении к web-браузерам, и такой пример мы приводим в разделе 15 5 3 Поскольку мы ждем завершения установления соединения с помощью функ- ции select, мы можем задать предел времени для функции select, что позво- лит нам сократить тайм-аут для функции connect Во многих реализациях тайм- аут функции connect лежит в пределах от 75 секунд до нескольких минут Бывают случаи, когда приложению нужен более короткий тайм-аут, и одним из решений может стать использование неблокируемой функции connect В раз- деле 13 2 рассматриваются другие способы помещения тайм-аута в операции с сокетами Как бы просто ни выглядела неблокируемая функция connect, есть ряд момен- тов, которые следует учитывать < Даже если сокет является неблокируемым, то когда сервер, с которым мы со- единяемся, находится на том же узле, обычно установление соединения про- исходит немедленно при вызове функции connect В Беркли-реализациях (а также Posix 1g) имеются два следующих правила, относящихся к функции sei ect и неблокируемой функции connect во-первых, когда соединение устанавливается успешно, дескриптор становится готовым для записи [105, с 531], и во-вторых, когда при установлении соединения встречается ошибка, дескриптор становится готовым как для чтения, так и для записи [105, с 530] ПРИМЕЧАНИЕ ------------------------------------------------------ Эти два правила в отношении функции select выпадают из общего ряда наших правил из раздела 6 3 относительно условий, при которых дескриптор становится готовым для чтения или записи В сокет TCP можно записывать если достаточно места в буфере отправки (что всегда будет выполнено в случае присоединенною сокета, поскольку мы еще ничего не записали в сокет) и сокет является присоединенным (что выполня- ется, только когда завершено трехэтапное рукопожатие) При наличии ошибки, ожи- дающей обработки, появляется возможность читать из сокета и записывать в сокет
454 Глава15. Неблокируемый ввод-вывод С неблокируемыми функциями connect связано множество проблем перено- симости, которые мы отметим в последующих примерах. 15.4. Неблокируемая функция connect: клиент времени и даты В листинге 15.7 показана наша функция connect_nonb, вызывающая неблокируе- мую функцию connect. Мы заменяем вызов функции connect, имеющийся в лис- тинге 1.1, следующим фрагментом кода: if (connect_nonb(sockfd. (SA *) &servaddr. sizeof(servaddr). 0) < 0) err_sys("connect error”). Первые три аргумента являются обычными аргументами функции connect, а четвертый аргумент — это число секунд, в течение которых мы ждем заверше- ния установления соединения. Нулевое значение подразумевает отсутствие тайм- аута для функции sei ect; следовательно, для установления соединения TCP ядро будет использовать свой обычный тайм-аут. Листинг 15.7. Неблокируемая функция connect //lib/connect_nonb с 1 #include "unp h" 2 3 4 5 6 7 8 int connect nonb(mt sockfd const SA *saptr. ^ocklen t salen. int nsep) { int flags, n. error socklen_t len. fd_set rset. wset struct timeval tval. 9 10 flags = FcntKsockfd. F_GETFL. 0). Fcntl(sockfd. F_SETFL. flags | O_NONBLOCK). 11 12 13 14 error = 0. if ( (n = connect(sockfd. saptr. salen)) < 0) if (errno EINPROGRESS) return (-1). 15 /* Пока соединение устанавливается мы можем заняться чем-то другим */ 16 17 if (п == 0) goto done /* функция connect завершилась немедленно */ 18 19 20 21 22 FD_ZERO(&rset). FD_SET(sockfd &rset): wset = rset tval tvsec = nsec, tval tv_usec = 0. 23 24 25 26 27 28 if ( (n = Select(sockfd + 1. &rset, &wset. NULL. nsec ? &tval NULL)) — 0) { close(sockfd). /* тайм-аут */ errno - ETIMEDOUT. return (-1). }
15.4. Неблокируемая функция connect: клиент времени и даты 455 29 if (FD_ISSET(sockfd. Reset) || FD_ISSET(sockfd. &wset)) { 30 len = sizeof(error). 31 if (getsockopt(sockfd. S0L_S0CKET. SD_ERROR. &error. &len) < 0) 32 return (-1). /*в Solaris ошибка, ожидающая обработки */ 33 } else 34 err_quit("select error sockfd not set") 35 done 36 Fcntl(sockfd, F_SETFL. flags). /* восстанавливаем флаги, задающие статус файла */ 37 if (error) { 38 close(sockfd). /* на всякий случай */ 39 errno = error. 40 return (-1) 41 } 42 return (0). 43 } Задание неблокируемого сокета 9-10 Мы вызываем функцию fcntl, которая делает сокет неблокируемым. 11-14 Мы инициируем неблокируемую функцию connect. Ошибка, которую мы ожи- даем (EINPROGRESS), указывает на то, что установление соединения началось, но еще не завершилось [105, с. 466]. Любая другая ошибка возвращается вызываю- щему процессу. Выполнение других процессов во время установления соединения 15 На этом этапе мы можем делать все. что захотим, ожидая завершения установ- ления соединения. Проверка немедленного завершения 16-17 Если неблокируемая функция connect возвратила нуль, установление соедине- ния завершилось. Как мы сказали, это может произойти, когда сервер находится на том же узле, что и клиент. Вызов функции select 18-24 Мы вызываем функцию sel ect и ждем, когда сокет будет готов либо для чте- ния, либо для записи. Мы обнуляем rset, включаем бит, соответствующий sockfd в этом наборе дескрипторов, и затем копируем rset в wset. Это присваивание, воз- можно, является структурным присваиванием, поскольку обычно наборы де- скрипторов представляются как структуры. Далее мы инициализируем структу- ру timeval и затем вызываем функцию select. Если вызывающий процесс задает четвертый аргумент нулевым (что соответствует использованию тайм-аута по умолчанию), следует задать в качестве последнего аргумента функции sel ect пу- стой указатель, а не структуру timeval с нулевым значением (означающим, что мы не ждем вообще). Обработка тайм-аутов 25-28 Если функция sel ect возвращает нуль, это означает, что время таймера истек- ло, и мы возвращаем вызывающему процессу ошибку ETIMEDOUT. Мы также закры- ваем сокет, чтобы трехэтапное рукопожатие не продолжалось.
456 Глава 15. Неблокируемый ввод-вывод Проверка возможности чтения или записи 9-34 Если дескриптор готов для чтения или для записи, мы вызываем функцию getsockopt, чтобы получить ошибку сокета (SO_ERROR), ожидающую обработки. Если соединение завершилось успешно, это значение будет нулевым. Если при уста- новлении соединения произошла ошибка, это значение является значением пе- ременной errno, соответствующей ошибке соединения (например, ECONNREFUSED, ETIMEDOUT и т. д.). Мы также сталкиваемся с нашей первой проблемой переноси- мости. Если происходит ошибка, Беркли-реализации функции getsockopt возвра- щают нуль, а ошибка, ожидающая обработки, возвращается в нашей переменной error. Но в системе Solaris сама функция getsockopt возвращает -1, а переменная errno при этом принимает значение, соответствующее ошибке, ожидающей обра- ботки. В нашем коде обрабатываются оба сценария. Восстановление возможности блокировки сокета и завершение 5-42 Мы восстанавливаем флаги, задающие статус файла, и возвращаемся. Если наша переменная errno имеет ненулевое значение в результате выполнения функции getsockopt, это значение хранится в переменной errno, и функция возвращает -1. Как мы сказали ранее, проблемы переносимости для функции connect связа- ны с различными реализациями сокетов и отключения блокировки. Во-первых, возможно, что установление соединения завершится и придут данные для собе- седника до того, как будет вызвана функция select. В этом случае сокет будет готов для чтения и для записи при успешном выполнении функции, как и в слу- чае неудачного установления соединения. В нашем коде, показанном в листин- ге 15.7, этот сценарий обрабатывается при помощи вызова функции getsockopt и проверки на наличие ошибки, ожидающей обработки, для сокета. Во-вторых, проблема в том, как определить, успешно завершилось установле- ние соединения или нет, если мы не можем считать возможность записи един- ственным указанием на успешное установление соединения. В Usenet предлага- лось множество решений этой проблемы, которые заменяют наш вызов функции getsockopt в листинге 15.7: 1. Вызвать функцию getpeername вместо функции getsockopt. Если этот вызов окажется неудачным и возвратится ошибка ENOTCONN, значит, соединение не было установлено, и чтобы получить ошибку, ожидающую обработки, следу- ет вызвать для сокета функцию getsockopt с SO_ERROR. 2. Вызвать функцию read с нулевым значением аргумента 1 ength. Если выполне- ,. ние функции read окажется неудачным, функция connect выполнилась неудачно, , и переменная errno из функции read при этом указывает на причину неудач- ной попытки установления соединения. Если соединение успешно установ- лено, функция read возвращает нуль. 3. Снова вызвать функцию connect. Этот вызов окажется неудачным, и если ошиб- ка — EISCONN, сокет уже присоединен, а значит, первое соединение заверши- лось успешно. К сожалению, неблокируемая функция connect — это одна из самых сложных областей сетевого программирования с точки зрения переносимости. Будьте го- товы к проблемам совместимости, особенно с более ранними реализациями. Бо-
15.5. Неблокируемая функция connect: клиент Web 457 лее простой технологией является создание потока (см. главу 23) для обработки соединения. Прерванная функция connect Что происходит, если наш вызов функции connect на обычном блокируемом со- кете прерывается, скажем, перехваченным сигналом, прежде чем завершится трех- этапное рукопожатие TCP? Если предположить, что функция connect не переза- пускается автоматически, то она возвращает ошибку EINTR. Но мы не можем снова вызвать функцию connect, чтобы добиться завершения установления соединения. Это приведет к ошибке EADDRINUSE. Все, что требуется сделать в этом сценарии, — это вызвать функцию select, так, как мы делали в этом разделе для неблокируемой функции connect. Тогда функция select завершится в том случае, если соединение успешно устанавлива- ется (делая сокет доступным для записи) или если попытка соединения неудач- на (сокет становится доступен для чтения и для записи). ПРИМЕЧАНИЕ ----------------------------------------------------- В Posix. 1g явно определено, что происходит, когда вызов функции XTI t_connect пре- рывается перехваченным сигналом, но ничего не говорится о том, как подобная ситуа- ция обрабатывается в том случае, когда была вызвана функция connect. То, что мы описали, соответствует обработке этого сценария Беркли-ядрами. 15.5. Неблокируемая функция connect: клиент Web Первое практическое использование неблокируемой функции connect относится к web-клиенту Netscape (см. раздел 13.4 [95]). Клиент устанавливает соединение HTTP с web-сервером и попадает на домашнюю страницу. На этой странице час- то присутствуют ссылки на другие web-страницы. Вместо того чтобы получать последовательно по одной странице за один раз, клиент может получить сразу несколько страниц, используя неблокируемые функции connect. На рис. 15.5 по- казан пример установления множества параллельных соединений. Сценарий, изображенный слева, показывает все три соединения, устанавливаемые одно за другим. Мы считаем, что первое соединение занимает 10 единиц времени, вто- рое — 15, а третье — 4, что в сумме дает 29 единиц времени. В центре рисунка показан сценарий, при котором мы выполняем два парал- лельных соединения. В момент времени 0 запускаются первые два соединения, а когда первое из них устанавливается, мы запускаем третье. Общее время сокра- тилось почти вдвое и равно 15, а не 29 единицам времени, но учтите, что это иде- альный случай. Если параллельные соединения совместно используют общий канал связи (допустим, клиент использует модем для соединения с Интернетом), то каждое из этих соединений конкурирует с другими за обладание ограничен- ными ресурсами этого канала связи, и время установления каждого соединения может возрасти. Например, время 10 может дойти до 15, 15 — до 20, а время 4 может превратиться в 6. Тем не менее общее время будет равно 21 единице, то есть все равно меньше, чем в последовательном сценарии.
458 Глава 15. Неблокируемый ввод-вывод Время О Время О Время О 29 Последовательное установление трех соединений 14 А 1-15 Параллельное установление трех соединений; одновременно устанавливается не более двух соединений Параллельное установление трех соединений; одновременно устанавливается не более трех соединений Рис. 15.5. Установление множества параллельных соединений В третьем сценарии мы выполняем три параллельных соединения и снова счи- таем, что эти три соединения не мешают друг другу (идеальный случай). Но об- щее время такое (15 единиц), как и во втором сценарии. При работе с web-клиентами первое соединение устанавливается само по себе, за ним следуют соединения по ссылкам, обнаруженным в данных от первого со- единения. Мы показываем это на рис. 15.6. Время О 4 JL14 Рис. 15.6. Установление первого соединения, азатем - множества параллельных соединений Для дальнейшей оптимизации клиент может начать обработку данных, воз- вращаемых по первому соединению, до того, как установление первого соедине- ния завершится, и инициировать дополнительные соединения, как только ему станет известно, что они нужны. Поскольку мы выполняем несколько неблокируемых функций connect одно- временно, мы не можем использовать нашу функцию connect_nonb, показанную в листинге 15.7, так как она не завершается, пока соединение не установлено. Вме- сто этого мы отслеживаем множество соединений самостоятельно.
15.5. Неблокируемая функция connect: клиент Web 459 Наша программа считывает около 20 строк с web-сервера. Мы задаем в каче- стве аргументов командной строки максимальное число параллельных соедине- ний, имя узла сервера, а затем каждое из имен файлов, получаемых с сервера. Типичное выполнение нашей программы выглядит так: Solaris Ж web 3 www.foobar.com / imagel.gif image2.gif \ 1mage3.gif image4.gif 1mage5.gif \ image6 gif image? gif Аргументы командной строки задают три одновременных соединения, имя узла сервера, имя файла домашней страницы (/ обозначает корневой каталог сервера) и семь файлов, которые затем нужно прочитать (в нашем примере это файлы с изображениями в формате GIF). Обычно на эти семь файлов имеются ссылки с домашней страницы, и чтобы получить их имена, web-клиент читает домашнюю страницу и обрабатывает код HTML. Чтобы не усложнять этот пример разбором кода HTML, мы просто задаем имена файлов в командной строке. Это большой пример, поэтому мы будем показывать его частями. В листинге 15.8 представлен наш заголовочный файл web h, который включен во все файлы. Листинг 15.8. Заголовок web. h //nonblock/web h 1 include "unp h" 2 #define MAXFILES 20 3 ^define SERV "80" 4 struct file { 5 char *f_name; 6 char *f_host. 7 int f_fd. 8 int f_f1ags: 9 } file[MAXFILES]; 10 #define F_CONNECTING 1 11 ^define F_READING 2 12 #define F_DONE 4 /* номер порта или имя Службы */ /* имя файла */ /* имя узла или адрес IPv4/IPv6 */ /* дескриптор */ /* F_xxx определены ниже */ /* connect!) в процессе выполнения */ /* соединение установлено . происходит считывание */ /* все сделано */ 13 ^define GET_CMD "GET its HTTP/1 O\er\en\er\en" 14 /* глобальные переменные */ 15 int nconn. nfiles. nlefttoconn nlefttoread. maxfd: 16 fd set rset. wset. 17 /* прототипы функций */ 18 void home_page(const char *. const char *). 19 void start connect!struct file *). 20 void write_get_cmd!struct file *). Задание структуры file 2-13 Программа считывает некоторое количество (не более MAXFILES) файлов с web- сервера. Структура fi 1 е содержит информацию о каждом файле: его имя (копи- руется из аргумента командной строки), имя узла или IP-адрес сервера, с которо- го читается файл, дескриптор сокета, используемый для этого файла, и набор флагов, которые указывают, что мы делаем с этим файлом (устанавливаем соеди- нение для получения файла или считываем файл). Флаг F_DONE указывает, что работа с данным файлом закончена. -
460 Глава 15. Неблокируемый ввод-вывод Определение глобальных переменных и прототипов функций 14-20 Мы определяем глобальные переменные и прототипы для наших функций, ко- торые мы вскоре опишем. Листинг 15.9. Первая часть программы одновременного выполнения функций connect: глобальные переменные и начало функции main //nonblock/web.с 1 #include "web h” 2 int 3 main(int argc. char **argv) 4 { 5 int i. fd. n. maxnconn, flags, error: 6 char buf[MAXLINE], 7 fd_set rs. ws. 8 if (argc < 5) 9 err_quit(''usage. web <#conns> <hostname> <homepage> <filel> ..."): 10 maxnconn = atoi(argv[l]): 11 nfiles = min(argc - 4. MAXFILES). 12 for (i = 0. i < nfiles: i++) { 13 file[i] f_name = argv[i + 4]: 14 file[i] f host = argv[2], 15 fileEi] f_flags = 0. 16 } 17 printfCnfiles = £d\en". nfiles): 18 home_page(argv[2], argv[3]). 19 FD_ZERO(&rset). 20 FD_ZERO(&wset). 21 maxfd = -1, 22 nlefttoread = nlefttoconn = nfiles. 23 nconn = 0. Обработка аргументов командной строки 11-17 Структуры fi 1 е заполняются соответствующей информацией из аргументов ко- мандной строки. Чтение домашней страницы 18 Функция home_page, которую мы показываем в следующем листинге, создает соединение TCP, посылает команду серверу и затем читает домашнюю страницу. Это первое соединение, которое выполняется самостоятельно, до того как мы начнем устанавливать параллельные соединения. Инициализация глобальных переменных 19-23 Инициализируются два набора дескрипторов, по одному для чтения и для за- писи. maxfd — это максимальный дескриптор для функции select (который мы инициализируем значением -1, поскольку дескрипторы неотрицательны), nl efttoread — число файлов, которые осталось прочитать (когда это значение ста- новится нулевым, чтение заканчивается), nlefttoconn — это количество файлов,
15.5. Неблокируемая функция connect: клиент Web 461 для которых пока еще требуется соединение TCP, а псопп — это число соедине- ний, открытых в настоящий момент (оно никогда не может превышать первый аргумент командной строки). В листинге 15.10 показана функция home_page, вызываемая один раз, когда на- чинается выполнение функции mam. Листинг 15.10. Функция home_page //nonblock/home_page с 1 include "web h" 2 void 3 home_page(const char *host, const char *fname 4 { 5 int fd. n. 6 char linefMAXLINE], 7 fd = Tcp_connect(host. SERV). /* блокируемая функция connectO */ 8 n = snprintf(line, sizeof(line). GET_CMD. fname), 9 Writen(fd. line, n), 10 for (,,) { 11 if ( (n = Read(fd, line, MAXLINE)) == 0) 12 break: /* сервер закрыл соединение */ 13 printfl"read £d bytes of home pageXen", n). 14 /* обрабатываем полученные данные */ 15 } 16 printf("end-of-file on home pageXen''): 17 Close(fd): 18 } Установление соединения с сервером 7 Наша функция tcp_connect устанавливает соединение с сервером. Отправка команды HTTP серверу, чтение ответа 8-17 Запускается команда HTTP GET для домашней страницы (часто обозначается символом /). Читается ответ (с ответом мы в данном случае ничего не делаем), и соединение закрывается. Следующая функция, start_connect, показанная в листинге 15.11, инициирует вызов неблокируемой функции connect. Листинг 15.11. Инициирование неблокируемой функции connect //nonblock/start_connect с 1 #include "web h” 2 void 3 sta reconnect (struct file *fptr) 4 { 5 int fd. flags, n. 6 struct addrinfo *ai. 7 ai = Host_serv(fptr->f_host. SERV. 0, SOCK_STREAM). 8 fd = Socket(ai->ai_family. ai->ai_socktype, ai->ai_protocol): продолжение^
462 Глава 15. Неблокируемый ввод-вывод Листинг 15.11 (продолжение) 9 fptr->f_fd = fd; 10 printf("start_connect for Xs. fd Xd\en". fptr->f_name. fd): 11 /* отключаем блокирование сокета */ 12 flags = Fcntl(fd. F_GETFL. 0). 13 FcntKfd. F_SETFL, flags | OJOIBLOCK). 14 /* инициируем неблокируемое соединение с сервером */ 15 if ( (n = connected. ai->ai_addr, ai->ai_addrlen)) < 0) { 16 if (errno != EINPROGRESS) 17 err_sys("nonblocking connect error"). 18 fptr->f_flags = F_CONNECTING. 19 FD_SET(fd. &rset). /* включаем дескриптор сокета в наборе чтения и записи */ 20 FD_SET(fd. &wset); 21 if (fd > maxfd) 22 maxfd = fd. 23 } else if (n >= 0) /* соединение уже установлено */ 24 write_get_cmd(fptr); /* отправляем команду GET серверу */ 25 } Создание сокета, отключение блокировки сокета 13 Мы вызываем нашу функцию host_serv для поиска и преобразования имени узла и имени службы. Она возвращает указатель на массив структур addri nfo. Мы используем только первую структуру. Создается сокет TCP, и он становится не- блокируемым. Вызов неблокируемой функции connect 1-22 Вызывается неблокируемая функция connect, и флагу файла присваивается значение F_CONNECTING. Включается дескриптор сокета и в наборе чтения, и в на- боре записи, поскольку функция sei ect будет ожидать любого из этих условий как указания на то, что установление соединения завершилось. При необходимо- сти мы также обновляем значение maxfd. Обработка завершения установления соединения -24 Если функция connect успешно завершается, значит, соединение уже установ- лено, и функция write_get_cmd (она показана в следующем листинге) посылает команду серверу. Мы делаем сокет неблокируемым для функции connect, но никогда не пере- устанавливаем его в блокируемый режим, заданный по умолчанию. Это нормаль- но, поскольку мы записываем в сокет только небольшое количество данных (ко- манда GET следующей функции) и считаем, что эти данные занимают значительно меньше места, чем имеется в буфере отправки сокета. Даже если из-за установ- ленного флага отсутствия блокировки при вызове функции write происходит ча- стичное копирование, наша функция writen обрабатывает эту ситуацию. Если оставить сокет неблокируемым, это не повлияет на последующее выполнение функций read, потому что мы всегда вызываем функцию select для определения того момента, когда сокет станет готов для чтения. В листинге 15.12 показана функция write_get_cmd, посылающая серверу коман- ду HTTP GET. ,
15.5. Неблокируемая функция connect: клиент Web 463 Листинг 15.12. Отправка команды HTTP GET серверу //nonblock /wr i te_get_cmd. с 1 #include "web h" 2 void 3 write_get_cmd(struct file *fptr) 4 { 5 int n. 6 char 11 neEMAXLINE]. 7 n = snprintfdine. sizeof(line). GET_CMD. fptr->f_name); 8 Writen(fptr->f_fd. line. n). 9 pnntf("wrote %'d bytes for Xs\en", n. fptr->f_name) 10 fptr->f_flags » F_READING. /* сброс F_CONNECTING */ 11 FD_SET(fptr->f_fd. &rset); /* прочитаем ответ сервера */ 12 if (fptr->f_fd > maxfd) 13 maxfd = fptr->f_fd. 14 } Создание команды и ее отправка 7-9 Команда создается и пишется в сокет. Установка флагов 10-13 Устанавливается флаг F_READING, при этом также сбрасывается флаг F_CONNECT I NG (если он установлен). Это указывает основному циклу, что данный дескриптор готов для ввода. Также включается дескриптор в наборе чтения, и при необходи- мости обновляется значение maxfd. Теперь мы возвращаемся в функцию main, показанную в листинге 15.13, начи- ная с того места, где закончили в листинге 15.9. Это основной цикл программы: пока имеется ненулевое количество файлов для обработки (значение nl efttoread больше нуля), устанавливается, если это возможно, другое соединение и затем вызывается функция select для всех активных дескрипторов, обрабатывающая как завершение неблокируемых соединений, так и прием данных. Можем ли мы инициировать другое соединение? 24-35 Если мы не дошли до заданного предела одновременных соединений и есть до- полнительные соединения, которые нужно установить, мы ищем еще не обрабо- танный файл (на него указывает нулевое значение f_fl ags) и вызываем функцию start_connect для инициирования соединения. Число активных соединений уве- личивается на единицу (псопп), а число соединений, которые нужно установить, на единицу уменьшается (nl efttoconn). Функция select: ожидание событий 36-37 Функция sei ect ожидает готовности сокета либо для чтения, либо для записи. Дескрипторы, для которых в настоящий момент происходит установление соеди- нения (неблокируемая функция connect находится в процессе выполнения), бу- дут включены в обоих наборах, в то время как дескрипторы с завершенным со- единением, ожидающие данных от сервера, будут включены только в наборе чтения, д.ц >,«„>
имва io. пеолокируемый ввод-вывод Листинг 15.13. Основной цикл функции main //nonblock/web с 24 while (nlefttoread > 0) { 25 while (nconn < maxnconn && nlefttoconn > 0) { 26 /* 4find a file to read */ 27 for (i = 0. i < nfiles, i++) 28 if (file[i] f_flags == 0) 29 break. 30 if (i == nfiles) 31 err_quit("nlefttoconn = 2d but nothing found”, nlefttoconn). 32 start_connect(&file[i]): 33 nconn++. 34 nlefttoconn--, 35 } 36 rs = rset. 37 ws = wset 38 n = Select(maxfd + 1. &rs. &ws. NULL. NULL); 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 for (i = 0 i < nfiles. i++) { flags = file[i] f_f1ags. if (flags == 0 || flags & F_DONE) continue. fd = file[i] f_fd. if (flags & F_CONNECTING && (FD_ISSET(fd. &rs) || FD_ISSET(fd. &ws))) { n = sizeof(error). if (getsockopt(fd S0L_S0CKET. SOJRROR, &error. &n) < 0 |J error != 0) { err_ret(“nonblocking connect failed for Xs”. file[i] f_name). } /* соединение установлено */ printfC'connection established for Xs\en", file[i] f_name); FD_CLR(fd. &wset). /* отключаем запись в этот сокет */ write_get_cmd(&file[i]). /* writeO команду GET */ 56 } else if (flags & F_READING && FD_ISSET(fd. &rs)) { 57 if ( (n = Read(fd. buf. sizeof(buf))) == 0) { 58 printf("end-of-fi1e on Xs\en“, file[i] f_name). 59 Close(fd). 60 fi1e[i] f_flags = F_D0NE. /* сбрасывает флаг F_READING */ 61 FD_CLR(fd. &rset). 62 nconn--, 63 nlefttoread--. 64 } else { 65 printf("read 2d bytes from 2s\en". n. file[i] f_name). 66 } 67 } 6B } 69 } 70 exit(0). 71 } Обработка всех готовых дескрипторов 55 Теперь мы обрабатываем каждый элемент массива структур file, чтобы опре- делить, какие дескрипторы нужно обрабатывать. Если установлен флаг F CON-
15.5. Неблокируемая функция connect: клиент Web 465 NECTING и дескриптор включен либо в наборе чтения, либо в наборе записи, небло- кируемая функция connect завершается. Как мы говорили при описании листин- га 15.7, мы вызываем функцию getsockopt, чтобы получить ошибку для сокета, ожидающую обработки. Если это значение равно нулю, соединение успешно за- вершилось. В этом случае мы выключаем дескриптор в наборе флагов записи и вы- зываем функцию wh te_get_ond для отправки запроса HTTP серверу. Проверка, есть ли у дескриптора данные 56-67 Если установлен флаг F_READING и дескриптор готов для чтения, мы вызываем функцию read. Если соединение было закрыто другим концом, мы закрываем со- кет, устанавливаем флаг F_DONE, выключаем дескриптор в наборе чтения и умень- шаем число активных соединений и общее число соединений, требующих обра- ботки. Есть два способа оптимизации, которые мы не используем в этом примере (что- бы не усложнять его еще больше). Во-первых, мы можем завершить цикл for в листинге 15.13, когда мы обработали число дескрипторов, которые, по сообще- нию функции sei ect, были готовы. Во-вторых, мы могли, где это возможно, умень- шить значение maxfd, чтобы функция sei ect не проверяла биты дескрипторов, ко- торые уже сброшены. Поскольку число дескрипторов, используемых в этом коде, в любой момент времени, вероятно, меньше 10, а не порядка тысяч, вряд ли ка- кая-либо из этих оптимизаций стоит дополнительных усложнений. Эффективность одновременных соединений Каков выигрыш в эффективности при установлении множества одновременных соединений? В табл. 15.1 показано время, необходимое для выполнения опреде- ленной задачи, которая состоит в том, чтобы получить от web-сервера домашнюю страницу и девять картинок. Время обращения RTT для данного соединения с сервером равно приблизительно 150 миллисекундам. Размер домашней стра- ницы 4017 байт, а средний размер девяти файлов с изображениями составил 1621 байт. Размер сегмента TCP равен 512 байтам. Для сравнения мы также пред- ставляем в этой таблице значения для версии данной программы с использова- нием потоков, которую мы создаем в разделе 23.9. Таблица 15.1. Время выполнения задания для разного количества одновремен- ных соединений в разных версиях программы Количество одновре- менных соединений Затраченное время (в секундах), отсутствие блокирования Затраченное время (в секундах), использование потоков 1 6,0 6,3 2 4,1 4,2 3 3,0 3,1 4 2,8 3,0 5 2,5 2,7 6 2,4 2,5 7 2,3 2,3 8 2,2 2,3 9 2,0 2,3
466 Глава 15. Неблокируемый ввод-вывод Максимальное увеличение эффективности происходит при трех одновремен- ных соединениях (время уменьшается вдвое), а при четырех и более одновремен- ных соединениях прирост производительности значительно меньше. ПРИМЕЧАНИЕ ---------------------------------------------------------- Мы показали пример использования одновременных соединений, поскольку он слу- жит хорошей иллюстрацией применения неблокируемого ввода-вывода, а также пото- му, что в данном случае эффективность применения одновременных соединений мо- жет быть измерена. Это свойство также используется в популярном приложении — * web-браузере Netscape. В этой технологии могут появиться некоторые «подводные кам- ни», если сеть перегружена. В главе 21 [94] подробно описываются алгоритмы TCP, называемые алгоритмами медленного старта (slow start), и предотвращения перегруз- ки сети (congestion avoidance). Когда от клиента к серверу устанавливается множество соединений, то взаимодействие между соединениями на уровне TCP отсутствует, то есть если на одном из соединений происходит потеря пакета, другие соединения с тем же сервером не получают соответствующего уведомления, и вполне возможно, что дру- гие соединения вскоре также столкнутся с потерей пакетов, пока не замедлятся. По этим дополнительным соединениям будет продолжаться отправка слишком большого количества пакетов в уже перегруженную сеть. Эта технология также увеличивает на- грузку на сервер. 15.6. Неблокируемая функция accept Как было сказано в главе 6, функция select сообщает, что прослушиваемый со- кет готов для чтения, когда установленное соединение готово к обработке функ- цией accept. Следовательно, если мы используем функцию select для определе- ния готовности входящих соединений, то нам не нужно делать прослушиваемый сокет неблокируемым, потому что когда функция sei ect сообщает нам, что со- единение установлено, функция accept обычно не является блокируемой. К сожалению, существует определенная проблема, связанная с временем, спо- собная запутать нас [31]. Чтобы увидеть эту проблему, изменим код нашего эхо- клиента TCP (см. листинг 5.3) таким образом, чтобы после установления соеди- нения серверу отсылался сегмент RST. В листинге 15.14 представлена новая версия. Листинг 15.14. Эхо-клиент TCP, устанавливающий соединение и посылающий серверу сегмент RST //nonblock/tcpcli03 с 1 include "unp h” 2 int 3 main(int argc. char **argv) 4 { 5 int sockfd. 6 struct linger ling, 7 struct sockaddr in servaddr: 8 if (argc '= 2) 9 err_quit(”usage tcpcli <IPaddress>”). 10 sockfd = Socket(AF_INET SOCK_STREAM, 0).
15.5. Неблокируемая функция accept 467 11 bzero(&servaddr. sizeof(servaddr)). 12 servaddr sinjamily = AFJNET. 13 servaddr sin_port = htons(SERVJORT). 14 I net_pton( AFJNET. argv[l], &servaddr sin_addr), 15 Connect(sockfd. (SA *) &servaddr. sizeof(servaddr)), 16 ling lonoff = 1, /* для отправки сегмента RST при закрытий*соединения */ 17 ling IJinger = 0. 18 Setsockopt(sockfd. SOL SOCKET. SOJINGER. &ling. sizeof(ling)). 19 Close(sockfd). 20 exit(0). 21 } Установка параметра сокета SOJLINGER 16-19 Как только соединение устанавливается, мы задаем параметр сокета S0JJNGER, устанавливая флаг l_onoff в единицу и обнуляя время IJinger. Как утвержда- лось в разделе 7.5, это вызывает отправку RST на сокете TCP при закрытии со- единения. Затем с помощью функции cl ose мы закрываем сокет. Затем мы изменяем наш сервер TCP, приведенный в листингах 6.3 и 6.4, с тем чтобы после сообщения функции select о готовности прослушиваемого сокета для чтения, но перед вызовом функции accept наступала пауза. В следующем коде, взятом из начала листинга 6.4, две добавленные строки помечены знаком +. if (FDJSSET(1istenfd. &rset)) { /* новое соединение */ + printf("listem ng socket readable\en"). + sleep(5). cl lien = sizeof(cliaddr). connfd = Accept(listenfd. (SA*) &cliaddr. &clilen). Здесь мы имитируем занятый сервер, который не может вызвать функцию accept сразу же, как только функция select сообщит, что прослушиваемый сокет готов для чтения. Обычно подобное замедление со стороны сервера не вызывает проблем (на самом деле именно для этих ситуаций предусмотрена очередь пол- ностью установленных соединений). Но поскольку после установления соедине- ния от клиента прибыл сегмент RST, у нас возникает проблема. В разделе 5.11 мы отмечали, что когда клиент разрывает соединение до того, как сервер вызывает функцию accept, в Беркли-реализациях прерванное соеди- нение не возвращается серверу, в то время как другие реализации должны воз- вращать ошибку ECONNABORTED, но часто вместо нее возвращают ошибку EPROTO. Рассмотрим Беркли-реализацию. <> Клиент устанавливает соединение и затем прерывает его, как показано в лис- тинге 15.14. й Функция sel ect сообщает процессу сервера, что дескриптор готов для чтения, но у сервера вызов функции accept занимает некоторое, хотя и непродолжи- тельное, время. « После того как сервер получил сообщение от функции sel ect и прежде, чем была вызвана функция accept, прибыл сегмент RST от клиента. # Установленное соединение удаляется из очереди, и мы предполагаем, что не существует никаких других установленных соединений. ' •
468 Глава 15. Неблокируемый ввод-вывод Сервер вызывает функцию accept, но поскольку установленных соединений нет, он оказывается заблокирован. Сервер останется блокированным в вызове функции accept до тех пор, пока какой-нибудь другой клиент не установит с ним соединение. Но если сервер ана- логичен показанному в листинге 6 4, в это время он заблокирован в вызове функ- ции accept и не может обрабатывать никакие другие готовые дескрипторы ПРИМЕЧАНИЕ ------------------------------------------------------ Проблема в некоторой степени аналогична проблеме, называемой атакой типа «отказ в обслуживании», описанной в разделе 6 8 Однако в данном случае сервер выходит из состояния блокировки, как только другой клиент установит соединение Чтобы решить эту проблему, нужно соблюдать два следующих правила: 1. Всегда делать прослушиваемый сокет неблокируемым, если мы используем функцию select для определения того, готово ли соединение к обработке функ- цией accept. 2. Игнорировать следующие ошибки, возникающие при повторном вызове функ- ции accept: EWOULDBLOCK (для Беркли-реализаций, когда клиент разрывает со- единение), ECONNABORTED (для реализаций Posix 1g, когда клиент разрывает соединение), EPROTO (для реализаций SVR4, когда клиент разрывает соедине- ние) и EINTR (если перехватываются сигналы). 15.7. Резюме В примере неблокируемого чтения и записи в разделе 15.2 использовался наш клиент str_cli, который мы изменили для применения неблокируемого ввода- вывода на соединении TCP с сервером. Функция select обычно используется с неблокируемым вводом-выводом для определения того момента, когда дескрип- тор станет готов для чтения или записи. Эта версия нашего клиента является са- мой быстродействующей из всех показанных версий, хотя требует нетривиаль- ного изменения кода. Затем мы показали, что проще разделить процесс клиента на две части при помощи функции fork. Мы используем ту же технологию при создании потоков в листинге 23.1. Неблокируемая функция connect позволяет нам во время трехэтапного руко- пожатия TCP выполнять другие задачи вместо блокирования в вызове функции connect. К сожалению, с этими функциями также связана проблема совместимости, так как различные реализации по-разному указывают, успешно ли установлено соединение или произошла ошибка. Мы использовали неблокируемые соедине- ния для создания нового клиента, аналогичного web-клиенту, открывающему одновременно множество соединений TCP для уменьшения затрат времени при получении нескольких файлов от сервера Подобное иницииирование множества соединений может сократить временные затраты, но также является «недруже- ственным по отношению к сети», поскольку не позволяет воспользоваться ал- горитмом TCP, предназначенным для предотвращения перегрузки (congestion avoidance).
Упражнения 469 Упражнения 1. Обсуждая листинг 15 6, мы отметили, что родительский процесс должен вы- звать функцию shutdown, а не функцию cl ose Почему? 2 Что произойдет в листинге 15.6, если процесс сервера завершится преждевре- менно и дочерний процесс получит признак конца файла, но не уведомит об этом родительский процесс? 3 Что произойдет в листинге 15 6, если родительский процесс непредвиденно завершится до завершения дочернего процесса, и дочерний процесс затем счи- тает конец файла на сокете? 4. Что произойдет в листинге 15.7, если мы удалим следующие две строки’ if (п ” 0) goto done /* функция connect завершилась немедленно */ 5. В разделе 15.3 мы сказали, что возможна ситуация, когда данные для сокета придут раньше, чем завершится функция connect. Когда это может случиться?
ГЛАВА 16 Операции функции ioctl 16.1. Введение Функция ioctl традиционно являлась системным интерфейсом, используемым для всего, что не входило в какую-либо другую четко определенную категорию. Posix постепенно избавляется от функции ioctl, создавая заменяющие ее функ- ции-обертки и стандартизуя их функциональность. Например, доступ к интер- фейсу терминала Unix традиционно осуществлялся с помощью функции ioctl, но в Posix. 1 были созданы 12 новых функций для терминалов: tcgetattr для полу- чения атрибутов терминала, tcf 1 ush для сброса незавершенного ввода или выво- да, и т. д. Аналогичным образом Posix. 1g заменяет одну функцию ioctl: новая функция sockatmark (см. раздел 21.3) заменяет SIOCATMARK ioctl. Тем не менее раз- личные функции ioctl остаются для зависящих от реализации свойств, относя- щихся к сетевому программированию, например для получения информации об интерфейсе и обращения к таблице маршрутизации и кэшу ARP (Address Resolu- tion Protocol — протокол разрешения адресов). В этой главе представлен обзор вызовов функции ioctl, имеющих отношение к сетевому программированию, многие из которых зависят от реализации. Кроме того, более новые Беркли-реализации используют сокеты семейства AF_ROUTE (мар- шрутизирующие сокеты) для выполнения многих из этих операций. Маршрути- зирующие сокеты мы рассматриваем в главе 17. Обычно сетевые программы (как правило, серверы) используют функцию i octi для получения информации обо всех интерфейсах узла при запуске программы, с тем чтобы узнать адрес интерфейса, выяснить, поддерживает ли интерфейс широковещательную передачу, многоадресную передачу и т. д. Для возвращения этой информации мы разработали нашу собственную функцию. В этой главе мы представляем ее реализацию с применением функции ioctl, а в главе 17 — дру- гую реализацию, использующую маршрутизирующие сокеты. 16.2. Функция ioctl Эта функция работает с открытым файлом, на который делается ссылка при по- мощи аргумента fd. #include <umstd h> int ioctl(int fd. int request /* void *arg */ ).
16.2. Функция ioctl 471 Третий аргумент всегда является указателем, но тип указателя зависит от ар- гумента request. ПРИМЕЧАНИЕ------------------------------------------------------------- В 4.4BSD второй аргумент имеет тип unsigned long вместо int, но это не вызывает про- блем, поскольку в заголовочных файлах определены константы, используемые для дан- ного аргумента. Некоторые реализации определяют третий аргумент как пустой указатель (void*), а не так, как он определен в ANSI С. Не существует единого стандарта заголовочного файла, определяющего прототип функ- ции для ioctl, поскольку он не стандартизован в Posix. Многие системы определяют этот прототип в файле <unistd.h>, как это показываем мы, но традиционные системы BSD определяют его в заголовочном файле <sys/ioctl.h>. Мы можем разделить аргументы requests, имеющие отношение к сети, на шесть категорий: операции с сокетами; С операции с файлами; м операции с интерфейсами; ж операции с кэшем ARP; S операции с таблицей маршрутизации; операции с потоками (см. главу 33). Помимо того что, как показывает табл. 7.5, некоторые операции ioctl пере- крывают часть операций fcntl (например, установка неблокируемого сокета), существуют также некоторые операции, которые с помощью функции i octi мож- но задать более чем одним способом (например, смена групповой принадлежнос- ти сокета). В табл. 16.1 перечислены аргументы requests вместе с типами данных, на ко- торые должен указывать адрес агд. В последующих разделах эти вызовы рассмат- риваются более подробно. Таблица 16.1. Обзор сетевых вызовов ioctl Категория request Описание Тип данных Сокет SIOCATMARK Находится ли указатель чтения сокета па отметке внеполосных данных mt SIOCSPGRP Установка идентификатора процесса или идентификатора группы процессов для сокета int SIOCGPGRP Получение идентификатора процесса или идентификатора группы процессов для сокета int Файл FIONBIO Установка/сброс флага отсутствия int блокировки FIOASYNC Устаповка/сброс флага асинхронного ввода-вывода int FIONREAD Получение количества байтов в приемном буфере int
472 Глава 16. Операции функции ioctl Таблица 16.1 (продолжение) Категория request Описание Тип данных FIOSETOWN Установка идентификатора процесса или идентификатора группы процессов для файла int FIOGETOWN Получение идентификатора процесса или идентификатора группы процессов для файла int Интерфейс SIOCGIFCONF Получение списка всех интерфейсов struct ifeonf SIOCSIFADDR Установка адреса интерфейса struct ifreq SIOCGIFADDR Получение адреса интерфейса struct ifreq SIOCSIFFLAGS Установка флагов интерфейса struct ifreq SIOCGIFFLAGS Получение флагов интерфейса struct ifreq SIOCSIFDSTADDR Установка адреса типа «точка-точка» struct ifreq SIOCGIFDSTADDR Получение адреса типа «точка-точка» struct ifreq SfOCGfFBRDADDR Получение широковещательного адреса struct ifreq SIOCSIFBRDADDR Установка широковещательного адреса struct ifreq SIOCGIFNETMASK Получение маски подсети struct ifreq SIOCSIFNETMASK Установка маски подсети struct ifreq SIOCGIFMETRIC Получение метрики интерфейса struct ifreq SIOCSIFMETRIC Установка метрики интерфейса struct ifreq SIOCxxx (Множество вариантов в зависимости от реализации) ARP SIOCSARP Созданне/модификация элемента ARP struct arpreq SIOCGARP Получение элемента ARP struct arpreq SIOCDARP Удаление элемента ARP struct arpreq Маршру- SIOCADDRT Добавление маршрута struct itentry тизация SIOCDELRT Удаление маршрута struct rtentry Потоки 1_ш (См. раздел 33.5) м. 16.3. Операции с сокетами Существует три типа вызова, или запроса (в зависимости от значения аргумента request), функции ioctl, предназначенные явным образом для сокетов [105, с. 551- 553]. Все они требуют, чтобы третий аргумент функции ioctl был указателем на целое число. в SIOCATMARK. Возвращает указатель на ненулевое значение в качестве третьего аргумента (его тип, как только что было сказано, — указатель на целое число), если указатель чтения сокета в настоящий момент находится на отметке вне- полосных данных (out-of-band mark), или указатель на нулевое значение, если указатель чтения сокета не находится на этой отметке. Более подробно внепо- лосные данные (out-of-band data) рассматриваются в главе 21. Posix.lg заме- няет этот вызов функцией sockatmark, и мы рассматриваем реализацию этой новой функции с использованием функции ioctl в разделе 21.3. И SIOCGRP. Возвращает в качестве третьего аргумента указатель на целое число — идентификатор процесса или группы процессов, которым будут посылаться
16.5. Конфигурация интерфейса 473 сигналы SIGI0 или SIGURG по окончании выполнения асинхронной операции или при появлении срочных данных. Этот вызов идентичен вызову F_GETOWN функции fcntl, и в табл. 7.5 мы отмечали, что Posix.lg стандартизирует функ- цию fcntl. SIOCSPGRP. Задает идентификатор процесса или группы процессов для отсыл- ки им сигналов SIGIO или SIGURG как целое число, на которое указывает третий аргумент. Этот вызов идентичен вызову F_SETOWN функции fcntl, и в табл. 7.5 мы отмечали, что Posix.lg стандартизирует функцию fcntl. 16.4. Операции с файлами Следующая группа вызовов начинается с FI0 и может применяться к определен- ным типам файлов в дополнение к сокетам. Мы рассматриваем только вызовы, применимые к сокетам [105, с. 553]. Следующие пять вызовов требуют, чтобы третий аргумент функции i octi ука- зывал на целое число. Ж FIONBIO. Флаг отключения блокировки при выполнении операций ввода-вы- вода сбрасывается или устанавливается в зависимости от третьего аргумента функции 1 oct 1. Если этот аргумент является пустым указателем, то флаг сбра- сывается (блокировка разрешена). Если же третий аргумент является указа- телем на единицу, то включается неблокируемый ввод-вывод. Этот вызов об- 1 ладает таким же действием, что и команда F_S ETFL функции fcntl, которая позволяет установить или сбросить флаг O_NONBLOCK, задающий статус файла. FIOASYNC. Флаг, управляющий получением сигналов асинхронного ввода-вы- вода (SIGIO), устанавливается или сбрасывается для сокета в зависимости от того, является ли третий аргумент функции ioctl пустым указателем. Этот флаг имеет то же действие, что и флаг статуса файла O_ASYNC, который можно установить и сбросить с помощью команды FSETFL функции ioctl. ? FIONREAD. Возвращает число байтов, в настоящий момент находящихся в при- емном буфере сокета, как целое число, на которое указывает третий аргумент функции ioctl. Это свойство работает также для файлов, каналов и термина- лов. Более подробно об этом вызове мы рассказывали в разделе 13.7. FIOSETOWN. Эквивалент SIOCSPGRP для сокета. > FIOGETOWN. Эквивалент SIOCGPGRP для сокета. 16.5. Конфигурация интерфейса Один из шагов, выполняемых многими программами, работающими с сетевыми интерфейсами системы, — это получение от ядра всех интерфейсов, сконфигури- рованных в системе. Это делается с помощью вызова SIOCGIFCONF, использующего структуру 1 fconf, которая, в свою очередь, использует структуру i freq. Обе эти структуры показаны в листинге 16.1*. 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter com/download.
474 Глава 16. Операции функции ioctl Листинг 16.1. Структуры ifconf и ifreq, используемые в различных вызовах функции ioctl, относящихся к интерфейсам <net/if h> struct ifconf { int ifc_len /* размер буфера «значение-результат» */ union { caddr_t ifcu_buf /* ввод от пользователя к ядру */ struct ifreq *ifcu_req /* ядро возвращает пользователю */ } ifc_ifcu } #define ifcjjuf ifc_ifcu ifcu_buf /* адрес буфера */ #define ifc_req ifc_ifcu ifcu_req /* массив возвращенных структур */ #define IFNAMSIZ 16 struct ifreq { char ifr_name[IFNAMSIZ] /* имя интерфейса например 'leO" */ union { struct sockaddr ifru_addr struct sockaddr ifru_dstaddr struct sockaddr ifru_broadaddr, 1 short ifru_flags ' int ifrujrietric caddr_t ifru_data } ifr_ifru } #def1пе ifr_addr ifr_ifru ifru_addr /* адрес */ #define ifr_dstaddr i f r_i f ru ifru_dstaddr /* другой конец линии передачи называемый «точка-точка» */ #def1ne ifr_broadaddr i f r_i f ru ifru_broadaddr /* широковещательный адрес */ #define ifr_f1ags i fr_ifru ifru_f1ags /* флаги */ #define ifrnietric ifr_ifru ifrujnetric /* метрика */ #def ine ifr_data i fr_ifru ifru_data /* с использованием интерфейса */ Прежде чем вызвать функцию ioctl, мы выделяем в памяти место для буфера и для структуры 1 fconf, а затем инициализируем эту структуру. Мы показываем это на рис. 16.1, предполагая, что наш буфер имеет размер 1024 байта Третий ар- гумент функции ioctl — это указатель на нашу структуру ifconf. ifconf{} Рис. 16.1. Инициализация структуры ifconf перед вызовом SIOCGIFCONF Если мы предположим, что ядро возвращает две структуры ifreq, то при за- вершении функции ioctl мы можем получить ситуацию, представленную на рис. 16 2. Затененные области были изменены функцией i octi. Буфер заполняет-
16 6 Функция getjfijnfo 475 ся двумя структурами, и элемент i fc_l еп структуры i fconf обновляется, с тем чтобы соответствовать количеству информации, хранимой в буфере. Предполагается, что на этом рисунке каждая структура i freq занимает 32 байта. ifconf{} ifreq{} ifreq{} Рис. 16.2. Значения, возвращаемые в результате вызова SIOCGIFCONF Указатель на структуру 1 freq также используется в качестве аргумента остав- шихся функций ioctl интерфейса, показанных в табл. 16.1, которые мы описыва- ем в разделе 16 7. Отметим, что каждая структура т f req содержит объединение (urn on), а директивы компилятора #def 1 пе позволяют непосредственно обращать- ся к полям объединения по их именам. Помните о том, что в некоторых системах в объединение i f r_i f ru добавлено много зависящих от реализации элементов 16.6. Функция getjfijnfo Поскольку многим программам нужно знать обо всех интерфейсах системы, мы разработаем нашу собственную функцию get_ifi_info, возвращающую связный список структур — по одной для каждого активного в настоящий момент интер- фейса. В этом разделе мы покажем, как эта функция реализуется с помощью вы- зова SIOCGIFCONF функции ioctl, а в главе 17 мы создадим ее другую версию, ис- пользующую маршрутизирующие сокеты. ПРИМЕЧАНИЕ -------------------------------------------------- BSD/OS предоставляет функцию getifaddrs, имеющую аналогичную функциональ- . ность Поиск во всему дереву исходного кода BSD/OS 2 1 показывает, что 12 программ вы- i полняют вызов SIOCGIFCONF функции ioctl для определения присутствующих ин- терфейсов
476 Глава 16. Операции функции ioctl Сначала мы определяем структуру ifi_info в новом заголовочном файле, ко- торый называется unptfi h, показанном в листинге 16.2. Листинг 16.2. Заголовочный файл unpifi.h //ioctl/unpifi h 1 /* Наш собственный заголовочный файл для программ которым требуется информация о конфигурации интерфейса 2 Включаем этот файл вместо файла "unp h" */ 3 #ifndef __unp_ifi_h 4 #define __unp_ifi_h 5 include "unp h" 6 include <net/if h> 7 #define IFI_NAME 16 8 #define IFI_HADDR 8 9 struct ifi_info { /* то же. что и IFNAMSIZ в заголовочном файле <net/if h> */ /* с учетом 64-битового интерфейса EUI-64 в будущем */ 10 char ifi_name[IFI_NAME]. 11 u char ifi haddr[IFI HADDR], 12 u short ifi hlen. 13 short ifi_flags. 14 short ifijnyfl ags. 15 struct sockaddr *ifi_addr. 16 struct sockaddr *ifi_brdaddr. 17 struct sockaddr *ifi_dstaddr. 18 19 struct }. ifi_info *ifi_next. 20 #define IFI_ALIAS 1 /* имя интерфейса, заканчивается символом конца строки */ /* аппаратный адрес */ /* количество байтов в аппаратном адресе 0. 6. 8 */ /* константы IFF_xxx из заголовочного файла <net/if h> */ /* наши флаги IFI_xxx */ /* первичный адрес */ /* широковещательный адрес */ /* адрес получателя */ /* следующая из этих структур */ /* Tfaaddr— это псевдоним */ 21 /* прототипы функций */ 22 struct Tfi_info *get_ifi_info(int. int). 23 struct ifi_info *Get_ifi_info(int int). 24 void free_ifi_info(struct ifi_info *) 25 #endif /*_unp_ifi_h */ 9-19 Связный список этих структур возвращается нашей функцией. Элемент i f i _next каждой структуры указывает на следующую структуру. Мы возвращаем в этой структуре информацию, которая может быть востребована в типичном приложе- нии: имя интерфейса, аппаратный адрес (например, адрес Ethernet), флаги ин- терфейса (чтобы позволить приложению определить, поддерживает ли прило- жение широковещательную или многоадресную передачу и относится ли этот интерфейс к типу «точка-точка»), адрес интерфейса, широковещательный адрес, адрес получателя для связи «точка-точка». Вся память, используемая для хране- ния структур 1 fi_info вместе со структурами адреса сокета, содержащимися в них, выделяется динамически. Следовательно, мы также предоставляем функцию free_ifi_info для освобождения всей этой памяти. Перед тем как представить реализацию нашей функции i fi_i nfo, мы покажем простую программу, которая вызывает эту функцию и затем выводит информа-
16.6. Функция getjfijnfo 477 цию. Эта программа, представленная в листинге 16.3, является уменьшенной вер- сией программы if config. ПРИМЕЧАНИЕ ---------------------------------------------------------- Большинство аппаратных адресов на сегодня являются 48-разрядпыми МАС-адреса- ми (например, Ethernet, Token Ring и т. д.). Однако существует тенденция использо- вания 64-разрядных идентификаторов, называемых EUI-64 [42]. Адреса IPv6 содер- жат значение EUI-64 в младших 64 битах (см. раздел А.5), и можно простым способом инкапсулировать 48-разрядпый МАС-адрес в 64-разрядный EUI. Поэтому мы выде- ляем достаточно места в нашей структуре ifi_info для 64-разрядного идентификатора и храним длину аппаратного адреса. Листинг 16.3. Программа prifinfo, вызывающая нашу функцию ifiinfo Пл octi/рп fi nfo с 1 #include "unpifi h" 2 3 4 5 6 7 8 mt main(int argc. char **argv) struct ifijnfo *ifi. *ifihead; struct sockaddr *sa. u_char *ptr int i. family doaliases 9 10 if (argc '= 3) err_quit("usage prifinfo <inet4|inet6> <doaliases>"); 11 12 13 14 15 16 17 18 19 if (strcmp(argv[l], "inet4") == 0) family = AFJNET #ifdef IPV6 else if (strcmp(argv[l], "inet6") == 0) family = AF_INET6 #endif else err_quit(“invalid <address-fami1y>”) doaliases = atoi(argv[2]). 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 for (ifihead = ifi = Getjfijnfo(family, doaliases). ifi '= NULL ifi = ifi->ifi_next) { printfC'fcs <" ifi->ifi_name). if (ifi->ifi_flags & IFFJJP) printfC'UP “). if (ifi->ifi_flags & IFF_BROADCAST) pnntfC'BCAST “). if (ifi->ifi_flags & IFF MULTICAST) printfCMCAST "). if (ifi->ifi_flags & IFF LOOPBACK) printfC'LOOP ”) if (ifi->ifi_flags & IFF_POINTOPOINT) printf(“P2P "). printf(">\en“). if ( (i = ifi->ifi_hlen) > 0) { ptr = ifi->ifi haddr. do { printfC'fcsfcx". (i == ifi->ifi hlen) 7 *ptr++): } while (--i > 0), printf("\en"). } if ( (sa = ifi->ifi_addr) '= NULL) ’i’ printfC IP addr: fcs\en". . продолжение A'
478 Глава 16 Операции функции ioctl Листинг 16.3(продолжение) 38 Sock_ntop_host(sa sizeof(*sa))) 39 if ( (sa = ifi >ifi_brdaddr) '= NULL) 40 printf( broadcast addr ^s\en 41 Sock_ntop_host(sa sizeof(*sa))) 42 if ( (sa = ifi >ifi_dstaddr) '= NULL) 43 printf( destination addr ^s\en 44 Sock_ntop_host(sa sizeof(*sa))) 45 } 46 free_ifi_info( ifi head) 47 exit(0) 48 } 20-45 Программа представляет собой цикл for, в котором один раз вызывается функ- ция get_i fi_infо, а затем последовательно перебираются все возвращаемые струк- туры ifi_info 22-35 Выводятся все имена интерфейсов и флаги Если длина аппаратного адреса больше нуля, он выводится в виде шестнадцатеричного числа (наша функция get_ifi_info возвращает нулевую длину ifi_h1en, если адрес недоступен) 36-44 Выводятся три IP-адреса, если они возвращаются Если мы запустим эту программу на нашем узле Solans (см рис 1 7), то полу- чим следующий результат solans % pnfinfo inet4 0 loO <UP MCAST LOOP > IP addr 127 0 0 1 leO <UP BOAST MCAST > IP addr 206 62 226 33 broadcast addr 206 62 226 63 Первый аргумент командной строки inet4 задает адрес IPv4, а второй, нуле- вой аргумент указывает, что не должно возвращаться никаких псевдонимов или альтернативных имен (альтернативные имена IP-адресов мы описываем в разде- ле А 4) Обратите внимание, что в Solans аппаратный адрес интерфейса Ethernet недоступен Если мы добавим к интерфейсу Ethernet (I еО) три альтернативных имени ад- реса с идентификаторами узла 44,45 и 46 и изменим второй аргумент командной строки на 1, то получим solans % prifinfo inet4 1 loO <UP MCAST LOOP > IP addr 127 0 0 1 leO <UP BCAST MCAST > IP addr 206 62 226 33 первичный IP-адрес broadcast addr 206 62 226 63 leO 1 <UP BCAST MCAST > IP addr 206 62 226 44 первый псевдоним broadcast addr 206 62 226 63 leO 2 <UP BCAST MCAST > IP addr 206 62 226 45 второй псевдоним broadcast addr 206 62 226 63 leO 3 <UP BCAST MCAST > IP addr 206 62 226 46 третий псевдоним broadcast addr 206 62 226 63 Если мы запустим ту же программу в BSD/OS, используя реализацию фуню ции get_ifi_info, приведенную в листинге 17.9 (которая может легко получить аппаратный адрес), то получим
16 6 Функция getjfijnfo 479 bsdi % prifinfo inet4 1 weO <UP BCAST MCAST > 0 0 CO 6f 2d 40 IP addr 206 62 226 66 broadcast addr 206 62 226 95 efO <UP BCAST MCAST > 0 20 af 9c ее 95 IP addr 206 62 226 35 IP-адрес отправителя broadcast addr 206 62 226 63 efO <UP BCAST MCAST > 0 20 af 9c ее 95 IP addr 206 62 226 50 псевдоним broadcast addr 206 62 226 63 loO <UP MCAST LOOP > IP addr 127 0 0 1 В этом примере мы указали программе выводить псевдонимы, и мы видим, что один из псевдонимов определен для второго интерфейса Ethernet (ef 0) с иден- тификатором узла 50 ПРИМЕЧАНИЕ ---------------------------------------------------------------------------- Получаемый в данном случае результат зависит от того, как задаются адреса с альтер- нативными именами интерфейсов В данном примере адрес с альтернативным именем задан интерфейсу Ethernet efO, и это обычная технология для BSD/OS 2 1 Но в BSD /OS 3 0 рекомендуется задавать адреса с альтернативными именами интерфейсу 1о0 (интерфейсу закольцовки) Теперь мы покажем нашу реализацию функции get_ifi_info, использующую вызов SIOCGIFCONF функции i octi В листинге 164 показана первая часть этой функ- ции, получающая от ядра конфигурацию интерфейса Листинг 16.4. Выполнение вызова SIOCGIFCONF для получения конфигурации интерфейса //lib/get_ifi_info с 1 #include unpifi h 2 struct ifi_info * 3 get_ifi_info(int family int doaliases) 4 { 5 struct ifi_info *ifi *ifihead **ifipnext 6 int sockfd len lastlen flags myflags 7 char *ptr *buf lastname[IFNAMSIZ] *cptr 8 struct ifconf ifc 9 struct ifreq *ifr ifrcopy 10 struct sockaddr_in *sinptr 11 sockfd = Socket(AF_INET SOCKJJGRAM 0) 12 lastlen = 0 13 len = loo * sizeoflstruct ifreq) /* исходное предположение 6 размере буфера */ 14 for ( ) { 15 buf = Malloc(len) 16 ifc ifc_len = len 17 ifc ifc_buf = buf 18 if (ioctl(sockfd SIOCGIFCONF &ifc) < 0) { 19 if (errno '= EINVAL || lastlen '= 0) 20 err_sys( ioctl error ) 21 } else { л продолжение ту
480 Глава 16. Операции функции ioctl Листинг 16.4 (продолжение) 22 if (ifc ifc_len == lastlen) 23 break. /* значение len не изменилось *7 24 lastlen = ifc ifc_len, 25 } 26 len += 10 * sizeoftstruct ifreq), /* увеличиваем значение len */ 27 free(buf). 28 } 29 iflhead = NULL. 30 ifipnext = 81fihead; 31 lastnaine[0] = 0. Создание сокета Интернета 11 Мы создаем сокет UDP, который будет использоваться с функциями- ioctl. Может применяться как сокет TCP, так и сокет UDP [105, с. 163]. Выполнение вызова SIOCGIFCONF в цикле 12-28 Фундаментальной проблемой, связанной с вызовом SIOCGIFCONF, является то, что некоторые реализации не возвращают ошибку, если буфер слишком мал для хранения полученного результата [105, с. 118-119]. В этом случае результат про- сто обрезается, так чтобы поместиться в буфер, и функция ioctl возвращает ну- левое значение, что соответствует успешному выполнению. Это означает, что единственный способ узнать, достаточно ли велик наш буфер, — сделать вызов, сохранить возвращенную длину, снова сделать вызов с большим размером буфе- ра и сравнить полученную длину со значением, сохраненным из предыдущего вызова. Только если эти две длины одинаковы, наш буфер можно считать доста- точно большим. ПРИМЕЧАНИЕ----------------------------------------------------------------- Беркли-реализации пе возвращают ошибку, если буфер слишком мал [ 105, с. 118-199], и результат просто обрезается так, чтобы поместиться в существующий буфер. Solaris 2.5 возвращает ошибку EINVAL, если возвращаемая длина больше или равна длине буфе- ра. Но мы не можем считать вызов успешным, если возврашаемая длина меньше раз- мера буфера, поскольку Беркли-реализации могут возвращать значение, меньшее раз- мера буфера, если часть структуры в него не помещается. В некоторых реализациях предоставляется вызов SIOCGIFNUM, который возвращает число интерфейсов. Это позволяет приложению перед выполнением вызова SIOCGIF- CONF выделить в памяти место для буфера достаточного размера, но такой подход не является широко распространенным. Выделение в памяти места под буфер фиксированного размера для результата вызова SIOCGIFCONF стало проблемой с ростом Сети, поскольку большие web-серверы ис- пользуют много альтернативных адресов для одного интерфейса. Например, в Solaris 2.5 был предел в 256 альтернативных адресов для интерфейса, по в версии 2.6 этот предел вырос до 8192. Обнаружилось, что на сайтах с большим числом альтернативных адре- сов перестают работать программы с буферами фиксированного размера для размеще- ния информации об интерфейсе. Хотя Solaris возвращает ошибку, если буфер слиш- ком мал, эти программы размещают в памяти буфер фиксированного размера, запускают функцию ioctl, по затем перестают работать при возвращении ошибки.
16.6. Функция getjfijnfo 481 L2-15 Мы динамически размещаем в памяти буфер, начиная с размера, достаточного для 100 структур 1 freq. Мы также отслеживаем длину, возвращаемую последним вызовом SIOCGIFCONF в lastlen, и инициализируем ее нулем. .9-20 Если функция ioctl возвращает ошибку EINVAL и функция еще не возвращалась успешно (то есть lastlen все еще равно нулю), значит, мы еще не выделили буфе- ра достаточного размера, поэтому мы продолжаем выполнять цикл. >2-23 Если функция 1 octi завершается успешно и возвращаемая длина равна last!еп, значит, длина не изменилась (наш буфер имеет достаточный размер) и мы с по- мощью функции break выходим из цикла, так как у нас имеется вся информация. ?6-27 В каждом проходе цикла мы увеличиваем размер буфера для хранения еще 10 структур ifreq. Инициализация указателей связного списка >9-31 Поскольку мы будем возвращать указатель на начало связного списка струк- тур 1 f i_i nfo, мы используем две переменные, i f i head и i f i pnext, для хранения ука- зателей на список по мере его создания. Это та же технология, которую мы опи- сывали применительно к листингу 11.24. Следующая часть нашей функции getjfijnfo, содержащая начало основно- го цикла, показана в листинге 16.5. Листинг 16.5. Конфигурация интерфейса процесса /Л ib/get_ifi_info с 32 for (ptr = buf. ptr < buf + ifc ifc_len ) { 33 ifr = (struct ifreq *) ptr I 34 #ifdef HAVE_SOCKADDR_SA_LEN 35 len = max(sizeof(struct sockaddr) ifr->ifr_addr.sa_letu. 36 #else 37 switch (ifr->ifr_addr sa_family) { 38 #ifdef IPV6 39 case AFJNET6 40 len = sizeoftstruct sockaddr_in6). 41 break 42 #endif 43 case AFJNET 44 default 45 len = sizeoftstruct sockaddr). 46 break. 47 } I 48 lendif /* HAVE_SOCKADDR_SA_LEN */ ptr += sizeof(ifr->ifr_name) + len. /* для следующей в буфере */ if (ifr->ifr_addr sajamily '= family) continue. /* игнорируем если это не искомое семейство адресов */ myflags = 0. >f ( (cptr = strchr(ifr->ifr_name, ' ')) '= NULL) *cptr = 0. /* заменяем двоеточие на нуль */ if (strncmpdastname, ifr->ifr_name. IFNAMSIZ) == 0) { if (doaliases == 0) continue. /* этот интерфейс уже обработан */ myflags = IFI_ALIAS, } продолжение & 49 50 51 52 " 53 54 55 56 57 г , 58 59 , '
482 Глава 16. Операции функции ioctl Листинг 16.5 (продолжение) 60 memcpytlastname. ifr->ifr_name. IFNAMSIZ). 61 ifrcopy ° *ifr. 62 Ioctl(sockfd. SIOCGIFFLAGS. &ifrcopy). 63 flags - ifrcopy.ifr_f1ags. 64 if ( (flags & IFFJJP) — 0) 65 continue. /* игнорируем, если этот интерфейс неактивен */ 66 1 fi - Callocd. sizeoftstruct ifi_info)) 67 *ifipnext ifi, /* prev указывает на созданную структуру */ 68 ifipnext - &ifi->ifi_next. /* сюда указывает указатель на следующую Структуру */ 69 ifi'>ifi_flags - flags. /* значения IFF_xxx */ 70 ifi'>ifi_myflags - myflags. /* значения IFI_xxx */ 71 memcpy(ifi->ifi_name ifr->ifr_name, IFI_NAME). 72 ifi'>ifi_natne[lFl_NAME - 1] = '\e0'. Переход к следующей структуре адреса сокета 32-49 При последовательном просмотре всех структур ifreq ifr указывает на теку- щую структуру, а мы увеличиваем ptr на единицу, чтобы он указывал на следую- щую. Необходимо предусмотреть особенность более новых систем, предоставля- ющих поле длины для структур адреса сокета, и вместе с тем учесть, что более старые системы этого поля не предоставляют. Хотя в листинге 16.1 структура адреса сокета, содержащаяся в структуре i freq, объявляется как общая структура адреса сокета, в новых системах она может относиться к произвольному типу. Действительно, в 4.4BSD структура адреса сокета канального уровня также воз- вращается для каждого интерфейса [105, с. 118]. Следовательно, если поддержи- вается элемент длины, то мы должны использовать его значение для переуста- новки нашего указателя на следующую структуру адреса сокета. В противном случае мы определяем длину, исходя из семейства адресов, используя размер об- щей структуры адреса сокета (16 байт) в качестве значения по умолчанию. ПРИМЕЧАНИЕ ------------------------------------------------- В системах, поддерживающих IPv6, не оговаривается, возвращается ли адрес IPv6 вы- зовом SIOCGIFCONF. Для более новых систем мы вводим оператор switch, в ко юром предусмотрена возможность возвращения адресов IPv6. Проблема состоит в том, что объединение в структуре ifreq определяет возвращаемые адреса как общие 16-байто- вые структуры sockaddr, подходящие для 16-байтовых структур sockaddr_m IPv4, по для 24-байтовых структур sockaddr_in6 IPv6 они слишком малы. В случае возвраще- ния адресов IPv6 возможно некорректное поведение существующего кода, созданного в предположении, что в каждой структуре ifreq содержится структура sockaddr фикси- рованного размера. 50-51 Мы игнорируем все адреса из семейств, отличных от указанного, вызывающим процессом в аргументе функции get_ini_info. Обработка альтернативных имен 52-60 Нам нужно обнаружить все альтернативные имена (псевдонимы), которые мо- гут существовать для интерфейса, то есть присвоенные этому интерфейсу допол- нительные адреса. Обратите внимание в наших примерах, следующих за листин-
16.6. Функция getjfijnfo 483 гом 16.3, что в Solaris псевдоним содержит двоеточие, в то время как в 4.4BSD имя интерфейса в псевдониме не изменяется. Чтобы обработать оба случая, мы сохраняем последнее имя интерфейса в 1 astname и сравниваем его только до дво- еточия, если оно присутствует. Если двоеточия нет, мы игнорируем этот интер- фейс в том случае, когда имя эквивалентно последнему обработанному интер- фейсу. Получение флагов интерфейса 61-65 Мы выполняем вызов SIOCGIFFLAGS функции ioctl (см. раздел 16.5), чтобы по- лучить флаги интерфейса. Третий аргумент функции ioctl — это указатель на структуру 1 freq, содержащую имя интерфейса, для которого мы хотим получить флаги. Мы создаем копию структуры i freq, перед тем как запустить функцию ioctl, поскольку в противном случае этот вызов перезаписал бы IP-адрес интер- фейса, потому что оба они являются элементами одного и того же объединения из листинга 16.1. Если интерфейс не активен, мы игнорируем его. Выделение памяти и инициализация структуры ifijnfo 66-72 На этом этапе мы знаем, что возвратим данный интерфейс вызывающему про- цессу. Мы выделяем память для нашей структуры ifijnfo и добавляем ее в конец связного списка, который мы создаем. Мы копируем флаги и имя интерфейса в эту структуру. Далее мы проверяем, что имя интерфейса заканчивается нулем, и поскольку функция cal 1 ос инициализирует выделенную в памяти область ну- лями, мы знаем, что ifijilen инициализируется нулем, a ifi_next — пустым указа- телем. Листинг 16.6. Получение и возвращение адресов интерфейса //1 octi /get_ifi_info с 73 switch (ifr->ifr_addr sajamily) { 74 case AFJNET 75 sinptr = (struct sockaddr_in *) &ifr->ifr_addr. 76 if (ifi->ifi_addr == NULL) { 77 ifi->ifi_addr = CallocCl sizeoftstruct sockaddr_in)): 78 tnemcpy(ifi->ifi_addr. sinptr sizeoftstruct sockaddr_in)); 79 flfdef SIOCGIFBRDADDR 80 if (flags & IFF_BROADCAST) { 81 Ioctl(sockfd SIOCGIFBROAOOR Xifrcopy) 82 sinptr = (struct sockaddr_in *) Xifrcopy ifr_broadaddr, 83 ifi->ifi_brdaddr = Callocd sizeoftstruct sockaddr_in)). 84 metncpy(ifi->ifi_brdaddr. sinptr sizeoftstruct sockaddr_in)). 85 } 86 #endif 87 lifdef SIOCGIFDSTADDR 88 if (flags & IFF_POINTOPOINT) { 89 Ioctl(sockfd SIOCGIFDSTADDR &ifrcopy), 90 sinptr = (struct sockaddr_in *) &ifrcopy ifr_dstaddr. 91 ifi->ifi_dstaddr = Callocd sizeoftstruct sockaddr_in)). 92 memcpy(ifi->ifi_dstaddr. sinptr sizeoftstruct sockaddr_in)); 93 > } 94 #£ndi f . 95 } j < 1 продолжение
484 Глава 16. Операции функции ioctl Листинг 16.6 (продолжение) 96 break 97 default 98 break. 99 } 100 } 101 free(buf) 102 return (ifihead) /* указатель на первую структуру в связном списке */ 103 } 73-78 Мы копируем IP-адрес, возвращенный из нашего начального вызова SIOCGIFCONF функции ioctl, в структуру, которую мы создаем. 79-96 Если интерфейс поддерживает широковещательную передачу, мы получаем широковещательный адрес с помощью вызова SIOCGIFBRDADDR функции i octi. Мы выделяем память для структуры адреса сокета, содержащей этот адрес, и добав- ляем ее к структуре ifi_info, которую мы создаем. Аналогично, если интерфейс является интерфейсом типа «точка-точка», вызов SIOCGIFBRDADDR возвращает IP- адрес другого конца связи. ПРИМЕЧАНИЕ --------------------------------------------------------------------------- Здесь отсутствует case для AF INET6, поскольку, как мы щмсчалп ранее, псизвсс i по, возвратит ли реализация IPv6 при вызове SIOCGIFCONF именно IPv6-anpec. В листинге 16.7 показана функция free_ifi_info, которой передается указа- тель, возвращенный функцией get_i fi_info. Эта функция освобождает всю дина- мически выделенную память. Листинг 16.7. Функция freejfijnfo: освобождение памяти, которая была динами- чески выделена функцией getjfijnfo 7/ioctl/getjfi jnfo с 104 void 105 freejfi_info(struct ifijnfo *ifihead) 106 { 107 struct ifijnfo *ifi *ifinext. 108 for (ifi = ifihead ifi l= NULL ifi = ifinext) { 109 if (ifi->ifi_addr '= NULL) 110 free(ifi->ifi_addr) ' 111 if (ifi->ifi_brdaddr '= NULL) 112 free(ifi->ifi_brdaddr) 113 if (ifi->ifi_dstaddr l= NULL) 114 free(ifi->ifi_dstaddr). 115 ifinext = ifi->ifijiext, /* невозможно получить ifijiext. после того как функция freeO */ 116 free(ifi) /* освободили память, которая была занята структурой ifi_info{} */ 117 } 118 } 16.7. Операции с интерфейсами Как мы показали в предыдущем разделе, запрос SIOCGIFCONF возвращает имя и структуру адреса сокета для каждого сконфигурированного интерфейса. Суще- ствует множество других вызовов, позволяющих установить или получить все
16.8. Операции с кэшем ARP 485 остальные характеристики интерфейса. Версия get этих вызовов (SIOCGxxx) часто запускается протраммой netstat, а версия set (SIOCSxxx) — программой if con fig. Любой пользователь может получить информацию об интерфейсе, в то время как установка этой информации требует прав привилегированного пользователя. Эти вызовы получают или возвращают структуру i freq, адрес которой задает- ся в качестве трегьего аргумента функции ioctl. Интерфейс всегда идентифици- руется по имени: 1 еО, 1 оО, рррО, — то есть по имени, заданному в элементе i frjiame структуры 1 freq. Многие из этих запросов используют структуру адреса сокета, для того чтобы задать или возвратить IP-адрес или маску адреса. Для IPv4 адрес или маска со- держится в элементе si n_addr из структуры адреса сокета Интернета. SIOCGIFADDR. Возвращаег адрес направленной передачи в элементе ifr_addr. SI DCS I FADDR. Устанавливает адрес интерфейса из элемент а i fr_addr. Также вы- зывается функция инициализации для интерфейса. ’ - SIOCGIFFLAGS. Возвращаег флаги интерфейса в элементе i fr_flags. Имена раз- личных флагов определяются в виде IFF_xxx в заголовочном файле <net/i f h>. Флаги указывают, например, включен ли интерфейс (IFF UP), является ли он интерфейсом типа «точка-точка» (IFF POINTOPOINT), поддерживает ли широко- вещательную передачу (IFF_BROADCAST) и т. д. SIOCSIFFLAGS. Устанавливает флаги из элемента i f r_fl ags. ’ SIOCGIFDSTADDR. Возвращает адрес типа «точка-точка» в элементе i fr_dstaddr. SIOCSIFDSTADDR. Устанавливает адрес типа «точка-точка» из элемента т fr_dstaddr. >' SIOCGIFBRDADDR. Возвращает широковещательный адрес в элементе i f r_broadaddr. Приложение сначала должно получить флаги интерфейса, а зал ем сделать кор- ректный вызов: SIOCGIFBRDADDR для широковещательного интерфейса или SIO- CGIFDSTADDR — для интерфейса типа «точка-точка». •" SIOCSIFBRDADDR. Устанавливает широковещательный адрес из элемента ifr_ broadaddr. SIOCGIFNETMASK. Возвращает маску подсети в элементе ifr_addr. ' SIOCSIFNETMASK. Устанавливает маску подсети из элемента i f r addr. ' SIOCGIFMETRIC. Возвращает метрику интерфейса в элементе i frjnetric. Метри- ка поддерживается ядром для каждого интерфейса, но используется демоном маршрутизации routed. Метрика интерфейса добавляется к счетчику количе- ства переходов. SIOCSIFMETRIC. Устанавливает метрику интерфейса из элемента ifrjnetric. В этом разделе мы описали наиболее типичные операции интерфейсов. Во многих реализациях появились дополнительные операции. 16.8. Операции с кэшем ARP Операции с кэшем ARP также осуществляются с помощью функции i octi. В этих запросах используется структура arpreq, показанная в листинге 16.8 и определяе- мая в заголовочном файле <net/i f_arp fi>.
486 Глава 16. Операции функции ioctl Листинг 16.8. Структура arpreq, используемая с вызовами ioctl для кэша ARP struct arpreq { struct sockaddr struct sockaddr int }. arp_pa. arp_ha. arp_flags #define ATFJNUSE 0x01 #define ATF_COM 0x02 ^define ATFJ’ERM 0x04 #define ATF_PUBL 0x08 /* адрес протокола */ /* аппаратный адрес */ /* флаги */ /* запись, которую нужно использовать */ /* завершенная запись */ /* постоянная запись */ /* опубликованная запись (отсылается другим узлам) */ Третий аргумент функции ioctl должен указывать на одну из этих структур. Поддерживаются три следующих вызова: .< SIOCSARP. Добавляет новую запись в кэш ARP или изменяет существующую запись. агр_ра — это структура адреса сокета Интернета, содержащая 1Р-ад- рес, a arp_ha — это общая структура адреса сокета с элементом sa_fami 1 у, рав- ным AFJJNSPEC, и элементом sa_data, содержащим аппаратный адрес (напри- мер, 6-баптовый адрес Ethernet). Два флага, ATF_PERM п ATF_PUBL, могут быть заданы приложением. Два других флага, ATF INUSE и ATF_COM, устанавливаются ядром. SIOCDARP. Удаляет запись из кэша ARP. Вызывающий процесс задает интер- нет-адрес удаляемой записи. si SIOCGARP. Получает запись из кэша ARP. Вызывающий процесс задает интер- нет-адрес, и соответствующий адрес Ethernet возвращается вместе с флагами. Добавлять или удалять записи может только привилегированный пользова- тель. Эти три вызова обычно делает программа а гр. ПРИМЕЧАНИЕ----------------------------------------------------- Запросы функции ioctl, связанные с ARP, не поддерживаются в некоторых более но- вых системах, использующих для описанных операций ARP маршрутизирующие со- кеты. Обратите внимание, что невозможно с помощью функции ioctl перечислить все записи кэша ARP. Большинство версий команды а гр при использовании фла- га -а (перечисление всех записей кэша ARP) считывают память ядра (/dev/kmem), чтобы получить текущее содержимое кэша ARP. Мы увидим более простой (и пред- почтительный) способ, основанный на применении функции sysctl, описанной в разделе 17.4. Пример: вывод аппаратного адреса узла Теперь мы используем нашу функцию my_addrs, приведенную в листинге 9.3, для того, чтобы возвратить все IP-адреса узла. Затем для каждого IP-адреса мы дела- ем вызов SIOCGARP функции ioctl, чтобы получить и вывести аппаратные адреса. Наша программа показана в листинге 16.9. Листинг 16.9. Вывод аппаратного адреса узла //ioctl/рплас с 1 #include "unp h" 2 #include «net/if arp.h>
16.8. Операции с кэшем ARP 487 3 int 4 maindnt argc. char **argv) 5 { 6 int family, sockfd. 7 char str[INET6_ADDRSTRLEN]: 8 char **pptr. 9 unsigned char *ptr. 10 struct arpreq arpreq. 11 struct sockaddr_in *sin, 12 pptr - rny_addrs(8family). 13 for (: *pptr != NULL. pptr++) { 14 printff^s ". Inet_ntop(family, *pptr, str. sizeof(str))); 15 switch (family) { 16 case AF_INET 17 sockfd = Socket(AF_INET. SOCK_DGRAM. 0). 18 sin = (struct sockaddrjn *) 8arpreq arp_pa. 19 bzerotsin. sizeoftstruct sockaddr_in)) 20 sin->sin_family = AF_INET. 21 memcpy(&sin->sin_addr *pptr. sizeoftstruct in_addr)), 22 Ioctl(sockfd. SIOCGARP, &arpreq) 23 ptr = Sarpreq arp_ha sa_data[0], 24 printf("fcx Xx Xx £x\en", *ptr. *(ptr -*-!). 25 *(ptr + 2) *(ptr т 3) *(ptr + 4). *(ptr + 5)). 26 break, 27 default 28 err_quit("unsupported address family ^d". family). 29 } 30 } 31 exit(0) 32 } Получение списка адресов и проход в цикле по каждому из них 12-13 Мы вызываем функцию my_addrs, чтобы получить IP-адреса узла, а затем вы- полняем цикл по всем адресам. ’ ‘ Вывод IP-адреса 14-17 Мы выводим IP-адреса, используя функцию inet_ntop, а затем вызываем опе- ратор swi tch для переключения между семействами адресов, возвращаемых функ- цией my_addrs. У нас предусмотрен только один вариант (case) для адресов IPv4, так как производители, вероятно, не будут поддерживать адреса IPv6 с запросом SIOCGARP. Вызов функции ioctl и вывод аппаратного адреса 18-26 Мы заполняем структуру агр ра как структуру адреса сокета IPv4, содержащую адрес IPv4. Вызывается функция ioctl и выводится полученный аппаратный адрес. При запуске этой программы на нашем узле solans мы получаем: solans % ргшас 206 62 226 33 8 0 20 78 еЗ еЗ
488 Глава 16. Операции функции ioctl 16.9. Операции с таблицей маршрутизации Для работы с таблицей маршрутизации предназначены два вызова функции i octi. Эти два вызова требуют, чтобы третий аргумент функции ioctl был указателем на структуру rtentry, которая определяется в заголовочном файле <net/route h>. Обычно эти вызовы исходят от программы route. Их может делать только приви- легированный пользователь. 4 SIOCADDRT. Добавить запись в таблицу маршрутизации. i, SIOCDELRT. Удалить запись из таблицы маршрутизации. Нет способа с помощью функции ioctl перечислить все записи таблицы мар- шрутизации. Эту операцию обычно выполняет программа netstat с флагом -г. Программа получает таблицу маршрутизации, считывая память ядра (/dev/kmem). Как и в случае с просмотром кэша ARP. в разделе 17.4 мы увидим более простой (и предпочтительный) способ, предоставляемый функцией sysctl. 16.10. Резюме Команды функции ioctl, используемые в сетевых приложениях, можно разде- лить на шесть категорий: 1. Операции с сокетами (находимся ли мы на отметке внеполосных данных?). 2. Операции с файлами (установить или сбросить флаг отсутствия блокировки). 3. Операции с интерфейсами (возвратить список интерфейсов, получить широ- ковещательный адрес). 4. Операции с кэшем ARP (создать, изменить, получить, удалить). 5. Операции с таблицей маршрутизации (добавить или удалить). 6. Операции с потоками (см. главу 33). Мы будем использовать операции с сокетами и файлами, а получение списка интерфейсов — это настолько типичная операция, что для этой цели мы разрабо- тали собственную функцию. Мы будем применять ее много раз в оставшейся ча- сти книги. Вызовы функции ioctl с кэшем ARP и таблицей маршрутизации ис- пользуются лишь несколькими специализированными программами. Упражнения 1. В разделе 16.7 мы сказали, что широковещательный адрес, возвращаемый зап- росом SIOCGIFBRDADDR, возвращается в элементе i fr_broadaddr. Но па с. 173 [105] сказано, что он возвращается в элементе i f r dstaddr. Имеет ли это значе- ние? 2. Измените программу get_ifi_info так, чтобы она делала первый вызов SIO- CGIFCONF для одной структуры 1 freq, а затем каждый раз в цикле увеличивайте длину на размер одной из этих структур. Затем поместите в цикл операторы, которые выводили бы размер буфера при каждом вызове независимо от того, возвращает функция ioctl ошибку или нет, и при успешном выполнении вы-
Упражнения 489 ведите возвращаемую длину буфера. Запустите программу pn f i nfo и посмот- рите, как ваша система обрабатывает вызов, когда размер буфера слишком мал. Выведите также семейство адресов для всех возвращаемых структур, семей- ство адресов которых не совпадает с указанным в первом аргументе функции get 1 fi_i nfo, чтобы увидеть, какие еще структуры возвращает ваша система. 3. Изменше функцию get_ifi_info так, чтобы опа возвращала информацию об адресе с альтерна! ивным именем, если дополнительный адрес находится не в той подсети, в которой находится предыдущий! адрес для данного интерфей- са. Таким образом, паша версия из раздела 16.6 будет игнорировать альтерна- тивные имена в диапазоне от 206.62.226.44 до 206.62.226.46, и это вполне нор- мально, поскольку они находятся в той же подсети, что и первичный адрес интерфейса 206.62.226.33. Но если альтернативное имя находится в другой подсети, допустим 192.3.4.5, возвратите структуру i fi_i nfo с информацией о до- полнительном адресе. 4. Если ваша система поддерживает вызов SI0CGIGNUM функции ioctl, измените листинг 16.4 так, чтобы запустить этот вызов, и используйте возвращаемое значение как начальный размер буфера.
ГЛАВА 17 Маршрутизирующие сокеты 17.1. Введение Традиционно доступ к таблице маршрутизации Unix внутри ядра осуществлял- ся с помощью команд функции ioctl В разделе 16 9 мы описали две операции SIOCADDRT и SIOCDELRT, предназначенные для добавления и удаления маршрута Мы также отметили, что не существует операции чтения всей таблицы маршрутиза- ции — вместо этого прохраммы такие как netstat, считывают память ядра, для того чтобы получить содержимое таблицы маршрутизации И еще одно добавле- ние Демонам маршрутизации, таким как gated, необходимо отслеживать сооб- щения ICMP (Internet Control Message Protocol — npoioi ол управляющих сооб- щений Интернета) об изменении маршрутов, получаемых ядром и для этого они часто создают символьный (неструктурированный) сокет ICMP (см главу 25), а затем прослушивают на этом сокете все получаемые сообщения ICMP В 4 3BSD Reno был упрощен интерфейс подсистемы маршрутизации ядра та счет создания семейства адресов (домена) AF_ROUTE Единственный тип сокетов, поддерживаемый для этого семейства, — эго символьный сокет Маршрутизиру ющие сокеты поддерживают три типа операции 1 Процесс может отправить ядру сообщение, записав его в маршрутизирующий сокет Таким образом добавляются и удаляются маршруты 2 Процесс может прочитать сообщение от ядра через маршрутизирующий со- кет Так ядро уведомляет процесс о том что сообщение ICMP об изменении маршрутизации было получено и обработано Некоторые операции включают оба шага например, процесс отправляет ядру сообщение через маршрутизирующий сокет, запрашивая всю информацию по данному маршруту, после чего через маршрутизирующий сокет считывает ответ ядра 3 Процесс может использовать функцию sysctl (см раздел 17 4) либо для про- смотра таблицы маршрутизации, либо для перечисления всех сконфигуриро- ванных интерфейсов Первые две операции требуют прав привилегированно! о пользователя, а гре-
17 2 Структура адреса сокета канального уровня 491 ПРИМЕЧАНИЕ -------------------------------------------------------- Технически третья операция выполняется при помощи общей функции sysctl а не мар шру гпзнрующего сокета Но мы увидим, что среди ее входных параметров есть семей ство адресов (для описываемых в этой главе операции используется семейство AT ROUTE) а результат опа вотвращаст в том же формате который используется ядром для маршрутизирующего сокет а Действительно в ядре 4 4BSD обработка функ ции sysctl для семейства AFROUTE является частью кода маршрутизирующею со кета 1105, с 632-643] Утилита sysctl появилась в 4 4BSD К сожалению, не все реализации поддерживаю щис маршрутизирующие сокеты предоставляютее Например AIX4 2 Digital Unix 4 0 и Solans 2 6 поддерживают маршрутизирующие сокеты но пн одна т з этих систем не поддерживает утилиту sysctl 17.2. Структура адреса сокета канального уровня Структуры адреса сокета канального уровня будут встречаться нам как значе- ния, содержащиеся в некоторых сообщениях, возвращаемых на маршрутизирую- щем сокете В листинге 17 1* показано определение структуры, задаваемой в за- головочном файле <net/if_dl h> Листинг 17.1. Структура адреса сокета канального уровня struct sockaddr dl { uint8_t sdljen sa_fann ly_t sdl_farmly /* AF_lINK */ uintl6_t sdl_index /* индекс интерфейса присвоенный системой если > 0 */ uint8_t sdl_type /* тип интерфейса из <net/if_types h> IFT_ETHERht д */ uint8_t sdljilen /* длина имени начинается с sdl_data[O] */ uint8_t sdl_alen /* длина адреса канального уровня */ uint8_t sd]_slen /* адрес селектора канального уровня */ char sdl_data[12] /* минимальная рабочая область может быть больше */ } содержит имя интерфейса и адрес канального уровня */ У каждого интерфейса имеется уникальный положительный индекс Далее в этой главе мы увидим, каким образом он возвращается функциями i f nametoindex и ifjiameindex В главе 19 при обсуждении параметров сокета многоадресной пе- редачи также рассматривается получение индекса интерфейса с помощью функ- ции 1 fjiametoi ndex Элемент sdl_data содержит и имя, и адрес канального уровня (например, 48-раз- рядный МАС-адрес интерфейса Ethernet) Имя начинается с sdl_datа [0] и не за- канчивается нулем Начало адреса канального уровня смещено на sdl_nlen бай- тов относительно начала имени В этом заголовочном файле для возвращения указателя на адрес канального уровня задается следующий макрос #define LLADDR(s) ((caddr_t)((s) >sdl_data + (s) >sdl_nlen)) Эти структуры адреса сокета имеют переменную длину [105, с 89] Если ад- рес канального уровня и имя превышают 12 байт, размер структуры будет боль- ' Все исходные коды программ опубликованные в этой книге вы можете нанти по адресу http//
492 Глава 17. Маршрутизирующие сокеты ше 20 байт. В 32-разрядных системах размер обычно округляется в большую сто- рону, до следующего числа, кратного 4 байтам. Мы также увидим на рис. 20.1, что когда одна из этих структур возвращается параметром сокета IP RECVIF, все три длины становятся нулевыми, а элемента sdl _data не существует. 17.3. Чтение и запись Создав маршрутизирующий сокет, процесс может отправлять ядру команды пу- тем записи в этот сокет и считывать из него информацию от ядра. Существует 12 различных команд маршрутизации, 5 из которых может запустить процесс. Они определяются в заголовочном файле <net/route.h> и показаны в табл. 17.1. Таблица 17.1. Типы сообщений, проходящих по маршрутизирующему сокету Тип сообщения К ядру? От ядра? Описание Тип структуры RTMADD • • Добавить маршрут rt_msghdr RTMCHANGE RTM_DELADDR • • • Поменять шлюз, метрику или флаги Адрес был удален из интерфейса rtnisghdr ifamsghdr RTMDELETE • • Удалить .маршрут rtnisghdr RTMGET RTMJFINFO • • • Сообщить о метрике и других харак геристиках ыаршру i а Находится ли интерфейс в активном состоянии rtmsghdr ifmsghdr RIMJ.OCK RTMLOSING RTMMISS RTMNEWSDDR RTM REDIRECT RTMRESOLVE • • • • • • • Блокировка указанной метрики rtnisghdг Возможно, неправильный маршрут rtnisghdr Поиск этого адреса завершился rtmsghdi неудачно Адрес добавлен к интерфейсу d.imsghch Ядро получило указание rt msghdr использовать другой маршрут Запрос на определение адреса ri_msghdr канального уровня по адресу получателя На маршрутизирующем сокете происходит обмен тремя различными струк- турами, как показано в последнем столбце таблицы: rtjnsghdr, if_ msgiidr и ifa_ msghdr. Эти структуры представлены в листинге 17.2. Листинг 17.2. Три структуры, возвращаемые с маршрутизирующими сообщениями struct rtjnsghdr { /* из <net/route */ u_short rtmjnsglen. /* чтобы пропускать нераспознанные сообщения */ u_char rtm_version. /* для обеспечения двоичной совместимости в будущем */ u_char rtm_type. /* тип сообщения */ sp 0.5v u_short rtm_index. /* индекс интерфейса, с которым связан адрес */ int r'tm_ flags. /* флаги */ int rtm_addrs. /* битовая маска идентифицирующая sockaddr (структуру адреса сокета) в msg */ pid_t rtm_pid. /* идентификация отправителя */ int rtm_seq. /* для идентификации действия отправителем */ int rtm_errno /* причина неудачного выполнения */
17.3. Чтение и запись 493 int rtm_use /* из reentry */ u_long rtm_imts. /* какую метрику мы инициализируем */ struct rt_metrics rtm_rmx . /* сами метрики */ }• struct if msghdr { /* из <net/if h> */ u_short ifmjnsglen. /* чтобы пропускать непонятые сообщения */ u_char ifm_version /* для обеспечения двоичной совместимости в будущем ”/ u_char i fm_type. /* тип сообщения */ int ifm_addrs. /* как rtm_addrs */ int ifm_flags. /* значение if_flags */ u_short i fm_index /* индекс интерфейса, с которым связан адрес */ struct if_data ifm_data: /* статистические и другие сведения */ J struct ifa msghdr { /* из <net/if h> */ u_short i famjnsg] en. /* чтобы пропускать непонятые сообщения */ u_char ifam_version. /* для обеспечения двоичной совместимости в будущем */ u_char ifam_type /* тип сообщения */ int ifam_addrs. /* как rtm_addrs */ । int ifam_f]ags /* значение ifa_flags */ u_short ifam_index /* индекс интерфейса, с которым связан адрес */ int ifam_metric /* значение ifajnetric */ } Первые три элемента каждой структуры одни и те же: длина, версия и тип сообщения. Тип — это одна из констант из первого столбца табл. 17.1. Элемент длины xxxjnsglen позволяет приложению пропускать типы сообщений, которые оно не распознает. Элементы rtm_addrs, ifm_addrs и ifam_addrs являются битовыми масками, ука- зывающими, какая из возможных восьми структур адреса сокета следует за сооб- щением. В табл. 17.2 показаны константы и значения для битовой маски, опреде- ляемые в заголовочном файле <net/route h>. Таблица 17.2. Константы, используемые для ссылки на структуры адреса сокета в маршрутизирующих сообщениях Битовая маска, Битовая мае- Индекс массива, Индекс массива, Структра адреса сокета содержит константа ка,значение константа значение RTA_DST 0x01 RTAXDST 0 Адрес получателя RTAGATEWAY 0x02 rtax_gateway 1 Адрес шлюза rtanetmask 0x04 rtaxnetmask 2 Маска cent rta_genmask 0x08 rtax_genmask 3 Маска клонирования rtajfp 0x10 rtaxjfp 4 Имя интерфейса rtajfa 0x20 rtaxjfa 5 Адрес пн герфеиса rta_author 0x40 RTAXAUTHOR 6 Отправитель запроса на перенаправление rta_brd 0x80 RTAX BRD 7 Адрес по |уча!еля типа «точка-точка» или широковеща- тельный rtax max 8 Максимальное коли- чсство элементов
494 Глава 17. Маршрутизирующие сокеты В том случае, когда имеется множество структур адреса сокета, они всегда рас- полагаются в порядке, показанном в таблице. Пример: получение и вывод записи из таблицы маршрутизации Теперь мы покажем пример использования маршрутизирующих сокетов. Наша программа получает аргумент командной строки, состоящий из адреса IPv4 в то- чечно-десятичной записи, и отправляет ядру сообщение RTM_GET для получения этого адреса. Ядро ищет адрес в своей таблице маршрутизации IPv4 и возвраща- ет сообщение RTM_GET с информацией о соответствующей записи из таблицы мар- шрутизации. Например, если мы выполним па нашем узле bsdi такой код: bsdi # getrt 4.5.6.7 dest 0000 gateway 206 62 226 62 netmask 0000 мы увидим, что этот адрес получателя используется маршрутом по умолчанию (который хранится в таблице маршрутизации с IP-адресом получателя 0.0.0.0 и маской 0.0.0.0). Маршрутизатор следующей ретрансляции — это наш маршру- тизатор gw (вспомните рис. 1.7). Если мы выполним bsdi # getrt 206 62 226 32 dest 206 62 226 32 gateway AF_LINK index=2 netmask 255 255 255 224 задав в качестве получателя главную сеть Ethernet, получателем будет сама сеть. Теперь шлюзом является исходящий интерфейс, возвращаемый в качестве струк- туры sockaddr_dl с индексом интерфейса 2. Перед тем как представить исходный код, мы показываем на рис. 17.1, что именно мы пишем в маршрутизирующий сокет и что возвращает ядро. Мы создаем буфер, содержащий структуру rtjnsghdr, за которой следует струк- тура адреса сокета, содержащая адрес получателя, информацию о котором долж- но найти ядро. Тип сообщения (rtm_type) — RTM_GET, а битовая маска (trm_addrs) — RTAJDST (вспомните табл. 17.2). Эти значения указывают, что структура адреса сокета, следующая за структурой rtjnsghdr, — это структура, содержащая адрес получателя. Эта команда может использоваться с любым семейством протоко- лов (предоставляющим таблицу маршрутизации), поскольку семейство адресов, в которое входит искомый адрес, указано в структуре адреса сокета. После отправки сообщения ядру мы с помошыо функции read читаем ответ, формат которого показан на рис. 17.1 справа: структура rt jnsghdr, за которой сле- дует до четырех структур адреса сокета. Какая из четырех структур адреса сокета возвращается, зависит от записи в таблице маршрутизации. Мы сможем иден- тифицировать возвращаемую структуру адреса сокета по значению элемента rtm_addrs возвращаемой структуры rtjnsghdr. Семейство каждой структуры адре- са сокета указано в элементе sa_farm 1у, и как мы видели в наших предыдущих примерах, первый раз сообщение RST_GET содержало информацию о том, что ад- рес шлюза является структурой адреса сокета IPv4, а второй раз это была струк- тура адреса сокета канального уровня. В листинге 17.3 показана первая часть нашей программы.
17.3. Чтение и запись 495 Буфер, отправленный ядру Буфер, возвращенный ядру rt_msghdx{) rtm_type= RTM-GET Структура адреса сокета, содержащая адрес получателя RTA_DST гt_msghdr{} rtm_type= RTM_GET Структура адреса сокета, содержащая адрес получателя Структура адреса сокета, содержащая адрес шлюза RTA_DST RTA GATEWAY Структура адреса сокета, содержащая маску сети Структура адреса сокета, содержащая маску клонирования RTA_NETMASK RTA_GANMASK Рис. 17.1. Обмен данными с ядром на маршрутизирующем сокете для команды RTM_GET Листинг 17.3. Первая часть программы, запускающая команду RTM_GE^Ha маршрутизирующем сокете //route/getrt с 1 include "unproute h" 2 #define BUFLEN (sizeof(struct rtjnsghdr) + 512) 3 /* sizeof(struct sockaddr_in6) * 8 = 192 */ 4 #define SEQ 9999 5 int 6 main(int argc. char **argv) 7 { В int sockfd. 9 char *buf. 10 pid_t pid. 11 ssize_t n. 12 struct rtjnsghdr *rtm 13 struct sockaddr *sa. *rti_info[RTAX_MAX]; 14 struct sockaddrjin *sin. 15 if (argc l= 2) 16 err_quit('usage getrt <IPaddress>").
496 Глава 17. Маршрутизирующие сокеты Листинг 17.3 (продолжение) 17 sockfd = Socket(AF_ROUTE SOCK_RAW, 0). /* необходимы права привилегированного пользователя */ 18 ouf = Callocd BUFLEN), /* инициализируется нулем */ 19 rtm = (struct rtjnsghdr *) buf 20 rtm->rtm_msglen = sizeoftstruct rtjnsghdr) + sizeoftstruct sockaddrjin). 21 rtm->rtm_version = RTM_VERSION, 22 rtm->rtm__type = RTM_GET. 23 rtm->rtm_addrs = RTA_DST. 24 rtm->rtm_pid = pid = getpidO 25 rtm->rtm_seq = SEQ. 26 sin = (struct sockaddr_in *) (rtm + 1). 27 sin->sin_len = sizeoftstruct sockaddr_in): 28 sin->sin_family = AF_INET 29 Inet_pton(AF_INET, argv[l] &sin->sin_addr). 30 Write(sockfd. rtm. rtm->rtm_msglen). 31 do { 32 n = Readtsockfd rtm. BUFLEN). 33 } while (rtm->rtm_type = RTM_GET || rtm->rtm_seq ’= SEQ || 34 rtm->rtm_pid l= pid). -3 Наш заголовочный файл unproute h подключает некоторые необходимые фай- лы, а затем включает наш файл unp h. Константа BUFLEN — эго размер буфера, ко- торый мы размещаем в памяти для хранения нашего сообщения ядру вместе с от- ветом ядра. Нам необходимо место для одной структуры rtjnsghdr и, возможно, восьми структур адреса сокета (максимальное число, которое может возвратить- ся через маршрутизирующий сокет). Поскольку структура адреса сокета IPv6 имеет размер 24 байта, то значения 512 нам более чем достаточно. Создание маршрутизирующего сокета 17 Мы создаем символьный сокет в домене AF_ROUTE, что, как мы отмечали ранее, требует прав привилегированного пользователя. Буфер размещается в памяти и инициализируется нулем. Заполнение структуры rtjnsghdr 1-25 Мы заполняем структуру rtjnsghdr данными нашего запроса. В этой структуре хранится идентификатор процесса и порядковый номер, который мы выбираем. Мы сравним эти значения, когда будем искать правильный ответ. Заполнение структуры адреса сокета адресом получателя >-29 Следом за структурой rtjnsghdr мы создаем структуру sockaddr i п, содержащую IPv4-адрес получателя, поиск которого будет проведен ядром в таблице маршру- тизации. Все, что мы задаем — это длина адреса, семейство адреса и адрес. Запись сообщения ядру (функция write) и чтение ответа (функция read) |-34 Мы пишем сообщение ядру с помощью функции write, и с помощью функции read читаем ответ. Поскольку у других процессов могут быть открытые маршру-
17.3. Чтение и запись 497 тизирующне сокеты, а ядро передает копию всех маршрутизирующих сообще- ний всем маршрутизирующим сокетам, мы должны проверить тип сообщения, порядковый номер и идентификатор процесса, чтобы узнать, что полученное со- общение — это ожидаемое нами сообщение. Вторая часть этой программы показана в листинге 17.4. Она обрабатывает ответ. Листинг 17.4. Вторая часть программы, запускающая команду RTM_GET на маршрутизирующем сокете //route/getrt с 35 rtm = (struct rtjnsghdr *) but 36 sa = (struct sockaddr *) (rtm + 1) 37 get_rtaddrs(rtm->rtm_addrs, sa rtijnfo). 38 if ( (sa = rti_info[RTAXDST]) ' = NULL) 39 pnntfC'dest Xs\en" Sock_ntop_host(sa sa->sa Jen)), 40 if ( (sa = rti_info[RTAX_GATEWAYJ) != NULL) 41 pnntf("gateway XsXen", Sock_ntop_host(sa sa->sa_len)): 42 if ( (sa = rtijnfo[RTAX_NETMASK]) '= NULL) 43 printf("netmask XsXen” Sock_masktop(sa, sa->sa_len)), 44 if ( (sa = rti_info[RTAX_GENMASK]) '= NULL) 45 printf("genmask ^s\en". Sock_masktop(sa sa->sa_len)). 46 exit(0). 47 } 34-35 Указатель rtm указывает на структуру rtjnsghdr, а указатель sa — на первую сле- дующую за ней структуру адреса сокета. 36 rtnjaddrs — это битовая маска той из возможных восьми структур адреса сокета, которая следует за структурой rtjnsghdr. Наша функция get_rtaddrs (она показа- на в следующем листинге), получив эту маску и указатель на первую структуру адреса сокета (sa), заполняет массив rti_info указателями на соответствующие структуры адреса сокета. В предположении, что ядро возвращает все четыре струк- туры адреса сокета, показанные на рис. 17.1, полученный в результате массив rti info будет таким, как показано на рис. 17.2. Затем наша программа проходит массив rtijinfo, делая все, что ей нужно, с непустыми указателями массива. 37-44 Каждый из присутствующих четырех возможных адресов выводится. Мы вы- зываем нашу функцию sock_ntop_host для вывода адреса получателя и адреса шлюза, но для вывода двух масок подсети вызываем нашу функцию sockjnasktop. Эту новую функцию мы покажем ниже. В листинге 17.5 показана наша функция get rtaddrs, которую мы вызывали в листинге 17.4. Листинг 17.5. Создание массива указателей на структуры адреса сокета в маршрутизирующем сообщении //libroute/get_rtaddrs с 1 #include "unproute h" 2 /* 3 * Округляем 'а' до следующего значения кратного 'size' 4 продолжение &
498 Глава 17. Маршрутизирующие сокеты rti_info rtl_info rti_info rti_info rti_info rti_info rti_info rti_mfo [RTAX_DST] [RTAX_GATEWAY] [RTAX_NETMASK] [RTAX_GENMASKJ [RTAX_IFP] [RTAX—IFP] [ RTAX_AUTHOR] Буфер, [RTAX_BRD] Рис. 17.2. Структура rtijnfo, заполненная с помощью нашей функции get rtaddrs Листинг 17.5 {продолжение) 5 #define R0UNDUP(a size) (((а) & ((size)-D) ’ (1 + ((а) | ((size)-l))) (а)) 6 /* Переходим к следующей структуре адреса сокета 7 * Если sa_len равно 0 это значит что 8 * размер выражен числом типа u_long) 9 */ 10 #define NEXT_SA(ap) ар = (SA *) \е 11 ((caddr_t) ар + (ap->sa_len ? ROUNDUP(ap->sa_1en. sizeof (ujong)) \e 12 sizeof(ujong))) 13 void 14 get_rtaddrs(int addrs SA *sa SA **rti_info) 15 { 16 int i. 17 for (1 - 0. т < RTAX_MAX. i++) { 18 if (addrs & (1 « i)) { 19 rti_info[i] = sa. 20 NEXT_SA(sa) 21 } else 22 rti info[i] = NULL: 23 } 1
17 3 Чтение и запись 499 Цикл по восьми возможным указателям Значение RTAX_MAX — максимальное число структур адреса сокета, возвращаемых от ядра в сообщении через маршрутизирующий сокет, — равно 8. В цикле функ- ции ведется поиск по каждой из восьми констант битовой маски RTA_xxx (см табл. 17.2), которые могут быть присвоены элементам rtm_addrs, i fm_addrs и i fam_ addrs структур, показанных в листинге 17.2. Если бит установлен, соответствую- щий элемент в массиве rti_info становится указателем на структуру адреса соке- та; иначе элемент массива становится пустым указателем. Переход к следующей структуре адреса сокета 2-12 Структуры адреса сокета имеют переменную длину, но в этом коде считается, что у каждой из них имеется поле sa_l еп, задающее длину структуры. Есть две сложности, с которыми придется столкнуться. Во-первых, маска подсети и маска клонирования могут возвращаться в структуре адреса сокета с нулевым значени- ем поля sa_len, по на самом деле они занимают размер, представленный числом типа unsigned 1 ong (В главе 19 [ 105] обсуждается свойство клонирования табли- цы маршрутизации 4.4BSD.) Эго значение соответствует маске, состоящей толь- ко из нулевых битов, что мы видели в одном из приведенных выше примеров, когда для заданного по умолчанию маршрута маска подсети имела вид О.О.О.О. Во-вторых, каждая структура адреса сокета может быть заполнена в конце таким образом, что следующая начнется на определенной границе, которая в данном случае соответствует значению типа unsi gned 1 ong (например, 4-байтовая грани- ца для 32-разрядной архитектуры). Хотя структуры sockaddr_i п занимают 16 байт, что не требует заполнения, маски часто имеют в конце заполнение. Последняя функция, которую мы покажем в примере пашей программы, — это функция sockjnasktop, представленная в листинге 17 6, возвращающая строку для одного из двух возможных значений масок. Маски хранятся в структурах адреса сокета. Элемент sa_fann 1у не задан, но имеется элемент sa_len, принимаю- щий значения 0, 5, 6, 7 или 8 для 32-битовых масок IPv4. Когда длина больше нуля, действительная маска начинается с того же смещения от начала структуры, что и адрес IPv4 в структуре sockaddr_in: 4 байта от начала структуры (как пока- зано на рис. 18.21, с. 577 [105]), что соответствует элементу sa_data[2] общей струк- туры адреса сокета. Листинг 17.6. Преобразование значения маски к формату представления //libroute/sockjnasktop с 1 #include "unproute h" 2 char * 3 sock_masktop(SA *sa socklen_t salen) 4 { 5 static char str[INET6_ADDRSTRLEN] 6 unsigned char *ptr = &sa->sa_data[2] 7 if (sa->sa_len == 0) 8 return ("000 0") 9 else if (sa->sa_len = 5) 10 snprintf(str sizeof(str) 'И0 0 0” *ptr). 11 else if (sa->sa_len == 6) 12 snprintf(str sizeof(str) "И Id 0 0’ *ptr *(ptr + D)
500 Глава 17 Маршрутизирующие сокеты Листинг 17.6 (продолжение) 13 else if (sa >sa_len == 7) 14 snprintf(str sizeof(str) $d 8d 8d 0 *ptr *(ptr + 1) *(ptr + 2)) 15 else if (sa >sa_len == 8) 16 snprintf(str sizeof(str) W M M 8d 17 *ptr *(ptr + 1) *(ptr + 2) *(ptr + 3)) 18 else 19 snprintf(str sizeof(str) (unknown mask len - family = M) 20 sa >sa_len sa >sa_family) 21 return (str) 22 } 21 Если длина равна нулю, то подразумевается маска 0 0 0 0 Если длина равна 5, хранится только первый байт 32-разрядпой маски, а для оставшихся трех байтов подразумевается нулевое значение Когда длина равна 8, хранятся все 4 байта маски В лом примере мы хотим прочитать ответ ядра поскольку он содержит ин- формацию, которую мы ищем Но в общем случае возвращаемое значение нашей функции write на маршрутизирующем сокете сообщает нам, успешно ли была выполнена команда Если это вся необходимая нам информация, мы вызываем функцию shutdown со вторым аргументом SHUT_RD чтобы предотвратить отправку ответа Например если мы удаляем маршрут то возвращение нуля функцией write означает успешное выполнение, а если удали гь маршрут не удалось, возвращает- ся ошибка ESRCH [105, с 608] Аналогично при добавлении маршрута возвраще- ние ошибки EEXIST при выполнении функции write означает, что запись уже су- ществует В нашем примере из листиша 17 3 функция write возвращает ошибку ESRCH если записи в таблице маршрутизации нс существует (допустим, у нашего узла пет заданного по умолчанию маршрута) 17.4. Операции функции sysctl Маршрутизирующие сокеты нужны нам (лавным образом для проверки табли- цы маршрутизации и списка интерфейсов при помощи функции sysctl В то вре- мя как создание маршрутизирующего сокета (символьною сокета в домене AF_ ROUTE) требует прав привилегированною пользователя, проверить таблицу марш- ру1изации и список интерфейсов с помошыо функции sysctl может любой процесс #inciude <sys/param h> #include <sys/sysctl h> int sysctl(int *name u_int name I er: void *oldp size_t *oldlenp void *newp size_t Tnewlen) Возвращает 0 в случае успешного выполнения Эта функция использует имена похожие на имена базы управляющей инфор- мации (Management Information Base, MIB) простого протокола управления се- тью (Simple Network Management Protocol, SNMP) В главе 25 [94] подробно они сываются SNMP и его MIB Эти имена являются иерархическими Аргумент паше — это массив целых чисел, задающий имя, a namel еп задает чис- ло элементов массива Первый элемент массива определяет, какой подсистеме ядра направлен запрос Второй элемент определяет некую часть этой подсисте- мы, и т д На рис 17 3 показана иерархическая организация с некоторыми кон- стантами, используемыми на первых трех уровнях
17 4 Операции функции sysctl 501 Рис. 17.3. Иерархическая организация имен функции sysctl Для получения значении используется аргумент oldp Он указывает на буфер в котором ядро сохраняет значение Аргумент ol denp имеет тип «значение-резуль тат» когда функция вызывается, значение, на которое указывает ol denp, задает размер этого буфера, а по завершении функции значением этого аргумента ста- новится количество данных сохраненных ядром в буфере Если размера буфера недостаточно, возвращается ошибка ENOMEM В специальном случае ol dp может быть пустым указателем, a ol denp - непустым указателем, и тогда ядро определяет, сколько данных возвратилось бы при вызове, сообщая это значение через oldenp Чтобы установить новое значение, используется аргумент newp, указывающий на бу фер размера newl еп Если новое значение пе задается newp должен быть пус тым указателем a newl еп должен быть равен нулю В руководстве по применению функции sysctl подробно описывается различ ная системная информация которую можно получить с помощью этой функции информация о файловых системах, впрту альной памяти, ограничениях ядра, ап парат ных характеристиках и т д Нас интересует сетевая подсистема, на которую указывает первый элемент массива паше, равный CTL NET (константы CTL xrr оп- ределяются в заголовочном файле <sys/sysctl h>) Тогда второй элемент может быть одним из перечисленных ниже AF_INET Получение или установка переменных, влияющих на протоколы Ин тернета Следующий уровень с помощью одной из констант IPR0T0_ixi задает протокол BSD/OS 3 0 предоставляет на этом уровне около 30 переменных управляющих такими свойствами как генерация ядром переадресации ICMP, использование параметров TCP из RFC 1323 отправка контрольных сумм UDP и т д Пример подобно! о применения функции sysctl мы покажем в кон це этого раздела AF LINK Получение и ти установка информации канального уровня, такой как число интерфейсов РРР AF_ROUTE Возвращение информации либо о таблице маршрутизации, либо о списке интерфейсов Мы вскоре опишем тту информацию AFJJNSPEC Получение или установка некоторых переменных уровня сокета, таких как максимальный размер буфера отправки или приема сокета Когда вторым элементом массива паше является AF ROUTE третий элемент (но- мер прот окола) всегда нулевой (поскольку протоколы внутри семейства AF ROUTE отличаются от протоколов, например, в семействе AF INET), четвертый элемент —
502 Глава 17. Маршрутизирующие сокеты это семейство адресов, а пятый и шестой элементы задают выполняемые действия. Вся эта информация обобщается в табл. 17.3. Таблица 17.3. Информация функции sysctl, возвращаемая для маршрутизирующего домена name[ ] Возвращает таблицу Возвращает кэш APR маршрутизации Возвращает список интерфейсов 0 CTL NET CTLNET CTLNET 1 AFROUTE AFROUTE AFROUTE 2 0 0 0 3 AFJNET AFJNET AFJNET 4 NETRTDUMP NET_RT_FLAGS NETRTJFLIST 5 0 RTF_LLINFO 0 Поддерживаются три операции, задаваемые элементом паше[4]. (Константы NET_RTjcxr определяются в заголовочном файле <sys/socket h>.) Информация воз- вращается через указатель о 1 dp при вызове функции sysct 1. Этот буфер содержит переменное число сообщений RTMjxr (см. табл. 17.1). 1. Операция NET_RT_DUMP возвращает таблицу маршрутизации для семейства ад- ресов, заданного элементом пате[3]. Если задано нулевое семейство адресов, возвращаются таблицы маршрутизации для всех семейств адресов. Таблица маршрутизации возвращается как переменное число сообщений RTM_GET, причем за каждым сообщением следует до четырех структур адреса сокета: получатель, шлюз, маска сети и маска клонирования записи в таблице марш- рутизации. Пример такого сообщения мы показали в правой части рис. 17.1, а в нашем коде в листинге 17.4 проводится анализ одного из сообщений. В ре- зультате применения этой операции функции sysctl ядром возвращается одно или несколько таких сообщений. 2. Операция NET_RT_FLAGS возвращает таблицу маршрутизации для семейства ад- ресов, заданного элементом паше[3], но учитываются только те записи табли- цы маршрутизации, для которых значение флага RTFjxr равно указанному в элементе пате[5]. У всех записей кэша ARP в таблице маршрутизации уста- новлен бит флага RTFJ.LINFO. Информация возвращается в том же формате, что и в предыдущем пункте. 3. Операция NET_RT_IFLIST возвращает информацию обо всех сконфигурирован- ных интерфейсах. Если элемент паше[5] ненулевой, это номер индекса интер- фейса и возвращается информация только об этом интерфейсе. (Более подробно об индексах интерфейсов мы поговорим в разделе 17.6.) Все адреса, присвоен- ные каждому интерфейсу, также возвращаются, и если элемент name[3] нену- левой, возвращаются только адреса для семейства адресов, указанного в этом элементе. Для каждого интерфейса возвращается по одному сообщению RTM_IFINFO, за которым следует одно сообщение RTMNEWADDR для каждого адреса, заданного для интерфейса. За сообщением RTM_IFINFO следует по одной структуре адреса сокета канального уровня, а за каждым сообщением RTM_ NEWADDR — до трех структур адреса сокета: адрес интерфейса, маска сети и широковещательный адрес. Эти два сообщения представлены на рис. 17.4.
17.4. Операции функции sysctl 503 Буфер, возвращенный ядру if_msghdr{} ifm_type= RTM-IFINFO Структура адреса сокета, содержащая адрес получателя if_msghdr{} ifm_type= RTM IFINFO Структура адреса сокета, содержащая маску сети Структура адреса сокета, содержащая адрес направленной передачи Структура адреса сокета, содержащая широковещательный адрес По одному на адрес, сконфигурированный для интерфейса По одному для каждого интерфейса имя интерфейса, индекс и аппаратный адрес Рис. 17.4. Информация, возвращаемая функцией sysctl для команд CTL.NET и NET_RT_IFLIST ПРИМЕЧАНИЕ-------------------------------------------------------- Поскольку к ядрам 4.4BSD добавлена поддержка IPv6, должно поддерживаться значе- ние IPv6 для элемента name[ 11 (чтобы устанавливать и получать характерные для IPv6 переменные), а также значение AF_INET6 для элемента паше|3] из табл. 17.3 (чтобы получить таблицу маршрутизации IPv6 и кэш соседнего узла или возвращать адреса интерфейсов IPv6). Пример: определяем, включены ли контрольные суммы UDP Теперь мы приведем простой пример использования функции sysctl с протоко- лами Интернета для проверки, включены ли контрольные суммы UDP. Некото- рые приложения UDP (например, BIND) проверяют при запуске, включены ли
504 Глава 17. Маршрутизирующие сокеты контрольные суммы UDP, и если нет, пытаются включить их. Для того чтобы включить подобное свойство, требуются права привилегированного пользовате- ля, но мы сейчас просто проверим, включено это свойство или нет. В листин- ге 17.7 представлена наша программа. Листинг 17.7. Проверка включения контрольных сумм //route/checkudpsum с 1 #include "unproute h" 2 #include <netinet/udp h> 3 #include <netinet/ip_var h> 4 include <netinet/udp_var h> /* для констант UDPCTL_xxx */ 5 int 6 main(int argc. char **argv) 7 { 8 int mib[4], val- 9 size_t len. 10 mb[0] = CTLNET. 11 mib[l] = AF_INET 12 mib[2] = IPPROTOJJDP- 13 mib[3] = UDPCTL_CHECKSUM. 14 len = sizeof(val). 15 Sysctl(mib 4 8val 81en. NULL 0) 16 printf("udp checksum flag Idler,". val): 17 exit(0) 18 } Включение системных заголовков 4 Следует включить заголовочный файл <neti net/udp_var h>, чтобы получить определение констант UDP функции sysctl. Для него требуются два других заголовка. Вызов функции sysctl -16 Мы размещаем в памяти массив целых чисел с четырьмя элементами и храним константы, соответствующие иерархии, показанной на рнс. 17.3. Поскольку мы только получаем переменную и не присваиваем ей значение, аргумент newp функ- ции sysctl мы задаем как пустой указатель, и поэтому аргумент newp этой функ- ции имеет нулевое значение, oldp указывает на нашу целочисленную перемен- ную, в которую сохраняется результат, a oldenp указывает па переменную типа «значение-результат», хранящую размер этого целого числа. Мы выводим либо 0 (отключено), либо 1 (включено). 17.5. Функция getjfijnfo Вернемся к примеру из раздела 16.6 — возвращение всех активных интерфейсов в виде связного списка сгруктур ifi info (см. листинг 16.2). Программа pmfinfo остается без изменений (см. лисгинг 16.3), но теперь мы покажем версию функ- ции get i f 11 nfo, использующую функцию sysctl вместо вызова SIOCGIFCONF функ- ции ioctl в листинге 16.4.
17.5 Функция getjfijnfo 505 Сначала в листинге 17.8 мы представим функцию ret_rt_i fl i st. Эта функция вызывает функцию sysctl с командой NET_RT_IFLIST, чтобы возвратить список интерфейсов для заданного семейства адресов. Листинг 17.8. Вызов фукнции sysctl для возвращения списка интерфейсов //libroute/netj'tjflist с 1 include "unproute h” 2 char * 3 netj-tjflist(int family int flags. size_t *1enp) 4 { 5 int mib[6], 6 char *buf 7 mib[0] = CTL JET. 8 mib[l] = AF_ROUTE. 9 mib[2] = 0. 10 mib[3] = family. /* только адреса этого семейства */ 11 mib[4] = NET_RT_IFLIST. 12 mib[5] = flags /* индекс интерфейса или 0 */ 13 if (sysctl(mib 6 NULL lenp. NULL 0) < 0) 14 return (NULL). 15 if ( (buf = malloc(*lenp)) == NULL) 16 return (NULL). 17 if (sysctl(mib. 6. buf. lenp. NULL. 0) < 0) { 18 free(buf). 19 return (NULL). 20 } 21 return (buf). 22 } 7-14 Инициализируется массив mi b, как показано в табл. 17.3, для возвращения списка интерфейсов и всех сконфигурированных адресов заданного семейства. Затем функция sysctl вызывается дважды. В первом вызове функции третий аргумент нулевой, в результате чего в переменной, на которую указывает 1 епр, возвращает- ся размер буфера, требуемый для хранения всей информации об интерфейсе. 15-21 Затем в памяти выделяется место для буфера, и функция sysctl вызывается снова, на этот раз с ненулевым третьим аргументом. При этом переменная, на которую указывает 1 епр, содержит при завершении функции число, равное коли- честву информации, хранимой в буфере, и эта переменная размешается в памя гп вызывающим процессом. Указатель на буфер также возвращается вызывающему процессу. ПРИМЕЧАНИЕ--------------------------------------------------------------------- Поскольку размер таблицы маршрутизации или число интерфейсов может изменять- ся между двумя вызовами функции sysctl, значение, возвращаемое при нервом вызове, содержит поправочный множитель 10% [105, с. 639-640] В листинге 17.9 показана первая половина функции get_ifi_info. Листинг 17.9. Функция getjfijnfo, первая половина //route/getjfi jnfo с 3 struct ifijnfo * 4 getjfijnfojnt family, int doaliases)
506 Глава 17. Маршрутизирующие сокеты Листинг 17.9 (продолжение) 5 { 6 int flags. 7 char *buf. *next. *lim, 8 size_t len. 9 struct if_msghdr *ifm. 10 struct ifa_msghdr *ifam, 11 struct sockaddr *sa. *rti_info[RTAX_MAX], 12 struct sockaddr_dl *sdl. 13 struct ifi_info *ifi. *ifisave. *ifinead. **ifipnext. 14 buf = Net_rt_iflist(family, 0. &len), 15 i fi head = NULL. 16 ifipnext = 8ifihead. 17 lim = buf + len. 18 for (next = buf. next < lim. next += ifm->ifm_msglen) { 19 ifm = (struct ifjnsghdr *) next. 20 if (ifm->ifm_type == RTMJFINFO) { 21 if ( ((flags = ifm->ifm_flags) & IFF_UP) == 0) 22 continue. /* игнорируем, если интерфейс неактивен */ 23 sa = (struct sockaddr *) (ifm + 1). 24 get_rtaddrs(ifm->ifm_addrs. sa. rti_info). 25 if ( (sa = rtl_info[RTAX_IFP]) '= NULL) { 26 ifi = Callocd. sizeoftstruct ifi_info)). 27 *ifipnext = ifi. /* предыдущий указатель указывал на эту структуру */ 28 ifipnext = 8ifi->ifi_next. /* указатель на следующую структуру */ 29 ifi->ifi_flags = flags: 30 if (sa->sa_family == AFJ.INK) { 31 sdl = (struct sockaddr_dl *) sa. 32 if (sdl->sdl_nlen > 0) 33 snprintf(ifi->ifi_name. IFI_NAME. "X*s". 34 sdl->sdl_nlen. 8sdl->sdl_data[01). 35 else 36 , snprintf(ifi->ifi_name. IFI_NAME. "index Xd". 37 sdl->sdl_index). 38 if ( (ifi->ifi_hlen = sdl->sdl_alen) > 0) 39 memcpy(ifi->ifi_haddr. LLADDR(sdl) 40 min(IFI_HADDR. sdl->sdl_alen)). 41 } 42 } 14 Мы объявляем локальные переменные и затем вызываем нашу функцию net_rt_ 1 fl ist. 19 Цикл for — это цикл по всем сообщениям маршрутизации, попадающим в бу- фер в результате выполнения функции sysctl. Мы предполагаем, что сообщение — это структура i fjnsghdr, и рассматриваем поле i fm_type (вспомните, что первые три элемента трех структур идентичны, поэтому все равно, какую из трех струк- тур мы используем для просмотра типа элемента). Проверка, включен ли интерфейс •22 Для каждого интерфейса возвращается структура RTMJFINFO. Если интерфейс не активен, он игнорируется.
17.5. Функция getjfi info 507 Определение, какие структуры адреса сокета присутствуют 23-24 sa указывает на первую структуру адреса сокета, следующую за структурой ifjnsghdr. Наша функция get_rtaddrs инициализирует массив rti_info в зависи- мости от того, какие структуры адреса сокета присутствуют. Обработка имени интерфейса 25-42 Если присутствует структура адреса сокета с именем интерфейса, в памяти раз- мещается структура т fi_info и хранятся флаги интерфейса. Предполагаемым се- мейством этой структуры адреса сокета является AF_LINK, что означает структуру адреса сокета канального уровня. Если элемент sdl_nlen ненулевой, имя интер- фейса копируется в структуру ifi_info. В противном случае в качестве имени хранится строка, содержащая индекс интерфейса. Если элемент sdl_alen ненуле- вой, аппаратный адрес (например, адрес Ethernet) копируется в структуру i fi_info, а его длина также возвращается как i fi_hl еп. В листинге 17.10 показана вторая часть нашей функции get_i fi_info, которая возвращает IP-адреса для интерфейса. Листинг 17.10. Функция getjfijnfo, вторая часть //route/get_ifi_info.с 43 } else if (ifm->ifm_type = RTM_NEWADDR) { 44 if (ifi->ifi_addr) { /* уже имеется IP-адрес для интерфейса */ 45 if (doaliases == 0) 46 continue: 47 /* у нас имеется новый IP-адрес для существующего интерфейса */ 48 1 fisave - ifi. 49 ifi = Callocll. sizeoftstruct ifi_info)). 50 *ifipnext = ifi. /* предыдущий указатель указывал на эту структуру */ 51 ifipnext = 8ifi->ifi_next. /* указатель на следующую структуру */ 52 ifi->ifi_flags = ifisave->ifi_flags. 53 ifi->ifi_hlen = ifisave->ifi_hlen. 54 rnerncpyOfi->ifi_name. ifisave->ifi_name, IFI_NAME). 55 memcpy(ifi->ifi_haddr. ifisave->ifi_haddr. IFI_HADDR). 56 } 57 ifam = (struct ifa_msghdr *) next, 58 sa = (struct sockaddr *) (ifam + 1). 59 get_ntaddrs(ifam->ifam_addrs. sa. rti_info), 60 if ( (sa = rtl_info[RTAX_IFA]) != NULL) { 61 ifi->ifi_addr = Callocd. sa->sa_len): 62 memcpy(ifi->ifi_addr. sa. sa->sa_len): 63 } 64 if ( (flags & IFF_BROADCAST) 88 65 (sa = rtl_info[RTAX_BRD]) '= NULL) { 66 ifi->ifi_brdaddr = Callocd. sa->sa_len): 67 memcpy(ifi->ifi_brdaddr. sa. sa->sa_len). 68 } 69 if ( (flags 8 IFF_POINTOPOINT) 88 70 (sa = rtl_info[RTAX_BRDJ) '= NULL) { 71 ifi->ifi_dstaddr = Callocd. sa->sa_len). 72 memcpy(ifi->ifi_dstaddr. sa. sa->sa len). 73 } } ^1 se -r&
508 Глава 17. Маршрутизирующие сокеты Листинг 17.10 {продолжение) 75 erT-qintCunexpected message type 8d" ifm->ifm_type) 76 } 77 /* "ifihead' указывает на первую структуру в связном списке */ 78 return (ifihead). /* указатель на первую структуру в связном списке */ 79 } Возвращение IP-адресов 3-63 Сообщение RTM_NEWADDR возвращается функцией sysctl для каждого адреса, свя- занного с интерфейсом: для первичного адреса и для всех альтернативных имен (псевдонимов). Если мы уже заполнили IP-адрес для этого интерфейса, то мы имеем дело с альтернативным именем. Поэтому если вызывающему процессу нужен адрес псевдонима, мы должны выделить память для другой структуры т f i_i nfo, скопировать заполненные поля и затем заполнить возвращенный адрес. Возвращение широковещательного адреса и адреса получателя 4-73 Если интерфейс поддерживает широковещательную передачу, возвращается широковещательный адрес, а если интерфейс является интерфейсом типа «точ- ка-точка», возвращается адрес получателя. 17.6. Функции имени и индекса интерфейса Документ RFC 2133 [32] определяет четыре функции, обрабатывающие имена и индексы интерфейсов. Эти четыре функции используются при многоадресной передаче по протоколу IPv6, как показано в главе 19 Основной принцип, объяв- ляемый в этом документе, состоит в том, что каждый интерфейс имеет уникаль- ное имя и уникальный положительный индекс (нуль в качестве индекса никогда не используется). #mclude <net/if h> unsigned int if_nametoindex(const char *ifname). Возвращает положительный индекс интерфейса в случае успешного выполнения. О в случае ошибки char *if_indextoname(unsigned int ifindex char *ifname) Возвращает указатель на имя интерфейса в случае успешного выполнения NULL в случае ошибки struct if_nameindex *if_nameindex(void) Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки void if_freenameindex(struct if_nameindex *Iptr). Функция i f_nametoi ndex возвращает индекс интерфейса, имеющего имя i fname. Функция 1 f_i ndextoname возвращает указатель на имя интерфейса, если задан его индекс 1 fi ndex. Аргумент i fname указывает на буфер размера IFNAMSIZ (определяе- мый в заголовочном файле <net/if h> из листинга 16.1), который вызывающий
17.6. Функции имени и индекса интерфейса 509 процесс должен выделить для хранения результата, и этот указатель возвращает- ся в случае успешного выполнения функции i f_indextoname. Функция i fjiamei ndex возвращает указатель на массив структур i f пашет ndex: struct if_nameindex { unsigned int if_index /*12 */ char *if_name /* имя завершаемое нулем 'leO' */ ) Последняя запись в этом массиве содержит структуру с нулевым индексом 1 f_i ndex и с пустым указателем i f name. Память для этого массива, а также для имен, на которые указывают элементы массива, выделяется динамически и осво- бождается при вызове функции i f_f reenamei ndex Теперь мы представим реализацию этих четырех функций с использованием маршрутизирующих сокетов. Функция if_nametoindex В листинге 17.11 показана функция if_nametoindex. Листинг 17.11. Возвращение индекса интерфейса по его имени //libroute/if_nametoindex с 1 #include “unpifi h" 2 #include "unproute h” 3 unsigned int 4 if_nametoindex(const char *name) 5 { 6 unsigned int index 7 char *buf *next, *lim 8 size_t len. 9 struct if_msghdr *ifm 10 struct sockaddr *sa *rti_info[RTAX_MAX] 11 struct sockaddr_dl *sdl 12 if ( (buf = net_rt_iflist(O 0. &len)) == NULL) 13 return (0) 14 lim = buf + len. 15 for (next = buf next < lim next += ifm->ifm_msglen) { 16 ifm = (struct if_msghdr *) next 17 if (ifm->ifm_type == RTM_IFINFO) { 18 sa = (struct sockaddr *) (ifm + 1) 19 get_rtaddrs(ifm->ifm_addrs sa rti_info) 20 if ( (sa = rti_info[RTAX_IFP]) i= NULL) { 21 if (sa->sa_family = AF_LINK) { 22 sdl = (struct sockaddr_dl *) sa 23 if (strncmp(&sdl->sdl_data[OJ name. sdl->sdl_nlen) == 0) { 24 index = sdl->sdl_index. /* сохраняем перед freed */ 25 free(buf). 26 return (index) 29 } 30 } 31 } 32 free(buf) 33 return (0). /* нет соответствия имени */ 34 }
510 Глава 17. Маршрутизирующие сокеты Получение списка интерфейсов 2-13 Наша функция net_rt_i fl i st возвращает список интерфейсов. Обработка только сообщений RTMJFINFO 7-30 Мы обрабатываем сообщения в буфере (см. рис. 17.4) в поисках сообщений типа RTM_IFINFO. Найдя такое сообщение, мы вызываем нашу функцию get_rtaddrs, чтобы установить указатели на структуры адреса сокета, а если присутствует структура имени интерфейса (элемент RTAX_IFP массива rtijnfo), то имя интерфейса срав- нивается с аргументом. Функция if indextoname Следующая функция, if_i ndextoname, показана в листинге 17.12. Листинг 17.12. Возвращение имени интерфейса по его индексу libroute/if_indextoname с 1 include "unpifi h” 2 include "unproute h" 3 char * 4 if_indextoname(unsigned int index, char *name) 5 ( 6 char *buf *next. *lim 7 size J: len. 8 struct ifjnsghdr *ifm 9 struct sockaddr *sa *rti_info[RTAX_MAX], 10 struct sockaddr_dl *sdl 11 if ( (buf = net_rt_iflist(O index &len)) — NULL) 12 return (NULL). 13 lim = buf + len. 14 for (next = buf. next < lim next += ifm->ifm_msglen) { 15 ifm = (struct ifjnsghdr *) next 16 if (ifm->ifm_type == RTM_IFINFO) { 17 sa = (struct sockaddr *) (ifm + 1) 18 get_rtaddrs(ifni->ifni_addrs. sa rti_info). 19 if ( (sa = rti_info[RTAX_IFP]) '= NULL) { 20 if (sa->sa_family == AF_LINK) { 21 sdl = (struct sockaddrjj] *) sa 22 if (sdl->sdl_index == index) { 23 strncpy(name. sdl->sdl_data. sdl->sdl_nlen). 24 name[sdl->sdl_nlen] = 0. /* завершающий нуль */ 25 free(buf). 26 return (name). 27 } 28 } 29 } 30 } 31 } 32 free(buf). 33 return (NULL). /* нет соответствия индексу */ 34 } Эта функция практически идентична предыдущей, но вместо поиска имени интерфейса мы сравниваем индекс интерфейса с аргументом вызывающего про-
17.6. Функции имени и индекса интерфейса 511 цесса. Кроме того, второй аргумент нашей функции net_rt_i f 1 т st — это заданный индекс, поэтому результат должен содержать информацию только для опреде- ленного интерфейса. Когда обнаруживается совпадение, возвращается имя ин- терфейса, к которому добавляется завершающий нуль. Функция if_nameindex Следующая функция, ifjiameindex, возвращает массив структур if_namei ndex*со- держащих все имена интерфейсов и индексы. Она показана в листинге47.13*. Листинг 17.13. Возвращение всех имен и индексов интерфейсов //libroute/if_nameindex с 1 include "unprfi h” 2 #mclude "unproute h' 3 struct ifjiameindex * 4 if_nameindex(void) 5 ( 6 char *buf. *next, *lim. 7 size_t len. 8 struct if_msghdr *ifm. 9 struct sockaddr *sa. *rti_info[RTAXJ1AXJ; 10 struct sockaddr_dl *sdl. 11 struct if_nameindex *result. *ifptr, 12 char *namptr, 13 if ( (buf = net_rt_iflist(0. 0. &len)) == NULL) 14 return (NULL) 15 if ( (result = malloc(len)) == NULL) /* завышенная оценка */ 16 return (NULL) 17 ifptr = result. 18 namptr = (char *) result + len. /* имена начинаются с конца буфера */ 19 lim = buf + len 20 for (next = buf. next < lim. next += ifm->ifm_msglen) { 21 ifm = (struct iCmsghdr *) next. 22 if (ifm->ifm_type == RTMJFINFO) { 23 sa = (struct sockaddr *) (ifm + 1). 24 get_rtaddrs(ifm->ifm_addrs sa rti_info). 25 if ( (sa = rti_info[RTAX_IFP]) ' = NULL) { 26 if (sa->sa_family == AF_LINK) { 27 sdl = (struct sockaddr dl *) sa 28 namptr -= sdl->sdl_nlen + 1, 29 strncpy(namptr. &sdl->sdl_data[0], sdl->sdl_nlen). 30 namptr[sdl->sdl_nlen] = 0. /* завершающий нуль */ 31 ifptr->if_name = namptr 32 ifptr->if_index = sdl->sdl_index. 33 ifptr++. 34 } 35 } 36 } 37 } 38 ifptr->if_name = NULL: /* отмечаем конец массива структур */ 39 ifptr->if_index - 0. 40 free(buf) 41 return (result). /* вызывающий процесс может освободить память с помощью freeO. когда все сделано */ 42 }
512 Глава 17. Маршрутизирующие сокеты Получение списка интерфейсов, выделение места для результата 3-18 Мы вызываем нашу функцию net_rt_i fl i st для возвращения списка интерфей- сов. Мы также используем возвращаемый размер в качестве размера буфера, который мы размещаем в памяти для записи массива возвращаемых структур 1 fjiamei ndex. Оценка необходимого размера буфера несколько завышена, но это проще, чем проходить список интерфейсов дважды один раз для подсчета числа интерфейсов и общего размера имен, а второй — для записи этой информации. Мы создаем массив i f_namei ndex в начале этого буфера и записываем имена ин- терфейсов, начиная с конца буфера. Обработка только сообщений RTMJFINFO 2-36 Мы обрабатываем все сообщения, ища сообщения RTM IFINFO и следующие за ними структуры адреса сокета. Имя и индекс интерфейса записываются в созда- ваемый нами массив Завершение массива 8-39 Последняя запись в массиве имеет пустой указатель if_name и нулевой индекс. Функция if_freenameindex Последняя функция, показанная в листинге 17.13, освобождает память, которая была выделена для массива структур i f_namei ndex и хранящихся в нем имен. Листинг 17.14. Освобождение памяти, выделенной функцией if nameindex 43 void 44 if_freenameindex(struct if_nameindex *ptr) 45 { 46 free(ptr) 47 } Эта функция тривиальна, поскольку мы хранили и массив структур, и имена в одном и том же буфере. Если бы мы каждый раз вызывали функцию mal I ос, то для освобождения памяти нам пришлось бы проходить через весь массив, осво- бождать память, выделенную для каждого имени, а затем удалять сам массив (ис- пользуя функцию free). 17.7. Резюме Последняя из структур адреса сокета, с которой мы встретились в книге, это sockaddr_dl — структура адреса сокета канального уровня, имеющая переменную длину. Ядра Беркли-реализаций связывают их с интерфейсами, возвращая в од- ной из этих структур индекс интерфейса, его имя и аппаратный адрес. В маршрутизирующий сокет процессом могут быть записаны 5 i ипов сообще- ний, и 12 различных сообщений могут быть асинхронно возвращены ядром через маршрутизирующий сокет. Мы привели пример, когда процесс запрашивает у ядра информацию о записи в таблице маршрутизации и ядро отвечает со всеми под- робностями. Ответы ядра содержат до восьми структур адреса сокета, поэтому нам приходится анализировать сообщение, чтобы получить все фрагменты ин- формации.
Упражнения 513 Функция sysctl предоставляет общий способ получения и хранения парамет- ров операционной системы. При выполнении функции sysctl нас интересует по- лучение следующей информации: список интерфейсов; < таблица маршрутизации; кэш ARP. Изменения API сокетов, требуемые IPv6, включают четыре функции для со- поставления имен интерфейсов и их индексов. Каждому интерфейсу присваива- ется уникальный положительный индекс. В Беркли-реализациях с каждым ин- терфейсом уже связан индекс, поэтому нам несложно реализовать эти функции с помощью функции sysctl. Упражнения 1. Как вы считаете, что будет хранить поле sdl_len в структуре адреса сокета ка- нального уровня для устройства с именем ethlO, адрес канального уровня ко- торого является 64-разрядным адресом IEEE EUI-64? 2. В листинге 17.3 отключите параметр сокета SOJJSELOOPBACK перед вызовом функ- ции write. Что происходит?
ГЛАВА 18 Широковещательная передача 18.1. Введение В этой главе мы расскажем о широковещательной передаче (hrodacasting), а в сле- дующей главе — о многоадресной передаче (multicasting). Во всех предыдущих примерах рассматривалась направленная (одноадресная) передача (unicasting), когда процесс общается только с одним определенным процессом. Действитель- но, TCP работает только с адресами направленной передачи, хотя UDP поддер- живает и другие парадигмы передачи. В табл. 18.1 представлено сравнение раз- личных видов адресации. Таблица 18.1. Различные формы адресации Тип IPv4? ipv6? TCP? UDP? Количество идентифици- руемых ин- терфейсов Количество ин- терфейсов, ку- да доставляет- ся сообщение Направленная передача • • • • Один Один Передача наиболее • Пока нет • Набор Один из набора подходящему узлу М ногоадресная Не обяза- • • Набор Все в наборе передача тельно Широковещатель- ная передача • • Все Все Мы добавили в эту таблицу передачу наиболее подходящему узлу (anycasting), поскольку планируется, что IPv6 в будущем будет поддерживать ее, но на сегод- няшний день эта идея еще не реализована. Этот тип передачи описан в RFC 1546 [75]. Вот наиболее важные положения из этой таблицы: Поддержка многоадресной передачи не обязательна для IPv4, но обязательна для IPv6. Поддержка широковещательной передачи не обеспечивается в IPv6: любое приложение IPv4, использующее широковещательную передачу, для совмес- тимости с IPv6 должно быть преобразовано так, чтобы использовать вместо широковещательной передачи многоадресную.
18.1. Введение 515 Широковещательная и многоадресная передачи требуют наличия протокола UDP и не работают с TCP. Одним из применений широковещательной передачи является поиск сервера в локальной подсети, когда известно, что сервер находится в этой локальной под- сети, но его IP-адрес для направленной передачи неизвестен. Иногда эту проце- дуру называют обнаружением ресурса (resource discovery). Другое применение — минимизация сетевого трафика в локальной сети, когда несколько клиентов взаи- модействуют с одним сервером. Можно привести множество примеров ин- тернет-приложений, использующих для этой цели широковещательную пе- редачу. Протокол разрешения адресов (Address Resolution Protocol, ARP). Хотя это фундаментальная часть IPv4, а не пользовательское приложение, ARP отправ- ляет широковещательный запрос в локальную подсеть, суть которого такова: «Система с IP-адресом a.b.c.d, идентифицируйте себя и сообщите свой аппа- ратный адрес». I- Протокол начальной загрузки (Bootstrap Protocol, ВВОТР). Клиент предпо- лагает, что сервер находится в локальной подсети, и посылает запрос на широ- ковещательный адрес (часто 255.255.255.255, поскольку клиент еще не знает IP-адреса, маски подсети и адреса ограниченной широковещательной переда- чи в этой подсети). . Протокол синхронизации времени (Network Time Protocol, NTP). В обычном сценарии клиент NTP конфигурируется с IP-адресом одного или более серве- ров, которые будут использоваться для определения времени, и опрашивает серверы с определенной частотой (с периодом 64 секунды или больше). Кли- ент обновляет свои часы, используя сложные алгоритмы, основанные на зна- чении истинного времени (time-of-day), возвращаемом серверами, и величи- не периода RTT обращения к серверам. Но в широковещательной локальной сети вместо того, чтобы каждый клиент обращался к одному серверу, сервер может отправлять текущее значение времени с помощью широковещатель- ных сообщений каждые 64 секунды для всех клиентов в локальной подсети, ограничивая тем самым сетевой трафик. Демоны маршрутизации. Наиболее часто используемый демон маршрутиза- ции routed распространяет по локальной сети широковещательные сообщения, содержащие таблицу маршрутизации. Это позволяет всем другим маршру- тизаторам, соединенным с локальной сетью, получать объявления маршрути- зации. При этом в конфигурацию каждого маршрутизатора не обязательно должны входить IP-адреса соседних маршрутизаторов. Это свойство также ис- пользуется (многие могут отметить, что «используется неправильно») узла- ми локальной сети, прослушивающими объявления о маршрутизации и изме- няющими в соответствии с этим свои таблицы маршрутизации. Следует отметить, что многоадресная передача может заменить оба варианта применения широковещательной передачи (обнаружение ресурса и ограничение сетевого трафика). Проблемы широковещательной передачи мы обсудим ниже в этой главе, а также в следующей главе.
516 Глава 18. Широковещательная передача ПРИМЕЧАНИЕ----------------------------------------------------- Широковещательная передача, ограничивающая сетевой трафик в локальной сети, может привести к нежелательному взаимодействию с бездисковыми системами. Бу- дем считать, что сервер NTP рассылает широковещательные сообщения, содержащие текущее значение времени, каждые 64 секунды. Если демон NTP на всех бездисковых клиентах в течение этих 64 секунд не присутствует в основной памяти, то каждые 64 се- кунды каждый из бездисковых клиентов получает дейтаграмму NTP и немедлен- но считывает демон NTP в основную память со своего дискового сервера, также через локальную сеть. Каждые 64 секунды в локальной сети возникает резкий скачок актив- ности, поскольку каждый бездисковый клиент считывает демон NTP. Периодичность этих событий можно проследить с помощью широковещательных приложений. К счас- тью, постоянно снижающаяся цена на дисковые накопители выводит бездиско- вые системы из употребления. 18.2. Широковещательные адреса Если мы обозначим адрес IPv4 в виде {netid. subnetid, hostid}, мы получим че- тыре типа широковещательных адресов. Поле, целиком состоящее из единичных битов, обозначим -1. 1. Широковещательный адрес подсети: {netid, subnet!d. -1}. Он предназначает- ся для всех интерфейсов в заданной подсети. Например, если мы используем 8-разрядный идентификатор подсети с адресом класса В 128.7, то 128.7.6.255 будет широковещательным адресом, направленным в подсеть, для всех интер- фейсов в подсети 128.7.6. Обычно маршрутизаторы не передают широковещательные сообщения даль- ше из подсети [105, с. 226-227]. На рис. 18.1 изображен маршрутизатор, со- единенный с двумя подсетями — 128.7.1 и 128.7.6. IP-адрес получателя^ 28.7.6.255 (Дейтаграмма направленной передачи) Рис. 18.1. Передает ли маршрутизатор дальше широковещательное сообщение, направленное в подсеть? Маршрутизатор получает дейтаграмму IP направленной передачи в подсети 128.7.1 с адресом получателя 128.7.6.255 (адрес широковещательной передачи для подсети другого интерфейса). Обычно маршрутизатор не передает дей- таграмму дальше в подсеть 128.7.6. У некоторых систем имеется параметр
18.3. Сравнение направленной и широковещательной передач 517 конфигурации, позволяющий передавать широковещательные сообщения, на- правленные в подсеть (см. приложение Е [94]). 2. Широковещательный адрес всех подсетей: {netid, -1, -1}. Он предназначен для всех подсетей в заданной сети. Этот тип адресов используется очень ред- ко, если используется вообще. 3. Широковещательный адрес сети: {net з d, -1}. Этот тип адреса используется в сети, в которой отсутствуют подсети, что на сегодняшний день почти не встречается. 4. Локальный широковещательный адрес: {-1, -1, -1} или 255.255.255.255. Дей- таграммы, предназначенные для этого ограниченного адреса, никогда не долж- ны передаваться маршрутизатором. Из четырех типов широковещательных адресов адрес широкого вещания для подсети является на сегодняшний день наиболее общим. Но более старые си- стемы продолжают отправлять дейтаграммы, предназначенные для адреса 255.255.255.255. Кроме того, некоторые более старые системы не восприни- мают широковещательный адрес подсети и только отправляемые на адрес 255.255.255.255 дейтаграммы интерпретируют как широковещательные. ПРИМЕЧАНИЕ---------------------------------------------------------- Адрес 255.255.255.255 предназначен с использованием в качестве адреса получателя во время процесса начальной загрузки такими приложениями, как TFTP и ВООТР, которым еще не известен IP-адрес узла. Возникает вопрос’ что делает узел, когда приложение посылает дейтаграмму UDP на адрес 255.255 255.255? Большинство узлов допускают это (если процесс установил пара- метр сокета SO BROADCAST) и преобразуют адрес получателя в широковещатель- ный адрес исходящего интерфейса, направленный в подсеть. В BSD/OS 3.0 имеется новый параметр сокета IP ONESBCAST. Когда он установлен, ядро задает IP-адрес получателя для широковещательной передачи, равный 255.255.255.255. независимо от того, какой широковещательный адрес (локальный или адрес подсети) задан в каче- стве адреса получателя функции sendto. Может появиться другой вопрос: что делает узел с несколькими сетевыми интерфей- сами, когда приложение посылает дейтаграмму UDP на адрес 255.255.255.255? Неко- торые системы посылают одно широковещательное сообщение с основного интерфей- са (с интерфейса, который был сконфигурирован первым) с IP-адресом получателя, равным широковещательному адресу подсети этого интерфейса [105, с. 736]. Другие системы посылают по одной копии дейтаграммы с каждого интерфейса, поддержива- ющего широковещательную передачу. В разделе 3.3.6 RFC 1122 [9] по этому вопросу не сказано ничего. Однако если приложению нужно отправить широковещательное сообщение со всех интерфейсов, поддерживающих широковещательную передачу, то в целях переносимости оно должно получить конфигурацию интерфейсов (см. раздел 16.6) и выполнить по одной функции sendto для каждого из них, указав в качестве адреса получателя широковещательный адрес подсети этого интерфейса. 18.3. Сравнение направленной и широковещательной передач Прежде чем рассматривать широковещательную передачу, необходимо уяснить, что происходит, когда дейтаграмма UDP отправляется на адрес направленной передачи. На рис. 18.2 представлены три узла Ethernet.
518 Глава 18. Широковещательнаяпередача IP-адрес получателя=128 7 6 5 Протокол=иОР Рис. 18.2. Пример направленной передачи дейтаграммы UDP Адрес подсети Ethernet — 128.7.6. Идентификатор подсети и идентификатор узла занимают в адресе по 8 бит. Приложение на узле, изображенном слева, вы- зывает функцию sendto для сокета UDP, отправляя дейтаграмму на адрес 128.7.6.5, порт 7433. Уровень UDP добавляет в начало дейтаграммы заголовок UDP и пе- редает дейтаграмму UDP уровню IP. IP добавляет заголовок IPv4 и определяет исходящий интерфейс. В случае использования сети Ethernet активизируется таблица ARP для определения адреса Ethernet, соответствующего IP-адресу по- лучателя: 08 00 20 03 f6 42. Затем пакет посылается как кадр Ethernet с 48-раз- рядным адресом получателя Ethernet. Поле типа кадра Ethernet будет равно 0800, что определяется пакетом IPv4. Тип кадра для пакета IPv6 — 86dd. Интерфейс Ethernet на узле, изображенном в центре, видит проходящий кадр и сравнивает адрес получателя Ethernet со своим собственным адресом Ethernet (02 60 8с 2f-4е 00). Поскольку они не равны, интерфейс игнорирует кадр. Если бы кадр был кадром направленной передачи, этот узел вообще никак не участво- вал бы в процессе. Интерфейс Ethernet на узле, изображенном справа, также видит проходящий кадр, и когда он сравнивает адрес получателя Ethernet со своим собственным адре-
18.3. Сравнение направленной и широковещательной передач 519 сом Ethernet, они оказываются одинаковыми. Этот интерфейс считывает весь кадр, возможно, генерирует аппаратное прерывание при завершении считывания кад- ра и драйвер устройства читает кадр из памяти интерфейса. Поскольку тип кад- ра — 0800, пакет помещается в очередь ввода IP. Когда уровень IP обрабатывает пакет, он сначала сравнивает IP-адрес получа- теля (128.7.6.5) со всеми собственными IP-адресами. (Вспомним, что узел имеет несколько сетевых интерфейсов. Также вспомним наше обсуждение модели сис- темы с жесткой привязкой (strong end system model) и системы с гибкой привяз- кой (weak end system model) в разделе 8.8.) Поскольку адрес получателя — это один из собственных IP-адресов узла, пакет принимается. Затем уровень IP проверяет поле протокола в заголовке IPv4. Его значение для UDP равно 17, поэтому далее дейтаграмма IP передается UDP. Уровень UDP проверяет порт получателя (и, возможно, также порт отправи- теля, если сокет UDP является присоединенным) и в нашем примере помещает дейтаграмму в соответствующий приемный буфер сокета. При необходимости процесс возобновляется для чтения вновь полученной дейтаграммы. Ключевым моментом на этом рисунке является то, что дейтаграмма IP при направленной передаче принимается только одним узлом, заданным с помощью IP-адреса получателя. Другие узлы подсети не задействуются в этом процессе. Теперь мы рассмотрим похожий пример в той же подсети, но при этом отправ- ляющее приложение будет отправлять дейтаграмму UDP на широковещатель- ный адрес для подсети 128.7.6.255. Этот пример представлен на рис. 18.3. Когда узел, изображенный слева, отправляет дейтаграмму, он замечает, что IP-адрес получателя — это широковещательный адрес подсети, и сопоставляет ему адрес Ethernet, состоящий из 48 единичных битов: ff ff ff ff ff ff. Это за- ставляет каждый интерфейс Ethernet в подсети получить кадр. Оба узла, изобра- женные на правой части рисунка, работающие с IPv4, получат кадр. Поскольку тип кадра Ethernet — 0800, оба узла передают пакет уровню IP, так как IP-адрес получателя совпадает с широковещательным адресом для каждого из двух узлов, и поскольку поле протокола — 17 (UDP), оба узла передают пакет UDP. Узел, изображенный справа, передает дейтаграмму UDP приложению, свя- занному с портом UDP 520. Приложению не нужно выполнять никаких специ- альных действий, чтобы получить широковещательную дейтаграмму UDP — оно лишь создает сокет UDP и связывает номер приложения порта с сокетом. (Пред- полагается, как обычно, что связанный IP-адрес — INADDR_ANY.) Но на узле, изображенном в центре, с портом UDP 520 не связано никакого приложения. Код UDP-приложения игнорирует полученную дейтаграмму. Узел не должен отправлять сообщение ICMP о недоступности порта, поскольку это может вызвать лавину широковещательных сообщений (broadcast storm)-, условие, при котором множество узлов сети генерируют ответы приблизительно в одно и то же время, в результате чего сеть невозможно будет использовать в течение нескольких секунд. В этом примере мы также показываем дейтаграмму, которую изображенный слева узел доставляет сам себе. Это свойство широковещательных сообщений: по определению широковещательное сообщение идет к каждому узлу подсети, включая отправляющий узел [105, с. 109-110]. Мы также предполагаем, что от- правляющее приложение связано с портом, на который оно отправляет дейта-
520 Глава 18. Широковещательная передача IP-адрес получателям 28 7 6 255 Протокол=UDP Рис. 18.3. Пример широковещательной дейтаграммы UDP граммы (порт 520), поэтому оно получит копию каждой отправленной им широ- ковещательной дейтаграммы. (Однако в общем случае не требуется, чтобы про- цесс связывался с портом UDP, на который он отправляет дейтаграммы.) ПРИМЕЧАНИЕ--------------------------------------------------------- В этом примере мы демонстрируем закольцовку, которая осуществляется либо на уров- не IP, либо на канальном уровне, создающем копию [105, с 109-110] и отправляющем ее вверх по стеку протоколов. Сеть могла бы использовать физическую закольцовку, но это может вызвать проблемы в случае сбоев сети (например, линия Ethernet без терминатора). Этот пример отражает фундаментальную проблему, связанную с широкове- щательной передачей: каждый узел IPv4 в подсети, не участвующий в приложе- нии, должен полностью обрабатывать широковещательную дейтаграмму UDP при ее прохождении вверх по стеку протоколов, включая уровень UDP, прежде чем сможет ее проигнорировать. (Вспомните наше обсуждение следом за листин- гом 8.11.) Каждый не-1Р-узел в подсети (скажем, узел, на котором работает IPX
18.4. Функция dg cli при использовании широковещательной передачи 521 Novell) должен также получать целый кадр на канальном уровне, перед тем как он сможет проигнорировать этот кадр (в данном случае мы предполагаем, что узел не поддерживает тип получаемого кадра, для дейтаграммы IPv4 равный 0800). Если приложение генерирует дейтаграммы IP с большой скоростью (например, аудио- или видеоданные), то такая ненужная обработка может серьезно повли- ять на остальные узлы подсети. В следующей главе мы увидим, как эта проблема решается с помощью многоадресной передачи. ПРИМЕЧАНИЕ-------------------------------------------------------- Для рис. 18.3 мы специально выбрали порт UDP 520. Это порт, используемый демо- ном routed для обмена пакетами по протоколу информации о маршрутизации (Routing Information Protocol, RIP). Все маршрутизаторы в подсети, использующие RIP, будут отправлять широковещательную дейтаграмму UDP каждые 30 секунд Если в подсети имеется 200 узлов, в том числе два маршрутизатора, использующих RIP, то 198 узлов должны будут обрабатывать (и игнорировать) эти широковещательные дейтаграммы каждые 30 секунд, если ни на одном из них не запущен демон routed 18.4. Функция dg_cli при использовании широковещательной передачи Мы еще раз изменим нашу функцию dg_cl 1, на этот раз дав ей возможность от- правлять широковещательные сообщения стандартному серверу времени и даты UDP (см. табл. 2.1) и выводить все ответы. Единственное изменение, внесенное нами в функцию main (см. листинг 8.3), состоит в изменении номера порта полу- чателя на 13: servaddr sin_port = htons(13). Сначала мы откомпилируем измененную функцию main с прежней функцией dg_cl 1 из листинга 8.4 и запустим ее на узле bsdi: bsdi X udpcl101 206.62.226.63 hi sendto error Permission denied Аргумент командной строки — это широковещательный адрес подсети для присоединенной сети Ethernet. Мы вводим строку, программа вызывает функ- цию sendto, и возвращается ошибка EACCESS. Мы получаем ошибку, потому что нам не разрешается посылать дейтаграмму на широковещательный адрес полу- чателя, если мы не указали ядру явно, что будем передавать широковещательное сообщение. Мы выполняем это условие, установив параметр сокета SO_BROADCAST (см. табл. 7.1). ПРИМЕЧАНИЕ ----------------------------------------------------- Беркли-реализации реализуют эту «защиту от дурака» (sanity check). Однако Solaris 2.5 принимает дейтаграмму, предназначенную для широковещательного адреса, даже если мы не задаем параметр сокета SOBROADCAST. В Posix 1g говорится, что ядро «мо- жет» возвратить ошибку. В 4 2BSD широковещательная передача была привилегированной операцией, и пара- метра сокета SO BROADCAST не существовало. В 4.3BSD этот параметр был добав- лен, и каждому процессу стало разрешено его устанавливать
1044 Алфавитный указатель ICMP6_FILTER_WILLBLOCK, макрос, 714 ICMP6_FILTER_WILLPASS, макрос, 714 icmpcode_v4, функция, 737 icmpcode_v6, функция, 737 icmpd, программа, 740,743, 745, 885, 890,1022 icmpd.h, заголовочный файл, 746 icmpd_dest, элемент, 743 icmpd err, элемент, 742,745, 755 icmpd_errno, элемент, 742 ICMPv4 (Internet Control Message Protocol v. 4), 65, 709, 714, 740, 943,955 заголовок, 727 заголовочный файл, 717 контрольная сумма, 711,724, 775, 956 типы сообщений, 956 ICMPv6 (Internet Control Message Protocol v. 6), 65, 242, 709, 712, 740,955 заголовочный файл, 717, 729 контрольная сумма, 712,725, 956 параметр сокета, 242 типы сообщений, 957 фильтрация,714 IEC (International Electrotechnical Commission), 57, 1029 IEEE, 57,477, 513, 535, 952,1029 IEEEIX, 57 IETF (Internet Engineering Task Force), 60, 952,1027 if_freenameindex, функция, 509 исходный код, 512 определение, 508 if_index, поле, 980 if_index, элемент, 509 s f if_indextoname, функцид, 508,548, 588 исходный код, 510 определение, 508 if_msghdr, структура, 492,507 if_name, поле, 980 if name, элемент, 509, 512 if_nameindex, структура, 980 определение, 509 if_nameindex, функция, 491,509 исходный код, 511 определение, 508 if_nametoindex, функция, 491,508, 548 исходный код, 509 определение, 508 ifamsghdr, структура, 492 ifam_addrs, элемент, 492,499 ifc_buf, элемент, 474 ifc_len, элемент, 100, 475,476 ifcreq, элемент, 474 ifconf, структура, 100,471 ifconfig, программа, 55,126,249, 477,485 IFFBROADCAST, константа, 485 IFFPOINTOPOINT, константа, 485 IFF PROMISC, константа, 761 IFF_UP, константа, 485 IFI ALIAS, константа, 568 ifi_hlen, элемент, 478,483,507 ifi_info, структура, 474,476,478, 481,483,489, 504, 507,567, 571, 606 ifi_next, элемент, 476 ifm_addrs, элемент, 492,499 ifm_type, элемент, 506 IFNAMSIZ, константа, 508 ifr_addr, элемент, 474,485 ifr_broadaddr, элемент, 474,485, 488 ifr_data, элемент, 474 ifr_dstaddr, элемент, 474,485,488 ifr_flags, элемент, 474,485 ifr_metric, элемент, 474, 485 ifr_name, элемент, 476,485 ifreq, структура, 471,481,483,485, 488, 550
Алфавитный указатель 1045 IGMP (Internet Group Management Protocol), 65, 542, 709, 713, 943 контрольная сумма, 725 ILP32, модель программиро- вания, 61 imr_interface, элемент, 544, 550 imr_multiaddr, элемент, 544 in-addr.arpa, домен, 283, 294 in.rdisc, программа, 709 in addr, структура, 95,220, 287, 293,309,544 определение, 92 in_addr_t, тип данных, 94 in_cksum, функция, 725 исходный код, 726 inpcbdetach, функция, 166 in pktinfo, структура, 582, 585, 979 определение, 582 in_port_t, тип данных, 94 in6_addr, структура, 96,220, 287, 293 IN6_IS_ADDR_LINKLOCAL, макрос, 310 IN6_IS_ADDR_LOOPBACK, макрос, 310 IN6_IS_ADDR_MC_GLOBAL, макрос, 310 IN6_IS_ADDR_MC_ LINKLOCAL, макрос, 310 IN6_IS_ADDR_MC_ NODELOCAL, макрос, 310 IN6_IS_ADDR_MC_ORGLOCAL, макрос, 310 IN6_IS_ADDR_MC_SITELOCAL, макрос, 310 IN6_IS_ADDR_MULTICAST, макрос, 310 IN6_IS_ADDR_SITELOCAL, макрос, 310 IN6ISADDRUNSPECIFIED, макрос, 310 IN6_IS_ADDR_V4COMPAT, макрос, 310 IN6_IS_ADDR_V4MAPPED, макрос, 306, 311, 314,719, 822 определение, 310 in6_pktinfo, структура, 582, 612, 706 определение, 612 in6addr_any, константа, 125, 955 IN6ADDrJaNY_INIT, константа, 125, 320,322, 352,418, 613, 955 in6addr_loopback, константа, 954 IN6ADDRLOOPBACKJNIT, константа, 954 INADDR ANY, константа, 46, 80, 125,148,151, 240, 257,320, 322, 352,418,543, 828,916,950,989 INADDRLOOPBACK, константа, 950 INADDR_MAX_LOCAL_ GROUP, константа, 989 INADDR_NONE, константа, 96, 978, 989 inetaddr, функция, 40, 92, 105, 117 определение, 105 INET_ADDRSTRLEN, константа, 110, 978 inet_aton, функция, 105, 117 определение, 105 INET IP, константа, 895 inet_ntoa, функция, 92,105, 345, 662 определение, 105 inet_ntop, функция, 92,105, 116, j 135,289,343, 345,371, 373,487, 588 версия только для IPv4, исходный код, 108 определение,107 inet jton, функция, 40,43, 92,105, 116, 334,345, 355, 1004 версия только для IPv4, исходный код, 108 определение, 107 inet_pton_loose, функция, 117 inet_srcrt_add, функция, 691, 695
1046 Алфавитный указатель inet srcrt init, функция, 691, 695 inet_srcrt_print, функция, 693 INET6_ADDRSTRLEN, константа, 108,110, 289, 979 inet6_option_alloc, функция, 702 inet6_option_append, функция, 702 inet6_option_find, функция, 703 определение, 702 inet6_option_next, функция, 702 определение, 702 inet6_option_space, функция, 705 inet6_rthdr_add, функция, 705 определение, 704 inet6_rthdr_getaddr, функция, 706 определение, 705 inet6_rthdr_getflags, функция, 706 определение, 705 inet6_rthdr_init, функция, 705 определение, 704 inet6_rthdr lasthop, функция, 705 определение, 704, 705 inet6_rthdr_reverse, функция, 706 inet6_rthdr_segments, функция, 706 определение, 705 inet6_rthdr space, функция, 704 определение,704 inetd, программа, 29, 87, 138,143, 180, 311, 374,383, 552, 581, 611, 791,817, 975, 1008,1014,1021 INFTIM, константа, 210,980 init, программа, 157, 172,1012 intl6_t, тип данных, 94 int32_t, тип данных, 94,822 int8_t, тип данных, 94 ioctl, функция, 59,216, 248, 295, 409,416,426,435,470,475,479, 483,490, 504,550,580, 619, 626, 642,644, 647,760, 761,769,839, 907, 914, 925,933,937, 940, 965, 967 ARP, операции с кэшем, 485 определение, 470 конфигурация интерфейса, 474 операции с интерфейсом, 485 ioctl, функция (продолжение) операции с сокетами, 470 операции с таблицей маршрутизации, 488 операции с файлами, 471 потоки, 914 iov_base, элемент, 400, 931 iov_len, элемент, 400,402, 931 IOV_MAX, константа, 400 iovec, структура, 400,403, 598 определение, 400 IP (Internet Protocol), 65 маршрутизация, 942 определение адреса локального узла, 295 поле номера версии, 942, 944 фабрикация вымышленного IP-адреса, 132,1028 фрагментация и многоадресная передача, 552 фрагментация и широковещательная передача, 524 IP_ADD_MEMBERSHIP, параметр сокета, 220, 543 IPDROPMEMBERSHIP, параметр сокета, 220,543 IP_HDRINCL, параметр сокета, 220, 240, 688, 710, 725, 727, 759, 762,768,775 ip_ id, элемент, 713,776 ip len, элемент, 711,713 ipmreq, структура, 220,543, 550 определение, 544 IP MULTICAST IF, параметр сокета, 220, 543,1021 IPMULTICASTLOOP, параметр сокета, 220,543,546 IP_MULTICAST_TTL, параметр сокета, 220, 543, 546, 943,1022 ipoff, элемент, 711,713 IP ONESBCAST, параметр сокета, 517, 972 IP_OPTIONS, параметр сокета, 220,240, 687,697,707, 896,1021
Алфавитный указатель 1047 IP RECVDSTADDR, параметр сокета, 220, 237, 241, 266,402,404, 581,584, 587, 605, 613, 644,971 вспомогательные данные, схема, 404 IP RECVIF, параметр сокета, 220, 240,405,492, 582, 584, 587, 605, 615,644 IP TOS, параметр сокета, 220, 241, 895,897,942, 966, 971 IP_TTL, параметр сокета, 220, 241, 244, 727,895, 897,943, 971 ip6.int, домен, 283, 293 IPC, взаимодействие процессов, 29,417, 530,652 ipi_addr, поле, 979 ipi_addr, элемент, 582 ipi_ifindex, поле, 979 ipiifindex, элемент, 582 ipi6_addr, элемент, 613 ipi6_ifindex, элемент, 613 IPng (Internet Protocol next generation), 944 ipoptdst, элемент, 693 ipopt_list, элемент, 693 ipoption, структура определение,693 IPPROTO EGP, константа, 710 IPPROTO ICMP, константа, 710 IPPROTO ICMPV6, константа, 220, 242,712,714 IPPROTO IP, константа, 240,313, 404, 688 IPPROTO_IPV6, константа, 242, 313,405, 613, 700, 705 IPPROTO RAW, константа, 711 IPPROTOTCP, константа, 244, 319, 368,414 IPPROTOUDP, константа, 319 IPTOS LOWCOST, константа, 241 IPTOS—LOWDELAY, константа, 241 IPTOSRELIABILITY, константа, 241 IPTOSTHROUGHPUT, константа, 241 IPv4 (Internet Protocol v. 4), 65 адрес, 946 многоадресной передачи, 534 отправителя, 944 получателя, 944 заголовок, 942 заголовочный файл, 717, 729 и IPv6, совместимость, 304 клиент и IPv6-cepBep, совместимость, 305 контрольная сумма, 240,725 заголовка, 711, 943 маршрутизация от отправителя, 689 параметры, 687, 737, 896, 944 сокета, 240 поле длины заголовка, 942 идентификации, 943 общей длины, 943 протокола, 943 смещения фрагмента, 943 сервер и 1Ру6-клиент, совместимость, 308 структура адреса сокета, 92 IPV4, константа, 349 IPv4-адрес, преобразованный к виду IPv6,117, 292,305, 322, 336, 357,719, 953 1Р\’4-совместимый адрес IPv6,294, 954 IPv4/IPv6, узел, 66 IPv6 (Internet Protocol v. 6), 65 адрес, 951 многоадресной передачи, 536 отправителя, 945 получателя, 945 заголовок, 944 маршрутизации, 703 расширения, 699 заголовочный файл, 729 закрепленные параметры, 706 и IPv4, совместимость, 304 и доменный сокет Unix, функция getaddrinfo, 322 клиент и IPv4-cepBep, совместимость, 308
1048 Алфавитный указатель IPv6 (Internet Protocol v. 6) {продолжение) контрольная сумма, 242,712, 946 маршрутизация от отправителя, 703 параметры получателя, 699 сокета, 242 транзитных узлов, 699 поле длины данных, 945 метки потока, 945 следующего заголовка, 945 получение информации о пакете, 612 сервер и IPv4-клиент, совместимость, 305 структура адреса сокета, 96 функция gethostbyaddr, 294 IPV6, константа, 349 IPV6_ADD_MEMBERSHIP, параметр сокета, 220, 543 IPV6_ADDRFORM, параметр сокета, 220, 242,312 IPV6_CHECKSUM, параметр сокета, 220, 242, 712 IPV6DROPMEMBERSHIP, параметр сокета, 220, 543 IPV6DSTOPTS, параметр сокета, 220,243,405,701,703 вспомогательные данные, схема, 700 IPV6_HOPLIMIT, параметр сокета, 220, 243, 405, 614, 945 вспомогательные данные, схема, 612 IPV6_HOPOPTS, параметр сокета, 220, 243,405, 701 вспомогательные данные, схема, 700 ipv6_mreq, структура, 220,543,551 определение, 544 IPV6MULTICASTHOPS, параметр сокета, 220, 543, 546, 614,945 IPV6MULTICASTIF, параметр сокета, 220, 543, 613 IPV6MULTICASTLOOP, параметр сокета, 220, 543, 546 IPV6 NEXTHOP, параметр сокета, 220, 243,405, 614 вспомогательные данные, схема, 612 IPV6 PKTINFO, параметр сокета, 220, 243, 266,405,545,605, 613,644 вспомогательные данные, схема, 612 IPV6 PKTOPTIONS, параметр сокета, 220, 244, 707 IPV6 RTHDR, параметр сокета, 220, 244,405, 704 вспомогательные данные, схема, 705 IPV6RTHDRLOOSE, константа, 705 IPV6_RTHDR_STRICT, константа, 705 IPV6RTHDRTYPE0, константа, 705 IPV6UNICASTHOPS, параметр сокета, 220, 244,614, 727, 733,945 ipv6mr_interface, элемент, 544, 551 ipv6mr_multiaddr, элемент, 544 IPX (Internetwork Packet Exchange), 842, 1031 IRS (Information Retrieval Service), 285 isfdtype, функция, 115 исходный код, 115 определение, 115 ISO, 50, 57, 1029 ISO 8859,555 ISP (Internet Service Provider), 948, 953 ITU (International Telecommunication Union), 556
Алфавитный указатель 1049 «г J Jackson, А., 700, 1030 Jacobson, V., 70, 77, 553, 591, 594, 711,758,761,857,896,975,1029 Jamin, S., 30 Johnson, D., 31 Johnson, M., 31 Johnson, S., 30 Jones, R. A., 30 Josey, A., 59, 1030 Joy,W. N„ 128, 1030 К Каскег, M., 30 Karels, M.J., 52,316,1031 Karn, P„ 595,1030 Kaslo, P.,31 Katz, D„ 534, 688, 700,1030 kdump, программа, 968 Kent, S. T„ 688, 698,1030 Kernighan, B. W„ 31,44, 984,1030 Key, структура, 665, 668 kill, программа, 167, 168,1022 Korn, D. G., 412, 635,1030 KornShell, 169, 290, 843 kp_onoff, элемент, 898 kp_timeout, элемент, 898 ksh, программа, 152 ktrace, программа, 968 Kureshi, Y., 31 L Ifixedpt, элемент, 560 l ien, элемент, 800 l_linger, элемент, 228, 252,467,835, 896 l_onoff, элемент, 228, 252,467,835, 896 l_start, элемент, 800 1 type, элемент, 800 l_whence, элемент, 800 Lampo, M., 30 Lanciani, D., 121, 253, 1030 LAST_ACK состояние, 72 ' Leisner, M., 30 len, элемент, 777, 826,829, 837, 847, 863, 894,896, 902,912, 940 Leres, C., 975 level, элемент, 894 LF (перевод строки), 41 LF (символ новой строки), 971, 990 Li, Т„ 948,1028 hbpcap, библиотека, 757, 762 LIFO (последним пришел, первыу обслужен), 868,873 Lin.J.C., 234,1028 linger, структура, 217,995 определение, 228 Linux, 31, 53,54, 64, 102, 120,132, 188,265, 272,277,524, 552, 644, 711,713,757,761, 767, 777, 780, 1014 LISTEN, состояние, 72,126,151, 390,856,860,996 listen, функция, 44,46, 68, 124,126, 144,148, 151,158,165,204, 233, 236,239,314,320, 332,340,385, 390,413, 748, 792,807,828, 860, 874,989,999 backlog и длина очереди XTI, 874 определение, 128 Listen функция-обертка,исходный код, 129 LISTENQ, константа, 46, 870 определение, 980 LISTENQ переменная окружения, 129,860 Liu, С., 283, 301,1027 LLADDR, макрос, 491 /local, имя узла, 325, 336, 351,370,. 1010 LOCAL CREDS, параметр сокета, 435 localtime, функция, 662 localtime_r, функция, 662
1050 Алфавитный указатель LOGALERT, константа, 376 LOG_AUTH, константа, 377 LOG_AUTHPRIV, константа, 377 LOG—CONS, константа, 378 LOG—CRIT, константа, 376 LOG—CRON, константа, 377 LOG—DAEMON, константа, 377, 391 LOG DEBUG, константа, 376 LOG EMERG, константа, 376 LOG ERR, константа, 376, 984 LOG_FTP, константа, 377 LOG—INFO, константа, 376, 984 LOG_KERN, константа, 377 LOG LOCALO, константа, 377 LOG LOCALl, константа, 377 LOG_LOCAL2, константа, 377 LOG—LOCAL3, константа, 377 LOG—LOCAL4, константа, 377 LOG—LOCALS, константа, 377 LOG—LOCAL6, константа, 377 LOGLOCAL7, константа, 377 LOG—LPR, константа, 377 LOG_MAIL, константа, 377 LOG NDELAY, константа, 378 LOG NEWS, константа, 377 LOG—NOTICE, константа, 376, 391 LOGPERROR, константа, 378 LOG_PID, константа, 378 LOG SYSLOG, константа, 377 LOGUSER, константа, 377,381, 390 LOG—UUCP, константа, 377 LOG_WARNING, константа, 376 logger, программа, 378 login-name, поле, 383, 385,438 loom, программа, 32 Lothberg, P., 952,1032 LP64, модель программиро- вания, 61 Is, программа, 420 Iseek, функция, 185,410,965 Isof, программа, 976 LSRR гибкая маршрутизация от отправителя с записью, 687 свободная маршрутизация от отправителя с записью, 703 Lucchina, Р., 31 м М_DATA, константа, 911,923, 967 М_PCPROTO, константа, 911,918, 922, 967 М-PROTO, константа, 911,918, 921,922,924 MAC (Media Access Control), 477, 491,952 machine, элемент, 294 malloc, функция, 61, 262,317,320, 330,333, 347,432,512, 523, 645, 662, 664,686, 751, 846,881 Marques, P., 31 Maslen, T. M„ 348,1030 Maufer, T., 542,1030 MAX—IPOPTLEN, константа, 693 MAXFILES, константа, 459 MAXHOSTNAMELEN, константа, 296 maxlen, элемент, 826, 847, 880,899, 912 MAXLINE, константа, 38, 114, 587, 889, 977 определение, 980 MAXLOGNAME, константа, 434 MAXSOCKADDR, константа, 144, 330,389 определение,980 MBone, магистраль многоадресной передачи, 31, 54, 534, 579, 959, 1015 анонсирование сеанса, 553 mcast_get_if, функция, 547 определение, 547 mcast—get—loop, функция, 547 определение, 547
Алфавитный указатель 1051 mcast_get_ttl, функция, 547 определение, 547 mcast_join, функция, 547,554, 558, 562 исходный код, 550 определение, 547 mcast_leave, функция, 547 определение, 547 mcast_set_if, функция, 547, 572, 580 определение, 547 mcast_set_loop, функция, 547,558, 573 определение, 547 mcast_set_ttl, функция, 547 определение, 547 McCann, J., 30,84,1031 McCanne, S„ 758, 761, 975,1031 McDonald, D. L., 120, 698,1031 McKusick, M. K„ 52, 1031 memcmp, функция, 104, 262 определение, 105 memcpy, функция, 104,302,872, 877,880,918,1003, 1024 определение,104 memmove, функция, 105,872,876, 1004,1024 ‘ memset, функция, 40, 104, 979 определение, 104 Mendez, T., 514,1031 meter, функция, 796 Metz, C. W., 30,120, Ю31 Meyer, D„ 537,1031 MF, флаг IP-заголовка, 943 MIB (Management Information Base), 500 Milliken, W., 514,1031 Mills, D. L„ 560,1031 min, функция, 880 mkfifo, функция, 427 mktemp, функция, 800 mmap, функция, 58, 797,802, 965 MODE_CLIENT, константа, 563, 572 MODE_SERVER, константа, 577 Mogul, J. C., 84, 948, 1031 MORE_flag, элемент, 923 MORECTL, константа, 913 MOREDATA, константа, 913 mrouted, программа, 709,960,1015 msg_accrights, элемент, 401,427, 432 msg_accrightslen, элемент, 401 MSG_ANY, константа, 914 MSG_BAND, константа, 914 MSG_BCAST, константа, 402,588 msg_control, элемент, 401,404,408, 427,432, 584 msg_controllen, элемент, 100,401, 404,405,408 MSG CTRUNC, константа, 402 MSGDONTROUTE, константа, 224, 398,402 MSGDONTWAIT, константа, 398,402,408 MSG_EOF, константа, 399,414 MSG_EOR, константа, 399,402, 414,439,1010 msg-flags, элемент, 399,404,582, 584,589 MSG_HIPRI, константа, 914, 967 msg—iov, элемент, 401 msg—iovlen, элемент, 401 MSG—MCAST, константа, 402, 588 msg_name, элемент, 401,404 msg—namelen, элемент, 100,401, 404,584 MSG_OOB, константа, 233, 248, 398,402, 618, 624, 626, 629, 639, 831,935 MSG—PEEK, константа, 398,402, 408,416,427, 972,1009 MSG_TRUNC, константа, 402, 589 MSG_WAITALL, константа, 113, 398,402,440 msghdr, структура, 100, 399,403, 408,427,434, 582, 584,589, 598 определение, 401
1052 Алфавитный указатель MSL, максимальное время жизни сегмента, 75,178,228, 989 определение, 75 MSS, максимальный размер сегмента, 70,74, 84,90, 234, 245, 252,413,414,892, 898, 971,988, 995 определение, 70, 245 параметр, TCP, 70 MTU, максимальная единица передачи, 51,55, 82,234, 524,590, 711,743,946, 957, 988, 1013 минимальная канальная MTU, 83 определение, 82 транспортная MTU, 86,90, 245, 449,743,946, 995,1031 обнаружение, 83 определение, 83 MULTICAST, константа, 56 MX (Mail Exchange Record, DNS), 283, 288, 301 my addrs, функция, 295, 302,487, 1003 исходный код, 295, 1003 my_lock_init, функция, 799, 802 my_lock_release, функция, 803 my_lock_wait, функция, 803 my open, функция, 428,429,433 my_read, функция, 115, 670 mycat, программа, 427 mydg echo, функция, 606 N n_addrs, элемент, 844 n_cnt, элемент, 844 name, элемент, 894 nc_ device, элемент, 843 nc jlag, элемент, 843 nc_lookups, элемент, 843 nc netid, элемент, 843 nc_nlookups, элемент, 843 nc_proto, элемент, 843 ncjrotofmly, элемент, 843 nc_semantics, элемент, 843 nc_unused, элемент, 843 ND_ADDRLIST, константа, 845 nd addri ist, структура, 844, 851, 881 определение, 844 nd_hostserv, структура, 844, 851, 854 определение, 844 NDHOSTSERVLIST, константа, 846 ndhostservlist, структура, 845 определение, 845 Nelson, R., 30 Nemeth, E., 30, 69,1031 Net/1,53, 697 Net/2, 53,711 Net/3, 53,399 <net/if.h>, заголовочный файл, 485, 508 <net/if_arp.h>, заголовочный файл, 485 <net/if_dl.h>, заголовочный файл, 491, 584 <net/route.h>, заголовочный файл, 488,492 NET_RT_DUMP, константа, 502 NETRTFLAGS, константа, 502 NET_RT_IFLIST, константа, 502, 505 net_rt_iflist, функция, 505, 506, 510,512 <netdb.h>, заголовочный файл, 287, 300,316,342 NetBIOS, 1031 NetBSD, 52, 241 netbuf, структура, 826, 840,844, 851,853, 861,880,894, 912,940, 1023 определение, 826 netconfig, структура, 841, 849,852, 859,881, 939,965,1023 определение, 843 netdir, функция, 841
Алфавитный указатель 1053 netdirfree, функция, 846,851,860, 881,887 netdirgetbyaddr определение, 845 netdir_getbyaddr, функция, 815, 848,853 netdirgetbyname определение, 844 netdir getbyname, функция, 841, 844, 851,853, 859,860,880,887 netent, структура, 300 <netinet/icmp6.h>, заголовочный файл,714 <netinet/in.h>, заголовочный файл, 92, 96,108, 125, 145, 613, 710 <netinet/ip.h>, заголовочный файл, 241 <netmet/ip_var.h>, заголовочный файл, 693 <netmet/udp_var.h>, заголовочный файл, 504 NETPATH переменная окружения, 842, 851, 859 netpath, функция, 844 Netscape, 457,466 netstat, программа, 55, 63, 68, 72, 80, 89, 110, 151, 153,167,178, 252, 263,273, 301,390,485,488, 490, 557, 609,975, 991, 1001 Netware, 842, 1031 next_рсар, функция, 777 nfds_t, тип данных, 210 NFS, сетевая файловая система, 89, 233,239, 254, 592, 759 N1_DGRAM, константа, 342,372 Nl—MAXHOST, константа, 342 NI_MAXSERV, константа, 342 NI_NAMEREQD, константа, 342, 371, 373 NI NOFQDN, константа, 342, 371 N1NUMERICHOST, константа, 342,371,1007 N1NUMERICSERV, константа, 342,372,1007 NIS (Network Information System), 285 NIT (Network Interface Tap), 758, 762 NLA (Next-Level Aggregation Identifier), 952 NNTP (Network News Transfer Protocol), 89 NO_ADDRESS, константа, 288 NO_DATA, константа, 288 NO RECOVERY, константа, 288 NO AO (National Optical Astronomy Observatory), 31, 54, 949 Noble, J. C„ 30 nodename, элемент, 295 NOP (нет действий), 687,690,697, 708 NPI (Network Provider Interface), 910,1033 nselcoll, переменная, 798 ntohl, функция, 103,179, 992 определение, 103 ntohs, функция, 135 определение, 103 NTP (Network Time Protocol), 88, 515,523,545,556,580,643,651, 1016,1031 ntp, программа, 567 ntp.h, заголовочный файл, 560,5661 ntpdata, структура, 561 NVT (Network Virtual Terminal), 990 О O_ASYNC, константа, 248,473, 642, 647 O_NONBLOCK, константа, 248, 473,647,821,926,940 O RDONLY, константа, 429 O RDWR, константа, 821 O SIGIO, константа, 642 O'Dell, M„ 952,1029 Open Group, 57, 820,1031 open, функция, 160,380, 422,427, 429,433, 760,803, 965 OPEN—MAX, константа, 211
1054 Алфавитный указатель ореп_рсар, функция, 768, 770 OpenBSD, 52 openfile, программа, 428,430,432 openlog, функция, 377, 381, 388 определение, 377 opt, элемент, 827,829,835,837, 846, 857,880, 884, 893,899,901,932 OPTJength, элемент, 921 OPT offset, элемент, 921 optvalstr, элемент, 221 optarg, переменная, 695 opterr, переменная, 695 opthdr, структура, 894 optind, переменная, 695 options, элемент, 822 optopt, переменная, 695 OSF (Open Software Foundation), 59 OSI (Open Systems Interconnection), 50, 93, 120,399, 403, 405, 820,824,855,1031 модель, 50 OSPF (Open Shortest Path First), 88, 634, 709, 988 P Papanikolaou, S„ 32 Partridge, C„ 271, 514, 595, 700, 725, 1028,1030,1031 PATH переменная окружения, 55 PATH переменная откужения, 138 PATH MAX, константа, 876,1024 pause, функция, 214, 314, 452, 630 PAWS, алгоритм, 1030 Paxson, V., 31,83,1031 PCSOCKMAXBUF, константа, 235 pcap_compile, функция, 759, 770 pcap_datalink, функция, 770, 776 pcap_lookupdev, функция, 769 pcap_lookupnet, функция, 770 pcap next, функция, 777 pcap_open_live, функция, 769, 777 pcappkthdr, структура определение, 777 pcapsetfilter, функция, 770, 778 pcap_stats, функция, 779 PCM (Pulse Code Modulation), 556 pfmod, потоковый модуль, 760 Phan, В. G., 120,1031 Pike, R„ 44,1030 ping, программа, 56, 64,89, 106, 280, 580, 708, 999,1021 реализация, 715 ping.h, заголовочный файл, 716 Pink, S., 271,1031 pipe, функция, 421,427 Piscitello, D. M., 311,1032 pkey, структура, 668 Plauger, P. J., 409,1032 poll, функция, 168,172,177,180, 181,182,189, 195, 208, 214, 640, 741,896, 927,935,938, 939, 974, 1019 определение, 208 POLLERR, константа, 208, 213 pollfd, структура, 208, 210,935, 939 определение, 208 <poll.h>, заголовочный файл, 210 POLLHUP, константа, 208 POLLIN, константа, 208, 939, 973 POLLNVAL, константа, 208 POLLOUT, константа, 208 POLLPRI, константа, 208,973 POLLRDBAND, константа, 208, 973 POLLRDNORM, константа, 208, 213,935, 973 POLLWRBAND, константа, 208 POLLWRNORM, константа, 208 Posix (Portable Operating System Interface), 57, 229 Posix.l, 60,155,185, 212, 294, 379, 441,470, 524,526, 602, 641, 649, 657, 662, 665, 683, 746, 799, 820, 994,1024, 1029 определение, 58 Posix.lb, 57, 206 Posix. 1c, 57, 653
Алфавитный указатель 1055 Posix. 1g, 59,93,96,98, 102,115, 120,121,128,131,144,166,180, 188,199,206, 209, 226, 228, 235, 240, 242,244, 248, 268, 315, 322, 325, 343, 348, 351,400,408,417, 420,427,441,453,457,468, 521, 528, 589, 624, 642, 647, 662,820, 826, 857, 867, 874, 891, 895, 931, 934,996,1029 определение, 58 Posix. li, 57 Posix.2, 57, 60, 170,420,695 Postel, J. B„ 66, 77, 239, 242, 709, 942,943, 948,952,956,1029,1032 PPP (Point-to-Point Protocol), 83, 501,776 pr_cpu_time, функция, 789, 793 prifinfo, программа, 489, 504 PRIM_type, элемент, 917, 918,921, 923 printf, функция вызов из обработчика сигналов, 158 proc, структура, 794 proc_v4, функция, 721 proc_v6, функция, 721 .profile, файл, 290 proto, структура, 717, 719,729 protoent, структура, 300 ps, программа, 152, 163 pselect, функция, 180, 206,210,214, 527,528,683 исходный код, 529 определение, 206 <pthread.h>, заголовочный файл, 657, 673 Pthread, структура, 665 pthread_attr_t, тип данных, 654 pthread_cond_broadcast, функция, 683 определение, 683 pthread_cond signal, функция, 683, 816 pthread_cond_t, тип данных, 681 pthread_cond_timedwait, функция, 683 определение, 683 pthread_cond_wait, функция, 681, 685,816 pthread_create, функция, 653, 659, 810 pthread_detach, функция, 653 pthread_exit, функция, 653 pthread getspecific, функция, 666, 669 определение, 669 pthread_join, функция, 653, 674, 680, 684 pthread_key_create, функция, 666 668 определение, 668 pthread_key_t, тип данных, 669 pthread—muteX—init, функция, 678 803 PTHREAD-MUTEX- INITIALIZER, константа, 678, 800,803 Pthread_mutex_Iock wrapper, функция исходный код, 44 pthread—mutex lock, функция, 813 определение, 678 pthread mutex t тип данных, 678, 800,803 pthread_mutex_unlock, функция, 682,813 определение, 678 pthread_mutexattr_t, тип данных, 803 pthread—once, функция, 666, 668 определение, 668 pthread_once_t, тип данных, 669 PTHREAD_PROCESS_PRIVATE, константа, 803 PTHREADPROCESS-SHARED, константа, 802 pthread—self, функция, 653 pthread—setspecific, функция, 666, 669 определение, 669
1056 Алфавитный указатель pthread t, тип данных, 654 PTR, запись указателя (DNS), 283, 293,334 Pusaten, Т, 534, 1032 PUSH, флаг ТСР-заголовка, 831 pule unlocked, функция, 662 putcharunlocked, функция, 662 putmsg, функция, 907,911,918, 921,924, 934,964 определение, 912 putpmsg, функция, 907,911, 913, 925, 934, 972 определение, 913 Q qlen, элемент, 827, 840, 855,861, 874, 1022 QSIZE, константа, 645 Quarterman, J S , 52, 1031 R Rafsky, L С , 31 Rago, S А, 31,907,910,1032 rand, функция, 662 rand r, функция, 662 RARP (Reverse Address Resolution Protocol), 65, 757,759 read, функция, 39, 41,43, 61, 111, 113, 115,116,141,151,160, 185, 194, 206, 209, 225, 230, 236, 255, 268, 271, 280, 392, 398,400,404, 409,415,431,436, 439,440,442, 445,456,462,465,494, 532, 622, 626, 628, 643, 759, 777, 808, 830, 834.835, 839, 862, 864, 872. 908, 912,924,935, 965, 968, 987, 998, 1010 read_cred, функция, 436 readfd, функция, 431,434, 750, 80£ исходный код, 432 read loop, функция, 565, 568, 573 readable conn, функция, 750 readable listen, функция, 749 readable_timeo, функция, 395 исходный код, 395 readable_v4, функция, 752 readable_v6, функция, 755 readdir, функция, 662 readdirr, функция, 662 readkne, функция, 111, 116,146, 149, 150,153, 158,168, 171, 177, 195, 203, 206, 213,410, 657, 662, 666, 668, 686,811, 977, 990, 994, 995,998 исходный код, 113, 670 определение,111 readline destructor, функция, 669, 686 readline once, функция, 669, 686 readloop, функция, 719, 724 readn, функция, 111, 116,175, 399, 440,993 исходный код, 112 определение, 111 readv, функция, 236, 392,400,404, 415, 440, 931 определение, 400 reason, элемент, 827, 835 тес, структура, 729 recv, функция, 113, 236, 255, 268, 392, 398, 404,409,415, 440, 589, 620, 622, 624, 629, 639,831, 935 определение, 398 recv all, функция, 559 recv_v4, функция, 734, 737 recv_v6, функция, 734, 736 recvfrom, функция, 93, 98,160, 181, 182, 186, 236, 255, 258, 267, 271, 280,285,302,307,309, 311,313, 320,336, 342, 392,399,404,409, 415,425,440, 523,525, 526, 528, 555, 559,562, 577, 582, 584, 586, 589,596, 598, 608, 611, 620, 642, 650,734, 735,740,761, 777,879, 890,999, 1009,1021 определение, 255 с тайм-аутом, 394
Алфавитный указатель 1057 recvfrom flags, функция, 582,586 RFC 1112, 534, 1028 recvmsg, функция, 93, 100, 236, 241, RFC 1122, 75, 253, 263, 517, 557, 255, 267, 392, 399,407, 415,427, 583,951.1027 431,440, 545, 582,584, 587, 589, RFC 1185, 77,1030 598, 600, 612, 620, 701, 703, 706, RFC 1191, 83,1031 893,931,1010, 1017 RFC 1305, 560,1031 определение, 401 RFC 1323, 70,252,501,595,69ft Red Hat Software, 31 959,1028,1030 Regina, N, 32 RFC 1337, 228,1027 Reid, J, 30 RFC 1349, 241,1027 Rekhter, Y, 952, 1032 RFC 1379, 412,1027 release, элемент, 294 RFC 1390, 534, 1030 rename, функция, 377 RFC 1469, 534,1032 Reno, 53,93,98, 236, 399,401,49Q, RFC 1519, 948,1028 582,689,711,727 RFC 1546, 514,1031 RESINIT, константа, 293 RFC 1639, 311,1032 resinit, функция, 290, 293,301,356 RFC 1644, 412,1028 RES_length, элемент, 921 RFC 1700, 77, 242, 709,943,1032 RES_offset, элемент, 921 RFC 1812, 743,1027 RES_OPTIONS переменная RFC 1826, 698,1027 окружения, 293 RFC 1827, 698,1027 RESOPTIONS, переменная RFC 1832, 177, 1032 окружения, 290 RFC 1883, 243, 700, 70ft 70ft 1028 RES_USE_INET6, константа, 290, RFC 1884, 952,1029 301,308,322, 357 RFC 1885, 956,1028 revents, элемент, 208, 939 RFC 1886, 283,1033 rewind, функция, 410 RFC 1897, 953,1029 Reynolds,} К, 77, 242, 709,943, RFC 1972, 536,1028 1032 RFC 1981, 84,1031 RFC (Request for Comments), 60, RFC 2019, 536, 1028 66,988,1027 RFC 2026, 60,1028 получение, 988 RFC 2030, 560, 1031 RFC 768, 66, 1032 RFC 2073, 952, 1029,1032 RFC 791, 942,1032 RFC 2113, 688, 1030 RFC 792, 956,1032 RFC 2133, 60,96, 242, 343, 508, RFC 793, 66, 239 545,1029 RFC 862, 87 RFC 2147, 84, 1027 RFC 863, 87 RFC, требование к узлам, 1027 RFC 864, 87 RIP (Routing Information RFC 867, 87 Protocol), 84,88, 521 RFC 868, 87 Ritchie, D M , 31,907, 984,1030, RFC 950, 948, 1031 1032 RFC 1071, 725,1028 rl cnt, элемент, 671 RFC 1108, 688,1030 rlkey, функция, 669
1058 Алфавитный указатель rl_once, функция, 669 rlim_cur, элемент, 993 rlim_max, элемент, 993 RLIMIT_NOFILE, константа, 993 Rline, структура, 669 Rlogin, 227,241, 245, 287, 633, 639 rlogin, программа, 78 rlogind, программа, 697, 708,1021 rmt_addrs, элемент, 492 Roberts, М., 31 Rose, М.Т., 316 routed, программа, 225,485, 515, 521 RPC (Remote Procedure Call), 124, 177,383, 592, 1022 RR, запись ресурса (DNS), 283 rresvport, функция, 78 RS_HIPRI, константа, 913,918 rsh, программа, 77, 78, 297, 343 rshd, программа, 697 RST, флаг ТСР-заголовка, 76,122, 131,165,168, 194, 205, 209, 213, 215, 226, 232, 251, 272,466, 758, 763,830,831, 834, 838, 840,856, 867, 868,874,896, 924, 990,995, 1012, 1022 RTA_AUTHOR, константа, 493 RTA BRD, константа, 493 RTADST, константа, 493 RTA_GATEWAY, константа, 493 RTA_GENMASK, константа, 493 RTA_IFA, константа, 493 RTA_IFP, константа, 493 RTA NETMASK, константа, 493 RTAX_AUTHOR, константа, 493 RTAX BRD, константа, 493 RTAXDST, константа, 493 RTAX_GATEWAY, константа, 493 RTAX—GENMASK, константа, 493 RTAX IFA, константа, 493 RTAX IFP, константа, 493,510 RTAX—MAX, константа, 493,499 RTAX—NETMASK, константа, 493 rtentry, структура, 471,488 RTF—LLINFO, константа, 502 RTM_ADD, константа, 492 rtm_addrs, элемент, 492,494,499 RTM CHANGE, константа, 492 RTM—DELADDR, константа, 492 RTM DELETE, константа, 492 RTM GET, константа, 492, 502 RTM—IFINFO, константа, 492,502, 506,510,512 RTMLOCK, константа, 492 RTM—LOSING, константа, 492 RTM—MISS, константа, 492 RTM—NEWADDR, константа, 492, 502, 508 RTMREDIRECT, константа, 492 RTM RESOLVE, константа, 492 rtm_type, элемент, 492 RTO, тайм-аут повторной передачи, 594, 600 RTP (Real-time Transport Protocol), 556 RTT, период обращения, 67,129, 196, 235, 246, 253,413,441, 449, 452,465, 590, 593, 616, 715, 719, 721, 724, 734, 856, 937 rtt_init, функция, 598, 602 исходный код, 601 rtt_into, структура, 598 rtt_minmax, функция, 601 исходный код, 601 rtt_newpack, функция, 599, 603 исходный код, 602 RTT_RTOCALC, макрос, 601 rtt_start, функция, 600, 602 исходный код, 602 rttstop, функция, 600, 603 исходный код, 603 rtt_timeout, функция, 600, 604 исходный код, 604 rtt ts, функция, 599, 602, 1017 исходный код, 602 RUSAGECHILDREN, константа, 790 RUSAGE_SELF, константа, 790 S S—addr, элемент, 94 saliases, элемент, 296
Алфавитный указатель 1059 SBANDURG, константа, 933 S_ERROR, константа, 933 sfixedpt, элемент, 561 S_HANGUP, константа, 933 S HIPRI, константа, 933 S_IFSOCK, константа, 115 S INPUT, константа, 933 SISSOCK, константа, 115 S MSG, константа, 934 s_name, элемент, 296 S_OUTPUT, константа, 933 s port, элемент, 296 sproto, элемент, 296 S RDBAND, константа, 934 S_RDNORM, константа, 934 S WRNORM, константа, 934 s6_addr, элемент, 96 SA, константа, 40 sa_data, элемент, 95, 486, 761 sa_family, элемент, 95,486.494,499 sa_family_t, тип данных, 94 sa_handler, элемент, 156 SA_INTERRUPT, константа, 156 sa_len, элемент, 95,499 sa_mask, элемент, 157 SA_RESTART, константа, 156, 159, 188, 394 Salus, Р. Н„ 62,1032 SAP (Session Announcement Protocol), 553, 555 _SC_OPEN_MAX, константа, 2J2 Schimmel, C., 796,1032 SCM CREDS, параметр сокета, 405, 435 SCM R1GHTS, параметр сокета, 405 SCO, 31 script, программа, 678 sdl alen, элемент, 491, 507,1013 sdldata, элемент, 491 sdl family, элемент, 491 sdl_index, элемент, 491 sdl len, элемент, 491, 513 sdl nlen, элемент, 491, 507,1013 sdl_slen, элемент, 491 sdl_type, элемент, 491 SDP (Session Description Protocol), 553,555 sdr, программа, 553 SEEK SET, константа, 800 select, функция, 28, 100, 160, 166, 168, 172,177,180,182, 187, 200, 203, 214, 225, 226, 235, 263, 277, 320,375,385, 392,395, 410,415, 441,445,449,452,457, 460,465, 532,533,574, 581, 602, 609, 612, 615, 620, 623, 627, 629, 632, 635, 639, 656,673, 683, 741, 745, 749, 751, 781, 785, 797,805, 807, 818, 896,927,935,993,999,1013,1017 TCP и UDP серверы, 277 готовность дескриптора, 191 коллизии, 797 Semeria, С„ 542,1030 send, функция, 224, 236, 255, 267, 392,398,402,404,409, 413, 439, 440, 617, 621, 631, 639, 710,831, 1010 определение, 398 send all, функция, 558 send dns query, функция, 772 send__v4, функция, 724, 726 send_v6, функция, 724 sendmail, программа, 301, 374, 388 sendmsg, функция, 93,100, 224, 236, 243, 255, 392, 399,415,426, 433,440,573,582, 598, 600, 612, 701,704, 707,710, 893, 931 определение, 401 sendto, функция, 93,98, 224, 236, 255, 258, 264, 267, 280, 285,307, 317, 319, 336,339, 392,401, 404, 413,421,425,440, 517, 521, 558, 572,596, 598, 608, 648, 710, 733, 776,879,890,999 определение, 255 SEQnumber, элемент, 922 sequence, элемент, 827, 829, 835, 847, 857, 862, 867, 872, 876 Sequent Computer Systems, 856
1060 Алфавитный указатель SERVPORT, константа, 148,150, 214, 257,596, 606 определение, 980 servent, структура, 296,300 определение, 296 servtype, элемент, 822, 824 setaddresses, функция, 239 SET TOS, константа, 897 SETBLOCK, константа, 714 SETBLOCKALL, константа, 714 setgid, функция, 385 setnetconfig, функция, 842,859, 1023 setnetpath, функция, 851 определение, 843 SETPASS, константа, 714 SETPASS ALL, константа, 714 setrlnnit, функция, 214,993 setsid, функция, 379, 390 setsockopt, функция, 216, 228, 244, 312, 398,414, 538, 543, 550, 587, 688, 696,708,714, 733,893,899, 903, 906, 996, 1021 определение, 216 setuid, функция, 385, 719, 768 setvbuf, функция, 412 sfio library, 412 Shao, С, 30 SHUT_RD, константа, 199, 214, 240, 500,978 SHUT_RDWR, константа, 199, 215,978 SHUT_WR, константа, 199, 230, 834, 978 shutdown, функция, 72, 142,145, 198, 214, 230, 240, 411,445,452, 469, 500, 658,785, 834,864, 994, 1012 Siegel, D, 31 sig alrm, функция, 599, 640, 724, 733, 737, 772 sig chid, функция, 158,163, 279, 789 SIG_DFL, константа, 155, 1009 SIG IGN, константа, 155,158,170 sigaction, функция, 154,184 sigaddset, функция, 526, 647 SIGALRM, сигнал, 156,344, 392, 394,415, 523, 525, 526, 528, 532, 533,597,600,615,635,639, 716, 719, 724, 733, 735, 771 SIGCHLD, сигнал, 27, 154, 157, 162, 165,279,387,452, 612, 789 sigemptyset, функция, 526 Sigfunc, тип данных, 156 SIGHUP, сигнал, 375, 379,389, 647,650 SIGINT, сигнал, 207, 273,381, 789, 793, 797,804,809,813, 872 SIGIO, сигнал, 154,184, 225, 249, 473,641,647, 933, 971 и TCP, 642 и UDP, 642 SIGKILL, сигнал, 154, 172 siglongjmp, функция, 394, 530, 597, 600, 615, 771 signal, функция, 155,158,163,394, 642,937, 1009 исходный код, 155 определение, 156 SIGPIPE, сигнал, 168, 178,191, 227,835,991,1012, 1022 SIGPOLL, сигнал, 154, 641, 933, 937 sigprocmask, функция, 157, 527, 647 sigsetjmp, функция, 394, 530 597, 600,615, 771, 1022 SIGSTOP, сигнал, 155 sigsuspend, функция, 647 SIGTERM, сигнал, 172, 452, 793, 1012 SIGURG, сигнал, 154, 249, 473, 620, 623, 627, 629, 632, 637, 639, 933 SIGWINCH, сигнал, 381 sin_addr, элемент, 93, 125, 315, 485 sin_family, элемент, 93, 270 sin len, элемент, 93 sin_port, элемент, 62, 93, 125 sin zero, элемент, 94
Алфавитный указатель 1061 sin6_addr, элемент, 96, 125, 315 sin6_family, элемент, 96,270 sin6_flowinfo, элемент, 96,945 SIN6LEN, константа, 93, 96 sin6_len, элемент, 96 sm6_port, элемент, 96, 125 SIOCADDRT, константа, 471,488, 490 SIOCATMARK, константа, 248, 470, 626 SIOCDARP, константа, 471,486 SIOCDELRT, константа, 471,488, 490 SIOCGARP, константа, 471, 486 SIOCGIFADDR, константа, 471, 485, 550 SIOCGIFBRDADDR, константа, 471,484, 485,488 SIOCGIFCONF, константа, 248, 295, 471, 479, 482,488, 504, 769 SIOCGIFDSTADDR, константа, 471,484,485 SIOCGIFFLAGS, константа, 471, 483, 761 SIOCGIFMETRIC, константа, 471, 485 SIOCGIFNETMASK, константа, 471,485 SIOCGIFNUM, константа, 481, 489 SIOCGPGRP, константа, 248,471 SIOCGSTAMP, константа, 644 SIOCSARP, константа, 471 SIOCSIFADDR, константа, 471 SIOCSIFBRDADDR, константа, 471,485 SIOCSIFDSTADDR, константа, 471,485 SIOCSIFFLAGS, константа, 471, 485, 761 SIOCSIFMETRIC, константа, 471, 485 SIOCSIFNETMASK, константа, 471,485 SIOCSPGRP, константа, 248,471 size_t, тип данных, 40, 61, 830 sizeof, оператор, 40, 919 Sklower, К, 239, 316 SLA (идентификатор объединения на уровне сайта), 952 sleep, функция, 178, 189,439,525, 559, 621,628, 632,990,1010 sleep—iis, функция, 189 SLIP (Serial Line Internet Protocol), 83, 776 Smosna, M , 102,1028 SMTP (Simple Mail Tiansfer Protocol), 41, 88,1013 SNA (Systems Network Architecture), 831,1031 SNMP (Simple Network Management Protocol), 84, 88, 254, 275,500,593 snoop, программа, 975 snpnntf, функция, 47,174, 370, 430 SNTP (Simple Network Time Protocol), 560,1031 sntp h, заголовочный файл, 566 sntp_proc, функция, 562, 566, 577 sntp send, функция, 565, 568, 571, 577 SO_ACCEPTCON, параметр сокета, 253, 999 SO BROADCAST, параметр сокета, 220, 223,224, 251,517,523, 572,756,896, 971,1021 SO_BSDCOMPAT, параметр сокета, 265 SODEBUG, параметр сокета, 220, 223, 224, 253,895,971,997 SODONTROUTE, параметр сокета, 220, 223,224, 398, 615, 896, 972 SO_ERROR, параметр сокета, 191, 220,225, 251,456 SO—error, переменная, 225 SO_KEEPALIVE, параметр сокета, 171,177,220, 223, 225, 244, 251, 253, 634, 898,971 SO_LINGER, параметр сокета, 85, 141,145, 166, 199, 220, 223, 227,
1062 Алфавитный указатель 228, 251,467, 835,856, 864,873, 896,971 SO OOBINLINE, параметр сокета, 220, 223, 233,620,626,628, 639,935 so_pgid, элемент, 250 SORCVBUF, параметр сокета, 70, 220, 223, 233, 251, 258, 275, 896, 971,1000 SORCVLOWAT, параметр сокета, 220, 235, 896 SO RCVTIMEO, параметр сокета, 220,235,392, 397, 971 SO_REUSEADDR, параметр сокета, 126, 220, 228, 236, 251,278, 314,332, 341, 372, 554, 558, 570, 574, 580, 606, 607, 897, 971, 997, 1008,1016 SOREUSEPORT, параметр сокета, 126,220, 221,238, 252, 580, 971, 1016 SO—SNDBUF, параметр сокета, 85, 220, 223, 233, 251, 896,971, 1000 SOSNDLOWAT, параметр сокета, 191, 220, 235, 896 SOSNDTIMEO, параметр сокета, 220, 235, 392, 397, 971 sosocket, функция, 969 so_timeo, структура, 795 SOTIMESTAMP, параметр сокета, 644 SO_TYPE, параметр сокета, 220, 223, 239 SO_USELOOPBACK, параметр сокета, 199, 220,240, 513 sock, программа, 252,281, 588, 969, 1000 параметры, 971 sock_bind—wild, функция, 110, 744, 751 определение, 110 sock_cmp_addr, функция, 110 определение, 110 sock_cmp_port, функция, 110 определение, ПО SOCK—DGRAM, константа, 120, 239, 257,316,319,360, 361, 368, 421,939 sock get port, функция, 110 определение, ПО sockmasktop, функция, 497 sockntop, функция, 110,135, 145, 334,342,372,587,848, 1007, 1016 исходный код, 110 определение, 109 sockntophost, функция, 110,497, 523 определение, ПО sockopts, структура, 221 SOCK PACKET, константа, 64, 120, 757,761,780 SOCK—RAW, константа, 120, 710 SOCK_SEQPACKET, константа, 120 sock_set_addr, функция, ПО, 1006 определение, ПО sock_set_port, функция, ПО, 733, 1006 определение, ПО sock_set_wild, функция, ПО, 562, 568 определение, ПО sock_str_flag, функция, 222 SOCK_STREAM, константа, 39, 120,223, 239, 319, 329, 332,360, 361,364,421,939 sockaddr, структура, 40,96, 220, 336,482 определение, 95 sockaddr_dl, структура, 494,512, 584 определение,491 схема, 98 sockaddr_in, структура, 41, 92, 100, 309, 313,322,367,482,496, 499, 743,823, 837, 845,880, 881, 918, 965,989, 1003 определение, 92, 95 схема, 98
Алфавитный указатель 1063 sockaddr_in6, структура, 64,97,100, 322,367,482,615,743,823,849,945 определение, 96 схема, 98 sockaddr_un, структура, 98, 100, 367,418,420, 423 определение, 418 схема, 98 sockargs, функция, 93 sockatmark, функция, 58, 249,470, 625 исходный код, 626 определение, 625 socket, функция, 26, 39,43,46, 62, 68,118, 126,127, 133,139, 144, 151, 166, 204, 236, 251, 257, 299, 313, 317, 320, 325, 332, 368, 372, 390,413, 414, 423,425, 696, 710, 761, 795, 821,825, 840, 964, 968, 987,999, 1017 определение, 118 Socket, функция-обертка исходный код, 43 socketpair, функция, 420, 426,430,, 532 определение, 420 sockfd_to_family, функция, 144,551 исходный код, 144 sockfs, файловая система, 969 socklen_t, тип данных, 58, 61,94, 98, 989 sockmod, потоковый модуль, 909, 914,965 sockproto, структура, 121 sofree, функция, 166 SOL_SOCKET, константа, 405, 435 Solaris, 31, 53, 55, 78, 102, 123,132, 136,158,166, 170,196, 228, 264, 268, 272, 274,277, 285, 295, 345, 348,391,400, 420, 449,452,456, 478, 480, 483,491, 521, 524, 547, 552, 580, 587, 589, 606, 624, 674, 678, 684, 697, 709, 744, 762, 775, 783, 798, 800, 809, 810, 813, 822, 839, 873,874,966, 972, 975,987, 993,996,1014 soo_select, функция, 192 soreadable, функция, 192 sorwakeup, функция, 642 sowriteable, функция, 192 spfamily, элемент, 121 spprotocol, элемент, 121 Spafford, Е. Н„ 47, 1028 SPT (время обработки сервером), 590 SPX (Sequenced Packet Exchange), 842, 1031 Srinivasan, R., 177, 1032 sscanf, функция, 174 ssize t, тип данных, 830 SSRR жесткая маршрутизация от , отправителя с записью, 688,, 703 st_mode, элемент, 115 Stallman, R. М., 57 start_connect, функция, 461,463 static, квалификатор, 115, 344 status, элемент, 894, 895 stderr, константа, 376 <stdio.h>, заголовочный файл, 412 Stevens, D. А., 30 Stevens, Е. М., 30 Stevens, S. Н., 30 Stevens, W. R., 26, 60, 96, 242, 343, 408, 508,545, 711, 717, 1029, 1032 str ch, функция, 150, 153, 161, 167, 174, 193,195,197, 214, 411,424, 441,446,449, 468, 635, 656,696 str_echo, функция, 148,151,153, 174,175,280,410,423, 437, 637, 659, 686 strbuf, структура, 912, 923 определение, 912 strcat, функция, 47 strcpy, функция, 47 strdup, функция, 860 strerror, функция, 746,984 <strmg.h>, заголовочный файл, 104 strlen, функция, 990
1064 Алфавитный указатель strncat, функция, 47 strncpy, функция, 47, 370,419 проблемы, 370 strrecvfd, структура, 435 strtok, функция, 662 strtok_r, функция, 662 SUID, 768 sum.h, заголовочный файл, 175 Summit, S., 31 Sun, 55,89 Sun RPC, 41, 89 sun_family, элемент, 418,420 SUNJLEN, макрос, 418,979 sun ken, элемент, 418,420 sun_path, элемент, 418,424 SunOS 4,54,132, 156,758, 762 SunOS 5, 55 SunSoft, 31 SVR3 (System V Release 3), 187, 208, 820,907, 929 SVR4 (System V Release 4), 53, 66, 158, 166,187, 208, 251, 277,338, 380, 420, 426,435,438,441,468, 532,589, 641, 744, 751,757, 760, 780, 783, 797, 798, 802, 820, 841, 907, 908,911,913,925, 929,934, 964,966,968 SYN flooding, атака, 132 SYN, флаг синхронизации (TCP- заголовок), 68, 77, 84,122, 124, 127, 131, 233, 239, 245,305,308, 314,413, 422, 439,441, 689,696, 758, 824, 838,855, 867, 874, 974, 991,995, 1022 лавинная адресация, 132,1028 SYN RCVD, состояние, 72,128 SYN_SENT, состояние, 72, 123 <sys/errno.h>, заголовочный файл, 45,441, 654, 884,987 <sys/ioctl.h>, заголовочный файл, 471 <sys/param.h>, заголовочный файл, 296, 584 <sys/poll.h>, заголовочный файл, 974 <sys/select.h>, заголовочный файл, 189, 214 <sys/signal.h>, заголовочный файл, 642 <sys/socket.h>, заголовочный файл, 94, 121, 228, 253,406, 502 <sys/stat.h>, заголовочный файл, 116 <sys/stropts.h>, заголовочный файл, 210 <sys/sysctl.h>, заголовочный файл, 501 <sys/tihdr.h>, заголовочный файл, 915,917 <sys/types.h>, заголовочный файл, 193, 214 <sys/ucred.h>, заголовочный файл, 434 <sys/uio.h>, заголовочный файл, 400 <sys/un.h>, заголовочный файл, 418 sysconf, функция, 212, 215 sysctl операции, маршрутизирующий сокет, 500 sysctl, функция, 100,486,488,490, 500, 506,513 определение, 500 syslog, функция, 297,343,375,390, 697,984,1008 определение, 376 syslogd, программа, 374,382,390 sysname, элемент, 295 т Т/ТСР (TCP для транзакций), 75, 256,399, 413, 590, 1028, 1032 t_accept, функция, 829,856,858, 860,867,874, 876, 893, 926,929, 937 определение, 860 T ADDR, константа, 847, 854, 880, 888 T ALL, константа, 847, 854, 1023
Алфавитный указатель 1065 talloc, функция, 846, 854, 870, 880, 888,904,928, 940,1023 определение, 846 TBIND, константа, 846 tbind, структура, 827, 847, 849, 853, 860, 875 определение, 827 tbind, функция, 827, 837, 840, 851, 856, 860, 867, 870, 875, 880,887, ' 919,929,931 определение, 827 TBINDACK, константа, 918,919 Tbindack, структура, 919 определение, 918 T_BIND_REQ, константа, 918,965 T_bind_req, структура, 918,965 определение, 917 T CALL, константа, 846 t_call, структура, 827, 829, 835, 837, 846, 851, 854, 857, 862, 866, 870, 876,893, 927, 932 определение, 829 Т—СНЕСК, константа, 899, 901 t_close, функция, 852, 864, 929 T_CLTS, константа, 822, 824 T_CONN_CON, константа, 922, 966 Т_сопп_соп, структура определение, 921 T_CONN_REQ, константа, 966 T_conn_req, структура, 921 определение, 921 tconnect, функция, 457,828,837, 839, 852, 856, 875,893,926,937, 940,967 определение, 829 TCOTS, константа, 824 T_COTS_ORD, константа, 822, 824 T CRITIC ECP, константа, 897 T_CURRENT, константа, 897,899, 903 T DATA, константа, 832,935 T DATA IND, константа, 923, 966 T_data_ind, структура, 923 T DATAXFER, константа, 928 T_DEFAULT, константа, 895, 899, 902 T_DIS, константа, 846 t discon, структура, 827, 835, 847, 869,933 определение, 835 TDISCONIND, константа, 922, 923 T discon ind, структура определение, 922 T DISCONNECT, константа, 831, 835, 837, 867, 868,872 terrno, переменная, 825, 827, 830, 831, 838,866, 883, 888,926 t_error, функция, 825 определение, 825 T_ERROR_ACK, константа, 918, 919, 921 Т_errorack, структура определение,918 T EXDATA, константа, 832,935, 937 T_EXDATA_REQ, константа, 972 T EXPEDITED, константа, 830, 935,937,940, 972,974 T_FLASH, константа, 897 t_free, функция, 846,854, 1023 определение, 846 T GARBAGE, константа, 898 t getinfo, функция, 928 определение, 928 t getname, функция, 848 t_getprotaddr, функция, 848, 853 определение, 848 t getstate, функция, 928 определение, 928 T_GODATA, константа, 832 Т_GOEXDATA, константа, 832 T_HIREL, константа, 897 T_HITHRPT, константа, 897 T IDLE, константа, 860, 928 T_IMMED1ATE, константа, 897 T_INCON, константа, 928 TINETIP, константа, 892, 899 TINETTCP, константа, 892,982 TINETUDP, константа, 892
1066 Алфавитный указатель TINETCONTROL, константа, 897 Т_INFINITE, константа, 822, 940 T INFO, константа, 846 t_info, структура, 61, 821, 824, 826, 835, 846, 928, 933,940 определение, 822 T_INREL, константа, 928 TINVALID, константа, 822 T_IOV_MAX, константа, 931 t_iovec, структура, 932 определение, 931 T_IP_BROADCAST, константа, ‘‘ 982 T_IP_BROADCAST, параметр XTI, 892, 896 T_IP_DONTROUTE, параметр XTI, 892, 896 TIPOPTIONS, параметр XTI, 892, 896, 902 Т_1Р_REUSEADDR, параметр XTI, 891,897 T_IP_TOS, параметр XTI, 892,897 T_IP_TTL, параметр XTI, 892,897, 902 t_kpalive, структура, 892 определение, 898 T_LDELAY, константа, 897 tlinger, структура, 892,896 определение, 896 T-LISTEN, константа, 832,867, 872, 873,876 t_listen, функция, 832, 855, 857, 860, 867,869, 874,876,893, 926, 930 определение, 857 TLOCOST, константа, 897 t_look, функция, 832, 837,840, 867, 872, 935,937 определение, 832 Т-MORE, константа, 830,879, 888, 890 TNEGOTIATE, константа, 899, 905 TNETCONTROL, константа, 897 T_NO, константа, 897,898 T_NOTOS, константа, 897 T NOTSUPPORT, константа, 902 ТОКАСК, константа, 966 t open, функция, 837, 840, 842, 846, 851, 856,860,861,866, 872, 880, 887,926, 939 Т—ОРТ, константа, 847, 854 TOPTDATA, макрос, 895 TOPTFIRSTHDR, макрос, 895 TOPTNEXTHDR, макрос, 895 i_opthdr, структура, 61, 894,901, 905,966 определение, 894 T_OPTMGMT, константа, 846 t_optmgmt, структура, 827,847, 894, 899,901 определение, 899 t optmgmt, функция, 893, 895, 905>- 929 определение, 899 TORDREL, константа, 831,838, 937 TORDRELIND, константа, 923, 966,968 Т_ordrel_req, структура, 924 определение, 924 T_ORDRELDATA, константа, 824, 933 TOUTCON, константа, 928 Т_OUTREL, константа, 928 T OVERRIDEFLASH, константа, 897 TP ARTSUCCESS, константа, 905 T_primitives, структура, 921 T-PRIORITY, константа, 897 T_PUSH, константа, 831 t rcv, функция, 831, 838, 862, 864, 878,926, 931,935,937,939, 967, 973 определение, 830 t_rcvconnect, функция, 832, 893, 926, 940 определение, 927 t_rcvdis, функция, 824, 832, 834, 851,868, 872,932 определение, 834
Алфавитн ы й указател ь 1067 t_rcvrel, функция, 832, 838, 864, 932 определение, 833 t_rcvreldata, функция, 824,932 определение, 933 t_rcvudata, функция, 831, 832, 878, 884,888, 890, 893, 898,926,932 определение, 878 t_rcvuderr, функция, 265, 740, 832, 878,884,888, 890,893 определение, 884 t_rcvv, функция, 832, 926,932, 940 определение, 931 t_rcwudata, функция, 832, 893, 926, 931,940 определение, 931 T READONLY, константа, 902, 904 TROUTINE, константа, 897 t_scalar_t, тип данных, 61, 822 T_SENDZERO, константа, 825 t snd, функция, 831, 839, 862,864, 927,932, 935, 940,972 определение, 830 t_snddis, функция, 824, 834, 856, 896,932 определение, 834 tsndrel, функция, 832, 864, 924, 933 определение, 833 t_sndreldata, функция, 824, 932 определение,933 t_sndudata, функция, 832, 878, 882, 888,890, 893, 927,932 определение,878 t sndv, функция, 832,927,932, 940 определение, 932 t_sndvudata, функция, 832, 893, 927,932,940 определение, 932 t_strerror, функция, 825 определение, 825 T_SUCCESS, константа, 895,901, 904 t sync, функция, 929 определение, 930 Т ТСР КЕЕРALIVE, параметр XTI, 892, 898 TTCPMAXSEG, параметр XTI, 892,898 TTCPNODELAY, параметр ХТ1,892,898 T_UDATA, константа, 847, 854 T UDERR, константа, 832 t_uderr, структура, 827, 847, 884, 894 определение, 884 T_UDERROR, константа, 846 TUDPCHECKSUM, параметр ХТ1, 892,898, 902,929 t_unbind, функция, 931 определение, 931 T UNBND, константа, 928 T_UNINIT, константа, 928 T_UNITDATA, константа, 846 t_unitdata, структура, 827, 847, 878, 881,888, 893,932 определение, 878 t uscalar t, тип данных, 61, 822, 894 T_YES, константа, 896, 898 taddr2uaddr, функция, 848 TADDRBUSY, константа, 828 Tahoe, 53 Tanenbaum, A. S., 39,1032 tar, программа, 57 Taylor, I. L., 30 Taylor, R., 30 TBADADDR, константа, 825 TBADF, константа, 825 TBADOPT, константа, 902 TBUFOVFLW, константа, 827 tcflush, функция, 470 tcgetattr, функция, 470 TCP, 68 параллельный сервер один дочерний процесс каждому клиенту, 787 один программный поток каждому клиенту, 809 сервер с предварительным порождением потоков, 811
1068 Алфавитный указатель TCP (продолжение) сервер с предварительным порождением процессов, 791 коллизии при вызове функции select, 797 слишком много процессов, 801 TCP (Transmission Control Protocol), 65 XTI, 820, 855 альтернативное устройство клиента, 785 внеполосные данные, 617, 632 диаграмма состояний, 72 завершение соединения, 69 и UDP, 590 и UDP, введение, 63 и сигнал SIGIO, 643 контрольная сумма, 725 отправка, 84 отслеживание пакетов, 74 параметр MSS, 70 параметр масштабирования окна, 70, 233,896,1030 параметр отметки времени, 70, 245, 1030 параметры, 70 параметры сокета, 244 сегменты, 67 сокет, 118 присоединенный, 133 срочное смещение, 618 срочный режим, 617 срочный указатель, 618 трехэтапное рукопожатие, 69 установление соединения, 68 TCP/IP, обзор протоколов, 63 tcpclose, функция, 166 tcp_connect, функция, 319,328, 339, 372, 461, 675,696,744,849 исходный код, 328,850 определение, 328 TCP KEEPALIVE, параметр сокета, 220, 226, 244 tcplisten, функция, 331,340,372, 389, 658,748,789,859, 866, 870, 886,936, 1008 исходный код, 333, 858 определение, 331 TCP MAXRT, параметр сокета, 220, 244 TCPMAXSEG, параметр сокета, 70, 220, 245, 898, 971 TCP NODELAY, параметр сокета, 220,245, 253, 400, 898, 971,997 TCP NOPUSH, параметр сокета, 414 TCP STDURG, параметр сокета, 220, 248,618 tcpdump, программа, 64, 123, 167, 170, 214, 264, 272, 281,448, 533, 548, 579, 633, 690, 697, 757, 759, 762,770, 780, 969,974,995, 1000 Telnet, протокол удаленного терминала, 87,178, 227, 241, 245, 633,639, 990 termcap, файл, 196 Terzis, А., 30 test_udp, функция, 768, 770 TFLOW, константа, 927 TFTP (Trivial File Transfer Protocol), 84, 88, 269, 517,581, 592,611 Thaler, D., 30 Thomas, M, 31,60,242, 408, 711, 717,1010, 1032 Thomas, S., 536,1033 Thomson, S„ 60, 96, 242. 283,343, 508,545, 1029,1033 thrjoin, функция, 674, 680, 684 <thread.h>, заголовочный файл, 673 Thread, структура, 813 thread_main, функция, 813, 815 threadmake, функция, 812, 815 TIBIND, константа, 965 ticlts, константа, 939 ticots, константа, 939
Алфавитный указатель 1069 ticotsord, константа, 939 time, программа, 87,452,1008 time, функция, 47, 864 time_t, тип данных, 208 TIME WAIT, состояние, 72, 75, 88, 154, 178, 228, 232, 252, 341, 786, 873,976, 989,996 times, функция, 602 timespec, структура, 206, 683,980 определение, 206 timestamp, запрос ICMP, 956 timeval, структура, 187, 206, 220, 236, 396,455, 602, 644, 683, 722, 1016 определение, 187 timod, модуль, 909,914, 930,966 tirdwr, модуль, 830, 839,862,909, 914,924 TLA, 952 TLI, интерфейс транспортного уровня, 820, 828, 848, 856, 894, 904.910,919, 929,940 TLI_error, элемент, 918 TLOOK, константа, 830, 832, 837, 840, 851, 866,872, 873, 883, 888, 890 TLV, формат (тип/длина/ значение), 699, 702 tmpnam, функция, 425, 662 TNODATA, константа, 926 Token Ring, 65, 90, 224, 477, 534, 988 Torek, С., 239, 1033 TOS, поле типа службы (сервиса), 241,895, 897, 942,956, 1027 Townsend, М., 31 TPI, интерфейс поставщика транспортных служб, 909,914, 1033 tpi_bind, функция, 916, 921,965 tpi_close, функция, 916,924 tpiconnect, функция, 916,919 tpi_daytime.li, заголовочный файл, 915 tpi read, функция, 916,922 trace.h, заголовочный файл, 728 traceloop, функция, 729, 731, 737 traceroute, программа, 64, 89, 614 реализация, 727 trpt, программа, 224 truss, программа, 964, 969,972 TRY_AGAIN, константа, 288 ts, элемент, 777 TSDU, 822 tsdu,элемент, 822 TSYSERR, константа, 825 TTL (время жизни), 75, 241, 537, 544, 546, 549,556, 721, 727, 731, 743,895,897,943,945,956,960 ttyname, функция, 662 ttyname_r, функция, 662 tv nsec, поле, 980 tv_nsec, элемент, 206 tv_sec, поле, 980 tv_sec, элемент, 187, 206 tv_sub, функция, 722 исходный код, 722 tv_usec, элемент, 187, 207 и u_char, тип данных, 94, 543, 894 u_int, тип данных, 94, 543 ulong, тип данных, 94 u_short, тип данных, 94 uaddr2taddr, функция, 848 ucred, структура, 434 udata, элемент, 827, 829, 835, 847, 857,878,882,932 UDP (User Datagram Protocol), 65 connect, функция, 268 XTI, 878 и TCP, 590 и TCP, введение, 63 и сигнал SIGIO, 642 контрольная сумма, 273,501, 503,725, 762,898 неприсоединенный сокет, 267 обрезанные дейтаграммы, 589
1070 Алфавитный указатель UDP (продолжение) определение исходящего интерфейса, 276 отправка, 86 отсутствие управления потоком, 272 параллельный сервер, 609 порядковый номер, 593 потерянные дейтаграммы, 261 приемный буфер сокета, 275 присоединенный сокет, 267 проверка полученного ответа, 261 связывание адреса интер- фейса, 605 сервер не работает, 264 сокет, 254, 581 тайм-аут, 593 увеличение надежности приложения, 593 чтение дейтаграмм по частям, 888 udp_check, функция, 777, 778 UDP_CHECKSUM, константа, 891 udpclient, функция, 336, 372, 554, 558, 562,567,568, 571, 616,879, 1010, 1017 исходный код, 338, 879 определение, 336 udp connect, функция, 338,372, 1010 исходный код, 339 определение, 338 udp_read, функция, 772, 776, 780 udp server, функция, 339, 372, 886, 1008 исходный код, 340, 886 определение, 339 udp_server_reuseaddr, функция, 1008 udp_wnte, функция, 774 udpcksum h, заголовочный файл, 764 udpInDatagramb, переменная, 275 udpInOverflowb, переменная, 275 udpiphdr, структура, 775 ш_1еп, элемент, 775 ui_sum, элемент, 775 uintl6_t, тип данных, 94 uint32_t, тип данных, 94, 98, 822 uint8_t, тпп данных, 94 umask, функция, 420 , uname, функция, 294, 302, 364,559 определение, 294 <unistd h>, заголовочный файл, 471, 695 /ишх, служба, 325, 351, 1010 Unix, 60 Unix 95, 59,867 Unix 98, 62,158, 210, 296, 348, 376, 602, 662,746, 820, 822, 833, 841, 867,876, 891, 895, 898,907,994, 1031 определение, 59 Unix International, 760, 910, 1033 UNIX error, элемент, 918 UNIXDG PATH, константа, 425 определение, 980 UNIXDOMAIN, константа, 349 UNIXSTRPATH, константа, 423 определение, 980 UnixWare, 31,53,54,102,132,170, 272,277, 524, 746, 822, 873, 874, 885,967,996, 1015 unlink, функция, 367,419,423,425, 439, 800,1009 unp h, заголовочный файл, 38, 46, 101,144,148, 150, 156, 257, 289, 330,333,349,423,425,496, 582, 587,657, 764, 860, 889, 977, 1024 исходный код, 977 unpicmpd h, заголовочный файл, исходный код, 742 unpih h, заголовочный фапл, 476 исходный код, 476 unproute h, заголовочный файл, 496 unprtt li, заголовочный файл, 598, 600 исходный код, 600
Алфавитный указател ь 1071 unpthread h, заголовочный файл, 657 unpxti h, заголовочный файл, 837, 982,1024 исходный код, 982 URG, флаг срочного указателя, 619, 633 URI, универсальный идентификатор ресурса, 556 URL, унифицированный указатель ресурса, 1027, 1032 /usr/hb/hbnsl so, файл, 842 /usr/lib/resolv so, файл, 842 /usr/lib/tcpip so, файл, 842 UTC, универсальное скоординированное время, 47, 87, 556, 564, 602, 683 _UTS_NAMESIZE, константа, 294 _UTS_NODESIZE, константа, 294 utsname, структура, 294 определение, 294 UUCP, протокол для обмена между системами Unix, 377 V /var/adm/messages, файл, 382 /var/log/messages, файл, 390 /var/run/log, файл, 375,377 Varadhan, К, 948,1028 version, элемент, 294 vi, программа, 32, 57 Vixie, Р А , 31,287,1033 Vo, К Р,412, 635,1030 void, тип данных, 40, 95, 111,156, 336, 654, 656, 660,989 volatile, квалификатор, 771 W Wait,J W,31 wait, функция, 157, 177, 387, 611, 674, 786, 793 определение, 160 waitpid, функция, 157,177,387, 430, 655, 674 определение, 160 wakeup_one, функция, 796 web h, заголовочный файл, 459 web_child, функция, 663, 791, 794, 810,811,816 WEXITSTATUS, константа, 161, 430 WIFEXITED, константа, 161 Wise, S , 31,316 WNOHANG, константа, 161,163 Wolff, R., 31 Wolff, S, 31 Wollongong Group, 856 Wright, G R, 26,31, 1033 wntabletimeo, функция, 396 write, функция, 47, 61, 85, 111, 141, 160, 170, 178,225, 236, 247, 253, 255,267, 271,339,346, 392, 398, 400,404,409,413,439, 440, 442, 445, 449,462,496, 500, 513, 621, 631,640, 643, 710,760, 809,830, 838.862,872,909, 911,935, 966, 968, 974,987,990, 993,997,1010, 1012, 1021 write_fd, функция, 433, 744, 808 исходный код, 434 wnte_get_cmd, функция, 462,465, 675 v nten, функция, 111, 116, 146,149, 151, 168,170,175, 195. 410,442, 462, 635 исходный код, 112 определение, 111 writev, функция, 236, 248, 253,392, 400, 404,415, 440, 598, 710, 931, 998 определение, 400 WWW (World Wide Web), 129, 788 X X/Open, 59 XDR, стандарт представления внешних данных, 177 XNS (Xerox Network Systems), 59, 120
1072 Алфавитный указатель XNS, сетевые службы консорциума Х/Ореп, 59,1031 XPG, руководство Х/Open по обеспечению мобильности, 59 XTI, транспортный интерфейс консорциума Х/Ореп, 59,265, 457,740 TCP, 820,855 UDP, 878 аварийное завершение, 831 асинхронные события, 832 взаимодействие с сокетами, 838 внеполосные данные, 934, 973 гибкий адрес, 939 длина очереди и аргумент backlog функции listen, 874 неблокируемый ввод-вывод, 926 несколько соединений в очереди, 865 нормальное завершение, 831 обязательные параметры, 891 параметры, 891 параметры, локальные, 891 параметры, получение значений по умолчанию, 899 параметры, сквозные, 891 поставщик службы связи, 820 поставщик транспортных служб закольцовки, 939 состояние точки доступа, 928 структуры, 826 точка доступа службы связи, 820 транспортный адрес, 848 универсальный адрес, 848 управляемый сигналом ввод-вывод, 933 <xti h>, заголовочный файл, 821, 824, 897, 931 <xti_met h>, заголовочный файл, 821, 895 xti accept, функция, 861,867,936 исходный код, 861, 870 определение, 861 xti_accept_dump, функция, 866 XTIDEB UG, параметр XTI, 892, 895 xti_flags_str, функция, 937 XTIGENERIC, параметр XTI, 892 xti getopt, функция, 903 исходный код, 903 определение, 903 XTILINGER, параметр XTI, 864, 892,896,898 xti_ntop, функция, 848,853,864 определение, 849 xti_ntop_host, функция, 849, 882 XTI RCVBUF, параметр XTI, 892, 896 XTIRCVLOWAT, параметр XTI, 892,896 xti_rdwr, функция, 839,861,872, 935 исходный код, 839 определение, 839 xti_read, функция, 840 xti_serv dev, переменная, 859, 862, 876 xti_sery_dev, устройство, 983 xti_setopt, функция, 903 исходный код, 905 определение, 903 XTI SNDBUF, параметр XTI, 892 896 XTI SNDLOWAT, параметр XTI, 892,896 xti_tlook_str, функция, 937 Y уасс, программа, 57 Yu,J Y,948,1028 Z Zhang, L, 77,1030 Ziel, В, 31 A абсолютное врем#, 683 абсолютное имя, DNS, 282 автоматический туннель, 954 административно задаваемая область действия, 537
Алфавитный указатель 1073 адрес бЬопе, тестовый, 953 IPv4,946 многоадресной передачи, 534 отправителя, 944 получателя, 944 преобразованный к виду IPv6,117, 292, 336,719,953 совместимый с IPv6,294,954 IPv6, 951 многоадресной передачи, 536 отправителя, 945 получателя, 945 XTI, гибкий, 939 XTI, транспортный, 848 XTI, универсальный, 848 административно задаваемая область действия, 537 бесклассовый, 947 закольцовки,135,353,375,439, 588, 950, 954 индивидуальный, зависящий от провайдера, 952 локальные на уровне сайта, 955 локальный в пределах подсети, 955 многоадресной передачи, 534 неопределенный, 950, 954 объединяемый глобальный индивидуальный, 952 определение локального IP-адреса, 295 подсети, 948, 1031 псевдоним, 126, 951 универсальный, 80, 111,125, 148, 151,172, 239,305, 308,322, 352, 385, 544, 550,562, 568, 606, 608, 744, 751, 859, 950, 955 широковещательный, 515 адрес отправителя IPv4, 944 IPv6, 945 адресация протокол, многоадресная передача, 540 различные формы, 514 активное закрытие, 75, 88,988,990,996 открытие, 72, 80, 970 активный сокет, 354 алгоритм Карна, 595 альтернативные имена, 126, 951 аргумент типа «значение- результат», 98, 99, 133,190, 208, 216,223,262,399,401,404,420, 474,501, 504, 584, 689, 696,831, 878, 904, 913, 989, 1007 асинхронная модель ввода-вывода, 184 асинхронные ошибки, 264, 265, 268, 740,883, 888 события, XTI, 832 асинхронный ввод-вывод, 184, 186, 473, 641 атака по типу отказа в обслуживании, 133, 206, 468, 969, 1008 Б бездисковый узел, 65,516 безопасность в многопоточной среде, 110, 115, 348, 662, 669,811, 854,860,1008,1023 Беркли-реализации, определение, 52 бесклассовые адреса, 947 библиография, 1027 библитека сетевых служб, 842 блокировка взаимная, 990 блуждающая копия, 76 брандмауэр, 969,1028 буферизованный по строкам поток ввода-вывода, 412 В ввод-вывод асинхронная модель, 184 асинхронный, 184,473,641
1074 Алфавитный указатель ввод-вывод (продолжение) блокируемый, 181 модель, 181 мультиплексирование, 182 неблокируемый, 111,182, 201, 249,398,408,440, 473, 643, 647, 650, 831,926, 993,1021 определение, Unix, 409 синхронный, 186 сравнение моделей, 186 стандартный, 195, 347,409,415, 442, 635, 1009, 1030 управляемый сигналом, 225, 249, 641 ввод-вывод Unix, 409 версии Unix и их переносимость, 60 взаимная блокировка, 990 взаимное исключение, 674 виртуальная сеть, 959 владелец сокета, 250, 622,642, 647 вложенное подтверждение, 74 внеполосные данные, 155,188, 209, 214, 233, 248, 398,403,472, 617, 823,830, 839,911,934 TCP, 617, 632 XTI, 934,973 воплощение соединения, 76 временная группа (многоадресна^ передача), 536 время абсолютное, 683 процессорное, 115 разница во времени, 683 системное, 115 сообщение ICMP о превышении времени передачи, 727, 734, . 736, 742,956 время транзакции, 590 вспомогательные данные, 405 IPRECVDSTADDR, 404 IPV6 DSTOPTS, 700 IPV6 HOPLIMIT, 612 IPV6_HOPOPTS, 700 вспомогательные данные (продолжение) IPV6 NEXTHOP, 612 IPV6_PKTINFO, 612 IPV6 RTHDR, 705 объект, определение, 406 выключение узла сервера, 172 выравнивание, 177, 330,693, 701, 751 64-разрядное, 97,946 Г гибкий адрес, XTI, 939 главный программный поток, 653 главный процесс в группе, 379 в сеансе, 379 глобальная область действия многоадресная передача, 537 глобальная сеть, 67, 245,453, 534, 541,592,640 головной модуль потока, 908 граница записи, 41, 66, 116,414, 422,823, 1010 группа всех маршрутизаторов (многоадресная передача), 535, 536 всех узлов (многоадресная передача), 535, 536 групповой идентификатор, 434, 438, 653 д дамп, 155,169,380 данные из полосы пропускания,617 данные с высоким приоритетом,208,910 двоичные структуры, 174 двойная буферизация, 759 двустороннее соединение, 68 двусторонний канал, 421 двухстековый узел, 66
Алфавитный указатель 1075 двухфазный протокол с подтверждением завершения транзакций, 413 дейтаграмма надежный сервис, 593 обрезанная, UDP, 589 сокет, 66 демон, 48 определение, 374 процесс, 374 дескриптор набор, 188 передача, 426, 741, 804 счетчик ссылок, 141,427 деструктор, функция, 668 детальная копия, 322 джумбограмма, 945 диаграмма состояний TCP, 72 динамически назначаемый порт, 78, 111,122, 124,135,145, 148, 260, 265,277,343,422,610, 740, 744, 751,989, 1015, 1022 определение, 78 домен Unix и функция getaddrinfo и IPv6, 322 различие в функциях сокетов, 421 сокеты, 417 структура адреса сокета, 418 драйвер, потоки, 907 3 завершение, XTI аварийное, 831 нормальное, 831 завершение процесса сервера, 166 зависимость от протокола, 41 заголовки расширения, IPv6,699 заголовок длина расширенного заголовка, 699, 703 контрольная сумма IPv4,943 поле длины, IPv4, 942 заголовок аутентификации, 698, 1027,1030 задержанный сегмент АСК, 247, 253,998 задержка, 188 закольцовка адрес, 135, 375,439,588,950,954 интерфейс, 55,479,546, 761, 777, 842, 950 логическая, 520 маршрутизация, 199, 240,513 многоадресная передача, 544, 546, 549, 552, 558, 565,573, 576 поставщик транспортных услуг, XTI, 939 физическая, 520 широковещательная передача, 520, 576 закрепленные параметры, IPv6,706 закрытие активное, 71, 75,88, 988,990, 996 одновременное, 72 пассивное, 71 половинное, 72, 199, 833, 971 запись в таблице файла, 139, 427 запись маршрута, 689 запрос адреса, ICMP, 713, 956 заранее известная группа многоадресной передачи, 536, 567 заранее известный адрес, 79 группа многоадресной передачи, 554 порт, 77 зарегистрированный порт, 77,148 зарезервированный порт, 78, 124, 136,148, 239 захват порта, 239,372 защита от дурака, 521 зомби, 154, 157, 163,165 И идентификатор пользователя, 373, 385,434,438, 653, 719, 732,768
1076 Алфавитный указатель изменение маршрутов, ICMP, 490, 501,956 индекс интерфейса, 243,494,502, 507, 544, 548,558, 612, 613,706 индивидуальные адреса, зависящие от провайдера, 952 Интернет, 31 интерфейс закольцовка, 479 закольцовки,55, 761, 777,842, 950 идентификатор, 952 индекс, 243,494, 502,507, 544, 548,558, 612, 613,706 индекс, функция recvmsg, 582 исходящий, определение (UDP), 276 конфигурация, функция ioctl, 474 логический, 951 операции, функция ioctl, 485 основанный на сообщениях, 914 связывание с адресами, UDP, 604 информация о потоке, 945 исходный код местонахождение, 30,32 условные обозначения, 38 итеративный (последовательный) сервер, 48, 138, 258, 787 К канал с повышенной вместимостью, 70,235,252,595. 896 определение, 70 Карна алгоритм, 595 контрольная сумма, 1028 ICMPv4, 711, 725, 775,956 ICMPv6, 712, 725,956 IGMP, 725 IPv4, 240,711,725 IPv4-заголовка, 943 IPv6, 242, 712,946 TCP, 725 UDP, 273,501,503,725,762,779, 898 копирование детальное, 322 поверхностное, 322 копирование при записи, 652 копия блуждающая, 76 потерянная, 76 лавинная адресация сегмента SYN, 132, 1028 широковещательная передача, 543 логический интерфейс, 951 локальная в пределах сайта область действия, 537 локальная в пределах сети (подсети) многоадресная передача, 537 локальная сеть, 246,453,515,524, 534,536, 560,592, 640,952,959, 961 локальный IP-адрес узла, определение, 295 локальный в пределах сети (подсети) адрес, 955 локальный на уровне сайта адрес, 955 м маркер конца записи, 232 маршрутизатор запрос (ICMP), 709,956 извещение (ICMP), 709, 714,956 маршрутизация IP, 942 заголовок, IPv6, 703 операции с таблицей маршрутизации, функция ioctl, 488 сокеты, 490 сокеты, операции sysctl, 500 сокеты, чтение и запись, 492
Алфавитный указатель 1077 маршрутизация (продолжение) счетчик количества переходов, 485 тип, 703 маршрутизация от отправителя IPv4, 689 IPv6,703 масштабирование окна, TCP, 70, 233,896, 1030 медленный старт, 414,466,591,1029 методики отладки, 964 минимальная канальная MTU, 83 минимальный размер буфера, 84 минимальный размер буфера соединения фрагментов, 84 Мичиганский университет, 31 многоадресная передача, 534 IPv4, административно задаваемая область действия, 537 IPv4-адрес, 534 1Р\'6-адрес, 536 адрес, 534 в глобальной сети, 540 временная группа, 536 группа всех маршрутизаторов, 535 группа всех узлов, 535, 536 групповой адрес, 534 групповой идентификатор, 534 заранее известная группа, 536 заранее известный адрес, 554, 567 и фрагментация IP, 552 и широковещательная передача, 538 область действия,311,537 глобальная, 537 локальная в пределах организации, 537 локальная в пределах региона, 537 локальная в пределах сайта, 537 локальная в пределах сети, 537 локальная в пределах узла, 537 многоадресная передача (продолжение) отправка и получение, 556 параметры сокета, 543 протокол маршрутизации, 540 многоинтерфейсный узел, 80, 126, 148, 172,262, 263, 265,277, 297, 325, 517,545, 562, 565, 588, 756, 767,844,951,1000 модель блокируемого ввода-вывода, 181 модель программирования ILP32, 61 LP64, 61 модель системы с гибкой привязкой, 126,519, 589, 605, 644, 991,1020 определение, 263 модель системы с жесткой привязкой, 126, 519 определение, 263 модули,потоки, 908 мультиплексор, потоки, 908 Н Нагла алгоритм, 253,400 определение, 245 надежный сервис дейтаграмм^ 593 наполовину открытое соединение, 226, 251 направленная (одноадресная) передача адрес, зависящий от провайдера, 952 и широковещательная передача, 517 начальный программный поток, 653 неблокируемая функция accept, 466 connect, 454 неблокируемый ввод-вывод, 111, 182, 201,249, 398,408,440,473,643, 647, 650, 831,926, 993, 1021 ввод-вывод XTI, 926
1078 Алфавитный указатель небуферизованный поток стандартного ввода-вывода, 412 независимость от протокола, 41 нелокальный оператор goto, 529, 772 неопределенный адрес, 950,954 непрнсоединенный сокет UDP, 267 несовершенная фильтрация, 538 неструктурированный сокет, 51, 63, 89,120, 240, 242,417,490,496, 500, 709, 710, 712, 761, 768, 774, 778,958,1021 нормальное завершение, XTI, 831 О область действия локальная в пределах организации, 537 локальная в пределах региона, 537 локальная в пределах узла, 537 многоадресной передачи, 311, 537 глобальная, 537 локальная в пределах континента, 537 локальная в пределах организации, 537 локальная в пределах региона, 537 локальная в пределах сайта, 537 локальная в пределах сети, 537 локальная в пределах узла, 537 облегченный процесс, 652 обнаружение ресурса, 515, 565 обратный порядок байтов, 100 обрезанные дейтаграммы UDP, 589 общая побудка, 795,801, 813 объединяемый глобальный индивидуальный адрес, 952 объединяющая запись, 400,932 объявление о сеансе, МВопе, 553 обычное сообщение, 208,910 одновременное закрытие, 72 открытие, 72 соединение, 458 октет, 103 опрос, 182,187, 681, 927 отказ в обслуживании, 133, 206, 468,969, 1008 отключение отправителя, ICMP, 742,956 открытие активное, 68, 72,80, 970 одновременное, 72 пассивное, 68, 72, 79,316,970 отложенный прием, 856 отметка времени запрос ICMP, 713 параметр TCP, 70, 245, 1030 отправитель, 1Ру4-адрес, 944 отправка TCP, 84 UDP, 86 отсоединенный программный поток, 655 очередь аргумент backlog функции listen и XTI, 874 данные, 408 не полностью установленных соединений, 127 полностью установленных соединений, 127 потоки,910 сигналы, 157,163, 649 ошибка с параметром (ICMP), 700 ошибка, требующая обработки, 225 ошибки асинхронные, 264, 265, 268, 740, 883,888 функции,984 п пакет информация, получение (IPv6), 612
Алфавитный указатель 1079 пакет (продолжение) слишком большой (ICMP), 83, 742,957 пакетный ввод, 195 параллельное программиро- вание, 676 параллельный сервер, 48,138 UDP, 609 и номера порюв, 79 один дочерний процесс каждому клиенту, TCP, 787 один программный поток каждому клиенту, TCP, 809 параметры TCP, 70 XTI, 891 локальные, XTI, 891 обязательные, XTI, 891 получение по умолчанию, XTI, 899 сквозные, XTI, 891 сокета, 216 параметры для транзитных узлов, IPv6, 699 параметры сокета, 216 ICMPv6, 242 IPv4, 240 IPv6,242 TCP, 244 многоадресная передача, 543 общие, 224 получение значений по умолчанию, 220 состояния сокета, 224 пассивное закрытие, 71 открытие, 68,72, 79, 316,970 пассивный сокет, 126,352 передача аргумента, программные потоки, 660 перезагрузка узла сервера, 171 перекрывание областей памяти при копировании, 876, 1024 переменная окружения DISPLAY, 417 переменная окружения (продолжение) LISTENQ, 129,860 NETPATH, 842,851,859 PATH, 55,138 RES OPTIONS, 290, 293 переносимость исходного кода, 3&9 поверхностная копия, 322 повторная передача, проблема неопределенности, 594 повторное вхождение, 106, 110, 115,343,372,662 подсеть адрес, 948, 1031 идентификатор, 952 маска, 948 поиск собеседника, 955 поле длины данных, IPv6, 945 идентификации, IPv4,943; кода, ICMP, 956 метки потока, IPv6, 945 номера версии, IP, 942, 944 области действия многоадресной передачи, 537 общей длины, IPv4,943 протокола, IPv4,943 следующего заголовка IPv6,945 типа, ICMP, 956 типа службы (сервиса), 942 полностью буферизованный поток ввода-вывода, 412 полностью дублированное связывание, 237, 580, 997 половинное закрытие, 72, 199,833, 971 полоса приоритета, сообщения, 208, 910 полубайт, 283 получашль IP-адрес, функция recvmsg, 582 1Р\'6-адрес, 945 недоступен, необходима фрагментация (ICMP), 742, 956
1080 Алфавитный указатель получатель (продолжение) параметры IPv6,699 сообщение о недоступности (ICMP), 122, 123, 171, 226, 265, 734, 736, 742, 746,830,838,840, 883, 922,956, 957, 1022 получение IP-адреса получателя, 582 данных, идентифицирующих отправителя, 434 индекса интерфейса, 582 тайм-аута, BPF, 759 флагов, 582 поправочный множитель, 128,432, 505 порт динамически назначаемый (эфемерный), 78, 111,122,124, 135,145,148,260, 265, 277,343, 422,610, 740, 744, 751, 989, 1015, 1022 заранее известный, 77 зарегистрированный, 77, 148 зарезервированный, 78, 124,136, 148, 239 захват, 239,372 номера, 77 номера и параллельный сервер,79 программа отображения, RPC, 124,1022 сообщение о недоступности (ICMP), 265, 269,272, 281,519, 727,734, 736, 742, 763, 780,883, 888,956,1000,1015 частный, 78 порядковый номер, UDP, 593 порядок байтов обратный, 100 прямой, 100 сетевой, 94,102,135,178,296, 319,711,713,992 узла, 101,125, 135, 145, 175, 711, 713,989 последовательный (итеративный) сервер, 48,138, 258, 787 поставщик транспортной службы, 821 постоянное соединение, 790 потерянная копия, 76 потерянные дейтаграммы, UDP, 261 поток небуферизованный стандартный ввода-вывода, 412 полностью буферизован- ный, 412 стандартный ввода-вывода, 409 стандартный ввода-вывода, буферизованный по строкам, 412 потоки ioctl, функция, 914 головной модуль, 908 драйвер, 908 модули, 908 мультиплексор, 908 очередь, 910 сообщение из полосы приоритета, 208,910 обычное, 208,910 с высоким приоритетом, 208, 910 типы сообщений, 910 потоковый канал, определение, 421 протокол, 41, 63, 66, 116,120, 403,422,440, 633,823 сокет, 66 предельное количество транзитных узлов (прыжков), 75, 243,537,544, 547, 549, 612, 614, 724,727,731, 733, 743,945,957 предотвращение перегрузки, 414, 466,591,1029 прерывания, программные, 154 префиксная длина, 948
Алфавитный указатель 1081 привилегированный пользователь, 78, 135,145, 239, 333,374,485,486,490, 496,500, 504, 560, 615,689, 710, 716,720, 733, 761,768,919,1012 приложение протокол, 29,427,838 сегмент АСК, 232 примеры клиентов и серверов, список, 48 присоединенный программный поток, 655 присоединенный сокет TCP, 133 присоединенный сокет UDP, 267 проблема с параметром (ICMP), 956 проверка полученного ответа, UDP, 261 проверочное сообщение, 225, 244, 253,634,898,998 программа netstat, 89 программа отображения портов, 124,1022 программные потоки, 652 атрибуты, 654 идентификатор, 654 отсоединенные, 655 передача аргумента, 660 присоединенные, 655 программные прерывания, 154 произведение пропускной способности на задержку, 234 прослушиваемый сокет, 80,133 простое имя, DNS, 282 протокол зависимость, 41,259 независимость, 41, 259 поле IPv4, 943 потоковый, 41, 63,66,116, 120, 403,422,440, 633,823 приложение, 29, 427,838 с подтверждением завершения транзакций, двухфазный, 413 протоколы используемые в большинстве приложений, 88 процесс главный в группе, 379 групповой идентификатор, 249, 379,472 демон, 374 идентификатор, 160, 249,379, 472 облегченный, 652 прямой порядок байтов, 100 псевдозаголовок, 243, 712, 766, 775 псевдонимы, 126, 951 Р размер увеличенного поля данных, 700 размеры буфера, 82 разница во времени, 683 распознаватель, 284, 309, 311, 314, 317,322,348,357,593,1003,1008 распределяющее чтение, 400, 931 реализация демон сообщений ICMP, 740 программа ping, 715 программа traceroute, 727 редактор См. AppBrowser решения к упражнениям, 987 С сбой и перезагрузка на узле сервера, 171 сбой на узле сервера, 170 сборка фрагментов, 83,943,956, 988,1001 связывание адреса интерфейса, UDP, 604 сегмент АСК (флаг подтверждения, заголовок TCP), 69,71, 76,85 задержанный сегмент, 247, 253, 998 сегмент, TCP, 67
1082 Алфавитный указатель сервер имен, 284, 293,314,317, 757, 762, 772, 779 параллельный, 48, 138 последовательный, 48,138, 258, 787 с предварительным порождением дочерних процессов, 791 TCP, 791 коллизии при вызове функции select , 797 распределение соединений по процессам, 796 слишком много процессов, 796, 801 сервис, не ориентированный на соединение, 66 сервис, ориентированный на установление соединения, 66 сетевая топология, определение, 55 сетевой порядок байтов, 94, 102, 135,178, 296,319, 711, 713,992 сети и узлы, используемые в примерах, 54 сеть виртуальная, 959 глобальная, 67, 245,453, 534, 541,592, 640 локальная, 246,453, 515, 524, 534, 536, 560,592, 640, 952, 959, 961 сигнал, 154 блокирование, 156, 525, 527, 528, 530,647 генерация,527 действие, 154,159,170, 653 доставка, 156,159, 162, 525, 527, 530, 647, 933,1022 маска, 156, 207, 528, 647, 653, 771 обработчик, 154, 653, 933 определение,154 очередь, 157, 163, 649 перехватывание, 154 символьный (неструктурированный) сокет, 51, 63, 89, 120, 240, 242,417,490,496, 500, 709, 761, 768, 774, 778, 958, 1021 ввод, 712 вывод, 710 создание, 710 синхронный ввод-вывод, 186 системное время, 115 системный вызов медленный, 159, 635 отслеживание, 964 прерванный, 156,159,165, 635 ситуация гонок, 252,395, 524,996 определение,524 сконфигурированный туннель, 954 служба связи поставщик, XTI, 820 точка доступа, ХТ1, 820 случайный сбой, 122 смешанный режим, 540, 757, 761, 770 смещение, экпонециальное, 594 смещение, экспоненциальное, 772 собственные данные потоков, 663 собственные данные программных потоков, 115,345, 348 совершенная фильтрация, 540 совместимость IPv4 и IPv6, 304 1Ру4-клиент, IPv6-cepBep, 305 1Ру6-клиент, IPv4-сервер, 308 переносимость исходною кода, 313 сокеты и XTI, 838 соединение завершение, TCP, 69 очередь не полностью установленных соединений, 127 полностью установленных соединений,127 постоянное, 790
Алфавитный указатель 1083 соединение (продолжение) прерывание, функция accept, 165 установление, TCP, 68 сокет TCP, 118 UDP, 254, 581 активный, 126,354 буфер приема, UDP, 275 введение, 92 владелец, 249, 622, 642, 647 дейтаграммный, 66 домена Unix, 417 маршрутизация, 490 определение, 39, 79 пара, определение, 79 пассивный, 126,352 потоковый, 66 структура адреса IPv4, 92 IPv6,96 домен Unix, 418 универсальная, 95 структуры адресов, 92 структуры адресов, сравнение, 97 тайм-аут, 236, 392 сокеты и XTI, совместимость, 838 сокеты и стандартный ввод-вывод, 409 сообщение из полосы приоритета, 208,910 обычное, 208, 910 с высоким приоритетом, 208, 910 типы ICMPv4, 956 ICMPv6, 957 потоки, 910 сообщение о соединении, 856 список замеченных опечаток, 30 срочное смещение, TCP, 619 срочный режим, TCP, 617 указатель, TCP, 619 стандартные службы Unix, 79 стандартные службы Интернета, 87,388,969 стандартный ввод-вывод, 195,347, 409,415, 442, 635,1009, 1030 и сокеты, 409 поток, буферизованный по строкам, 412 поток, не буферизованный, 412 поток, полностью буферизованный, 412 потоковый, 409 стандарты Unix, 57 счетчик количества переходов, маршрутизация, 485 счетчик ссылок, дескриптор, 141 427 таблица соответствия примеров, 48 тайм-аут (время ожидания) BPF, получение, 759 connect, функция, 393 UDP, 593 сокеты, 236, 392 функция recvfrom, 394 текстовые строки, 174 тестовые адреса, 953 тестовые программы, 972 тип кадра, 519, 540, 761 точечно-десятичная запись, 947 точка доступа, 821 точка доступа XTI, состояние, 92§ точность указания времени, 188 транспортная MTU, 86, 90, 245, 449,743, 946, 995, 1031 обнаружение, 83 определение, 83 транспортная служба, поставщик* ТЫ, 821 транспортная точка доступа, ТЫ, 821 транспортный адрес, XTI, 848
1084 Алфавитный указатель трехэтаппое рукопожатие, 68, 122, 127,131,223,233, 267, 272, 394, 412,441,453,455, 622, 628, 696, 792, 837,856,866, 893, 896,926, 1013 туннелирование, 959 автоматическое, 954 сконфигурированное, 954 У узел с двойным стеком, 284, 304, 310,322,326,332, 334,352 указатель-деструктор, 668 универсальная структура адреса сокета, 95 универсальный адрес, 80, 111,125, 148,151,172, 239, 305,308, 314, 322,352, 385, 544, 562, 568, 606, 608,744,751,859,950,955 универсальный адрес, XTI, 848 управление потоком, 67 отсутствие в случае UDP, 272 управляемый сигналом ввод-ывод, 225, 249, 641 XTI, 933 условная переменная, 680 условные обозначения в исходном коде, 38 соглашения, 38 устойчивая неисправность, 122 утечка памяти, 347 Ф фильтрация ICMPv6, 714 несовершенная, 538 совершенная, 540 флаг подтверждения, заголовок TCP, 69, 72, 76, 85 задержанный cei мент, 253, 998 отложенный сегмент, 86 форматный префикс, 952 форматы данных, 174 двоичные структуры данных, 175 текстовые строки, 174 фрагментация, 83, 533, 698, 711, 713, 743,943,946,956,957, 988, 1001 и многоадресная передача, 552 поле смещения фрагмента IPv4, 943 функции манипулирования байтами, 103 проверки пульса, 634 эхо-клиент и эхо-сервер, 634 функция деструктор, 668 и системный вызов, 964 функция-обертка, 43 Listen, исходный код, 129 Socket, исходный код, 43 X хакеры, 47,132,698,756 ц циклическое обслуживание, DNS, 788 Ч частный порт, 78 Ш широковещательная передач#, 224, 514 адрес, 515 и 1Р-фрагментация, 524 и многоадресная передача, сравнение, 538 и направленная передача, 517 лавина широковещательных сообщении, 519 лавинная адресация, 543
Алфавитный указатель 1085 э экспоненциальное смешение, 594, 772 эпоха Unix, 47, 602 эфемерный (динамически назначаемый) порт, 78,111,122, 124,135,148, 260, 265, 277,343, 422, 610, 740, 744, 751,1015,1022 определение, 78 эхо-запрос, ICMP, 709, 713, 715, 956, 1015 эхо-ответ, ICMP, 709, 715,956
564 Глава 19. Многоадресная передача Дробная часть — это 32-разрядное целое без знака, которое может принимать значение от 0 до 4 294 967 295 включительно. Оно копируется из 32-разрядного целого (useci) в переменную с плавающей точкой двойной точности (usecf) и делится на 4 294 967 296 (232). Результат больше либо равен 0.0 и меньше 1.0. Мы умножаем это число на 1 000 000 — число микросекунд в секунде, записывая результат в переменную useci как 32-разрядное целое без знака. Число микросекунд мы получим в интервале от 0 до 999 999 (см. упражне- ние 19.8). Мы преобразуем значение в микросекунды, поскольку отметка време- ни Unix, возвращаемая функцией gettnueofday, возвращается как два целых чис- ла: число секунд и микросекунд, прошедшее с 1 января 1970 года (UTC). Затем мы вычисляем и выводим разницу между истинным временем узла и истинным временем сервера NTP в микросекундах. Один из факторов, не учитываемых нашей программой, — это задержка в сети между клиентом и сервером. Но мы считаем, что пакеты NTP обычно приходят как широковещательные или многоадресные пакеты в локальной сети, а в этом случае задержка в сети составит всего несколько миллисекунд. Если мы запустим эту программу на узле sol an s с сервером NTP на узле bsdi, который с помощью многоадресной передачи отправляет пакеты NTP в сеть Ethernet каждые 64 секунды, то получим следующий результат: solans # ssntp 224.0.1.1 joined 224 0 1 1 123 on 1о0 joined 224 011 123 on leO v3 mode 5 strat 3 clock v3. mode 5. strat 3. clock v3. mode 5. strat 3. clock v3 mode 5 strat 3. clock v3 mode 5. strat 3 clock difference = 621 usee difference = 1205 usee difference = 1664 usee difference = 2291 usee difference = 2942 usee v3. mode 5. strat 3. clock difference = 3558 usee Перед запуском нашей программы мы завершили на узле работу NTP-серве- ра, поэтому когда наша программа запускается, время очень близко к времени сервера. Мы видим, что этот узел отстает примерно на 600 микросекунд каждые 64 секунды, то есть примерно на 810 миллисекунд в сутки. 19.11. SNTP (продолжение) Теперь мы расширим наш пример SNTP, дополнив его другими свойствами. При запуске наша программа будет создавать не один сокет, как было показано в раз- деле 19.10, а три: один сокет для адреса направленной передачи, один — для адре- са широковещательной передачи, и еще один — для интерфейса, на котором про- исходит присоединение к многоадресной передаче. Это позволит определять адрес получателя пришедшего пакета. Далее с помощью широковещательной и много- адресной передачи программа будет передавать запрос клиента SNTP со всех при- соединенных интерфейсов, чтобы получить начальную оценку разницы во вре- мени.
19.11. SNTP (продолжение) 565 ВНИМАНИЕ --------------------------------------------------------- Этот код исследует воспринимать как рекомендуемый способ создания клиента SNTP Наша цель — рассказать о широковещательной и многоадресной передаче примени- тельно к узлу с несколькими сетевыми интерфейсами или к маршрутизатору, а также о свойстве закольцовки широковещательной и mhoi оадресной передачи. В качестве при- мера мы взяли клиент SNTP, поскольку это полезное реально используемое приложе- ние. Мы отправляем запросы клиента SNTP со всех интерфейсов, поддерживающих широковещательную и многоадресную передачу, лишь для того, чтобы продемонстри- ровать, как некоторые программы осуществляют обнаружение ресурса (resource disco- very) при запуске. На самом деле для клиента SNTP это не рекомендуется — более разумно было бы просто прослушивать широковещательные или многоадресные сооб- щения сервера, как показано в разделе 19.10. На рис. 19.6 представлен обзор функций, составляющих нашу программу. Сначала мы вызываем нашу функцию get_i fт_т nfo (см. раздел 16.6), чтобы полу- чить список интерфейсов. Для каждого интерфейса мы создаем сокет UDP и при помощи функции bind связываем его с адресом направленной передачи, затем создаем другой сокет UDP, который связываем с адресом широковещательной передачи, и, наконец, создаем третий сокет UDP, связываем его с группой NTP (224.0.1.1) и присоединяемся к группе на этом интерфейсе. Наша функция sntp_send отправляет широковещательный запрос на определение текущего вре- мени с каждого сокета, поддерживающего широковещательную передачу, каждо- му серверу NTP в присоединенной подсети, а также отправляет групповой за- прос со всех сокетов, поддерживающих многоадресную передачу. Целью этой первой отправки пакетов является обнаружить все серверы NTP в присоединен- ных подсетях и получить начальную оценку текущего времени. Процесс отвечает на первый запрос и обрабатывает все последующие ответы Рис. 19.6. Обзор функций в нашем клиенте SNTP Затем программа входит в бесконечный цикл read_l оор, считывая все прихо- дящие ответы. Сначала мы предполагаем получить ответы на некоторые дейта- граммы, посланные функцией sntp_send, а затем мы должны просто получать пе- риодические передачи от серверов NTP в присоединенной подсети. Большинство
566 Глава 19. Многоадресная передача серверов NTP, выполняющих широковещательную или многоадресную переда- чу, отправляют по одной дейтаграмме каждые 64 секунды. Функция sntp_proc — та же, что показана в листинге 19.12, — обрабатывает полученный пакет NTP. Мы начинаем представление нашего кода с листинга 19.13, где содержится наш заголовочный файл sntp.h, включенный во все наши программы. Заголовочный файл ntp.h, включенный в sntp.h, тот же, что показан в листинге 19.10. В листин- ге 19.14 показана функция main. Листинг 19.13. Заголовок sntp.h //sntp/sntp h 1 #include "unpifi h" 2 ^include "ntp h" 3 #define MAXNADDRS 128 /* максимальное количество адресов для функции bindO *7 4 typedef struct { 5 struct sockaddr *addr_sa. /* указатель на связанный адрес */ 6 socklen_t addr salen. /* длина адреса сокета */ 7 const char *addr_ifname. /* имя интерфейса, для многоадресной передачи *7 8 int addr_fd. /* дескриптор сокета */ 9 int addr_flags. /* флаги ADDR_xxx (см ниже) */ 10 }Addrs. 11 Addrs addrs[MAXNADDRSL /* массив структур */ 12 int naddrs. /* индекс в массиве */ 13 #define ADDR BCAST 1 14 #define ADDR_MCAST 2 15 const int on. /* для setsockoptO */ 16 /* прототипы функций */ 17 void bind mcast(const char k SA *. socklen t. int). 18 void bind ubcast(SA *. socklen t. int. int. int). 19 void readjoop(void). 20 void sntp proc(char *. ssize t nread. struct timeval *): 21 void sntp_send(void). Листинг 19.14. Функция main //sntp/main c 1 #include "sntp h” 2 const int on = 1. /* флаги для setsockoptO */ 3 int 4 main(int argc, char **argv) 5 { 6 int sockfd, port: 7 socklen_t salen. 8 struct ifi_info *ifi. 9 struct sockaddr *mcastsa, *wild: 10 if (argc !- 2) 11 err_quit("usage. sntp <IPaddress>"), 12 sockfd - Udp_client(argv[l], "ntp”. (void **) 8mcastsa. Ssalen); 13 port - sock_get_port(mcastsa, salen). 14 Close(sockfd).
19.11. SNTP (продолжение) 567 15 /* получение списка интерфейсов и работа с ними */ 16 for (ifi = Get_ifi_info(mcastsa->sa_family. 1). ifi |= NULL. 17 ifi = ifi->ifi_next) { 18 bind_ubcast(ifi->ifi_addr. salen. port. 19 ifi->ifi_myflags & IFI ALIAS, 0). /* направленная передача */ \ 20 if (ifi->ifi_flags & IFF_BROADCAST) 21 bind_ubcast(ifi->ifi_brdaddr salen. port. 22 ifi->ifi_myflags & IFI_ALIAS. 1). /* широковещательная передача */ 23 tfifdef MCAST 24 if (ifi->ifi_flags & IFF_MULTICAST) 25 bind_mcast(ifi->ifi_name. mcastsa. salen. 26 ifi->ifi_myflags & IFI_ALIAS). /* многоадресная передача */ 27 tfendif 28 } 29 wild = Malloc(salen). /* структура адреса сокета для универсального адреса */ ' 30 memcpylwild. mcastsa. salen), 31 sock_set_wild(wild. salen), 32 bind_ubcast(wild. salen. port. 0. 0). 33 sntp_send(). /* посылаем первый запрос */ 34 read_loop(). /* никогда не завершается */ 35 } Задание структуры Addrs 3-12 Мы задаем структуру типа Addrs, содержащую необходимую нам информацию о каждом адресе, возвращаемом для данного интерфейса. Поскольку структура ifi_info, возвращаемая нашей функцией get_ifi_info, может иметь два адреса (например, адрес направленной передачи и широковещательный адрес), нам нуж- но поддерживать две структуры Addrs, по одной для каждого адреса. Элемент addr_sa указывает на структуру адреса сокета, возвращаемую функцией get_i f i_i nfo, a addr_salen — это ее длина. Мы сохраняем указатель на имя интерфейса в функ- ции addr_i fname. Этот указатель будет использоваться для многоадресной передачи. Для каждого адреса мы создадим по сокету, при этом дескриптор будет сохранен в элементе addr_fd. Нам также нужно знать, был ли этот сокет связан с широкове- щательным или групповым адресом. Эта информация сохраняется в элементе addr_fl ags. Получение заранее известного адреса многоадресной передачи и заранее известного порта 10-14 Обычно аргументом командной строки является имя ntp.mcast.net, сопостав- ляемое с адресом многоадресной передачи 224.0.1.1. Имя службы — ntp. Наша функция udp_cl lent выделяет в памяти пространство для структуры адреса соке- та нужного типа (IPv4 или IPv6) и записывает в эту структуру адрес многоадрес- ной передачи и порт. Если эта программа выполняется на узле, не поддерживаю- щем многоадресную передачу, может быть задан любой IP-адрес, поскольку в этой структуре будет задействован только порт. Затем мы закрываем сокет, поскольку целью вызова функции udp_cl lent является только заполнение структуры адреса сокета.
568 Глава 19. Многоадресная передача Получение списка интерфейсов .5-17 Наша функция get i f i _i nfo возвращает информацию обо всех интерфейсах и ад- ресах. Запрашиваемое нами семейство адреса берется из структуры адреса соке- та, заполненной функцией udp_client на основе аргумента командной строки. Второй аргумент функции get_i f i_info, имеющий значение 1, указывает, что тре- буется возвратить информацию о дополнительных (альтернативных) адресах. Обработка каждого адреса направленной и многоадресной передачи .8-22 Мы вызываем нашу функцию bind_ubcast один или два раза для каждого адре- са. При первом вызове последний аргумент равен 0 — это означает, что первый аргумент указывает на адрес направленной передачи, а при втором вызове этот аргумент равен 1, следовательно, первый аргумент указывает на широковещатель- ный адрес. Затем мы передаем флаг IFI_ALIAS нашим функциям bind_XXX, предо- ставляя каждой функции возможность решить, как обрабатывать альтернатив- ные адреса, что мы рассмотрим далее. Присоединение к группе !3-27 Если интерфейс имеет возможность передавать многоадресные сообщения, мы вызываем нашу функцию bind mcast также для каждого адреса направленной пе- редачи, поскольку мы хотим присоединиться к группе на каждом интерфейсе. Связывание с универсальным адресом '9-32 После обработки всей информации об интерфейсах мы размещаем в памяти еще одну структуру адреса сокета и копируем в нее содержимое из структуры, заполненной нашей функцией udp client. Затем мы вызываем нашу функцию sock_set_wi 1 d, чтобы сохранить в этой структуре соответствующий универсаль- ный адрес. Функция bind_ubcast создает сокет и связывает его с универсальным адресом. Он обрабатывает дейтаграммы, предназначенные для адресов, с кото- рыми мы не можем связаться, таких как 255.255.255.255. Отправка запросов и чтение последующих ответов 13 34 Функция sntp_send посылает широковещательный запрос SNTP со всех интер- фейсов, поддерживающих широковещательную передачу, и групповой запрос SNTP со всех интерфейсов, поддерживающих многоадресную передачу. Функ- ция read_l оор затем будет считывать все ответы на эти запросы, а также все паке- ты NTP широковещательной или многоадресной передачи, которые будут при- няты впоследствии. На рис. 19.7 показаны сокеты, которые будут созданы для нашего узла bsdi. Вспомните, что на рис. 1.7 этот узел действительно является маршрутизатором с двумя интерфейсами Ethernet. Мы показали интерфейсы и их IP-адреса направ- ленной и широковещательной передачи в примерах, следующих за листингом 16.3, но теперь мы считаем, что дополнительные (альтернативные) адреса отсутству- ют. Мы создадим девять сокетов и при помощи функции bind свяжем их с одним и тем же портом девять раз. Чтобы различать связывание сокета с адресом направленной, широковеща- тельной и многоадресной передачи, мы пометили восемь верхних сокетов симво-
19.11. SNTP (продолжение) 569 Подсеть 206.62 226 64/27 Подсеть 206 62 226.32/27 Рис. 19.7. Девять сокетов, созданных нашим клиентом SNTP на узле bsdi ламп и, b или т соответственно. Мы показываем IP-адреса получателей пакетов, которые будут получены па каждом сокете, и отмечаем, что сокет, связанный с ад- ресом 0.0.0.0, изображенный справа, может получать пакеты, предназначенные для любого другого IP-адреса (часто 255.255.255.255), приходящие па любой ин- терфейс. Наша функция Ы nd ubcast показана в листинге 19.15. Эта функция вызывает- ся для всех адресов направленной передачи, всех широковещательных и всех уни- версальных адресов. Последний аргумент в трех вызовах листинга 19.14 будет равен 1 только для широковещательного адреса. Листинг 19.15. Функция bind_ubcast: создание сокета для адреса направленной или широковещательной передачи //sntp/bind_ubcast с 1 include "sntp h" 2 void 3 bind_ubcast(struct sockaddr *sabind socklen_t salen. int port. 4 int alias, int beast) 5 { 6 int i, fd 7 /* Сначала проверяем, не связан ли уже этот адрес */ 8 for (1 0. 1 < naddrs, i++) { 9 if (sock_cmp_addr(addrs[i) addr_sa. sabind salen) == 0) 10 return. 11 ) 12 fd = Socket(sabind->sa_family. SOCK_DGRAM. 0). 13 sock_set_port(sabind salen port). продолжение
570 Глава 19. Многоадресная передача Листинг 19.15 (продолжение) 14 Setsockopt(fd. SOL_SOCKET SO_REUSEADDR. &on. sizeof(on)). 15 printfC’binding fcs\en", Sock_ntop(sabind. salen)). 16 if (bind(fd. sabind. salen) < 0) { 17 if (errno — EADORINUSE) { 18 printf(" (address already in use)\en"). 19 close(fd). 20 return. 21 } else 22 err_sys("bind error"). 23 } 24 addrs[naddrs] addr_sa - sabind; /* сохраняем указатель на структуру sockaddr{} */ 25 addrstnaddrs] addr_salen - salen, 26 addrstnaddrs] addr_fd - fd. 27 if (beast) 28 addrs[naddrs] addr_flags - ADDR_BCAST; 29 naddrs++. 30 } Определяем, связан ли адрес 7-11 Сначала мы проверяем, связан ли уже этот адрес. Для адреса направленной пе- редачи этого не может произойти, но в том случае, когда несколько альтернатив- ных адресов имеют общий широковещательный адрес, мы хотим связываться с широковещательным адресом только один раз. Создание сокета и связывание с адресом 12-23 Мы создаем сокет UDP, задаем порт и устанавливаем параметр сокета S0_ REUSEADDR (поскольку мы связываемся с одним и тем же портом для каждого адре- са). При помощи функции bind мы связываем адрес с сокетом. Структура адреса сокета, в которую мы записываем порт для функции bi nd, — это структура, запол- ненная и возвращенная функцией get_i fi_i nfo. Мы допускаем неудачное выпол- нение функции bi nd, что возможно, только если демон NTP сам запущен на этом узле. Сохранение информации об этом сокете 24-29 Информация хранится в нашей структуре Arrds, включая флаг, указывающий, является ли этот адрес широковещательным. Для интерфейсов, поддерживающих многоадресную передачу, нам нужно со- здать сокет и связать его с известным портом, а затем присоединиться к группе на интерфейсе. Это делается с помощью нашей функции Ы ndjncast, показанной в лис- тинге 19.16. Листинг 19.16. Функция bindjneast: присоединение к группе на интерфейсе //sntp/bindjncast с 1 #include "sntp h" 2 void 3 bind_mcast(const char *ifname. SA *mcastsa. socklen_t salen. int alias) 4 { 5 #ifdef MCAST 6 int fd.
19.11. SNTP (продолжение) 571 7 struct sockaddr *msa. 8 if (alias) 9 return /* для каждого интерфейса допускается только одно присоединение к группе */ 10 printfCjoining 2s on 2s\en". Sock_ntop_host(mcastsa. salen). ifname). 11 fd = Socket(mcastsa->sa_farmly. SOCK_DGRAM. 0). 12 Setsockopt(fd. SOL_SOCKET. SO_REUSEADDR. &on. sizeof(on)), 13 Bind(fd. mcastsa. salen). 14 Mcast_join(fd. mcastsa. salen, ifname 0). 15 addrs[naddrs] addr_sa = mcastsa. 16 addrstnaddrs] addr_salen - salen. 17 addrstnaddrs] addr_ifname - ifname: /* сохраняем указатель, а не копию строки */ 18 addrstnaddrs] addr_fd - fd. 19 addrstnaddrs] addr_flags - ADDR_MCAST, 20 naddrs++. 21 #endif 22 } Создание сокета и связывание 8-13 Если этот адрес является альтернативным (дополнительным), мы немедленно возвращаемся, поскольку нам нужно присоединиться к группе только один раз для каждого интерфейса независимо от того, сколько у интерфейса имеется до- полнительных адресов направленной передачи. Создается сокет, и группа и зара- нее известный порт связываются с этим сокетом. Присоединение к группе и сохранение информации 14-20 Присоединение к группе происходит на интерфейсе. Мы сохраняем информа- цию в нашей структуре Addrs. Указатель mcastsa, сохраненный в элементе addr_sa, указывает на структуру адреса сокета, размещенную в памяти в результате вызо- ва нашей функции udp_client в функции main. Элемент addr_sa для каждого ин- терфейса, поддерживающего многоадресную передачу, указывает на ту же струк- туру, но это нормально, поскольку структура никогда не изменяется. Указатель addr_i fname указывает на строку имени интерфейса в структуре i f i_i nfo, что так- же нормально, поскольку наша функция f ree_i f i_i nfo никогда не вызывается для освобождения этой области памяти. Следующая функция, sntp_send, представленная в листинге 19.17, вызывается функцией main после того, как вся информация об интерфейсах обработана и все сокеты созданы. Листинг 19.17. Функция sntp_send: широковещательные и многоадресные запросы SNTP //sntp/sntp_send с 1 include "sntp h" 2 void 3 sntp_send(void) 4 { 5 int fd. продолжение &
572 Глава 19 Многоадресная передача Листинг 19.17 (продолжение) 6 Addrs *aptr 7 struct ntpdata msg 8 /* используем для отправки сокет связанный с адресом ООО 0/123 */ 9 fd = addrs[naddrs - 1] addr_fd 10 Setsockopt(fd SOL_SOCKET SO_BROADCAST &on sizeof(on)) 11 bzero(&msg sizeof(msg)) 12 msg status = (0 « 6) | (3 « 3) | MODE_CLIENT /* cm RFC 2030 */ 13 for (aptr = &addrs[0] aptr < &addrs[naddrs] aptr++) { 14 if (aptr->addr_flags & ADDR_BCAST) { 15 printf( sending broadcast to Xs\en 16 Sock_ntop(aptr >addr_sa aptr >addr_salen)) 17 Sendtoffd &msg sizeof(msg) 0 18 aptr >addr_sa aptr->addr_salen) 19 } 20 #1fdef MCAST 21 if (aptr >addr_flags & ADDR_MCAST) { 22 /* сначала нужно соответствующим образом установить интерфейс для исходящих пакетов */ 23 Mcast_set_if(fd aptr >addr_ifname 0) 24 Mcast_set_loop(fd 0) /* отключаем закольцовку */ 25 printf( sending multicast to &s on &s\en 26 Sock_ntop(aptr >addrsa aptr >addr_salen) 27 aptr >addr_ifname) 28 Sendto(fd &msg sizeof(msg) 0 29 aptr >addr_sa aptr >addr_salen) 30 } 31 #endif 32 } 33 } Установка параметра сокета SO_BROADCAST и формирование запроса 8 12 Выест о создания специального сокета для отправки мы используем для вызо- вов функции sendto сокет, связанный с универсальным адресом. Поскольку IP- адрес задан через символы подстановки, ядро использует в качестве IP-адреса отправителя дейтаграммы UDP первичный адрес направленной передачи исхо- дящего интерфейса Сначала мы устанавливаем параметр сокета S0_BR0ADCAST Затем мы формируем запрос SNTP обнуляем LI (leap indicator — индикатор при- ращения), задаем версию 3 и режим MODE_CLIENT В запросе клиента нужно уста- навливать только эти поля Отправка широковещательного запроса 14 19 Если адрес является широковещательным адресом, запрос отправляется соот- ветствующим образом Отправка группового запроса 21 30 Если адрес является адресом многоадресной передачи, мы сначала задаем ин- терфейс для исходящих групповых сообщений на сокете (наша функция mcast_
19 11 SNTP (продолжение) 573 set_i f ), а затем отключаем на этом сокете закольцовку многоадресной передачи (наша функция mcast_set_l оор). Если мы не отключим закольцовку, мы можем получить множество копий пакета на принимающих сокетах (см. упражнение 1911) Дейтаграмма отправляется на адрес многоадресной передачи ПРИМЕЧАНИЕ------------------------------------------------------------------ Этот код иллюстрирует типичную парадигму направленной передачи, что мы можем обобщить так for (каждый интерфейс) { mcast_set_if( ) sendto( ) ) Дейтаграмма отправляется с каждого интерфейса, таким образом, исходящий интер- фейс должен быть установлен с использованием параметра сокета перед каждым вы- зовом функции sendto Это включает дополнительный системный вызов для каждой исходящей дейтаграммы Альтернативой может быть создание одного отправляющы о сокета для каждого интерфейса и однократная установка исходящего интерфейса для каждого сокета, выполняемая после создания сокета В разделе 20 8 мы увидим, что IPv6 допускает использование вспомогательных данных при вызове функции sendmsg для задания исходящего интерфейса, что сокращает число системных вызовов в этом сценарии Существует также класс приложений многоадресной передачи, которые не заботятся об исходящем интерфейсе, отправляя за один раз только одну дейтаграмму и по аволяя ядру выбирагь исходящий интерфейс Для ситуации, изображенной на рис 19 7, функцией sntp_send будут отправ- лены пять дейтаграмм, по одной для каждого широковещательного и многоад- ресного сокета Через сокеты направленной передачи ничего не отправляется Следующей будет расмотрена функция read_l оор Ее первая половина нахо- дится в листинге 19 18 Эта функция вызывается в конце функции main и только считывает из созданных сокетов любые широковещательные, групповые или на- правленные пакеты NTP, появляющиеся на любом из интерфейсов узла Мы пред- полагаем, что большинство получаемых пакетов NTP будут широковещательны- ми или групповыми, но ответы на запросы, отправляемые функцией sntp_send, будут направленными Листинг 19.18. Функция readjoop первая часть //sntp/read_loop с 1 include sntp h 2 static int check_loop(struct sockaddr * socklen_t). 3 static int check_dup(socklen_t) 4 static char buf1[MAXLINE] buf2[MAXLINE] 5 static char *buf[2] - { bufl buf2 } 6 struct sockaddr *from[2] 7 static ss1ze_t nread[2] - { -1 1 } 8 static int currb - 0 lastb - 1 9 void 10 readjoop(void) И { 12 int nsel maxfd продолжение &
574 Глава 19. Многоадресная передача Листинг 19.18 (продолжение) 13 Addrs *aptr, 14 fd_set rset. allrset: 15 socklen_t len: 16 struct timeval now: 17 /* размещаем в памяти две структуры адреса сокета */ 18 from[0] - Malloc(addrs[0].addr_salen); 19 from[l] - Malloc(addrs[0].addr_salen): 20 maxfd - -1. 21 for (aptr = &addrs[0], aptr < &addrs[naddrs]: aptr++) { 22 FO_SET(aptr->addr_fd. &allrset): 23 if (aptr->addr_fd > maxfd) 24 maxfd - aptr->addr fd: 25 } Выделение множества буферов и структур адресов сокетов 4-19 Мы выделяем два буфера, в которые получаем дейтаграммы, две структуры адресов сокетов, в которых хранятся соответствующие адреса отправителей, и две переменные, хранящие размеры дейтаграмм. Индекс, соответствующий те- кущей дейтаграмме, — currb, а индекс последней дейтаграммы — 1 astb. Один ин- декс будет иметь значение 0, другой — значение 1. Следует предусмотреть место для двух дейтаграмм, поскольку мы будем получать множество копий дейтаграмм многоадресной передачи и нам необходима возможность обнаруживать эти ко- пии и игнорировать их. Даже на узле, имеющем только один интерфейс, пакет многоадресной передачи NTP может быть получен на сокет, связанный с этим интерфейсом, и на сокет, связанный с универсальным адресом (см. упражне- ние 19.10). Множественные копии появляются в данном примере многоадресной переда- чи, потому что мы связались с одним и тем же портом несколько раз: по одному разу для интерфейсов и еще раз — для универсального адреса. Вспомните из нашего обсуждения параметра сокета SO_REUSEADDR в главе 7, что одна копия дейтаграммы многоадресной или широковещательной передачи доставляется на каждый подходящий сокет, в то время как дейтаграмма направ- ленной передачи доставляется только на один сокет. Наш сокет, связанный с уни- версальным адресом, является «подходящим» для каждого полученного группо- вого пакета. Если нам не обязательно знать, для какого адреса был предназначен данный пакет многоадресной передачи, мы можем просто создать один сокет и связать его с адресом многоадресной передачи (224.0.1.1) и известным портом (123), в ре- зультате чего мы всегда будем получать только одну копию. Так мы поступили в нашем примере в листинге 19.4. Этот пример SNTP несколько сложнее за счет того, что мы хотим знать адрес получателя каждой дейтаграммы. Подготовка набора дескрипторов для функции select 20-25 Мы подготавливаем набор дескрипторов для функции sel ect и вычисляем мак- симальный из дескрипторов, из которого можем читать.
19.11. SNTP (продолжение) 575 Вторая часть нашей функции read loop показана в листинге 19.19. Она вызы- вает функцию select, которая ждет, когда один или более дескрипторов будут готовы для чтения, затем считывает дейтаграмму и обрабатывает пакет NTP. Листинг 19.19. Функция readjoop: вторая часть //sntp/read_loop.c 26 for (.;) { 27 rset - allrset: 28 nsel - Select(maxfd + 1. &rset, NULL. NULL. NULL). 29 Gettimeofday(&now. NULL). /* узнаем время возвращения функции select */ 30 for (aptr - &addrs[0J. aptr < &addrs[naddrs). aptr++) { 31 if (FD_ISSET(aptr->addr_fd. &rset)) { 32 len - aptr->addr_salen. 33 nreadtcurrb] - recvfrom(aptr->addr_fd. 34 buftcurrb], MAXLINE. 0. 35 fromtcurrb]. &len); 36 if (aptr->addr_flags & ADDR_MCAST) { 37 printf("Xd bytes from Xs". nreadtcurrb], 38 Sock_ntop(from[currb], len)); 39 printf("""multicast to Xs". aptr->addr_1fname): 40 } else if (aptr->addr_flags & ADDR_BCAST) ( 41 pr1ntf("Xd bytes from Xs", nreadtcurrb], 42 Sock_ntop(from[currb], len)): 43 printf(""broadcast to Xs". 44 Sock_ntop(aptr->addr_sa. len)); 45 ) else { 46 pnntfC'Xd bytes from Xs". nreadtcurrb]. 47 Sock_ntop(from[currb]. len)); 48 printf("”to Xs", 49 Sock ntop(aptr->addr sa. len)); 50 ) 51 if (check_loop(fromtcurrb], len)) { 52 printfC (ignored)\en“): 53 continue. /* это наш пакет, который вернулся назад через закольцовку */ 54 } 55 if (check_dup(len)) { 56 printfC (dup)\en”); 57 continue; /* это дубликат */ 58 } 59 sntp_proc(buftlastb], nreadtlastb], &now); 60 if (--nsel <- 0) 61 break; /* работа с функцией select закончена */ 62 ) 63 } 64 } 65 } Функция select и получение текущего времени и даты 27-29 Как только функция sei ect возвращает управление, мы вызываем функцию gettimeofday, чтобы получить текущее время, которое будет использовано для вычисления разности со временем пакета NTP.
576 Глава 19. Многоадресная передача Определение, какой из дескрипторов готов для чтения Мы проверяем каждый дескриптор, чтобы определить, какой из них готов для чтения, вызываем функцию recvfrom и выводим данные о том, какая дейтаграмма (широковещательная, многоадресная или направленная) и на каком интерфейсе была получена. Проверка пакетов, пришедших через закольцовку 51-54 Наша функция ckeck_loop возвращает 1, если пакет является одним из наших собственных пакетов, пришедших через закольцовку. Мы отключили закольцов- ку многоадресной передачи в листинге 19.17, но автоматическую закольцовку для отправляемых нами широковещательных пакетов отключить невозможно. Проверка дублированных пакетов 55-58 Поскольку мы можем получать несколько копий любого полученного группового или широковещательного пакета, наша функция check_dup проверяет, является ли текущий пакет точной копией предыдущего, и если это так, она возвращает 1. Обработка пакета NTP 59 На этом этапе мы знаем, что пакет не является копией, пришедшей через за- кольцовку, или дубликатом, поэтому мы можем вызвать нашу функцию sntp_proc (см. листинг 19.12) для обработки этого пакета. Наша функция check_loop, показанная в листинге 19.20, проверяет, является ли пакет копией отправленного нами пакета, пришедшей через закольцовку. Листинг 19.20. Функция checkjoop: возвращает 1, если дейтаграмма является отправленной нами дейтаграммой //sntp/read_loop с 66 int 67 check_loop(struct sockaddr *sa socklen_t salen) 68 { 69 Addrs *aptr 70 for (aptr = &addrs[O], aptr < &addrs[naddrs] aptr++) { 71 if (sock_cmp_addr(sa. aptr >addr_sa salen) == 0) 72 return (1) /* это один из наших адресов */ 73 } 74 return (0) 75 } Проверка адреса отправителя 70-74 Чтобы проверить, не является ли дейтаграмма копией, пришедшей через за- кольцовку, мы перебираем все адреса в нашем массиве Addrs и сравниваем их с IP- адресом отправителя полученной дейтаграммы. Наша функция ckeck_dup показана в листинге 19.21. Она проверяет, является ли полученная дейтаграмма полной копией предыдущей дейт аграммы. Листинг 19.21. Функция checkjdup: возвращает 1, если дейтаграмма является дублированной //sntp/read_loop с 76 int 77 check_dup(socklen_t salen)
19.11. SNTP (продолжение) 577 78 { 79 int temp. 80 if (nread[currb] == nread[lastb] && 81 memcmp(from[currb]. from[lastb] salen) — 0 && 82 memcmp(buf[currb]. buf[lastb] nread[currb]) ~ 0) { 83 return (1). /* это дубликат */ 84 } 85 temp = currb /* меняем currb и lastb */ 86 currb = lastb. 87 lastb = temp. 88 return (0). 89 } Проверка длины, адреса отправителя и содержимого дейтаграммы 80-88 Мы считаем одну дейтаграмму копией другой, если у этих дейтаграмм совпада- ют длина, адреса протокола отправителя и действительное содержимое. Если дей- таграмма не дублирована, индексы curbb и 1 astb меняют своп значения. Обратите внимание, что вызов функции sntp_proc в конце листинга 19.19 передает дейта- грамму с индексом lastb, поскольку при вызове функции check_dup индексы ме- няются для следующего вызова функции recvfrom. Вспомните девять сокетов, изображенных на рис. 19.7. В листинге 19.22 пока- зан результат запуска программы на нашем узле bsdi с сервером NTP, запущен- ным на узле solans. Первые девять строк показывают созданные сокеты, IP-ад- реса, связанные с сокетами, и интерфейсы, на которых происходит присоединение к группе. Следующие пять строк показывают пакеты, отправленные функцией sntp_send: по одному пакету на каждый широковещательный адрес и по одному — на каж- дый адрес многоадресной передачи. В следующих шести строках показаны первые пять дейтаграмм, полученных на различных сокетах. Эта дейтаграммы пpi гходят в ответ на запросы, отправленные функцией sntp send. Первая дейтаграмма игнорируется, поскольку она является пришедшей через закольцовку копией одного из широковещательных запросов, который мы отправили (посмотрите на адрес отправителя). Вторая дейтаграмма пришла с сервера NTP (режим 4 — это MODE SERVER в листинге 19.10) на узле sol a n s (206.62.26.33), который является версией 3 NTP-сервера, работающего на слое 3. Разница во времени между двумя узлами составляет около 116 миллисекунд. Следующие гри дейтаграммы игнорируются: первая является копией (пришед- шей через закольцовку) широковещательной дейтаграммы, отправленной в дру- гую сеть Ethernet, а две других — это копии наших широковещательных дейта- грамм, полученных на сокет, связанный с универсальным адресом, которые также вернулись благодаря наличию закольцовки. Вот что произошло с двумя отправ- ленными широковещательными дейтаграммами: по одной копии каждой из них оказалось в закольцовке, а затем одна копия каждого пакета, пришедшего через закольцовку, была доставлена на сокет, связанный с соответствующим широко- вещательным адресом, а другая — на сокет, связанный с универсальным адресом. Двумя широковещательными пакетами были сгенерированы четыре копии, при- шедшие через закольцовку. В случае многоадресной передачи мы можем отклю- чить закольцовку, но для широковещательной передачи это невозможно.
578 Глава 19. Многоадресная передача В последних пяти строках представлена информация о четырех полученных дейтаграммах, соответствующих одному полученному от узла sol a n s пакету NTP. Первая дейтаграмма получается как групповая и обрабатывается для вычисле- ния разницы во времени — около 117 миллисекунд. Следующие три дейта- граммы являются копиями этого пакета многоадресной передачи, получен- ными на двух других сокетах, связанных с одним и тем же IP-адресом и портом (224.0.1.1 123), и на сокете, связанном с тем же портом (0.0.0.0.123). Листинг 19.22. Вывод нашей программы sntp bsdi # sntp 224 0.1.1 binding 206 62 226 66 123 weO binding 206 62 226 95 123 weO joining 224 0 1 1 on weO weO binding 206 62 226 35 123 efO binding 206 62 226 63 123 efO joining 224 0 1 1 on efO efO binding 127 0 0 1 123 loO joining 224 0 1 1 on loO loO binding 0 0 0 0 123 направленная дейтаграмма Ethernet широковещательная дейтаграмма Ethernet групповая дейтаграмма направленная дейтаграмма Ethernet широковещательная дейтаграмма Ethernet групповая дейтаграмма закольцовка групповая дейтаграмма универсальный адрес sending broadcast to 206 62 226 95 123 sending multicast to 224 0 1 1 123 on weO sending broadcast to 206 62 226 63 123 sending multicast to 224 0 1 1 123 on efO sending multicast to 224 0 1 1 123 on loO 48 bytes from 206 62 226 66 123 broadcast to 206 62 226.95.123 (ignored) 48 bytes from 206 62 226 33 123 to 206 62 226 35 123 v3 mode 4 strat 3 clock difference = -116013 usee 48 bytes from 206 62 226 35 123 broadcast to 206 62 226 63.123 (ignored) 48 bytes from 206 62 226 66 123 to 0 0 0 0 123 (ignored) 48 bytes from 206 62 226 35 123 to 0 0 0 0 123 (ignored) 48 bytes from 206 62 226 33 123 multicast to weO v3 mode 5 strat 3 clock difference = -117043 usee 48 bytes from 206 62 226 33 123 multicast to efO (dup) 48 bytes from 206 62 226 33 123 multicast to loO (dup) 48 bytes from 206 62 226 33 123 to 0 0 0 0 123 (dup) Если мы продолжим выполнение программы в этой же среде, мы увидим, что каждые 64 секунды сервер NTP на узле solans передает с помощью многоадрес- ной передачи пакет NTP, и наша программа получает четыре копии. Первая из четырех копий обрабатывается, а остальные три — это дубликаты, которые игно- рируются. 19.12. Резюме Для запуска приложения многоадресной передачи в первую очередь требуется присоединиться к группе, заданной для этого приложения. Тем самым уровень IP получает указание присоединиться к группе, что, в свою очередь, указывает канальному уровню на необходимость получать кадры многоадресной передачи, отправляемые на соответствующий адрес многоадресной передачи аппаратного уровня. Многоадресная передача использует преимущество аппаратной фильт- рации, имеющееся у большинства интерфейсных карт, и чем качественнее филь-
Упражнения 579 трация, тем меньше число нежелательных получаемых пакетов. Использование аппаратной фильтрации сокращает нагрузку на все узлы, не задействованные в приложении. Многоадресная передача в глобальной сети требует наличия маршрутизато- ров, поддерживающих многоадресную передачу, и протокола маршрутизации многоадресной передачи. Поскольку не все маршрутизаторы в Интернете имеют возможность многоадресной передачи, для этой цели используется виртуальная сеть МВопе (см. раздел Б.2). API для многоадресной передачи обеспечивают пять параметров сокетов: присоединение к группе на интерфейсе; ’>< выход из группы; > установка интерфейса по умолчанию для исходящих пакетов многоадресной передачи; ? установка значения TTL или предельного количества транзитных узлов для исходящих пакетов многоадресной передачи; включение или отключение закольцовки для пакетов многоадресной пе- редачи. Первые два параметра предназначены для получения пакетов многоадресной передачи, последние три — для отправки. Сушествует достаточно большая раз- ница между указанными параметрами сокетов IPv4 и IPv6. Вследствие этого код многоадресной передачи, зависящий от протокола, очень быстро становится «за- мусорен» директивами #i fdef. Мы разработали восемь наших собственных функ- ций с именами, начинающимися с mcast_, для упрощения написания приложений многоадресной передачи, работающих как с IPv4, так и с IPv6. Упражнения 1. Скомпилируйте программу, показанную в листинге 18.5, и запустите ее, задав в командной строке IP-адрес 224.0.0.1. Что произойдет? 2. Измените программу из предыдущего примера, чтобы связать IP-адрес 224.0.0.1 и порт 0 с сокетом. Запустите ее. Разрешается ли вам связывать адрес много- адресной передачи с сокетом при помощи функции bi nd? Если у вас есть такая программа, как tcpdump, понаблюдайте за пакетами в сети. Каков IP-адрес от- правителя посылаемой вами дейтаграммы? 3. Один из способов определить, какие узлы в вашей подсети имеют возмож- ность многоадресной передачи, заключается в запуске утилиты ping для груп- пы всех узлов, то есть для адреса 224.0.0.1. Попробуйте это сделать. 4. Если мы введем команду ping 224 0 0 1 на нашем узле umxware, который имеет возможность многоадресной передачи, мы получим следующий вывод: umxware % ping 224.0.0.1 PING 224 0 0 1 56 data bytes 64 bytes from gw konala com (206 62 226 62) icmp_seq-0 time-0 ms 64 bytes from gw kohala com (206 62 226 62) icmp_seq=l time=0 ms 64 bytes from gw kohala com (206 62 226 62) icmp_seq=2 time=0 ms Что происходит?
580 Глава 19. Многоадресная передача 5. Одним из способов обнаружения маршрутизаторов многоадресной передачи в вашей подсети является запуск утилиты pi ng для группы всех маршрутиза- торов — 224.0.0.2. Попробуйте это сделать. 6. Один из способов узнать, соединен ли ваш узел с виртуальной сетью МВопе, — запустить нашу программу из раздела 19.8, подождать несколько минут и по- смотреть, появляются ли анонсы сеанса. Попробуйте сделать это и посмотри- те, получите ли вы какие-нибудь анонсы. 7. Идентификатор сеанса и версия в строке о= в листинге 19 6 часто являются отметками времени NTP. Имеют ли показанные значения какой-либо смысл? 8. Выполните вычисления в листинге 19.12, когда дробная часть отметки време- ни NTP равна 1 073 741 824 (одна четвертая от 232). Выполните еще раз эти же вычисления для максимально возможной дробной части (212 - 1). 9. В листингах 19.15 и 19.16 мы установили параметр сокета SO REUSEADDR, чтобы позволить связывать один и тот же порт несколько раз. Но в листинге 19.16 мы выполняем полностью дублированное связывание для каждого адреса многоадресной передачи (224.0.1.1) и порта 123, и три из них мы видим в лис- тинге 19.22. Как мы можем выполнить это для Беркли-ядра, не устанавливая параметр сокета SO_REUSEPORT вместо SO_REUSEADDR? 10. Последняя строка в листинге 19.22 показывает дейтаграмму многоадресной передачи, получаемую сокетом, связанным с универсальным адресом. Но если мы запустим наш клиент SNTP в Solaris 2.5, этот сокет не будет получать дей- таграмм многоадресной передачи. Почему? 11. Сколько дополнительных копий мы получили бы в примере, приведенном в листинге 19.22, если бы в листинге 19.17 мы не отключили закольцовку для многоадресной передачи? 12. Измените реализацию функции mcast_set_if для IPv4 так, чтобы запоминать имя каждого интерфейса, для которого она получает IP-адрес. Это позволит избежать нового вызова функции ioctl для данного интерфейса.
ГЛАВА 20 Дополнительные сведения о сокетах UDP 20.1. Введение Эта глава объединяет различные темы, которые затрагиваются приложениями, использующими сокеты UDP. Для начала нас интересует, как определяется ад- рес получателя дейтаграммы UDP и интерфейс, на котором дейтаграмма была получена, поскольку сокет, связанный с портом UDP и универсальным адресом, может получать дейтаграммы направленной, широковещательной и многоадрес- ной передачи на любом интерфейсе. TCP — это потоковый протокол, использующий окно переменной величины (sliding window), поэтому в TCP отсутствуют такие понятия, как граница записи или переполнение буфера получателя отправителем в результате передачи слиш- ком большого количества данных. Однако в случае UDP каждой операции ввода соответствует дейтаграмма UDP (запись), поэтому возникает вопрос: что про- изойдет, когда полученная дейтаграмма окажется больше приемного буфера при- ложения? UDP — это ненадежный протокол, однако существуют приложения, в кото- рых UDP целесообразно использовать вместоТСР. Мы рассмотрим факторы, под влиянием которых UDP оказывается предпочтительнее TCP. В UDP-приложе- ния необходимо включать ряд свойств, в некоторой степени компенсирующих ненадежность UDP: тайм-аут и повторную передачу, обработку потерянных дей- таграмм и порядковые номера для сопоставления ответов запросам. Мы разра- ботаем набор функций, которые сможем вызывать из наших приложений UDP для реализации перечисленных свойств. Когда реализация не поддерживает параметр сокета IP_RECVDSTADDR, один из способов определить IP-адрес получателя UDP-дейтаграммы заключается в свя- зывании всех интерфейсных адресов и использовании функции sei ect. Подоб- ный пример мы показали в разделе 19.11, и в этой главе мы еще немного порабо- таем с этой технологией. Большинство серверов UDP являются последовательными, но существуют приложения, обменивающиеся множеством дейтаграмм UDP между клиентом и сервером, что требует определенного согласования. Примером может служить TFTP (Trivial File Transfer Protocol — простейший протокол передачи файлов). Мы рассмотрим два варианта подобного согласования — с использованием су- персервера 1 netd и без него.
582 Глава 20. Дополнительные сведения о сокетах UDP В завершение этой главы мы рассмотрим информацию о пакете, которая мо- жет быть передана во вспомогательных данных для дейтаграммы IPv6: IP-адрес отправителя, отправляющий интерфейс, предельное количество транзитных уз- лов исходящих дейтаграмм и адрес следующего транзитного узла. Аналогичная информация — IP-адрес получателя, принимающий интерфейс и предельное ко- личество транзитных узлов входящих дейтаграмм — может быть получена вмес- те с дейтаграммой IPv6. 20.2. Получение флагов, IP-адреса получателя и индекса интерфейса Исторически функции sendmsg и recvmsg использовались только для передачи де- скрипторов через доменные сокеты Unix (см. раздел 14.7), но даже это происхо- дило сравнительно редко. Однако в настоящее время популярность этих двух функций растет по двум причинам: 1. Элемент msg_f 1 ags, добавленный в структуру msghdr в реализации 4.3BSD Reno, возвращает приложению флаги. Эти флаги мы перечислили в табл. 13.2. 2. Вспомогательные данные используются для передачи все большего количе- ства информации между приложением и ядром. В главе 24 мы увидим, что в IPv6 эта тенденция продолжается. В качестве примера функции recvmsg мы напишем функцию recvfrom_flags, аналогичную функции recvfrom, но дополнительно позволяющую получить: возвращаемое значение msg_flags; адрес получателя полученной дейтаграммы (из параметра сокета IP_RECVDST - ADDR); & индекс интерфейса, на котором была получена дейтаграмма (параметр сокета IP_RECVIF). Чтобы можно было получить два последних элемента, мы определяем в на- шем заголовке unp. h следующую структуру: struct in_pktinfo { struct in_addr ipi_addr; /*IPv4-anpec получателя */ int ipi_ifindex. /*индекс интерфейса, на которой была получена дейтаграмма */ }• Мы выбрали имена структуры и ее элементов так, чтобы сохранить сходство со структурой IPv61 n6_pkti nfo, возвращающей те же два элемента для сокета IPv6 (см. раздел 20.8). Наша функция recvfrom_flags будет получать в качестве аргу- мента указатель на структуру i n_pkti nfo, и если этот указатель не нулевой, воз- вращать структуру через указатель. Проблема формирования этой структуры состоит в том, что неясно, что воз- вращать, если недоступна информация, которая должна быть получена из пара- метра сокета IP_RECVDSTADDR (то есть реализация не поддерживает данный пара- метр сокета). Обработать индекс интерфейса легко, поскольку нулевое значение соответствует неизвестному индексу. Но для IP-адреса все 32-разрядные значе- ния являются действительными. Мы выбрали такое решение: адрес получателя 0.0 0 0 возвращается в том случае, когда действительное значение недоступно.
20.2. Получение флагов, IP-адреса получателя и индекса интерфейса 583 Хотя это реальный IP-адрес, использовать его в качестве IP-адреса получателя не разрешается (RFC 1122 [9]). Он будет действителен только в качестве IP-ад- реса отправителя во время начальной загрузки узла, когда узел еще пе знает свое- го IP-адреса. ПРИМЕЧАНИЕ ----------------------------------------------------------------------------- К сожалению, Беркли-ядра принимают дейтаграммы, предназначенные для адреса 0.0.0.0 [105, с. 218-219]. Это устаревшие адреса широковещательной передачи, генерируемые ядрами 4.2BSD. Первая часть нашей функции reevf rom_f 1 ags представлена в листинге 20.11. Эта функция предназначена для использования с сокетом UDP. Листинг 20.1. Функция recvfromjlags: вызов функции recvmsg //advio/recvfromflags с 1 include "unp IT 2 #include <sys/param h> /* макрос ALIGN для макроса CMSG_NXTHDR() */ 3 #ifdef HAVE_SOCKADDR_DL_STRUCT 4 include <net/if_dl h> 5 #endif 6 ssize_t 7 recvfrom_flags(int fd. void *ptr. size_t nbytes. int *flagsp. 8 SA *sa. socklen_t *salenptr. struct in_pktinfo *pktp) 9 { 10 struct msghdr msg. 11 struct icvec iov[l], 12 ssize_t n. 13 #ifdef HAVE_MSGHDR_MSG_CONTROL 14 struct cmsghdr *cmptr. 15 union { 16 struct cmsghdr cm. 17 char control[CMSG_SPACE(sizeof(struct in_addr)> 18 CMSG_SPACE(sizeof(struct in_pktinfo>>]: 19 } control_un: 20 msg.msg_control - control_un control: 21 msg.msg_controllen - sizeof(control_un.control): 22 msg msg_flags - 0: 23 #else 24 bzero(&msg. sizeof(msg)); /* убеждаемся, что msg_accrightslen - 0 */ 25 #endif 26 msg.msg_name - sa. 27 msg msg_namelen - *salenptr. 28 iOv[0J.iov_base - ptr: 29 iov[0].iov_len - nbytes: 30 msg msg_iov - iov. 31 msg msg_iovlen - 1: 32 if ( (n = recvmsglfd. &msg. *flagsp)) < 0) 33 return (n). 34 *salenptr - msg msgjiamelen. /* передаем обратно результаты */ продолжение 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http // www piter.com/download. ,
584 Глава 20. Дополнительные сведения о сокетах UDP Листинг 20.1 (продолжение) 35 if (pktp) 36 bzerotpktp sizeoftstruct in_pktinfo)). /* 0 0 0 0 индекс интерфейса - 0 */ Подключаемые файлы 2-5 Использование макроопределения CMSG_NXTHDR требует подключения заголовоч- ного файла <sys/param h>. Нам также нужно подключить заголовочный файл <net/ 1 f_dl h>, определяющий структуру sockaddr_dl, в которой возвращается получен- ный индекс интерфейса. Аргументы функции 6-8 Аргументы функции аналогичны аргументам функции rcvf rom за исключением того, что четвертый аргуменг является указателем на целочисленный флаг (так что мы можем возвратить флаги, возвращаемые функцией recvmsg), а седьмой аргумент новый: это указатель на структуру in_pktinfo, содержащую 1Р\’4-адрес получателя пришедшей дейтаграммы и индекс интерфейса, на котором дейта- грамма была получена. Различия реализаций 13-25 При работе со структурой msghdr и различными константами MSGjtxr мы встре- чаемся со множеством различий в реализациях. Одним из вариантов обработки таких различий может быть использование имеющейся в языке С возможности условного подключения (директива #i fdef). Если реализация поддерживает эле- мент msg_control, то выделяется пространство для храпения значений, возвраща- емых параметрами сокета IP_RECVDSTADDR и IP_RECVIF, и соответствующие элемен- ты инициализируются. Заполнение структуры msghdr и вызов функции recvmsg ’6-36 Заполняется структура msghdr, и вызывается функция recvmsg. Значения эле- ментов msg_namelen и msg_flags должны быть переданы обратно вызывающему процессу. Они являются аргументами типа «значение-результат». Мы также ини- циализируем структуру вызывающего процесса in_pktinfo, устанавливая IP-ад- рес 0.0.0.0 и нулевой индекс интерфейса. В листинге 20.2 показана вторая часть нашей функции. Листинг 20.2. Функция recvfromjlags: возвращаемые флаги и адрес получателя 37 #ifndef HAVE_MSGHDR_MSG_CONTROL 38 *flagsp - 0, /* передаем результаты обратно */ 39 return (п). 40 #else 41 *flagsp - msg msg_flags. /* передаем результаты обратно */ 42 if (msg msg_controllen < sizeoftstruct cmsghdr) || 43 (msg msg_flags & MSG_CTRUNC) || pktp -- NULL) 44 return (n). 45 for (emptr - CMSG-FIRSTHDRt&msg). emptr NULL. 46 emptr - CMSG_NXTHDR(&msg, emptr)) { 47 tfifdef IP_RECVDSTADDR 48 if (cmptr->cmsg level -- IPPROTD IP &&
20 2 Получение флагов, IP-адреса получателя и индекса интерфейса 585 49 cmptr->cmsg_type == IP_RECVDSTADDR) { 50 memcpy(&pktp->ipi_addr CMSG_DATA(cmptr) 51 sizeoftstruct in_addr)) 52 continue 53 } 54 #endif 55 #ifdef IP_RECVIF 56 if (cmptr->cmsg_level == IPPROTO_IP && 57 cmptr->cmsg_type == IP_RECVIF) ( 58 struct sockaddr_dl *sdl 59 sdl = (struct sockaddr dl *) CMSG_DATA(cmptr); 60 pktp->ipi_ifindex = sdl->sdl_index. 61 continue 62 } 63 #endif 64 err_quitCunknown ancillary data len = i:d level = %d type - W". 65 cmptr->cmsg_len cmptr >cmsg_level cmptr->cmsg_type) 66 } 67 return (n) 68 #endif /* HAVE_MSGHDR_MSG_CDNTROL */ 69 } Если реализация не поддерживает элемента msg_control, мы просто обнуляем возвращаемые флаги и завершаем функцию. Оставшаяся часть функции обраба- тывает информацию, содержащуюся в структуре msg control. Возвращение при отсутствии управляющей информации 41-44 Мы возвращаем значение msg_f 1 ags и возвращаемся в вызывающую функцию в том случае, если или нет никакой управляющей информации, или управляю- щая информация была обрезана, или вызывающий процесс не требует возвраще- ния структуры in pktinfo. Обработка вспомогательных данных 45-46 Мы обрабатываем произвольное количество объектов вспомогательных дан- ных с помощью макросов CMSG_FIRSTHDR и CMSG_NEXTHDR. Обработка параметра сокета IP_RECVDSTADDR 47-54 Если IP-адрес получателя был возвращен в сос гаве управляющей информации (см рис 13 2), он возвращается вызывающему процессу. Обработка параметра сокета IP_RECVIF 55-63 Если индекс полученного интерфейса был возвращен в составе управляющей информации, он возвращается вызывающему процессу. Па рис. 20.1 показано содержимое возвращенного объекта вспомогательных данных. Вспомните структуру адреса сокета канального уровня (см. листинг 17 1). Данные, возвращаемые в объекте вспомогательных данных, представлены в од- ной из этих структур, но длины трех элементов являются нулевыми (длина име- ни, адреса и селектора). Следовательно, нет никакой необходимости указывать эти значения, и таким образом структуры имеют размер 8 байт, а не 20, как было в листинге 17 1. Возвращаемая нами информация — это индекс интерфейса.
586 Глава 20. Дополнительные сведения о сокетах UDP cmsghdr{} cmsg_len cmsg_level cmsg_type 20 IPPROTO_IP IP_RECVIF 8,AF_LINK IFT-NONE, 0,0,0 sockaddr_dl{) len fam index ten fam alenjslen Рис. 20.1. Объект вспомогательных данных, возвращаемый для параметра IP_RECVIF Пример: вывод IP-адреса получателя и уведомления о том, что дейтаграмма обрезана Для проверки нашей функции мы изменим функцию dg_echo (см. листинг 8.2) так, чтобы она вызывала функцию reevfrom_fl ags вместо функции recvfrom. Но- вая версия функции dg echo показана в листинге 20.3. Листинг 20.3. Функция dg_echo, вызывающая нашу функцию recvfrom_flags 7/advio/dgechoaddr с 1 #include ’unpifi h" 2 #undef MAXLINE 3 #define MAXLINE 20 /* устанавливаем новое значение чтобы пронаблюдать за эффектом */ 4 void 5 dg_echo(int sockfd SA *pcliaddr. socklen_t clilen) 6 { 7 int flags. 8 const int on = 1 9 sockl en_t len. 10 ssize_t n 11 char mesg[MAXLINE]. str[INET6_ADDRSTRLEN], ifname[IFNAMSIZJ: 12 struct in_addr in_zero. 13 struct in_pktinfo pktinfo. 14 tfifdef IP_RECVDSTADDR 15 if (setsockopt(sockfd IPPROTOJP. IP_RECVDSTADDR. &on sizeof(on)) < 0) 16 err_ret("setsockopt of IP_RECVDSTADDR"). 17 #endif 18 #ifdef IP_RECVIF 19 if (setsockoottsockfd IPPROTO_IP, IP_RECVIF. &on sizeof(on)) < 0) 20 err_ret("setsockopt of IP_RECVIF") 21 #endif 22 bzero(&in_zero sizeoftstruct in_addr)). /* IPv4-aflpec состоящий из одних нулей */ 23 for (..) { 24 len = clilen 25 flags = 0 26 n = Reevfrom_flags(sockfd mesg MAXLINE Sflags. 27 pcliaddr &len &pktinfo) 28 printf('«d-byte datagram from «s’. n. Sock_ntop(pcliaddr. len)): 29 if (memcmpt&pktinfo ipi_addr. &in_zero. sizeof(in_zero)) '= 0) 30 printff to «s” Inet_ntop(AF_INET. &pktinfo ipi_addr, 31 str sizeof(str))) 32 if (pktinfo ipi_ifindex > 0)
20.2. Получение флагов, IP-адреса получателя и индекса интерфейса 587 33 printfC, recv i/f = «s’. 34 I’Mndextoname(pktinfo ipi_ifindex. ifname)). 35 #ifdef MSG_TRUNC 36 if (flags & MSG_TRUNC) 37 printff (datagram truncated)") 38 #endif 39 #ifdef MSG_CTRUNC 40 if (flags & MSG_CTRUNC) 41 printff’ (control info truncated)”); 42 #endif 43 #ifdef MSGJ3CAST 44 if (flags & MSGJ3CAST) 45 printfC (broadcast)") 46 #endif 47 #ifdef MSGJ-1CAST 48 if (flags & MSGJICAST) 49 pnntf(" (multicast)"). 50 #endif 51 printf("\en") 52 Sendtotsockfd. mesg. n 0 pcliaddr. len): 53 } 54 } Изменение MAXLINE 2-3 Мы удаляем существующее определение MAXLINE, имеющееся в пашем заголо- вочном файле unp h, и задаем новое значение — 20. Это позволит нам увидеть, что произойдет, когда мы получим дейта! рамму UDP, превосходящую размер буфе- ра, заданный для функции ввода (в данном случае функции recvmsg). Установка параметров сокета IP_RECVDSTADDR и IP_RECVIF 14-21 Если определен параметр сокета IP_RECVDSTADDR, on включается. Аналогично включается параметр сокета IP_RECVIF. ПРИМЕЧАНИЕ --------------------------------------------------------------------- К сожалению, проверок подобного рода недостаточно, потому что некоторые системы (например, Solans 2 5) определяют параметр сокета IP_RECVDSTADDR, даже если он не поддерживается. Следовательно, мы допускаем возможность неудачного вызова функции setsockopt, и если это происходит, то просто выводим некоторое сообщение и продолжаем работу Мы даже не можем точно определить, что произошла именно эта ошибка, поскольку различные реализации возвращают для неизвестного парамегра сокета различные ошибки. Например, в 4 4BSD функция getsockopt возвращает ошиб- ку ENOPROTOPORT, но функция setsockopt возвращает EINVAL, а для неизвестно- го параметра сокета многоадресной передачи возвращается ошибка EOPNOTSUPP Чтение дейтаграммы, вывод IP-адреса отправителя и порта 24-28 Дейтаграмма читается с помощью вызова функции recvf rom_fl ags. IP-адрес от- правителя и порт ответа сервера преобразуются в формат представления функ- цией sock_ntop.
588 Глава 20. Дополнительные сведения о сокетах UDP Вывод IP-адреса получателя 29-31 Если возвращаемый IP-адрес ненулевой, он преобразуется в формат представ- ления функцией 1 net_ntop и выводится. Вывод имени интерфейса, на котором была получена дейтаграмма 32-34 Если индекс интерфейса ненулевой, его имя будет получено при помощи вызо- ва функции 1 f_i ndextoname и выведено. Проверка различных флагов 35-51 Мы проверяем четыре дополнительных флага и выводим сообщение, если ка- кие-либо из них установлены. Если мы запустим наш сервер под BSD/OS 3.0 па узле bsdi (который имеет несколько сетевых интерфейсов), то увидим различные IP-адреса получателя и различные флаги: bsdi % udpservOl 9-byte datagram from 206 62 226 33 41164 to 206 62 226 35. recv i/f = efO 13-byte datagram from 206 62 226 65 1057. to 206 62 226 95. recv i/f = weO (broadcast) 4-byte datagram from 206 62 226 33 41176. to 224 001, recv i/f = efO (multicast) 20-byte datagram from 127 0 0 1 4632, to 127 0 0 1. recv i/f = loO (datagram truncated) 9-byte datagram from 206 62 226 33 4Ц78 to 206 62 226 66 recv i/f = efO Для удобства чтения мы поместили строки со значениями флагов в скобки. Чтобы сгенерировать эти пять строк вывода, мы запустили пашу программу sock (см. раздел В.З) на различных узлах. 1. Первая строка выводится от узла Solaris и предназначена для адреса 206.62.226.35 — одного из адресов направленной передачи узла сервера. 2. Вторая строка соответствует узлу laptop и предназначена для адреса 206.62.226.95 — это широковещательный адрес Ethernet, используемый совме- стно клиентом и сервером (см. рис. 1.7). Интерфейс отличается от указанного в первой строке, как мы и предполагали. Наш сервер получает широковеща- тельное сообщение, поэтому установлен флаг MSG_BCAST, указывающий на то, что дейтаграмма была получена как широковещательное сообщение на каналь- ном уровне. 3. Третья строка выводится от узла solans и предназначена для адреса 224.0.0.1 — адреса многоадресной передачи всех узлов (all-hosts group). В эту группу долж- ны входить все узлы подсети, имеющие возможность многоадресной передачи. Наш сервер получает многоадресные сообщения, поскольку операционная система BSD/OS поддерживает многоадресную передачу, и порт получателя совпадает с портом нашего сервера. Установленный флаг MSG_MCAST указывает, что дейтаграмма была получена как многоадресное сообщение на канальном уровне. 4. Четвертая строка выводится от самого узла и предназначена для адреса 127.0.0.1, адреса закольцовки. Указывается интерфейс 1о0, как мы и предпо- лагали. На этот раз мы ввели 41-байтовую строку на стороне клиента (здесь
20.3. Обрезанные дейтаграммы 589 мы это не показываем). Сервер получает только первые 20 байт дейтаграммы, и при этом установлен флаг MSG_TRUNC, указывающий, что дейтаграмма была обрезана. 5. Последняя строка выводится от узла solans, но предназначена для адреса 206.62.226.66 — адреса направленной передачи узла сервера в другой сети Ethernet (см. рис. 1.7). Сервер все равно получает дейтаграмму (поскольку узел реализует модель системы с гибкой привязкой, как показано в разделе 8.8), но IP-адрес получателя (206.62.226.66) не совпадает с адресом интерфейса, на котором дейтаграмма была получена. Более ранние реализации, происходящие от Беркли, игнорировали параметр сокета IP_RECVDSTADDR в тех случаях, когда полученная дейтаграмма была дейта- граммой широковещательной или многоадресной передачи [105, с. 776]. В более современных реализациях эта ошибка исправлена. Если адресом получателя является 255.255.255.255, то BSD/OS преобразует его в широковещательный адрес полученного интерфейса. 20.3. Обрезанные дейтаграммы Пример из предыдущего раздела показывает, что когда в BSD/OS приходит дей- таграмма UDP, размер которой больше буфера приложения, функция recvmsg ус- танавливает флаг MSG_TRUNC в элементе msg_flags структуры msghdr (см. табл. 13.2). Все Беркли-реализации, поддерживающие структуру msghdr с элементом msg_f 1 ags, предоставляют это уведомление. ПРИМЕЧАНИЕ-------------------------------------------------------- Это пример флага, который должен быть возвращен процессу ядром. В разделе 13.3 мы упомянули о проблеме разработки функций recv и recvfrom: их аргумент flags яв- ляется целым числом, что позволяет передавать флаги от процесса к ядру, по не наоборот. К сожалению, не все реализации подобным образом обрабатывают ситуацию, когда размер дейтаграммы UDP оказывается больше, чем предполагалось. Воз- можны три сценария: 1. Лишние байты игнорируются, и приложение получает флаг MSG_TRUNC, что тре- бует вызова функции recvmsg. 2. Игнорирование лишних байтов без уведомления приложения. 3. Сохранение лишних байтов и возвращение их в последующих операциях чте- ния на сокете. Мы уже видели развитие первого сценария в BSD/OS. Второй тип сценария можно увидеть в Solaris 2.5: лишние байты игнорируются, но поскольку структу- ра msghdr не поддерживает элемента msg_fl ags, нет возможности возвратить ошибку приложению. ПРИМЕЧАНИЕ-------------------------------------------------------- Posix. 1g задает первый тип поведения: игнорирование лишних байтов и установку флага MSG_TRUNC. Болес ранние реализации SVR4 действовали в подобных ситуациях по третьему сценарию.
590 Глава 20. Дополнительные сведения о сокетах UDP Поскольку способ обработки дейтаграмм, превышающих размер приемного буфера приложения, зависит от реализации, одним из решений, позволяющих обнаружить ошибку, будет всегда использовать буфер приложения на 1 байт боль- ше самой большой дейтаграммы, которую приложение предположительно может получить. Если все же будет получена дейтаграмма, длина которой равна разме- ру буфера, это послужит сигналом возможной ошибки. 20.4. Когда UDP оказывается предпочтительнее TCP В разделах 2.3 и 2.4 мы описали основные различия между UDP и TCP. Посколь- ку мы знаем, что TCP надежен, a UDP — нет, возникает вопрос: когда следует использовать UDP вместо TCP и почему? Сначала перечислим преимущества UDP: Как мы показали в табл. 18.1, UDP поддерживает широковещательную и на- правленную передачу. Действительно, UDP должен быть использован, если приложение использует широковещательную или многоадресную передачу. Эти два режима адресации мы рассматривали в главах 18 и 19. ь В U DP нет установления соединения или его разрыва. В соответствии с рис. 2.5 UDP требует только двух пакетов для обмена запросом и ответом (если пред- положить, что размер каждого из них меньше минимального размера MTU между двумя оконечными системами). В случае TCP требуется около 10 па- кетов, если считать, что для каждого обмена «запрос-ответ» устанавливается новое соединение TCP. В анализе количества обмениваемых пакетов важным фактором является так- же число циклов обращения пакетов, необходимых для получения ответа. Это становится важно, если время ожидания превышает пропускную способность, как показано в приложении А [95]. В этом тексте сказано, что минимальное время транзакции для запроса-ответа UDP равно RTT + SPT, где RTT — это время обращения между клиентом и сервером, a SPT — время обработки за- проса сервером. Однако в случае TCP, если для осуществления каждой после- довательности «запрос-ответ» используется новое соединение TCP, минималь- ное время транзакции будет равно 2 х RTT + SPT, то есть на один период RTT больше, чем для UDP. В [95] и разделе 13.9 также описывается модификация TCP — Т/ТСР, или TCP для транзакций, который обычно устраняет необхо- димость трехэтапного рукопожатия TCP, позволяя получить то же время тран- закции, что и для UDP, а именно RTT + SPT. В отношении второго пункта очевидно, что если соединение TCP использует- ся для множества обменов «запрос-ответ», то стоимость установления и разрыва соединения амортизируется во всех запросах и ответах. Обычно это решение пред- почтительнее, чем использование нового соединения для каждого обмена «запрос- ответ». Тем не менее существуют приложения, использующие новое соединение для каждого цикла «запрос-ответ» (например, HTTP). Кроме того, существуют приложения, в которых клиент и сервер обмениваются в одном цикле «запрос- ответ» (например, DNS), а затем могут не обращаться друг к другу в течение ча- сов или дней.
20.4. Когда UDP оказывается предпочтительнее TCP 591 Теперь мы перечислим свойства TCP, отсутствующие в UDP. Это означает, что приложение должно само предоставлять эти свойства, если они ему необхо- димы. Мы говорим «необходимы», потому что не все свойства требуются всем приложениям. Например, может не возникнуть необходимости повторно пере- давать потерянные сегменты для аудиоприложений реального времени, если при- емник способен интерполировать недостающие данные. Также для простых тран- закций «запрос-ответ» может не потребоваться оконное управление потоком, если два конца соединения заранее договорятся о размерах наибол ьшего запроса и о гве га. Положительные подтверждения, повторная передача потерянных пакетов, обнаружение дубликатов и упорядочивание пакетов, порядок следования ко- торых был изменен сетью. TCP подтверждает получение всех данных, позво- ляя обнаруживать потерянные пакеты. Реализация этих двух свойств требует, чтобы каждый сегмент дэдшых TCP содержал порядковый номер, по которо- му можно впоследствии проверить получение данного сегмента. Требуется также, чтобы TCP прогнозировал значение тайм-аута повторной передачи для соединения и чтобы это значение последовательно обновлялось по мере изме- нения сетевого трафика на обоих концах сети. Оконное управление потоком. Принимающий TCP сообщает отправляюще- му, какое буферное пространство он выделил для приема данных, и отправля- ющий не может превышать этого значения, то есть количество неподтверж- денных данных отправителя никогда не может стать больше объявленного размера окна принимающего. Медленный старт и предотвращение перегрузки. Это форма управления по- током, осуществляемого отправителем, служащая для определения текущей пропускной способности сети и позволяющая контролировать ситуацию во время переполнения сети. Все современные ТСР-прнложения должны под- держивать эти два свойства, и опыт (накопленный еще до того, как эти алго- ритмы были реализованы в конце 80-х) показывает, что протоколы, не отсту- пающие в случае переполнения, лишь усугубляют его (см., например, [43]). Суммируя вышесказанное, мы можем сформулировать следующие рекомен- дации: UDP должен использоваться для приложений широковещательной и много- адресной передачи. Если требуется какая-либо разновидность защиты от оши- бок, то соответствующая функциональность должна быть добавлена клиен- там и серверам. Но приложения часто используют широковещательную или многоадресную передачу, когда некоторое (предположительно небольшое) количество ошибок (таких, как потеря аудио- или видеопакетов) допустимо. Имеются приложения многоадресной передачи, требующие надежной достав- ки (например, пересылка файлов при помоши многоадресной передачи), но в каждом конкретном случае мы должны решить, компенсируется ли выиг- рышем в производительности, получаемым за счет использования многоад- ресной передачи (отправка одного пакета N получателям вместо отправки N копий пакета через N соединений TCP), дополнительное усложнение при- ложения для обеспечения надежности соединений. UDP может использоваться для простых приложений «запрос-ответ», но тогда определение ошибок должно быть встроено в приложение. Минимально это
592 Глава 20. Дополнительные сведения о сокетах UDP означает включение подтверждений, тайм-аутов и повторных передач. Управ- ление потоком часто не является существенным для обеспечения надежнос- ти, если запросы и ответы имеют достаточно разумный размер. Мы приводим пример этих свойств в приложении UDP, представленном в разделе 20.5. Фак- торы, которые нужно учитывать, — эго частота соединения клиента и сервера (нужно решить, имеет ли смысл оставить уже установленное между ними со- единение TCP для будущих транзакций) и количество данных, которыми об- мениваются клиент и сервер (если в большинстве случаев при работе данного приложения требуется много пакетов, стоимость установления и разрыва со- единения TCP становится менее значимым фактором). UDP не следует использовать для передачи большого количества данных (на- пример, при передаче файлов). Причина в том, что оконное управление пото- ком, предотвращение переполнения и медленней старт, — все эти свойства должны быть встроены в приложение вместе со свойствами из предыдущего пункта. Это означает, что мы фактически заново изобретаем TCP для одного конкретного приложения. Нам следует оставить производителям заботу об улучшении производительности TCP и сконцентрировать свои усилия на са- мом приложении. Из этих правил есть исключения, в особенности для существующих приложе- ний. Например, TFTP использует UDP для передачи большого количества дан- ных. Для TFTP был выбран UDP, поскольку, во-первых, его реализация проще в отношении кода начальной загрузки (800 строк кода С для UDP в сравнении с 4500 строками для TCP, например в [105]), а во-вторых, TFTP используется только для начальной загрузки систем в локальной сети, а не для передачи боль- шого количества данных через глобальные сети. Однако при этом требуется, что- бы в TFTP были предусмотрены такие свойства, как собственное поле порядко- вого номера (для подтверждений), тайм-аут и возможность повторной передачи. NFS (Network File System — сетевая файловая система) является другим ис- ключением из правила: она также использует UDP для передачи большого количе- ства данных (хотя некоторые могут возразить, что в действительности это прило- жение типа «запрос-ответ», использующее запросы и ответы больших размеров). Отчасти зто можно объяснить исторически сложившимися обстоятельствами: в середине 80-х, когда была разработана эта система, реализации UDP были быс- трее, чем TCP, и система NFS использовалась только в локальных сетях, где потеря пакетов, как правило, происходит на несколько порядков реже, чем в глобальных сетях. Но как только в начале 90-х NFS начала использоваться в глобальных се- тях, а реализации TCP стали обгонять UDP в отношении производительности при передаче большого количества данных, была разработана версия 3 системы NFS для поддержки TCP. Теперь большинство производителей предоставляют NFS как для и TCP, так и для UDP. Аналогичные причины (большая ско- рость по сравнению с TCP в начале 80-х плюс преобладание локальных сетей над глобальными) привели к тому, что в DCE (Distributed Computing En- vironment — среда распределенных вычислений), применявшейся до появле- ния RPC (Remote Procedure Call — удаленный вызов процедур), сначала ис- пользовали UDP, а не TCP, хотя современные реализации поддерживают и UDP, и TCP.
20.5. Добавление надежности приложению UDP 593 У нас мог бы возникнуть соблазн сказать, что применение UDP сокращается, поскольку сегодня хорошие реализации TCP не уступают в скорости сетям и все меньше разработчиков готовы встраивать в приложения UDP функциональность, свойственную TCP. Но увеличение количества мультимедиа-приложений за по- следнее десятилетие приводит к повышению популярности UDP, поскольку их работа обычно подразумевает использование многоадресной передачи, требую- щей наличия UDP. 20.5. Добавление надежности приложению UDP Если мы хотим использовать UDP для приложения типа «запрос-ответ», как было отмечено в предыдущем разделе, мы должны добавить нашему клиенту две функ- циональных возможности: тайм-аут и повторную передачу, которые позволяют решать проблемы, возни- кающие в случае потери дейтаграмм; порядковые номера, позволяющие клиенту проверить, что ответ приходит на определенный запрос. Эти два свойства предусмотрены в большинстве существующих приложений UDP, использующих простую модель «запрос-ответ»: например, распознаватели DNS, агенты SNMP, TFTP и RPC. Мы не пытаемся использовать UDP для пере- дачи большого количества данных: наша цель — приложение, посылающее запрос и ожидающее ответа иа этот запрос. ПРИМЕЧАНИЕ-------------------------------------------------------- Использование дейтаграмм по определению не может быть надежным, следовательно, мы специально не называем данный сервис «надежным сервисом дейтаграмм». Дей- ствительно, термин «падежная дейтаграмма» — это оксюморон. Речь идет лишь о том, что приложение до некоторой степени обеспечивает надежность, добавляя соответству- ющие функциональные возможности «поверх» ненадежного сервиса дейтаграмм (UDP) ’ Добавление порядковых номеров осуществляется легко. Клиент подготавли- вает порядковый номер для каждого запроса, а сервер должен отразить этот но- мер обратно в своем ответе клиенту. Это позволяет клиенту проверить, что дан- ный ответ пришел на соответствующий запрос. Более старый метод реализации тайм-аутов и повторной передачи заключал- ся в отправке запроса и ожидании в течение N секунд. Если ответ не приходил, осуществлялась повторная передача и снова на ожидание ответа отводилось N секунд. Если это повторялось несколько раз, отправка запроса прекращалась. Это так называемый линейный таймер повторной передачи (на рис. 6.8 [94] пока- зан пример клиента TFTP, использующего эту технологию. Многие клиенты TFTP до сих пор пользуются этим методом). Проблема при использовании этой технологии состоит в том, что количество времени, в течение которого дейтаграмма совершает цикл в объединенной сети, может варьироваться от долей секунд в локальной сети до нескольких секунд в глобальной. Факторами, влияющими на время обращения (RTT), являются рас-
594 Глава 20. Дополнительные сведения о сокетах UDP стояние, скорость сети и переполнение. Кроме того, RTT между клиентом и сер- вером может значительно меняться со временем при изменении условий в сети. Нам придется использовать тайм-ауты и алгоритм повторной передачи, который учитывает действительное (измеряемое) значение периода RTT и изменения RTT с течением времени. В этой области ведется большая работа, в основном направ- ленная на TCP, но некоторые идеи применимы к любым сетевым приложениям. Мы хотим вычислить тайм-аут повторной передачи (RTO), чтобы использо- вать его при отправке каждого пакета. Для того чтобы выполнить это вычисле- ние, мы измеряем RTT — действительное время обращения для пакета. Каждый раз, измеряя RTT, мы обновляем два статистических показателя: srtt — сглажен- ную оценку RTT, и rttvar — сглаженную оценку среднего отклонения. Послед- няя является лишь приближенной оценкой стандартного отклонения, но ее легче вычислять, поскольку для этого не требуется извлечения квадратного корня. Имея эти два показателя, мы вычисляем RTO как сумму srtt и rttvar, умноженного на четыре. В [43] даются все необходимые подробности этих вычислений, которые мы можем свести к четырем следующим уравнениям: delta-measuredRTT - srtt srtt «-srtt + gxdelta rttvar «-rttvar+ h(|delta| - rttvar) RTO - srtt + 4 x rttvar del ta — это разность между измеренным RTT и текущим сглаженным показателем RTT (srtt). g — это приращение, применяемое к показателю RTT, равное 1/8. h — это приращение, применяемое к сглаженному показателю среднего отклонения, равное]. ПРИМЕЧАНИЕ -------------------------------------------------------------- Два приращения и множитель 4 в вычислении RTO специально выражены степенями числа 2 и могут быть вычислены с использованием операций сдвига вместо деления и умножения. На самом деле реализация ядра TCP (см. раздел 25.7 [ 105]) для ускоре- ния вычислений обычно выполняется с помощью арифметики с фиксированной точ- кой, но мы для простоты используем в нашем коде вычисления с плавающей точкой. Другой важный момент, отмеченный в [43], заключается в том, что по истече- нии времени таймера повторной передачи для следующего RTO должно исполь- зоваться экспоненциальное смещение (exponential backoff). Например, если наше первое значение RTO равно 2 секундам и за это время ответа не получено, следу- ющее значение RTO будет равно 4 секундам. Если ответ все еще не последовал, следующее значение RTO будет 8 секунд, затем 16 и т. д. Алгоритмы Джекобсона (Jacobson) реализуют вычисление RTO при измере- нии RTT и увеличение RTO при повторной передаче. Однако, когда нам необхо- димо ретранслировать пакет и затем получить ответ, возникает проблема. Это называется проблемой неопределенности повторной передачи (retransmission ambi- guity problem). На рис. 20.2 показаны три возможных сценария, при которых ис- текает время ожидания повторной передачи: ® запрос потерян; ж ответ потерян; значение RTO слишком мало.
20.5. Добавление надежности приложению UDP 595 Когда клиент получает ответ на запрос, отправленный повторно, он не может сказать, какому из запросов соответствует ответ. На рисунке, изображенном спра- ва, ответ соответствует начальному запросу, в то время как на двух других рисун- ках ответ соответствуют второму запросу. Рис. 20.2. Три сценария, возможных при истечении времени таймера повторной передачи Сервер Клиент Твйм-аут RTO слишком мал Алгоритм Карна (Karn) [49] обрабатывает этот сценарий в соответствии со следующими правилами, применяемыми в любом случае, когда ответ получен на запрос, отправленный более одного раза. Если RTT измерялось, не используйте его для обновления оценочных значе- ний, так как мы не знаем, какому запросу соответствует ответ. Поскольку ответ пришел до того как истекло время нашего таймера повтор- ной передачи, используйте для следующего пакета текущее значение RTO. Только когда мы получим ответ на запрос, который не был передан повторно, мы изменяем значение RTT и снова вычисляем RTO. При написании наших функций RTT применить алгоритм Карна несложно, однако существует и более изящное решение. Оно связано с дополнительными возможностями TCP для сетей высокой вместимости (long fat pipe), то есть се- тей, обладающих либо широкой полосой пропускания, либо большим значением RTT, либо обоими этими свойствами (RFC 1323 [45]). Кроме добавления поряд- кового номера к началу каждого запроса, который сервер должен отразить, мы добавляем отметку времени, которую сервер также должен отразить. Каждый раз, отправляя запрос, мы сохраняем в этой отметке значение текущего времени. Когда приходит ответ, мы вычисляем величину RTT для этого пакета как теку- щее время минус значение отметки времени, отраженной сервером в своем отве- те. Поскольку каждый запрос несет отметку времени, отражаемую сервером, мы можем вычислить RTT для каждого ответа, который мы получаем. Теперь нет никакой неопределенности. Более того, поскольку сервер только отражает отметку времени клиента, клиент может использовать для отметок времени любые удоб- ные единицы, и при этом не требуется, чтобы клиент и сервер синхронизировали часы.
596 Глава 20. Дополнительные сведения о сокетах UDP Пример Свяжем теперь всю эту информацию воедино в примере. Мы запускаем фун цию ma in нашего клиента UDP, представленного в листинге 8.3, и изменяем тот: ко номер порта с SERV_PORT на 7 (стандартный эхо-сервер, см. табл. 2.1). В листинге 20.4 показана функция dg__cl т. Единственное изменение по срг нению с листингом 8 4 состоит в замене вызовов функций sendto и recvfrom вые вом нашей новой функции dg_send_recv Перед тем как представить функцию dg_send_recv и наши функции RTT, кот рые она вызывает, мы показываем в листинге 20 5 нашу схему реализации фун циональных свойств, повышающих надежность клиента UDP Все функции, име которых начинаются с rtt_, описаны далее Листинг 20.4. Функция dg_cli, вызывающая нашу функцию dg_send_recv //rtt/dg_cli с 1 include unp h' 2 ssize_t Dg_send_recv(int const void * size__t void * size_t 3 const SA * socklen_t) 4 void 5 dg_cli(FILE *fp int sockfd const SA *pservaddr socklen_t servlen) 6 { 7 ssize_t n 8 char sendlme[MAXLINE] recvline[MAXLINE + 1] 9 while (Fgets(sendline MAXLINE fp) '= NULL) { 10 n = Dg_send_recv(sockfd sendline strlen(sendline) 11 t. recvline MAXLINE pservaddr servlen) 12 recvline[n] = 0 /* завершающий нуль */ 13 Fputs(recvline stdout) 14 } 15 } Листинг 20.5. Схема функций RTT и последовательность их вызова static sigj(np_buf jmpbuf ( запрос signal(SIGALRM, s1g_alwn); /* устанавливаем обработчик сигнала */ rtt newpackO; /t* инициализируем значение счетчика rexnit ,нулрн */ sendagain sendto() alam(rtt_start()). /* задаем аргумент функции alarm равным значению RTO I*/ if (sigsetjmp(jmpbuf 1) <- 0) { if (rtt_timeout()) /* удваиваем RTO обновляем оценочные значения */ отказываемся от дальнейших попыток goto sendagain /* повторная передача */
20 5 Добавление надежности приложению UDP 591 } while (неправильный порядковый номер) alarm(C) /* отключаем функцию alarm */ rtt_stop() /* вычисляем RTT и обновляем оценочные значения */ обрабатываем ответ } void sig_alrm(int signo) { siglongjmp(jmpbuf 1) ) Если приходит ответ, но его порядковый номер отличается от предполагаемо- го, мы снова вызываем функцию recvfrom, но не отправляем снова тот же запрос и не перезапускаем работающий таймер повторной передачи Обратите внима- ние, что в крайнем правом случае на рис. 20 2 последний ответ, полученный на отправленный повторно запрос, будет находиться в приемном буфере сокета дс тех пор, пока клиент не решит отправить следующий запрос (и получить на неге ответ) Это нормально, поскольку клиент прочитает этот ответ, отметит, что по- рядковый номер отличается от предполагаемого, проигнорирует ответ и снова вызовет функцию recvfrom Мы вызываем функции sigsetjmp и siglongjmp, чтобы предотвратить возник- новение ситуации гонок с сигналом SIGALRM, который мы описали в разделе 18 5 В листинге 20 6 показана первая часть нашей функции dg_send_recv Листинг 20.6. Функция dg_send_recv: первая часть //rtt/dg_send_recv с 1 ^include 'unprtt h' 2 ^include <setjmp h> 3 #defme RTT_DEB(JG 4 static struct rtt_info rttinfo 5 static int rttimt = 0 6 static struct msghdr msgsend msgrecv /* предполагается что обе структуры инициализированы нулем .*/ 7 static struct hdr { 8 uint32_t seq 7* порядковый номер */ 9 uint32_t ts /* отметка времени при отправке */ 10 } sendhdr recvhdr 11 static void sig_alrm(mt signo) 12 static sigjmpjbuf jmpbuf 13 ssize_t 14 dg_send_recv(int fd const void *outbuff size_t outbytes. 15 void *inbuff size_t mbytes 16 const SA *destaddr socklen t destlen) II { 18 ssize_t n
598 Глава20 Дополнительные сведенияосокетах UDP Листинг 20.6 (продолжение) 21 rtt_imt(&rttinfo) /* первый вызов */ 22 rttimt - 1 23 rtt_d_flag = 1 24 } 25 sendhdr seq++ 26 msgsend msg_name = destaddr 27 msgsend msg_namelen = destlen 2B msgsend msg_iov = novsend 29 msgsend msg_iovlen - 2 30 iovsend[0] iov_base = Ssendhdr 31 iovsend[0] iov_len = sizeoftstruct hdr) 32 iovsend[l] icvbase = outbuff 33 iovsend[l] iov_len = cutbytes 34 msgrecv msg_name = NULL 35 msgrecv msg_namelen = 0 36 msgrecv msg_iov = lovrecv 37 msgrecv msg_iovlen = 2 3B iovrecv[0] iov_base = &recvhdr 39 iovrecv[0] iov_len = sizeoftstruct hdr) 40 iovrecv[l] iov_base = inbuff 41 iovrecv[l] iov_len = mbytes 1 5 Мы включаем новый заголовочный файл unprtt h, показанный в листинге 20 8, который определяет структуру rtt_i nfo, содержащую информацию RTT для кли- ента Мы определяем одну из этих структур и ряд других переменных Определение структур msghdr и структуры hdr 6 10 Мы хотим скрыть от вызывающего процесса, что добавляем порядковый номер и отметку времени в начало каждого пакета Проще всего использовать для этого функцию wri tev, записав свой заголовок (структура hdr), за которым следуют дан- ные вызывающего процесса, в виде одной дейтаграммы UDP Вспомните, что ре- зультатом выполнения функции writev на дейтаграммном сокете является отправ- ка одной дейтаграммы Это проще, чем заставлять вызывающий процесс выделять для нас место в начале буфера, а также быстрее, чем копировать наш заголовок и данные вызывающего процесса в один буфер (под который мы должны выде- лить память) для каждой функции sendto Но поскольку мы работаем с UDP и нам необходимо задать адрес получателя, следует использовать возможности предо- ставляемые структурой iovec функций sendmsg и recvmsg и отсутствующие в фун- кциях sendto и recvfrom Вспомните из раздела 13 5, что в некоторых системах доступна более новая структура msghdr, включающая вспомогательные данные (msg_control), тогда как в более старых системах вместо них применяются эле- менты msg_accright (так называемые права доступа — access rights), расположен- ные в конце структуры Чтобы избежать усложнения кода директивами #i fdef для обработки этих различий, мы объявляем две структуры msghdr как static При этом они инициализируются только нулевыми битами, а затем неиспользован- ные элементы в конце структур просто игнорируются Инициализация при первом вызове 20-24 При первом вызове нашей функции мы вызываем функцию rtt imt
20 5 Добавление надежности приложению UDP 599 Заполнение структур msghdr 25-41 Мы заполняем две структуры msghdr, используемые для ввода и вывода Для данного пакета мы увеличиваем на единицу порядковый номер отправки, но не устанавливаем отметку времени отправки, пока пакет не будет отправлен (по- скольку он может отправляться повторно, а для каждой повторной передачи тре- буется текущая отметка времени) Вторая часть функции вместе с обработчиком сигнала sig_al rm показана в ли- стинге 20 7 Листинг 20.7. Функция dg_send_recv вторая половина //rtt/dg_send_recv с 42 Signal(SIGALRM sig_alrm) 43 rtt_newpack(&rttinfo) /* инициализируем для этого пакета */ 44 sendagain 45 sendhdr ts = rtt_ts(&rttinfo) 46 Sendmsgtfd tasgsend 0) 47 alarm(rtt_start(&rttinfo)) /* вычисляем величину тайм аута и запускаем таймер */ 48 if (sigsetjmp(jmpbuf 1) '= 0) { 49 if (rtt_timeout(&rttinfo) < 0) { 50 err_msg( dg_send_recv no response from server giving up ) 51 rttimt = 0 /* повторная инициализация для следующего ВЫзойа */> 52 errno = ETIMEDOUT 53 return ( 1) 54 } 55 goto sendagain 56 } 57 do { 58 n = Recvmsg(fd tasgrecv 0) 59 } while (n < sizeoftstruct hdr) || recvhdr seq '= sendhdr seq). 60 alarm(O) /* останавливаем таймер SIGALRM */ 61 /* вычисляем и записываем новое значение оценки RTT */ 62 rtt_stop(&rttinfo rtt_ts(&rttinfo) recvhdr ts) 63 return (n sizeoftstruct hdr)) /* возвращаем размер полученной дейтаграммы 64 } 65 static void 66 sig_alrm(int signo) 67 { 68 siglongjmp(jmpbuf 1) 69 } Установка обработчика сигналов 42 43 Для сигнала SIGALRM устанавливается обработчик сигналов, а функция rtt_newpack сбрасывает счетчик повторных передач в нуль Отправка дейтаграммы 45 47 Функция rtt_ts получает текущую отметку времени Отметка времени хранит- ся в структуре hdr, которая добавляется к данным пользователя Одиночная дей-
600 Глава 20. Дополнительные сведения о сокетах UDP таграмма UDP отправляется функцией sendmsg. Функция rtt_start возвращает количество секунд для этого тайм-аута, а сигнал SIGALRM контролируется функ- цией alarm. Установка буфера перехода 48 Мы устанавливаем буфер перехода для нашего обработчика сигналов с помощью функции sigsetjmp. Мы ждем прихода следующей дейтаграммы, вызывая функ- цию recvmsg. (Совместное использование функций sigsetjmp и siglongjmp вместе с сигналом SIGALRM мы обсуждали применительно к листингу 18.5.) Если время тай- мера истекает, функция sigsetjmp возвращает 1. Обработка тайм-аута 9-55 Когда возникает тайм-аут, функция rtt_timeout вычисляет следующее значе- ние RTO (используя экспоненциальное смещение) и возвращает -1, если нужно прекратить попытки передачи дейтаграммы, или 0, если нужно выполнить оче- редную повторную передачу. Когда мы прекращаем попытки, мы присваиваем переменной еггпо значение ETIMEDOUT и возвращаемся в вызывающую функцию. Вызов функции recvmsg, сравнение порядковых номеров > 7-59 Мы ждем прихода дейтаграммы, вызывая функцию recvmsg. Длина полученной дейтаграммы не должна быть меньше размера структуры hdr, а ее порядковый номер должен совпадать с порядковым номером запроса, ответом па который предположительно является эта дейтаграмма. Если при сравнении хотя бы одно из этих условий не выполняется, функция recvmsg вызывается снова. Выключение таймера и обновление показателей RTT > 0-62 Когда приходит ожидаемый ответ, функция alarm отключается, а функция rtt_stop обновляет оценочное значение RTT. Функция rtt ts возвращает теку- щую отметку времени, и отметка времени из полученной дейтаграммы вычитает- ся из текущей отметки, что дает в результате RTT. Обработчик сигнала SIGALRM > 5-69 Вызывается функция siglongjmp, результатом выполнения которой является то, что функция sigsetjmp в dg_send_recv возвращает 1. Теперь мы рассмотрим различные функции RTT, которые вызывались нашей функцией dg_send_recv. В листинге 20.8 показан заголовочный файл unprtt. h. Листинг 20.8. Заголовочный файл unprtt.h //lib/unprtt h 1 #ifndef __unp_rtt_h 2 #define _unp_rtt_h 3 #include "unp h” 4 struct rtt_info { 5 float rtt_rtt. /* самое последнее измеренное значение RTT. в секундах */ 6 float rtt_srtt. /* сглаженная оценка RTT. в секундах *7 7 float rtt_rttvar. /* сглаженные средние значения отклонений в секундах *7 8 float rtt_rto /* текущее используемое значение RTO в секундах */ 9 int rtt_nrexmt. 7* количество повторных передач 0 1.2 */
20,5. Добавление надежности приложению UDP 6Q1 10 uint32_t rtt_base. /* количество секунд, прошедших после 1 1 1970 в начале */ И }• 12 #define RTT_RXTMIN 2 /* минимальное значение тайм-аута для повторной передачи, в секундах */ 13 #define RTT_RXTMAX 60 /* максимальное значение тайм-аута для повторной передачи, в секундах */ 14 #define RTT_MAXNREXMT 3 /* максимально допустимое количество повторных передач одной дейтаграммы */ 15 /* прототипы функций */ 16 void rtt_debug(struct rtt_info *). 17 void rtt_imt(struct rtt_info *). 18 void rtt_newpack(struct rtt_info *). 19 int rtt_start(struct rtt_info *) 20 void rtt_stop(struct rtt_info *. uint32_t): 21 int rtt_timeout(struct rtt_info *). 22 uint32_t rtt_ts(struct rtt_info *). 23 extern int rtt_d_flag, /* может быть ненулевым при наличии дополнительной информации */ 24 #endif /* _unp_rtt_h */ Структура rttjnfo -11 Эта структура содержит переменные, необходимые для того, чтобы определить время передачи пакетов между клиентом и сервером. Первые четыре перемен- ных взяты из уравнений, приведенных в начале этого раздела. 2-14 Эти константы определяют минимальный и максимальный тайм-ауты повтор- ной передачи и максимальное число возможных повторных передач. В листинге 20.9 показан макрос RTT_RTOCALC и первые две из четырех функций RTT. Листинг 20.9. Макрос RTT_RTOCALC, функции rttjninmax и rttjnit //lib/rtt.c 1 #include “unprtt.h" 2 int rtt_d_flag = 0: /* отладочный флаг, может быть установлен в ненулевое значение вызывающим процессом */ 3 /* Вычисление значения RT0 на основе текущих значений: 4 * сглаженное оценочное значение RTT + 4 х сглаженная 5 * величина отклонения. 6 */ 7 #define RTT_RTOCALC(ptr) ((ptr)->rtt_srtt + (40* (ptr)->rtt_rttvar)) 8 static float 9 rtt_minmax(float rto) 10 { 11 if (rto < RTT_RXTMIN) 12 rto - RTT RXTMIN: 13 else If (rto > RTT RXTMAX) 14 rto « RTT_RXTMAX? 15 return (rto"): 16 } 17 void
602 Глава 20. Дополнительные сведенияосокетах UDP 18 rtt init(struct rtt_info *ptr) 19 { 20 struct timeval tv. 21 Gettimeofday(&tv. NULL) 22 ptr->rtt_base = tv tv_sec. /* количество секунд прошедших cli 1970 в начале */ 23 ptr->rtt_rtt = О 24 ptr->rtt_srtt = О 25 ptr->rtt_rttvar = 0 75. 26 ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr)) 27 /* первое RTO (sett + (4 * rttvar)) = 3 секунды */ 28 } J-7 Макрос вычисляет RTO как сумму оценочной величины RTT и оценочной ве- личины среднего отклонения, умноженной на четыре. -16 Функция rttjmnmax проверяет, что RTO находится между верхним и нижним пределами, заданными в заголовочном файле unprtt h. 7-28 Функция rtt_imt вызывается функцией dg_send_recv при первой отправке па- кета. Функция gettimeofday возвращает текущее время и дату в той же структуре timeval, которую мы видели в функции sei ect (см. раздел 6.3). Мы сохраняем толь- ко текущее количество секунд с момента начала эпохи Unix, то есть с 00:00.00 1 января 1970 года (UTC). Измеряемое значение RTT обнуляется, а сглаженная оценка RTT и среднее отклонение принимают соответственно значение 0 и 0,75, в результате чего начальное RTO равно 3 секундам (4x0,75). ПРИМЕЧАНИЕ--------------------------------------------------------------------------- Функция gettimeofday еще не является частью Posix. 1g и может не поддерживаться некоторыми реализациями. Она требуется в Unix 98 Мы используем ее, поскольку она общеупотребительна и на многих узлах предоставляет разрешение до микросекунд. Альтернативной функцией Posix. 1g является функция times, но ее разрешение зависит от «частоты тиканья», то есть от разрешающей способности часов, используемых яд- ром: обычно 100 отсчетов в секунду соответствую! разрешению 10 миллисекунд. В листинге 20.10 показаны следующие три функции RTT. Листинг 20.10. Функции rtt_ts, rtt_newpack и rtt_start //lib/rtt с 34 uint32_t 35 rtt ts(struct rttjnfo *ptr) 36 { 37 uint32_t ts. 38 struct~timeval tv. 39 Gettimeofday(&tv NULL), 40 ts - ((tv tv_sec - ptr->rtt_base) * 1000) + (tv.tv_usec / 1000): 41 return (ts). 42 } 43 void 44 rtt_newpack(struct rtt_info *ptr) 45 { 46 ptr->rtt_nrexmt = 0. 47 } 48 int
20,5. Добавление надежности приложению UDP 603 49 rtt_start(struct rtt_info *ptr) 50 { 51 return ((int) (ptr->rtt_rto + 0 5)) /* округляем float до int */ 52 /* возвращенное значение может быть использовано как аргумент alarm(rtt_start(&foo)) */ 53 } 4-42 Функция rtt_ts возвращает текущую отметку времени для вызывающего про- цесса, которая должна содержаться в отправляемой дейтаграмме в виде 32-раз- рядного целого числа без знака. Мы получаем текущее время и дату из функции gettimeofday и затем вычитаем число секунд в момент вызова функции rtt_init (значение, хранящееся в элементе rtt_base структуры rtt_info). Мы преобразуем это значение в миллисекунды, а также преобразуем в миллисекунды значение, возвращаемое функцией gettimeofday в микросекундах. Тогда отметка времени является суммой этих двух значений в миллисекундах. Разница между двумя вызовами функции rtt_ts заключается в числе милли- секунд между этими двумя вызовами. Но мы храним отметки времени в 32-раз- рядном целом числе без знака, а не в структуре timeval 3-47 Функция rtt_newpack просто обнуляет счетчик повторных передач. Эта функ- ция должна вызываться всегда, когда новый пакет отправляется в первый раз. 1-53 Функция rtt_start возвращает текущее значение RTO в миллисекундах. Возвра- щаемое значение затем может использоваться в качестве аргумента функции alarm. Функция rtt_stop, показанная в листинге 20.11, вызывается после получе- ния ответа для обновления оценочного значения RTT и вычисления нового зна- чения RTO. Листинг 20.11. Функция rtt_stop: обновление показателей RTT и вычисление нового //lib/rtt с 62 void 63 rtt_stop(struct rtt_info *ptr, uint32_t ms) 64 { 65 double delta. 66 ptr->rtt_rtt - ms / 1000 0; /* измеренное значение RTT в секундах */ 67 /* 68 * Обновляем оценочные значения RTT среднего отклонения RTT 69 * см статью Джекобсона (Jacobson). SIGCOMM'88. приложение А 70 * Здесь мы для простоты используем числа с плавающей точкой 71 */ 72 delta - ptr->rtt_rtt - ptr->rtt_srtt 73 ptr->rtt_srtt += delta /8. /* g - 1/8 */ 74 if (delta < 0 0) 75 delta = -delta /* |delta| */ 76 ptr->rtt_rttvar += (delta - ptr->rtt_rttvar) /4: /*h • 1/4 */ 77 ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr)). 78 } -78 Вторым аргументом является измеренное RTT, йолученное вызывающим про- цессом при вычитании полученной в ответе отметки времени из текущей (функ-
604 Глава 20. Дополнительные сведения о сокетах UDP ция rtt_ts). Затем применяются уравнения, приведенные в начале этого раздела, и записываются новые значения переменных rtt_srtt, rtt_rttvar и rtt_rto. Последняя функция, rtt_timeout, показана в листинге 20.12. Эта функция вы- зывается, когда истекает время таймера повторных передач. Листинг 20.12. Функция rtt_timeout: применение экспоненциального смещения //lib/rtt с 83 int 84 rtt_timeout(struct rttjnfo *ptr) 85 { 86 ptr->rtt_rto *= 2. /* следующее значение RTO */ 87 if (++ptr->rtt_nrexmt > RTT_MAXNREXMT) 88 return (-1) /* закончилось время, отпущенное на попытки отправить этот пакет */ 89 return (0). 90 } 86 Текущее значение RTO удваивается — в этом и заключается экспоненциаль- ное смещение. 57-89 Если мы достигли максимально возможного количества повторных передач, возвращается значение -1, указывающее вызывающему процессу, что дальней- шие попытки передачи должны прекратиться. В противном случае возвращает- ся 0. В нашем примере клиент соединялся дважды с двумя различными эхо-серве- рами в Интернете утром рабочего дня. Каждому серверу было отправлено по 500 строк. По пути к первому серверу было потеряно 8 пакетов, по пути ко второ- му — 16. Один из потерянных шестнадцати пакетов, предназначенных второму серверу, был потерян дважды, то есть пакет пришлось дважды передавать повтор- но, прежде чем был получен ответ. Все остальные потерянные пакеты пришлось передать повторно только один раз. Мы могли убедиться, что эти пакеты были действительно потеряны, посмотрев на выведенные порядковые номера каждого из полученных пакетов. Если пакет лишь опоздал, но не был потерян, после по- вторной передачи клиент получает два ответа: соответствующий запоздавшему первому пакету и повторно переданному. Обратите внимание, что у нас нет воз- можности определить, что именно было потеряно (и привело к необходимости повторной передачи клиентского запроса) — сам клиентский запрос или же от- вет сервера, высланный после получения такого запроса. ПРИМЕЧАНИЕ--------------------------------------------------------------------- Для первого издания этой книги автор написал для проверки этого клиента сервер UDP, который случайным образом игнорировал пакеты. Теперь он не используется. Все, что нам нужно сделать — соединить клиент с сервером через Интернет, и тогда нам почти гарантирована потеря некоторых пакетов! 20.6. Связывание с адресами интерфейсов Одно из типичных применений функции get_1f1_info связано с приложениями UDP, которым нужно выполнять мониторинг всех интерфейсов на узле, чтобы знать, когда и на какой интерфейс приходит дейтаграмма. Это позволяет полу-
20.6. Связывание с адресами интерфейсов 605 чающей программе узнавать адрес получателя дейтаграммы UDP, так как имен- но по этому адресу определяется сокет, на который доставляется дейтаграмма, даже если узел не поддерживает параметр сокета PI_RECVDSTADDR. ПРИМЕЧАНИЕ----------------------------------------------------------- Вспомните наше обсуждение в конце раздела 20.2. Если узел использует более распро- страненную модель системы с гибкой привязкой (см. раздел 8.8), IP-адрес получателя может отличаться от IP-адреса принимающего интерфейса. Все, что мы в этом случае можем определить, — это адрес получат еля дейт аграммы, который не обязательно дол- жен быть адресом, присвоенным принимающему интерфейсу. Чтобы определить при- нимающий интерфейс, требуется парамегр сокета IP RECVIF или IPV6 PKTINFO. Ранее в нашем примере SNTP (см. раздел 19.11) мы продемоистировали связывание всех адресов интерфейса. В листинге 20.13 показана первая часть примера применения этой техноло- гии к эхо-серверу UDP, который связывается со всеми адресами направленной передачи, широковещательной передачи и, наконец, с универсальными адре- сами. Листинг 20.13. Первая часть сервера UDP, который с помощью функции bind связывается со всеми адресами //advio/udpserv03 с 1 #include "unpifi h" 2 void mydg_echo(int. SA * socklen_t. SA *). 3 int 4 main(int argc. char **argv) 5 { 6 int sockfd 7 const int on = 1. 8 pid_t pid. 9 struct ifi_info *ifi. *ifihead. 10 struct sockaddr_in *sa. cliaddr. wildaddr. 11 for (ifihead = ifi = Get_ifi_info(AF_INET. 1): 12 ifi i= NULL ifi = ifi->ifi_next) { 13 /* связываем направленный адрес */ 14 sockfd = Socket(AF_INET SOCK_DGRAM 0) 15 Setsockopt(sockfd. SOL_SOCKET. SO_REUSEADDR. &on. Sizeof(on)); 16 sa = (struct sockaddr_in *) ifi->ifi_addr 17 sa->sin_family = AF_INET, 18 sa->sin_port = rtons(SERV_PORT). 19 Bind(sockfd. (SA *) sa. sizeof(*sa)l. 20 printfCbound £s\en" Sock_ntop((SA *) sa. sizeof(*sa))): 21 if ( (pid = ForkO) == 0) { /* дочерний процесс */ 22 mydg_echo(sockfd (SA*) &cliaddr sizeof(cliaddr). (SA*) sa): 23 exit(O). /* не выполняется */ 24 }
606 Глава 20. Дополнительные сведения о сокетах UDP Вызов функции getjfijnfo для получения информации об интерфейсе 1-12 Функция get_i f i_i nfo получает все адреса IPv4, включая дополнительные (псев- донимы), для всех интерфейсов. Затем в программе выполняется цикл по всем возвращенным структурам ifijnfo. Создание сокета UDP и связывание адреса направленной передачи 3-20 Создается сокет UDP, и с ним связывается адрес направленной передачи. Мы также устанавливаем параметр сокета SO_REUSEADDR, поскольку мы связываем один и тот же порт (параметр SERV_PORT) для всех IP-адресов. ПРИМЕЧАНИЕ---------------------------------------------------- Не все реализации требуют, чтобы был установлен этот параметр сокета. Например, Беркли-реализацпи нс требуют этого параметра и позволяют с помощью функции bind связать уже связанный порт, если новый связываемый IP-адрес пе является универ- сальным адресом и отличается от всех IP-адресов, уже связанных с портом. Однако Solaris 2.5 для успешного связывания с одним и тем же портом вгорого адреса направ- ленной передачи требует установки этого параметра. Вызов функции fork и порождение дочернего процесса для данного адреса 11-24 Вызывается функция fork, порождающая дочерний процесс. В этом дочернем процессе вызывается функция mydg_echo, которая ждет прибытия любой дейта- граммы на сокет и отсылает ее обратно отправителю. В листинге 20.14 показана следующая часть функции main, которая обрабаты- вает широковещательные адреса. Листинг 20.14. Вторая часть сервера UDP, который с помощью функции bind связывается со всеми адресами //advio/udpserv03 с 25 if (lfl->ifl_flags & IFF_BROADCAST) { 26 /* пытаемся связать широковещательный адрес */ 27 sockfd = Socket(AF_INET. SOCK_DGRAM. 0). 28 Setsockopt(sockfd. SOL_SOCKET. SO_REUSEADDR &on. stzeof(on)); 29 sa = (struct sockaddr_in *) ifi->ifi_brdaddr. 30 sa->sin_family = AFJNET. 31 sa->sin_port = htons(SERV_PORT). 32 if (bind(sockfd (SA *) sa. sizeof(*sa)) < 0) { 33 if (errno == EADDRINUSE) { 34 printfCEADDRINUSE Ks\en". 35 Sock_ntop((SA *) sa. sizeof(*sa))). 36 Close(sockfd). 37 continue. 38 } else 39 err_sys(''bind error for ^s" 40 Sock_ntop((SA *) sa. sizeof(*sa))). 41 } 42 printfCbound ^s\en”. Sock_ntop((SA *) sa. sizeof(*sa))): 43 if ( (pid = ForkO) == 0) { /* дочерний процесс */
20.6. Связывание с адресами интерфейсов 607 44 mydg_eclio(sockfd. (SA *) &cliaddr. sizeof(cliaddr). 45 (SA *) sa). 46 exit(O): /* не выполняется */ 47 } 48 } 49 } Связывание с широковещательными адресами 5-42 Если интерфейс поддерживает широковещательную передачу, создается сокет UDP и с ним связывается широковещательный адрес. На этот раз мы позволим функции Ьт nd выполниться неудачно с ошибкой EADDRINUSE, поскольку если у ин- терфейса имеется несколько дополнительных адресов (псевдонимов) в одной подсети, то каждый из различных адресов направленной передачи будет иметь один и тот же широковещательный адрес. Подобный пример приведен после ли- стинга 16.3. В этом сценарии мы предполагаем, что успешно выполнится только первая функция bi nd. Порождение дочернего процесса 3-47 Порождается дочерний процесс, и он вызывает функцию mydg echo. Заключительная часть функции main показана в листинге 20.15. В этом коде при помощи функции bi nd происходит связывание с универсальным адресом для обработки любого адреса получателя, отличного от адресов направленной и ши- роковещательной передачи, которые уже связаны. На этот сокет будут прихо- дить только дейтаграммы, предназначенные для ограниченного широковещатель- ного адреса (255.255.255.255). Листинг 20.15. Заключительная часть сервера UDP, связывающегося со всеми адресами //advio/udpserv03 с 50 /* связываем универсальный адрес */ 51 sockfd = Socket(AF_INET SOCK_DGRAM 0) 52 Setsockopt(sockfd. SOL_SOCKET SO_REUSEADDR. &on. sizeof(on)): 53 bzero(&wildaddr. sizeof(wildaddr)), 54 wildaddr sin_fannly = AFJNET, 55 wildaddr sin_addr s_addr = htonl(INADDR_ANY). 56 wildaddr sin_port = htons(SERV_PORT) 57 Bind(sockfd. (SA *) &wildaddr. sizeof(wildaddr)). 58 prnrtf("bound ^s\en" Sock_ntop((SA *) &wildaddr. sizeof(wildaddr))). 59 if ( (pid = ForkO) == 0) { /* дочерний процесс */ 60 mydg_echo(sockfd (SA *) &cliaddr. sizeof(cliaddr). (SA *) sa). 61 exit(O), /* не выполняется*/ 62 } 63 exit(O), 64 } Создание сокета и связывание с универсальным адресом 0-62 Создается сокет UDP, устанавливается параметр сокета SO_REUSEADDR и проис- ходит связывание с универсальным IP-адресом. Порождается дочерний процесс, вызывающий функцию mydg_echo.
608 Глава 20. Дополнительныесведения о сокетах UDP Завершение работы функции main 63 Функция main завершается, и сервер продолжает выполнять работу, как и все порожденные дочерние процессы. Функция nydg_echo, которая выполняется всеми дочерними процессами, по- казана в листинге 20.16. Листинг 20.16. Функция mydg_echo 7/advio/udpserv03 с 65 void 66 rnydg_echo(int sockfd SA *pcliaddr socklen t clilen, SA *myaddr) 67 { 68 int n 69 char mesg[MAXLINE], 70 socklen_t len 71 for ( ) { 72 len = clilen. 73 n = Recvfrom(sockfd mesg MAXLINE 0 pcliaddr &len): 74 prmtfCchild Xd datagram from Xs' getpidO 75 Sock_ntop(pcliaddr. len)) 76 printfC. to Xs\en', Sock_ntop(myaddr clilen)). 77 Sendto!sockfd mesg n 0 pcliaddr len) 78 } 79 } Новый аргумент 65-66 Четвертым аргументом этой функции является IP-адрес, связанный с сокетом. Этот сокет должен получать только дейтаграммы, предназначенные для данного IP-адреса. Если IP-адрес является универсальным, сокет должен получать толь- ко те дейтаграммы, которые не подходят ни для какого другого сокета, связанно- го с тем же портом. Чтение дейтаграммы и отражение ответа 71-78 Дейтаграмма читается с помощью функции recvfrom и отправляется клиенту обратно с помощью функции sendto. Эта функция также выводит IP-адрес кли- ента и IP-адрес, который был связан с сокетом. Запустим эту программу на нашем узле bsdi после установки трех псевдони- мов (дополнительных адресов) для интерфейса efO Ethernet. Идентификаторы узлов для этих дополнительных адресов — 50, 51 и 52, но все они имеют один и тот же широковещательный адрес 206.62.226.63. bsdi X udpserv03 bound 206 62 226 66 9877 направленный адрес интерфейса weO bound 206 62 226 95 9877 широковещательный адрес интерфейса weO bound 206 62 226 35 9877 первичный направленный адрес интерфейса weO bound 206 62 226 63 9877 широковещательный адрес интерфейса efO bound 206 62 226 50 9877 первый EADDRINUSE 206 62 226 63 9877 bound 206 62 226 51 9877 второй EADDRINUSE 206 62 226 63 9877 bound 206 62 226 52 9877 третий EADDRINUSE 206 62 226 63 9877 псевдоним адреса направленной передачи псевдоним адреса направленной передачи псевдоним адреса направленной передачи
20,7, Параллельные серверы UDP 609 bound 127 0 0 1 9877 интерфейс закольцовки bound О О О 0 9877 универсальный адрес Обратите внимание, что три попытки связаться при помощи функции bind с широковещательными адресами оказываются неудачными, как мы и предпола- гали. Теперь при помощи утилиты netstat мы можем проверить, что все три соке- та связаны с указанным IP-адресом и портом: bsdi % netstat -па 1 grep 9877 udp 0 0 * 9877 * * udp 0 0 127 0 ( ) 1 9877 * * udp 0 0 206 62 226 52 9877 * * udp 0 0 206 62 226 51 9877 * * udp 0 0 206 62 226 50 9877 * * udp 0 0 206 62 226 63 9877 * * udp 0 0 206 62 226 35 9877 * * udp 0 0 206 62 226 95 9877 * * udp 0 0 206 62 226 66 9877 * * Следует отметить, что для простоты мы создаем по одному дочернему процес- су на сокет, хотя возможны другие варианты. Например, чтобы ограничить число процессов, программа может управлять всеми дескрипторами сама, используя функцию select и никогда не вызывая функцию fork. Проблема в данном случае будет заключаться в усложнении кода. Хотя использовать функцию select для всех дескрипторов несложно, нам придется осуществить некоторое сопоставле- ние каждого дескриптора связанному с ним IP-адресу (вероятно, с помощью мас- сива структур), чтобы иметь возможное гь вывести IP-адрес получателя после того, как на определенном сокете получена дейтаграмма. (Этот способ мы применяли в разделе 19.11.) Часто бывает проще использовать отдельный процесс или по- ток для каждой операции или дескриптора вместо мультиплексирования множе- ства различных операций или дескрипторов одним процессом. 20.7. Параллельные серверы UDP Большинство серверов UDP являются последовательными (iterative): сервер ждет запрос клиента, считывает запрос, обрабатывает его, отправляет обратно ответ и затем ждет следующий клиентский запрос. Но когда обработка запроса клиен- та занимает длительное время, желательно так или иначе совместить во времени обработку различных запросов. Определение «длительное время» означает, что другой клиент вынужден ждать в течение некоторого заметного для пего промежутка времени, пока обслужива- ется текущий клиент. Например, если два клиентских запроса приходят в тече- ние 10 миллисекунд и предоставление сервиса каждому клиенту занимает в сред- нем 5 секунд, то второй клиент будет вынужден ждать ответа около 10 секунд вместо 5 секунд, если запрос был принят в обработку сразу же по прибытии. В случае TCP проблема решается просто — требуется лишь породить дочер- ний процесс с помощью функции fork (или создать новый поток, что мы увидим в главе 23) и дать возможность дочернему процессу выполнять обработку нового клиента. При использовании TCP ситуация существенно упрощается за счет того, что каждое клиентское соединение уникально: пара сокетов TCP уникальна для каждого соединения. Но в случае с UDP мы вынуждены рассматривать два раз- личных типа серверов.
t> IU i лава zu. дополнительные сведения о сокетах uur 1. Первый тип — простой сервер UDP, который читает клиентский запрос, по- сылает ответ и затем завершает работу с клиентом. В этом сценарии сервер, читающий запрос клиента, может с помощью функции fork породить дочер- ний процесс и дать ему возможность обработать запрос. «Запрос», то есть со- держимое дейтаграммы и структура адреса сокета, содержащая адрес прото- кола клиента, передаются дочернему процессу в виде копии содержимого области памяти из функции fork. Затем дочерний процесс посылает свой от- вет непосредственно клиенту. 2. Второй тип — сервер UDP, обменивающийся множеством дейтаграмм с кли- ентом. Проблема здесь в том, что единственный номер порта сервера, извест- ный клиенту, — это номер заранее известного порта. Клиент посылает первую дейтаграмму своего запроса на этот порт, но как сервер сможет отличить по- следующие дейтаграммы этого клиента от запросов новых клиентов? Типич- ным решением этой проблемы для сервера будет создание .нового сокета для каждого клиента, связывание при помощи функции bi nd динамически назна- чаемого порта с этим сокетом и использование -того сокета для всех своих ответов. При этом требуется, чтобы клиент запомнил номер порта, с которого был отправлен первый ответ сервера, и отправлял последующие дейтаграммы на этот порт. Примером второго типа сервера UDP является сервер TFTP (Trivial File Trans- fer Protocol — простейший протокол передачи файлов). Передача файла с по- мощью TFTP обычно требует большого числа дейтаграмм (сотен или тысяч, в зависимости от размера файла), поскольку этот протокол отправляет в одной дейтаграмме только 512 байт. Клиент отправляет дейтаграмму на известный порт сервера (69), указывая, какой файл нужно отправить или получить. Сервер чита- ет запрос, но отправляет ответ с другого сокета, который он создает и связывает с динамически назначаемым портом. Все последующие дейтаграммы между кли- ентом и сервером используют для передачи этого файла новый сокет. Это позво- Сервер создает сокет, связывает его с заранее известным портом (69) при помощи функции bind, вызывает функцию recvfrom, блокируется до прибытия ответа от клиента, при помощи функции fork порождает дочерний процесс, затем вновь вызывает функцию recvfrom... Создает новый сокет, связывает его с динамически назначаемым портом (2134) при помощи функции bind, обрабатывает клиентский запрос, обменивается с клиентом дополнительными дейтаграммами через новый сокет Рис. 20.3. Процессы, происходящие на автономном параллельном UDP-сервере
20.7 Параллельные серверы UDP 611 ляет главному серверу TFTP продолжать обработку других клиентских запро- сов, приходящих на порг 69, в то время как происходит передача файла (возмож- но, в течение нескольких секунд или даже минут). Если мы рассмотрим автономный сервер TFTP (то есть случай, когда не ис- пользуется демон 1 netd), мы получим сценарий, показанный на рис. 20.3. Мы счи- таем, что динамически назначаемый порт, связанный дочерним процессом с его новым сокетом, — это порт 2134. Если используется демон i netd, сценарий включает еще один шаг. Вспомните из табл. 12.4, что большинство серверов UDP задают аргумент wai t - fl ад как wait. В описании, которое следовало за рис. 12.4, мы сказали, что при указанном значе- нии этого флага демон inetd приостанавливает выполнение функции select на сокете до завершения дочернего процесса, давая возможность этому дочернему процессу считать дейтаграмму, доставленную на сокет. На рис. 20.4 показаны все шаги. inetd I Сервер создает сокет и связывает его ' с заранее известным портом (69) । при помощи функции bind. > Когда прибывает запрос клиента TFTP, сервер при помощи функции fork порождает дочерний процесс, а затем отключает возможность вызова функции select на сокете, связанном ехес с портом UDP 69 Вызывает функцию recvfrom, при помощи функции fork порождает дочерний процесс, затем вызывает функцию exit Создает новый сокет, связывает его с динамически назначаемым портом (2134) при помощи функции bind, обрабатывает клиентский запрос, обменивается с клиентом дополнительными дейтаграммами через новый сокет Рис. 20.4. Параллельный сервер UDP, запущенный демоном inetd Сервер TFTP, являясь дочерним процессом функции i netd, вызывает функ- цию recvfrom и считывает клиентский запрос. Затем он с помощью функции fork Порождает собственный дочерний процесс, и этот дочерний процесс будет обра-
612 Глава 20. Дополнительные сведения о сокетах UDP батывать клиентский запрос. Затем сервер TFTP вызывает функцию exit, отправ- ляя демону 1 netd сигнал SIGCHLD, который, как мы сказали, указывает демону i netd снова вызвать функцию select на сокете, связанном с портом UDP 69. 20.8. Информация о пакете IPv6 IPv6 позволяет приложению определять до четырех характеристик исходящей дейтаграммы: 1. 1Р\’6-адрес отправителя. 2. Индекс интерфейса для исходящих дейтаграмм. 3. Предельное количество транзитных узлов для исходящих дейтаграмм. 4. Адрес следующего транзитного узла. Эта информация отправляется в виде вспомогательных данных с функцией sendmsg. Для полученного пакета могут быть возвращены три аналогичных харак- теристики. Они возвращаются в виде вспомогательных данных с функцией recvmsg: 1. IPv6-адрес получателя. 2. Индекс интерфейса для входящих дейтаграмм. 3. Предельное количество транзитных узлов для входящих дейтагарамм. На рис. 20.5 показано содержимое вспомогательных данных, о которых рас- сказывается далее. cmsghdr{) cmsghdr{} cmsg_len cmsg_level cmsg type 1Ру6-адрес 32 IPPROTO_IPV6 IPV6_RKTINFO cmsg_len cmsg_level cmsg_type Предельное количество транзитных узлов 16 IPPROTO_IPV6 IPV6_HOPLIMIT in6_j>ktinfo{ } Индекс интерфейса cmsghdr{} cmsg_len cmsg_level cmsg type 36 IPPROTO_IPV6 IPV6_NEXTHOP Структура адреса сокета Рис. 20.5. Вспомогательные данные для информации о пакете IPv6 Структура in6_pktinfo содержит либо 1Р\-6-адрес отправителя и индекс ин- терфейса для исходящей дейтаграммы, либо 1Р\'6-адрес получателя и индекс ин- терфейса для получаемой дейтаграммы:
20.8. Информация о пакете IPv6 613 struct in6_pktinfo { struct in6_addr ipi6_addr. /* IPv6-aapec отправителя/получателя*/ int i pi 6_if index /* индекс интерфейса для исходящей/получаемой дейтаграм- мы*/ }• Эта структура определяется в заголовочном файле <netinet/in h>, подключе- ние которого позволяет ее использовать. В структуре cmsghdr, содержащей вспомо- гательные данные, элемент cmsgjevel будет иметь значение IPPR0T0_IPV6, и первый байт данных будет первым байтом структуры in6_pktinfo. В примере, приведен- ном на рис. 20.5, мы считаем, что между структурой cmsghdr и данными нет запол- нения и целое число занимает 4 байта. Чтобы отправить эту информацию, никаких специальных действий не требу- ется — нужно только задать управляющую информацию во вспомогательных дан- ных функции sendmsg. Но возвращать эту информацию функция recvmsg будет только если приложение включит параметр сокета IPV6_PKTINF0. Исходящий и входящий интерфейс Интерфейсы на узле IPv6 идентифицируются небольшими целыми положитель- ными числами, как мы сказали в разделе 17.6. Вспомните, что ни одному интерфей- су не может быть присвоен нулевой индекс. При задании исходящего интерфейса ядро само выберет исходящий интерфейс, если значение ipi6_ifindex нулевое. Если приложение задает исходящий интерфейс для пакета многоадресной пере- дачи, то любой интерфейс, заданный параметром сокета IPV6_MULTICAST_IF, заме- няется на интерфейс, заданный вспомогательными данными (но только для дан- ной дейтаграммы). Адрес отправителя и адрес получателя IPv6 1Ру6-адрес отправителя обычно определяется при помощи функции bi nd. Но если адрес отправителя поставляется вместе с данными, это может снизить непроиз- водительные затраты. Э1 от параметр также позволяет серверу гарантировать, что адрес отправителя ответа совпадает с адресом получателя клиентского запроса — некоторым клиентам требуется такое условие, которое сложно выполнить в слу- чае IPv4 (см. упражнение 20.4). Когда IPv6-a;ipec отправителя задан в качестве вспомогательных данных и эле- мент ipi6_addr структуры in6_pktinfo имеет значение IN6ADDR_ANY_INIT, возможны следующие сценарии. Если адрес в настоящий момент связан с сокетом, он ис- пользуется в качестве адреса отправителя. Если в настоящий момент никакой адрес не связан с сокетом, ядро выбирает адрес отправителя. Если же элемент 1 pi 6_addr не является неопределенным адресом, но сокет уже связался с адресом отправителя, то значением элемента i pi 6_addr перекрывается уже связанный ад- рес, но только для данной операции вывода. Затем ядро проверяет, что запраши- ваемый адрес отправителя действительно является адресом направленной пере- дачи, присвоенным узлу. Когда структура i n6_pkti nfo возвращается в качестве вспомогательных дан- ных функцией recvmsg, элемент ipi6_addr содержит 1Ру6-адрес получателя из полученного пакета. По сути, это аналог параметра сокета IP RECVDSTADDR для IPv4.
614 Глава 20. Дополнительные сведения о сокетах UDP Задание и получение предельного количества транзитных узлов Предельное количество транзитных узлов обычно задается параметром сокета IPV6_UNICAST_H0PS для дейтаграмм направленной передачи (см. раздел 7.8) или параметром сокета IPV6_MULTICAST_H0PS для дейтаграмм многоадресной передачи (см. раздел 19.5). Задавая предельное количество транзитных узлов в составе вспомогательных данных, мы можем заменить как значение этого предела, задавае- мое ядром по умолчанию, так и ранее заданное значение — и для направленной, и для многоадресной передачи, но только для одной операции вывода. Предел количе- ства транзитных узлов полученного пакета используется в таких программах, как traceroute, и в некоторых приложениях IPv6, которым нужно проверять, что полу- ченное значение равно 255 (то есть пакет не был переслан с других адресов). Полученное предельное количество транзитных узлов возвращается в виде вспомогательных данных функцией recvmsg, только если приложение включает параметр сокета IPV6_H0PLIMIT. В структуре cmsghdr, содержащей эти вспомогатель- ные данные, элемент cmsg_level будет иметь значение IPPR0T0_IPV6, элемент cmsg_type — значение IPV6_H0PLIMIT, а первый байт данных будет первым байтом целочисленного предела повторных передач. Мы показали это на рис. 20.5. Нуж- но понимать, что значение, возвращаемое в качестве вспомогательных данных, — это действительное значение из полученной дейтаграммы, в то время как значе- ние, возвращаемое функцией getsockopt с параметром IPV6_UNICAST_H0PS, является значением по умолчанию, которое ядро будет использовать для исходящих дей- таграмм на сокете. Чтобы задать предельное количество транзитных узлов для исходящих паке- тов, никаких специальных действий не требуется — нам нужно только указать управляющую информацию в виде вспомогательных данных для функции sendmsg. Обычные значения для предельного количества транзитных узлов лежат в диа- пазоне от 0 до 255 включительно, но если целочисленное значение равно -1, это указывает ядру, что следует использовать значение по умолчанию. ПРИМЕЧАНИЕ--------------------------------------------------------- Предельное количество транзитных узлов не содержится в структуре in6_pktinfo по следующей причине. Некоторые серверы UDP хотят отвечать па запросы клиентов, посылая ответы на том же интерфейсе, на котором был получен запрос, с совпадением IPv6-адреса отправителя ответа и IPv6-адреса получателя запроса. Для этого прило- жение может включить параметр сокета IPV6_PKTINFO, азатем использовать полу- ченную управляющую информацию из функции recvmsg в качестве управляющей ин- формации для функции sendmsg при отправке ответа. Приложению вообще никак не нужно проверять или изменять структуру in6_pktinfo. Но если в этой структуре содер- жался бы предел количества транзитных узлов, приложение должно было бы проана- лизировать полученную управляющую информацию и изменить значение этого предела, поскольку полученный предел не является желательным значением для исхо- дящего пакета. Задание адреса следующего транзитного узла Объект вспомогательных данных IPV6_NEXTH0P задает адрес следующего транзит- ного узла дейтаграммы в виде структуры адреса сокета. В структуре cmsghdr, со-
Упражнения 615 держащей эти вспомогательные данные, элемент cmsg_l evel будет иметь значение IPPROTOJ Р V6, элемент cmsg_type — значение I PV6_NEXTH0P, а первый байт данных будет первым байтом структуры адреса сокета. На рис. 20.5 мы показали пример такого объекта вспомогательных данных, считая, что структура адреса сокета — это 24-байтовая структура sockaddr_in6. В этом случае узел, идентифицируемый данным адресом, должен быть соседним для отправляющего узла. Если этот адрес совпадает с адресом получателя IPv6- дейтаграммы, мы получаем эквивалент параметра сокета SO_DONTROUTE. Установка этого параметра требует прав привилегированного пользователя. 20.9. Резюме Существуют приложения, которым требуется знать IP-адрес получателя дейта- граммы UDP и интерфейс, на котором была получена эта дейтаграмма. Чтобы получать эту информацию в виде вспомогательных данных для каждой дейта- граммы, можно установить параметры сокета IP_RECVDSTADDR и IP_RECVIF. Анало- гичная информация вместе с предельным значением количества транзитных узлов полученной дейтаграммы для сокетов IPv6 становится доступна при включении параметра сокета IPV6_PKTINF0. Несмотря на множество полезных свойств, предоставляемых протоколом TCP и отсутствующих в UDP, существуют ситуации, когда следует отдать предпочте- ние UDP. UDP должен использоваться для широковещательной или многоад- ресной передачи. UDP может использоваться в простых сценариях «запрос-от- вет», но тогда приложение должно само обеспечить некоторую функциональность, повышающую надежность протокола UDP. UDP не следует использовать для передачи большого количества данных. В разделе 20.5 мы добавили нашему клиенту UDP определенные функцио- нальные возможности, повышающие его надежность за счет обнаружения факта потери пакетов, для чего используются тайм-аут и повторная передача. Мы изме- няли тайм-аут повторной передачи динамически, снабжая каждый пакет отмет- кой времени и отслеживая два параметра: период обращения RTT и его среднее отклонение. Мы также добавили порядковые номера, чтобы проверять, что дан- ный ответ — это ожидаемый нами ответ на определенный запрос. Наш клиент продолжал использовать простой протокол остановки и ожидания (stop-and-wait), а приложения такого типа допускают применение UDP. Упражнения 1. Почему в листинге 20.16 функция pnntf вызывается дважды? 2. Может ли когда-нибудь функция dg_send_recv (см. листинги 20.6 и 20.7) воз- вратить нуль? 3. Перепишите функцию dg_send_recv с использованием функции sei ect и ее тай- мера вместо alarm, SIGALRM, sigsetjmp и siglongjmp. 4. Как может сервер IPv4 гарантировать, что адрес отправителя в его ответе со- впадает с адресом получателя клиентского запроса? (Аналогичную функцио- нальность предоставляет параметр сокета IPV6_PKTINF0.)
616 Глава 20. Дополнительные сведения о сокетах UDP 5. Функция main в разделе 20.6 является зависящей от протокола (IPv4). Пере- пишите ее, чтобы она стала не зависящей от протокола. Потребуйте, чтобы пользователь задал один или два аргумента командной строки, первый из ко- торых — необязательный IP-адрес (например, 0.0.0.0 или 0::0), а второй — обя- зательный номер порта. Затем вызовите функцию udp_cl i ent, чтобы получить семейство адресов, номер порта и длину структуры адреса сокета. Что произойдет, если вы вызовете функцию udp_cl т ent, как было предложено, не задавая аргумент hostname, поскольку функция udp_client не задает значе- ние AI_PASSIVE функции getaddri nfo? 6. Соедините клиент, показанный в листинге 20.4, с эхо-сервером через Интер- нет, изменив функции rtt_ так, чтобы выводилось каждое значение RTT. Так- же измените функцию dg_send_recv, чтобы она выводила каждый полученный порядковый номер. Изобразите на графике полученные в результате значе- ния RTT вместе с оценочными значениями RTT и среднего отклонения.
ГЛАВА 21 Внеполосные данные 21.1. Введение Ко многим транспортным уровням применима концепция внеполосных данных {out-of-band data), которые иногда называются срочными данными {expediteddata). Суть этой концепции заключается в том, что если на одном конце соединения происходит какое-либо важное событие, то требуется быстро сообщить об этом собеседнику. В данном случае «быстро» означает, что сообщение должно быть послано прежде, чем будут посланы какие-либо обычные данные (называемые иногда данными из полосы пропускания), которые уже помещены в очередь для отправки, то есть внеполосные данные имеют более высокий приоритет, чем обыч- ные данные. Но вместо того, чтобы создавать новое соединение между сервером и клиентом, для передачи внеполосных данных используется уже существующее соединение. К сожалению, когда мы переходим от общих концепций к реальной ситуации, почти в каждом транспортном уровне имеется своя реализация внеполосных дан- ных. В этой главе мы уделим основное внимание модели внеполосных данных TCP. Мы приведем различные примеры обработки внеполосных данных в API сокетов, а затем используем их для написания нескольких простых клиент-сер- верных функций, способных определить, когда процесс-собеседник становится недоступным или дает сбой, — так называемых функций проверки пульса {heartbeat functions). 21.2. Внеполосные данные протокола TCP В протоколе TCP нет настоящих внеполосных данных. Вместо этого в TCP пре- дусмотрен так называемый срочный режим' {urgent mode), к рассмотрению кото- рого мы сейчас и приступим. Предположим, процесс записал N байт данных в со- кет протокола TCP, и эти данные образуют очередь в буфере отправки сокета и ожидают отправки собеседнику. Эту ситуацию иллюстрирует рис. 21.1. Байты данных пронумерованы от 1 до N. Теперь процесс записывает один байт внеполосных данных, содержащий сим- вол ASCII а, используя функцию send и флаг MSG_OOB: send(fd "а". 1. MSG_OOB). Иногда переводится как «экстренный режим» пли «режим срочности». — Примеч перев.
618 Глава 21 Внеполосные данные Буфер отправки сокета N Первый отправляемый байт Последний отправляемый байт Рис. 21.1. Буфер отправки сокета, содержащий данные для отправки TCP помещает данные в следующую свободную позицию буфера отправки сокета и устанавливает указатель на срочные данные (или просто срочный указа- тель* 1 — uigentpointei) для этого соединения на первую свободную позицию Этот буфер показан на рис 21 2, а бант, содержащий внеполосные данные, помечен буквами 00В Буфер отправки сокета Пераый отправляемый байт Последний отправляемый байт Рис. 21.2. Буфер отправки сокета, в который добавлен один байт внеполосных данных са О О Срочный указатель TCP ПРИМЕЧАНИЕ -------------------------------------------------------- Срочный у казатель TCP указывает на бан г данных, который следует за пос дсднпм бай том внеполосных данных (то есть данных снабженных флагом MSG_OOB) В кише [941 нас 292-296 говорится что это исторически сложившаяся особенность, которая теперь эмулируется во всех реализациях Если посылающий и принимающий прото колы TCP одинаково интерпретируют срочный укататель TCP беспокоиться не о чем В разделе 7 9 мы упомянули новый параметр сокета TCP STDURG, потволяю- щии итменягь интерпретацию срочною указатетя, то есть определять па какой байт он указывав г па последний бант срочных данных или па байт, следующий та ним Необходимое!и в установке этою параметра возникать не должно Если состояние буфера таково, как показано на рис 21 2, то в заготовке TCP следующего отправленного сегмента будет установлен флаг URG, а поле смеще- ния срочных данных (или просто поле срочного смещения2) будет указывать на байт, следующий за байтом с внеполосными данными Но этот сегмент может содержать бант, помеченный как 00В, а может и не содержагь ei о Будет ли послан этот байт, завися i от ко дичее т ва предшес гвующих ему бантов в буфере отправки сокета, от размера cei мента, который TCP пересылает собеседнику, йог текуще- го размера окна, объявлении о собеседником 1 Также (ошибочно) используется 1срмин «указатель срочности» — Примеч перев 1 Также используется термин «срочное смещение» — Примеч перев
21 2 Внеполосные данные протокола TCP 619 ПРИМЕЧАНИЕ ---------------------------------------------------------- Выше мы использовали термины «срочный указатель» (uigent pointci) и срочное сме- щение (urgent offset) На уровне TCP ли термины имеют par шчные значения Вели- чина, представленная 16 бигами в заголовке TCP, называется срочным смещением и должнабыть прибавленак по по последовательно!о номера в заголовке TCP для полу- чения 32 разрядного последовательно! о номера последпе! о байта срочных данных (то есть срочного указателя) TCP использует срочное смещение, только ее ш в за! оловке установлен другой бит, называемый флагом URG Программно гу можно не забо i ить- ся об этом различии и работать только со срочным указателем TCP Важная характеристика срочного режима TCP заключается в следующем за- головок TCP указывает на то, что отправитель вошел в срочный режим (то есть флаг URG установлен вместе со срочным смещением), но фактически посыпать байт данных, на который указывает срочный указатель, не требуется Действи- тельно, если поток данных TCP остановлен функциями управления потоком (ког- да буфер приема сокета получателя заполнен и TCP получателя объявил нуле- вое окно для отправляющего TCP), то срочное уведомление отправляется без каких-либо данных [105, с 1016-1017], как показано в листингах 21 8и21 9 Это одна из причин, по которой в приложениях используется срочный режим TCP (то есть внеполосные данные) срочное уведомление всегда отсылается собесед- нику, даже если поток данных остановлен функциями управления потоком TCP Что произойдет, если мы отправим несколько байтов внеполосных данных, как в следующем примере send(fd abc 3 MSG_OOB) В этом примере срочный указатель TCP указывает на баш, следующий за по- следним байтом, и, таким образом, последний байт (с) считается баиюм внепо- лосных данных Посмотрим теперь, как выглядит процесс отправки внеполосных данных с точки зрения принимающей стороны 1 Ко1да TCP получает сегмент, в котором установлен флаг URG срочный ука- затель проверяется для выяснения того, указывает ли он на новые внеполос- ные данные Иначе говоря, проверяется, впервые ли этот конкретный бант передается в срочном режиме TCP Депо в том, что часто отправляющий TCP посылает несколько cei ментов (обычно в течение короткого промежутка вре- мени), содержащих флаг URG, в ко горых срочный указатель указывает на один и тот же байт данных Только первый из этих сегментов фактически уведом- ляет принимающий процесс о прибытии новых внеполосных данных 2 Принимающий процесс извещается о гом, что прибыли новые внеполосные данные Сначала владельцу сокета посылается сигнал SIGURG При этом пред- полагается, что для установления владельца сокета была вызвана функция fcntl или ioctl (см табл 7 5) и что для данного сигнала процессом был уста- новлен обработчик сигнала Затем, если процесс блокирован в вызове функ- ции select, которая ждет возникновения исключительной ситуации для де- скриптора сокета, происходит возврат из этой функции 3 Когда байт данных, на который указывает срочный указа!ель, фактически прибывает на принимающий TCP, этот баи г может быть помещен отдельно или оставлен вместе с другими данными По умолчанию параметр сокета
620 Глава 21. Внеполосные данные SO_OOBINLINE не установлен, поэтому внеполосный байт не размещается в при- емном буфере сокета. Вместо этого содержащиеся в нем данные помещаются в отдельный внеполосный буфер размером в один байт, предназначенный спе- циально для этого соединения [105, с. 986-988]. Для процесса единственным способом прочесть данные из этого специального однобайтового буфера яв- ляется вызов функции recv, recvfrom или recvmsg с заданием флага MSG_OOB. Однако если процесс устанавливает параметр сокета SO_OOBINLINE, то байт дан- ных, на который указывает срочный указатель TCP, остается в обычном бу- фере приема сокета. В этом случае процесс не может задать флаг MSG_OOB для считывания данных, содержащихся во внеполосном байте. Процесс сможет распознать этот байт, только когда дойдет до него и проверит отметку внепо- лосных данных {out-of-band mark) для данного соединения, как показано в раз- деле 21.3. Возможны некоторые ошибки: 1. Если процесс запрашивает внеполосные данные (то есть устанавливает флаг MSG_OOB), но собеседник таких данных не послал, возвращается EINVAL. 2. Если процесс был уведомлен о том, что собеседник послал содержащий вне- полосные данные байт (например, с помощью функции select или сигнала SIGURG), и пытается считать эти данные, когда указанный байт еще не прибыл, возвращается ошибка EWOULDBLOCK. В такой ситуации все, что может сделать процесс, — это считать данные из приемного буфера сокета (возможно, сбра- сывая данные, если отсутствует свободное место для их хранения), чтобы осво- бодить место в буфере для приема байта внеполосных данных, посылаемых собеседником. 3. Если процесс пытается считать одни и те же внеполосные данные несколько раз, возвращается ошибка EINVAL. 4. Если процесс установил параметр сокета SO_OOBINLINE, а затем пытается счи- тать внеполосные данные, задавая флаг MSG OOB, возвращается EINVAL. Простой пример использования сигнала SIGURG Теперь мы рассмотрим тривиальный пример отправки и получения внеполос- ных данных. В листинге 21.11 показана программа отправки этих данных. Листинг 21.1. Простая программа отправки внеполосных данных 7/oob/tcpsendOl с 1 #include "unp h" 2 int 3 main(int argc. char **argv) 4 { 5 mt sockfd. 6 if (argc '= 3) 7 err_quit("usage tcpsendOl <host> <port#>"), 8 sockfd = Tcp_connect(argv[l], argv[2]). 9 Write(sockfd. "123”. 3). 10 printf("wrote 3 bytes of normal data\en") 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http-// www piter com/download.
21.2. Внеполосные данные протокола TCP 621 11 sleep(l). 12 Send(sockfd, "4". 1. MSG_OOB) 13 pnntf( "wrote 1 byte of COB dataXen"). 14 sleep(l). 15 Write(sockfd. ”56", 2). 16 printft "wrote 2 bytes of normal dataXen"), 17 sleep(l). 18 Send(sockfd. "7”. 1, MSG_OOB). 19 printft"wrote 1 byte of GOB dataXen"). 20 sleep(l). 21 Wnte(sockfd, "89". 2): 22 printfCwrote 2 bytes of normal dataXen"). 23 sleep(l). 24 exit(0). 25 } Отправлены 9 байт, промежуток между операциями по отправке установлен с помощью функции si еер равным одной секунде. Назначение этого промежутка в том, чтобы данные каждой из функций write или send были переданы и получе- ны на другом конце как отдельный сегмент TCP. Несколько позже мы обсудим некоторые вопросы согласования во времени при пересылке внеполосных дан- ных. После выполнения данной программы мы видим следующий результат: solans % tcpsendOl bsdi 9999 wrote 3 bytes of normal data wrote 1 byte of GOB data wrote 2 bytes of normal data wrote 1 byte of GOB data wrote 2 bytes of normal data В листинге 21.2 показана принимающая программа. Листинг 21.2. Простая программа для получения внеполосных данных 7/oob/tcprecvOl с 1 ^include "unp h" 2 int listenfd, connfd; 3 void sig_urg(int), 4 int 5 maindnt argc, char **argv) 6 { 7 int n. 8 char buffflOO]. 9 if (argc == 2) 10 listenfd = Tcp_listen(NULL, argvfl]. NULL). 11 else if (argc == 3) 12 listenfd = Tcp_listen(argv[l] argv[2], NULL). 13 else 14 err_quit("usage tcprecvOl [ <host> ] <port#>"), 15 connfd = Accept(listenfd NULL. NULL).
622 Глава 21. Внеполосные данные Листинг 21.2 (продолжение) 16 Signal(SIGURG, sig_urg), 17 Fcntl(connfd. F_SETOWN. getpidO); 18 for (..) { 19 if ( (n = Read(connfd. buff, sizeof(buff) - 1)) — 0) { 20 printfC"received EOFXen"): 21 exit(0)- 22 } 23 bufffn] = 0: /* завершающий нуль */ 24 printfC'read fcd bytes: fcsXen". n. buff): 25 } 26 } 27 void 28 sig_urg(int signo) 29 { 30 int n; 31 char buffflOO); 32 printf(“SIGURG receivedXen”): 33 n = RecvCconnfd, buff, sizeof(buff) - 1. MSG_OOB), 34 bufffn] =0: /* завершающий нуль */ 35 printfC'read Bd GOB byte ^s\en", n, buff): 36 } Установка обработчика сигнала и владельца сокета 16-17 Устанавливается обработчик сигнала для SIGURG и функция fcntl задает вла- дельца сокета для данного соединения. ПРИМЕЧАНИЕ --------------------------------------------------------------------- Обратите внимание, что мы не задаем обработчик сигнала, пока не завершается функ- ция accept. Существует небольшая вероятность того, что внеполосные данные могут прибыть после того, как TCP завершит трехэтапное рукопожатие, но до завершения функции accept. Внеполосные данные мы в этом случае потеряем. Допустим, что мы установили обработчик сигнала перед вызовом функции accept, а также задали вла- дельца прослушиваемого сокета (который затем стал бы владельцем присоединенного сокета). Тогда, если внеполосные данные прибудут до завершения функции accept, наш обработчик сигналов еще нс получит значения для дескриптора connfd. Если данный сценарий важен для приложения, следует инициализировать connfd, «вручную» при- своив этому дескриптору значение -1, добавить в обработчик проверку равенства connfd == -1 и при истинности этого условия просто установить флаг, который будет проверяться в главном цикле после вызова accept. За счет этого главный цикл сможет узнать о поступлении внеполосных данных и считать их. 18-25 Процесс считывает данные из сокета и выводит каждую строку, которая воз- вращается функцией read. После того как отправитель разрывает соединение, то же самое делает и получатель. Обработчик сигнала SIGURG 27-36 Наш обработчик сигнала вызывает функцию printf, считывает внеполосные данные, устанавливая флаг MSG_OOB, а затем выводит полученные данные. Обра- тите внимание, что при вызове функции recv мы запрашиваем до 100 байт, но,
21.2. Внеполосные данные протокола TCP 623 как мы вскоре увидим, всегда возвращается только один байт внеполосных данных. ПРИМЕЧАНИЕ-------------------------------------------------------- Как сказано ранее, вызов ненадежной функции printf из обработчика сигнала не реко- мендуется. Мы делаем это просто для того, чтобы увидеть, что произойдет с нашей программой. Ниже приведен результат, который получается, когда мы запускаем эту про- грамму, а затем — программу для отправки внеполосных данных, приведенную в листинге 21.1. bsdi % tcprecvOl 9999 read 3 bytes: 123 SIGURG received read 1 GOB byte: 4 read 2 bytes. 56 SIGURG received read 1 COB byte: 7 read 2 bytes- 89 received EOF Результаты оказались такими, как мы и ожидали. Каждый раз, когда отправи- тель посылает внеполосные данные, для получателя генерируется сигнал SIGURG, после чего получатель считывает один байт, содержащий внеполосные данные. Простой пример использования функции select Теперь мы переделаем код нашего получателя внеполосных данных и вместо сиг- нала SIGURG будем использовать функцию select. В листинге 21.3 показана при- нимающая программа. Листинг 21.3. Принимающая программа, в которой (ошибочно) используется функция select для уведомления о получении внеполосных данных 7/oob/tcprecv02 с 1 include "unp h" 2 int 3 main(int argc, char **argv) 4 { 5 mt listenfd, connfd. n; 6 char buffflOOJ. 7 fd_set rset. xset. 8 if (argc — 2) 9 listenfd = Tcp_listen(NULL. argvfl], NULL); 10 else if (argc == 3) 11 listenfd = Tcp_listen(argv[l] argv[2] NULL). 12 else 13 err_quit("usage tcprecv02 [ <host> ] <port#>"); 14 connfd = Acceptdistenfd, NULL. NULL). 15 FD_ZERO(&rset). 16 FD_ZLRO(&xset). 17 for (..) { 18 FD_SET(connfd. &rset).
624 Глава 21. Внеполосные данные 19 FD_SET(connfd &xset). 20 Selectlconnfd + 1 &rset. NULL &xset NULL) 21 if (FD_ISSET(connfd. &xset)) ( 22 n = Recv(connfd. buff sizeof(buff) - 1. MSG_OOB): 23 bufffn] =0. /* завершающий нуль */ 24 pnntf('read £d COB byte ^s\en" n. buff). 25 } 26 if (FD_ISSET(connfd, &rset)) { 27 if ( (n = Readlconnfd. buff, sizeof(buff) - 1)) — 0) { 28 printfC received EOFXen"). 29 exit(0) 30 } 31 buff[n] = 0 /* завершающий нуль */ 32 printf("read £d bytes ^s\en". n buff). 33 } 34 } 35 } 15-20 Процесс вызывает функцию sei ect, которая ждет либо обычпые данные (набор дескрипторов для чтения, rset), либо внеполосные (набор дескрипторов для об- работки исключений, xset). В обоих случаях полученные данные выводятся. Если мы запустим эту программу, а затем — программу для отправки, которая приведена в листинге 21.1, то столкнемся со следующей ошибкой: bsdi % tcprecv02 8888 read 3 bytes 123 read 1 00B byte 4 recv error Invalid argument Проблема заключается в том, что функция sei ect будет сообщать об исключи- тельной ситуации, пока процесс не считает данные, находящиеся за отметкой внеполосных данных (то есть после них [105, с. 530-531]). Мы не можем считы- вать внеполосные данные больше одного раза, так как после первого же их счи- тывания ядро очищает буфер, содержащий один байт внеполосных данных. Ког- да мы вызываем функцию recv, устанавливая флаг MSG_OOB во второй раз, BSD/ OS возвращает ошибку EINVAL, в то время как Solans 2.5 возвращает ошибку EAGAIN. (Согласно Posix.lg в данном случае должна возвращаться ошибка EINVAL.) Чтобы решить эту проблему, нужно вызывать функцию select для проверки на наличие исключительной ситуации только после того, как будут приняты все обычные данные. В листинге 21.4 показана модифицированная версия принима- ющей программы из листинга 21.3. В этой версии описанный сценарий обраба- тывается корректно. Листинг 21.4. Модификация программы, приведенной в листинге 21.3. Функция select применяется для проверки исключительной ситуации корректным образом 7/oob/tcprecv03 с 1 #include 'unp h” 2 int 3 mainlint argc. char **argv) 4 { 5 int listenfd connfd. n. justreadoob = 0 с brffrinni
21.3. Функция sockatmark 625 7 fd_set rset. xset 8 if (argc == 2) 9 listenfd = Tcp_listen(NULL. argv[l] NULL). 10 else if (argc == 3) 11 listenfd = Tcp_listen(argv[l] argv[2], NULL) 12 else 13 err_quit('usage tcprecv03 [ <host> ] <port#>"), 14 connfd = Accept(1istenfd NULL NULL) 15 FD_ZERO(&rset). 16 FD_ZERO(&xset). 17 for (..) { 18 FD_SET(connfd. &rset). 19 if (justreadoob — 0) 20 FD_SET(connfd. Sxset), 21 Select(connfd + 1 &rset. NULL &xset NULL). 22 if (FD_ISSET(connfd. &xset)) { 23 n = Recv(connfd. buff sizeof(buff) - 1 MSG_00B); 24 buff[n] = 0. /* завершающий нуль *7 25 printfC'read £d 008 byte $s\en". n. buff). 26 justreadoob = 1 27 FD_CLR(connfd &xset). 28 } 29 if (FD_ISSET(connfd &rset)) { 30 if ( (n = Read(connfd. buff, sizeof(buff) - D) -= 0) { 31 printf("received EOFXen") 32 exit(0) 33 } 34 buff[n] = 0. 7* завершающий нуль */ 35 printfC'read £d bytes &s\en'. n. buff), 36 justreadoob = 0. 37 } 38 } 39 } 5 Мы объявляем новую переменную с именем justreadoob, которая указывает, какие данные мы считываем — внеполосные или обычные. Этот флаг определя- ет, нужно ли вызывать функцию sei ect для проверки на наличие исключитель- ной ситуации. 26-27 Когда мы устанавливаем флаг justreadoob, мы также должны выключить бит соответствующего дескриптора в наборе для проверки исключительных ситуа- ций. Теперь программа работает так, как мы ожидали. 21.3. Функция sockatmark С приемом внеполосных данных всегда связана так называемая отметка внепо- лосных данных {out-of-bandmark). Это позиция в потоке обычных данных на сто- роне отправителя, соответствующая тому моменту, когда посылающий процесс отправляет байт, содержащий внеполосные данные. Считывая данные из сокета, принимающий процесс путем вызова функции sockatmark определяет, находится ЛИ ОН в ланный момент на этой отметке
626 Глава 21. Внеполосные данные include <sys/socket h> int sockatmark(int sockfd) /* Возвращает 1. если находится на отметке внеполосных данных 0 если не на отметке, -1 в случае ошибки */ ПРИМЕЧАНИЕ -------------------------------------------------------------------- Эта функция появилась в Posix.lg Разработчики стандарта Posix стремятся заменить отдельными функциями все вызовы ioctl с различными параметрами В листинге 21.5 показана реализация этой функции с помощью поддерживае- мого в большинстве систем параметра SIOCATMARK функции ioctl. Листинг 21.5. функция sockatmark реализована с использованием функции ioctl //1ib/sockatmark с 1 #include "unp h” 2 int 3 sockatmark(int fd) 4 { 5 int flag 6 if (ioctl(fd SIOCATMARK &flag) < 0) 7 return (-1). 8 return (flag '-0 7 1 0). 9 } Отметка внеполосных данных применима независимо от того, как принимаю- щий процесс получает внеполосные данные: вместе с обычными данными (пара- метр сокета SO_OOBINLINE) или отдельно (флаг MSG_OOB). Отметка внеполосных данных часто используется для того, чтобы принимающий процесс мог интер- претировать получаемые данные специальным образом до тех пор, пока он не дойдет до этой отметки. Пример Ниже мы приводим простой пример, иллюстрирующий следующие две особен- ности отметки внеполосных данных: 1. Отметка внеполосных данных всегда указывает на один байт дальше конеч- ного байта обычных данных. Это означает, что когда внеполосные данные по- лучены вместе с обычными, функция sockatmark возвращает 1, если следую- щий считываемый байт был послан с флагом MSG_OOB. Если параметр SO_OOBINLINE не включен (состояние по умолчанию), то функция sockatmark возвращает 1, когда следующий байт данных является первым байтом, посланным следом за внеполосными данными. 2. Операция считывания всегда останавливается на отметке внеполосных данных [105, с. 519-520]. Это означает, что если в приемном буфере сокета 100 байт, но только 5 из них расположены перед отметкой внеполосных данных, то ког- да процесс выполнит функцию read, запрашивая 100 байт, возвратятся только 5 байт, расположенные до этой отметки. Эта вынужденная остановка на от- метке позволяет процессу вызвать функцию sockatmark, которая определит, navnnuTra пи vira-iaTpm, (Svdipna пя птмртгр пмрпопосных данных.
21.3. Функция sockatmark 627 В листинге 21.6 показана наша программа отправки. Она посылает 3 байта обычных данных, 1 байт внеполосных данных, а затем еще один байт обычных данных. Паузы между этими операциями отсутствуют. В листинге 21.7 показана принимающая программа. В ней не используется ни функция select, ни сигнал SIGURG. Вместо этого в ней вызывается функция sock- atmark, чтобы определить, когда встречается байт внеполосных данных. Листинг 21.6. Программа отправки /7oob/tcpsen04 с 1 #include "unp h' 2 int 3 maindnt argc char **argv) 4 { 5 mt sockfd. 6 if (argc 3) 1 err_quit("usage tcpsend04 <host> <port#>''): 8 sockfd - Tcp_connect(argv[l] argv[2]) 9 VJrite(sockfd "123' 3). 10 printf("wrote 3 bytes of normal dataXen") 11 Send(sockfd "4' 1 MSG_OOB) 12 printf("wrote 1 byte of 00B dataXen"). 13 VJrite(sockfd "5" 1). 14 printf("wrote 1 byte of normal dataXen"). 15 exit(0) 16 } Листинг 21.7. Принимающая программа, в которой вызывается функция sockatmark //oob/tcprecv04 с 1 #include 'unp h” 2 int 3 main(int argc char **argv) 4 { 5 int listenfd connfd n. on - 1: 6 char buff[100] sp 0 5v 7 if (argc == 2) 8 listenfd = Tcp_listen(NULL argv[l] NULL). 9 else if (argc = 3) 10 listenfd = Tcp_listen(argv[l] argv[2], NULL) 11 else 12 err_quit('usage tcprecv04 [ ^host> ] <port#>"). sp 0 5v 13 Setsockopt(listenfd SOL_SOCKET. SO_OOBINLINE. &on sizeof(on)); sp 0 5v 14 connfd = Accept!listenfd NULL NULL) 15 sleep(5) sp 0 5v 16 for ( ) {
628 Глава 21. Внеполосные данные Листинг 21.7 (продолжение) 17 if (Sockatmark(connfd)) 18 printf("at 00B marklen”) sp 0 5v 19 if ( (n = ReadCconnfd buff sizeof(buff) - 1)) “ 0) { 20 printfC"received EOFXen"). 21 exit(O), 22 } 23 bufffn] = 0 /* завершающий нуль */ 24 printfC’read fcd bytes £s\en". n. buff). 25 } 26 } Включение параметра сокета SO_OOBINLINE 13 Мы хотим принимать внеполосные данные вместе с обычными данными, по- этому нам нужно включить параметр SO_OOBINLINE. Но если мы будем ждать, ког- да выполнится функция accept и установит этот параметр для присоединенного сокета, трехэтапное рукопожатие завершится и внеполосные данные уже могут прибыть. Поэтому нам нужно установить этот параметр для прослушиваемого сокета, помня о том, что все параметры прослушиваемого сокета наследуются присоединенным сокетом (см. раздел 7.4). Вызов функции sleep после вызова функции accept 14-15 После того как выполнена функция accept, получатель переходит в спящее со- стояние, что позволяет получить все данные, посланные отправителем. Это по- зволяет нам продемонстрировать, что функция read останавливается на отметке внеполосных данных, даже если в приемном буфере сокета имеются дополни- тельные данные. Считывание всех отправленных данных 16-25 В программе имеется цикл, в котором вызывается функция read и выводятся полученные данные. Но перед вызовом функции read функция sockatmark прове- ряет, находится ли указатель буфера на отметке внеполосных данных. После выполнения этой программы мы получаем следующий результат: bsdi % tcprecv04 6666 read 3 bytes 123 at 00B mark read 2 bytes 45 received EOF Хотя принимающий TCP получил все посланные данные, первый вызов функ- ции read возвращает только 3 байта, так как была обнаружена отметка внеполос- ных данных. Следующий считанный байт — это байт, содержащий внеполосные данные (его значение равно 4), так как мы дали ядру указание поместить внепо- лосные данные вместе с обычными. Пример Теперь мы покажем другой столь же простой пример, иллюстрирующий две до- полнительные особенности внеполосных данных, о которых мы уже упоминали панее.
21.3. Функция sockatmark 629 1. TCP посылает уведомление об отправке внеполосных данных (их срочный указатель), даже если поток данных остановлен функциями управления по- током. 2. Принимающий процесс может получить уведомление о том, что отправитель отослал внеполосные данные (с помощью сигнала SIGURG или функции sei ect) до того, как эти данные фактически прибудут. Если после получения этого уведомления процесс вызывает функцию recv, задавая флаг MSG OOB, а внепо- лосные данные еще не прибыли, то будет возвращена ошибка EWOULDBLOCK. В листинге 21.8 приведена программа отправки. Листинг 21.8. Программа отправки 7/oob/tcpsend05 с 1 include 'unp h" 2 int 3 maindnt argc. char **argv) 4 { 5 int sockfd size 6 char buff[16384] 7 if (argc '= 3) 8 err_quit('usage tcpsend04 <host> <port#>”) 9 sockfd = Tcp_connect(argv[l] argv[2]) 10 size = 32768 11 Setsockopt(sockfd SOL_SOCKET SO_SNDBUF &size sizeof(size)). 12 Write(sockfd. buff. 16384), 13 prirtf("wrote 16384 bytes of normal dataXen"). 14 sleep(5) 15 Send(sockfd. "a". 1 MSG_OOB) 16 printfC wrote 1 byte of 008 dataXen”) 17 Write(sockfd buff 1024). 18 printf( wrote 1024 bytes of normal dataXen"): 19 exit(0). 20 } 9-19 Этот процесс устанавливает размер буфера отправки сокета равным 32 768 бай- там, записывает 16 384 байта обычных данных, а затем на 5 секунд переходит в спящее состояние. Чуть ниже мы увидим, что приемник устанавливает размер приемного буфера сокета равным 4096 байтам, поэтому данные, отправленные отсылающим TCP, с гарантией заполнят приемный буфер сокета получателя. За- тем отправитель посылает один байт внеполосных данных, за которым следу- ют 1024 байта обычных данных, и, наконец, закрывает соединение. В листинге 21.9 представлена принимающая программа. Листинг 21.9. Принимающая программа 7/oob/tcprecv05 с 1 #include "unp h" 2 int listenfd. connfd
630 Глава 21. Внеполосные данные Листинг 21.9 (продолжение) 3 void sig_urg(int): 4 int 5 main(int argc. char **argv) 6 { 7 int size. 8 if (argc == 2) 9 listenfd = Tcp_listen(NULL. argv[l], NULL). 10 else if (argc == 3) 11 listenfd = Tcp_listen(argv[l], argv[2], NULL). 12 else 13 err_quit("usage tcprecv05 [ <host> ] <port#>"). 14 size = 4096. 15 Setsockopt(listenfd. SOL SOCKET, SO RCVBUF. &size. sizeof(size)); 16 connfd = Accept(listenfd. NULL. NULL). 17 Signal(SIGURG. sigjjrg). 18 Fcntl (connfd. F_SETOWN. getpidO), 19 for (::) 20 pause(). 21 } 22 void 23 sig_urg(int signo) 24 { 25 int n. 26 char buff[2048]: 27 printf("SIGURG receivedXen"). 28 n = Recv(connfd buff, sizeof(buff) - 1, MSG_OOB): 29 buff[n] =0. /* завершающий пустой байт */ 30 pnntfCread M 008 byteXen". n). 31 } 4-20 Принимающий процесс устанавливает размер приемного буфера сокета при- емника равным 4096 байт. Этот размер наследуется присоединенным сокетом после установления соединения. Затем процесс вызывает функцию accept, зада- ет обработчик для сигнала SIGURG и задает владельца сокета. В главном цикле вы- зывается функция pause. '2-31 Обработчик сигнала вызывает функцию recv для считывания внеполосных данных. Если мы запускаем сначала принимающую программу, а затем программу от- правки, то получаем следующий результат выполнения программы отправки: solans X tcpsend05 bsdi 5555 wrote 16384 bytes of normal data wrote 1 byte of OOB data wrote 1024 bytes of normal data Как и ожидалось, все данные помещаются в буфер отправки сокета отправи- теля, и программа завершается. Ниже приведен результат работы принимающей программы:
21.3. Функция sockatmark 631 bsdi % tcprecv05 5555 SIGURG received recv error Resource temporarily unavailable Строка ошибок, которую выдает наша функция err sys, соответствует ошибке EAGAIN, которая аналогична ошибке EWOULDBLOCK в BSD/OS. TCP посылает уведом- ление об отправке внеполосных данных принимающему TCP, который в резуль- тате генерирует сигнал SIGURG для принимающего процесса. Но когда вызывается функция recv и задается флаг MSG_OOB, байт с внеполосными данными не может быть прочитан. Для решения этой проблемы необходимо, чтобы получатель освобождал мес- то в своем приемном буфере, считывая поступившие обычные данные. В резуль- тате TCP объявит для отправителя окно ненулевого размера, что в конечном сче- те позволит отправителю передать байт, содержащий внеполосные данные. ПРИМЕЧАНИЕ ------------------------------------------------------------------ В реализациях, происходящих от Беркли [ 105, с. 1016— 1017 J, можно отмстить две близ- ких черты. Во-первых, даже если приемный буфер сокета заполнен, ядро всегда при- нимает от процесса внеполосные данные для отправки собеседнику. Во-вторых, когда отправитель посылает байт с внеполосными данными, немедленно посылается сегмент TCP, содержащий срочное уведомление. Все обычные проверки вывода TCP (алго- ритм Нагла, предо! вращение синдрома «глупого окна») при этом блокируются. Пример Нашим очередным примером мы иллюстрируем тот факт, что для данного соеди- нения TCP существует всего одна отметка внеполосных данных, и если новые внеполосные данные прибудут прежде, чем принимающий процесс начнет счи- тывать пришедшие ранее внеполосные данные, то предыдущая отметка будет уте- ряна. В листинге 21.10 показана посылающая программа, аналогичная программе, приведенной в листинге 21.6. Отличие заключается в том, что сейчас мы добави- ли еще одну функцию send для отправки внеполосных данных и еще одну функ- цию write для записи обычных данных. Листинг 21.10. Отправка двух байтов внеполосных данных друг за другом //oob/tcpsend06 с 1 #include "unp h" 2 int 3 main(int argc char **argv) 4 { 5 mt sockfd. 6 if (argc '= 3) 7 err_quit("usage tcpsendO4 <host> <port#>”). 8 sockfd = Tcp_connect(argv[l]. argv[2J); 9 Writefsockfd. "123". 3): 10 printf("wrote 3 bytes of normal dataXen"): 11 Send(sockfd. "4". 1. MSG_OOB), продолжение &
632 Глава 21. Внеполосные данные Листинг 21.10 (продолжение) 12 printfC"wrote 1 byte of OOB dataXen") 13 WnteCsockfd. "5". 1) 14 printfC"wrote 1 byte of normal dataXen"); 15 SendCsockfd "6". 1. MSG_OOB) 16 printfC"wrote 1 byte of OOB dataXen"); 17 WriteCsockfd. "7", 1), 18 printfC"wrote 1 byte of normal dataXen"); 19 exit(O). 20 } В данном случае отправка данных происходит без пауз, что позволяет быстро переслать данные принимающему TCP. Принимающая программа идентична программе, приведенной в листинге 21.7, где вызывается функция sleep, которая после установления соединения перево- дит получателя в спящее состояние на 5 секунд, чтобы позволить данным при- быть на принимающий TCP. Ниже приводится результат выполнения этой про- граммы: bsdi % tcprecv06 5555 read 5 bytes 12345 at OOB mark read 2 bytes 67 received EOF Прибытие второго байта внеполосных данных (6) изменяет отметку, которая ассоциировалась с первым прибывшим байтом внеполосных данных (4). Как мы сказали, для конкретного соединения TCP допускается только одна отметка вне- полосных данных. 21.4. Резюме по теме внеполосных данных TCP Все приведенные до сих пор примеры, иллюстрирующие использование внепо- лосных данных, были весьма тривиальны. К сожалению, когда мы начинаем учи- тывать возможные проблемы, связанные с согласованием во времени при пере- сылке внеполосных данных, ситуация заметно усложняется. В первую очередь, нужно осознать, что концепция внеполосных данных подразумевает передачу получателю трех различных фрагментов информации: 1. Сам факт того, что отправитель вошел в срочный режим. Принимающий про- цесс получает уведомление об этом либо с помощью сигнала SIGURG, либо с по- мощью функции sei ect. Это уведомление передается сразу же после того, как отправитель посылает байт внеполосных данных, поскольку, как показано в листинге 21.9, TCP посылает уведомление, даже если поток каких-либо дан- ных от сервера к клиенту остановлен функциями управления потоком. В результате получения такого уведомления получатель может входить в оп- ределенный специальный режим обработки последующих данных.
21.4. Резюме по теме внеполосных данных TCP 633 2. Позиция байта, содержащего внеполосные данные, то есть расположение это- го байта по отношению к остальным данным, посланным отправителем, иначе говоря, отметка внеполосных данных. 3. Фактическое значение внеполосного байта. Поскольку TCP является потоко- вым протоколом, который не интерпретирует данные, посланные приложени- ем, это может быть любое 8-разрядное значение. В срочном режиме TCP мы можем рассматривать флаг URG как уведомле- ние, а срочный указатель как внеполосную отметку. Проблемы, связанные с концепцией внеполосных данных, сформулированы в следующих пунктах: 1. Для каждого соединения имеется только один срочный указатель. 2. Для каждого соединения допускается только одна отметка внеполосных дан- ных. 3. Для каждого соединения имеется только один однобайтовын буфер, предназ- наченный для внеполосных данных (это имеет значение, только если внепо- лосные данные не считываются вместе с обычными данными). В листинге 21.10 показано, что вновь прибывшая отметка внеполосных дан- ных отменяет все предыдущие отметки, до которых принимающий процесс еще не дошел. Если внеполосные данные считываются вместе с обычными данными, то в случае прибытия новых внеполосных данных предыдущие не теряются, но теряются их отметки. Широкое применение внеполосных данных связано с протоколом Rlogin, ког- да клиент прерывает программу, выполняемую на стороне сервера [94, с. 393- 394]. Сервер должен сообщить клиенту, что нужно сбросить все данные, приня- тые от сервера, буферизованные и предназначенные для вывода на терминал. Сервер посылает клиенту специальный байт внеполосных данных, указывая тем самым, что необходимо сбросить все полученные данные. Когда клиент получает сигнал SIGURG, он просто считывает данные из сокета, пока не встречает отметку внеполосных данных, после чего он сбрасывает все данные вплоть до этой отмет- ки. (На с. 398-401 [94] показан пример подобного использования внеполосных данных вместе с выводом программы tcpdump.) Если в этом сценарии сервер по- сылает несколько внеполосных байтов, следующих с небольшими промежутка- ми друг за другом, то такая последовательность не оказывает влияния на клиент, поскольку клиент просто сбрасывает все данные, расположенные до последней отметки внеполосных данных. В итоге можно сказать, что польза применения внеполосных данных зависит от того, для каких целей они служат в приложении. Если их назначение в том, чтобы сообщить собеседнику о необходимости сбросить все обычные данные, расположенные до отметки, то утрата промежуточных внеполосных данных и их отметок не повлечет никаких последствий. Но если потеря внеполосных данных недопустима, то эти данные следует получать вместе с обычными данными. Бо- лее того, байты, посланные как внеполосные данные, требуется каким-то обра- зом отличать от обычных данных, так как промежуточные отметки могут быть перезаписаны при получении новых внеполосных данных. Telnet, например, по- сылает свои собственные команды в потоке обычных данных между клиентом
634 Глава 21. Внеполосные данные и сервером, но ставит перед этими командами байт, содержащий 255 (поэтому для отправки этого значения требуется послать последовательно два байта, со- держащих 255). Эти байты позволяют отличить команды сервера от обычных пользовательских данных, но при этом для обнаружения команд сервера требу- ется, чтобы клиент и сервер обрабатывали каждый байт данных. 21.5. Клиент-серверные функции проверки пульса Теперь мы разработаем несколько простых функций проверки пульса для наших эхо-сервера и эхо-клиента. Эти функции способны быстро выявить сбой как на самом узле, так и в канале связи с ним. Перед тем как рассматривать эти функции, мы хотели бы привести несколько советов по их применению. Некоторые программисты пытаются использовать для обеспечения этой функциональности параметр SO_KEEPALIVE сокета TCP. Но TCP не посылает собеседнику проверочное сообщение {keepalive probe), пока не прой- дет 2 часа, в течение которых соединение будет оставаться неактивным. Узнав об этой особенности, многие обычно интересуются тем, как уменьшить время про- стоя до значительно более короткого промежутка (порядка секунд), чтобы быстрее обнаружить сбой. Хотя многие системы позволяют уменьшить значения пара- метров таймера при определении «жизнеспособности» собеседника (см. приложе- ние Е [94]), обычно эти параметры задаются для всего ядра, а не отдельно для каждого сокета, так что их изменение повлияет на все сокеты, в которых включен параметр SO_KEEPALIVE. Также можно добавить, что этот параметр исходно не был предназначен для данной цели (высокочастотный опрос, high-frequency polling). Далее, временная потеря связи между двумя концами соединения не всегда означает, что система вышла из строя. В TCP предусмотрена возможность учесть это обстоятельство, а в Беркли-реализации TCP перед тем, как разорвать соеди- нение, предпринимаются попытки повторной передачи через 8-10 минут. Более новые протоколы маршрутизации IP (например, OSPF) способны обнаружить сбой и, возможно, предложить альтернативный канал за более короткое время (возможно, порядка нескольких секунд). Поэтому для каждого конкретного при- ложения следует разобраться и решить, нужно ли закрывать соединение после того, как в течение 5 или 10 секунд от собеседника не был получен ответ. Для некоторых приложений требуется такая функциональность, но для большинства она не нужна. Для регулярного опроса собеседника мы будем использовать срочный режим TCP. Запросы будут посылаться каждую секунду, но максимальное время ожи- дания ответа мы ограничим 5 секундами. Эти параметры могут быть изменены приложением (в отличие от интервалов отправки проверочных сообщений при использовании SO_KEEPALIVE). Рисунок 21.3 иллюстрирует организацию свя- зи между клиентом и сервером. В нашем примере клиент посылает внеполосный байт серверу раз в секунду, а сервер, приняв этот байт, посылает в подтверждение приема внеполосный байт клиенту. Каждая сторона должна получить извещение, если другая оказывается недоступна. И клиент, и сервер раз в секунду увеличивают на 1 значение своей переменной ent, а при получении внеполосного байта обнуляют ее. Если этот
21,5. Клиент-серверные функции проверки пульса 635 Одно соединение TCP Рис. 21.3. Использование внеполосных данных для определения «жизнеспособности» клиента и сервера счетчик доходит до 5 (это значит, что в течение 5 секунд от собеседника не был получен внеполосный байт), считается, что произошел сбой. И клиент, и сервер для получения уведомления о прибытии внеполосного байта используют сигнал SIGURG. На указанном рисунке видно, что обычные данные, эхо-данные (отражен- ные данные) и внеполосные данные передаются по одному и тому же соедине- нию TCP. Функция main клиента — это та же функция main, которая была представлена в листинге 5.3. Наша функция str_cl 1 (которую мы не приводим здесь) лишь не- многим отличается от функции, показанной в листинге 6.2. 1. Мы вызываем нашу функцию heartbeat_cl 1 перед входом в цикл for для уста- новления параметров на стороне клиента: heartbeat_cl1(sockfd. 1. 5) Второй аргумент функции — это период (в секундах) отправки внеполосного байта, а третий — максимально допустимое время (количество секунд), по прошествии которого соединение будет закрыто. 2. Если функция select возвращает ошибку EINTR, мы продолжаем (continue) выполнять цикл и снова вызываем функцию sei ect. Обратите внимание, что, согласно рис. 21.3, клиент теперь перехватывает два сигнала — SIGURG и SIGALRM, так что мы должны быть готовы к обработке прерванного системного вызова. 3. Вместо того чтобы вызывать функцию fputs для помещения отраженной строки в стандартный поток вывода, мы вызываем функцию wri tel п. Мы делаем так, поскольку, как только что было сказано, при перехвате двух сигналов могут быть прерваны медленные системные вызовы, а некоторые версии стандарт- ной библиотеки ввода-вывода не способны корректно обрабатывать прерван- ные системные вызовы [58]. В листинге 21.11 показаны три функции, обеспечивающие функциональность проверки пульса (сервера клиентом).
636 Глава 21 Внеполосные данные Листинг 21.11. Проверка пульса сервера клиентом //oob/heartbeatcli с 1 #i nclude "unp h" 2 static int servfd. 3 static mt nsec /* количество секунд между сигналами*/ 4 static mt maxnprobes. /* максимально допустимое число сигналов до закрытия соединения */ 5 static int nprobes. /* число сигналов отправленных с момента последнего ответа сервера*/ 6 static voic 1 sig_urg(int). signalrm(int). 7 void 8 heartbeat_cli(int servfd_arg. int nsec_arg. int maxnprobes_arg) 9 { 10 servfd = servfd_arg. /* задание значений глобальных переменных для обработчиков сигналов */ 11 if ( (nsec = nsec_arg) < 1) 12 nsec = 1 13 if ( (maxnprobes = maxnprobes_arg) < nsec) 14 maxnprobes = nsec. 15 nprobes - 0. 16 Signal(SIGURG. sig_urg), 17 Fcntl(servfd. F_SET0WN, getpidO): 18 Signal(SIGALRM. sig_alrm): 19 alarm(nsec). 20 } 21 static void 22 sig_urg(int signo) 23 { 24 int n. 25 char c. 26 if ( (n = recv(servfd. &c, 1. MSG 008)) < 0) { 27 if (errno '» EWOULDBLOCK) 28 err_sys("recv error"). 29 } 30 nprobes = 0 /* обнуление счетчика */ 31 return. /* может прервать выполнение клиентского кода */ 32 } 33 static void 34 sig_alrm(int signo) 35 { 36 if (++nprobes > maxnprobes) { 37 fprintf(stderr, "server is unreachable\en"); 38 exit(0) 39 } 40 Send(servfd "1" 1. MSG__OOB). 41 alarm(nsec) 42 return. /* может прервать выполнение клиентского кода */ 43 } Глобальные переменные 2-5 Первые три переменные — это копии аргументов функции heartbeat cli: де- скриптор сокета (он необходим обработчикам сигналов для отправки и получе-
21 5. Клиент-серверные функции проверки пульса 637 ния внеполосных данных), период сигналов SIGALRM и максимально допустимое количество таких сигналов (если на все эти сигналы не поступило ответа от сер- вера, то клиент решает, что сервер или соединение неисправны). Переменная nprobes представляет собой счетчик сигналов SIGALRM, отправленных с момента последнего ответа сервера. Функция heartbeatjcli 7-20 Функция heartbeat_cl 1 проверяет правильность аргументов и сохраняет их. Для сигналов SIGALRM и SIGURG устанавливаются обработчики сигналов, а функция fcntl делает процесс владельцем сокета. Функция alarm задает время отправки первого сигнала SIGALRM. Обработчик сигнала SIGURG 21-32 Этот сигнал генерируется, когда прибывает внеполосное уведомление. Мы пы- таемся считать внеполосный байт, но даже если он не прибыл (ошибка EWOULDBLOCK), это игнорируется. Обратите внимание, что мы не получаем внеполосный байт вместе с обычными данными, так как это нарушило бы считывание клиентом обычных данных. Поскольку сервер функционирует нормально, переменная nprobes обнуляется. Обработчик сигнала SIGALRM 33-43 Этот сигнал генерируется регулярно. При этом увеличивается счетчик nprobes, и если он достигает величины maxprobes, мы делаем заключение, что узел сервера либо недоступен, либо неисправен. В данном примере мы заканчиваем на этом клиентский процесс, хотя возможны и другие варианты: сигнал может быть по- слан в основной цикл или функция heartbeat^ 1 i может иметь другой аргумент — функцию, вызываемую в том случае, если сервер не отвечает в течение заданного времени. Байт, содержащий символ "1" (фактическое содержимое этого байта не имеет никакого специального значения), посылается в качестве внеполосных данных, а функция alarm управляет временем отправки очередного сигнала SIGALRM. Функция ma i п сервера идентична той, которая приведена в листинге 5.9. Един- ственное ее отличие от функции str echo из листинга 5.2 заключается в дополни- тельной строке heartbeat_serv(sockfd 1 5) перед началом цикла for. Таким образом происходи г инициализация функции проверки пульса клиента сервером. В листинге 21.12 показаны функции проверки пульса клиента сервером. Листинг 21.12. Функции проверки пульса клиента //oob/heartbeatserv с 1 include 'unp h" 2 static int servfd. 3 static int nsec /* количество секунд между сигналами */ 4 static int maxnalarms: /* максимально допустимое число сигналов до закрытия соединения *7 5 static int nprobes. /* число сигналов отправленных с момента последнего ответа клиента */ продолжение &
638 Глава 21. Внеполосные данные Листинг 21.12 (продолжение) б static void sig_urg(int) sig_alrm(int). 7 void 8 heartbeat_serv(int servfd_arg. mt nsec_arg. mt maxnalarms_arg) 9 { 10 servfd = servfd_arg /* задание значений глобальных переменных для обработчиков сигналов */ 11 if ( (nsec - nsec_arg) < 1) 12 nsec = 1. 13 if ( (maxnalarms = maxnalarms_arg) < nsec) 14 maxnalarms = nsec, 15 Signal(SIGURG, sig_urg). 16 FcntKservfd. F_SETOWN, getpidO); 17 Signal(SIGALRM. sig_a1rm), 18 alarm(nsec). 19 } 20 static void 21 sig urgfint signo) 22 { 23 mt n. 24 char c. 25 if ( (n - recv(servfd. &c. 1. MSG 00В» < 0) { 26 if (errno != EWOULDBLOCK) " ‘ 27 err sysC'recv error”). 28 } 29 Send(servfd, &c. 1. MSG_OOB). /* отражение внеполосного байта */ 30 nprobes -0. /* установка в 0 счетчика */ 31 return. /* может прервать выполнение кода сервера */ 32 } 33 static void 34 sig_alrm(int signo) 35 { 36 if (++nprobes > maxnalarms) { 37 printfCno probes from clientlen"). 38 exit(0). 39 } 40 alarm(nsec). 41 return /* может прервать выполнение кода сервера */ 42 } Функция heartbeat_serv 7-19 Объявления переменных и функция heartbeat_serv практически идентичны соответствующим переменным и функции на стороне клиента. Обработчик сигнала SIGURG 20-32 Когда внеполосное уведомление принимается сервером, он пытается считать полученный байт. Как и на стороне клиента, даже если внеполосный байт не при- был, ошибка игнорируется. Этот внеполосный байт отражается и посылается на- зад клиенту также в виде внеполосных данных. Обратите внимание, что если
21.6. Резюме 639 функция recv возвращает ошибку EWOULDBLOCK, то что бы ни содержалось в дина- мической локальной переменной (automatic variable) с, оно отражается и отсыла- ется назад клиенту. Мы не используем значение (содержимое) байта внеполос- ных данных, поэтому здесь не возникает никаких проблем. Имеет значение лишь сам факт отправки одного байта внеполосных данных, каково бы ни было его со- держимое. Переменная nprobes обнуляется, поскольку мы только что получили уведомление о том, что с клиентом все в порядке. Обработчик сигнала SIGALRM 33-42 Переменная nprobes увеличивается на 1, и если ее значение достигает заданной величины maxprobes, процесс на стороне сервера завершается. В противном слу- чае посылается очередной сигнал SIGALRM. 21.6. Резюме В TCP не существует настоящих внеполосных данных. Вместо этого при перехо- де отправителя в срочный режим собеседнику отсылается в TCP-заголовке сроч- ный указатель. Получение этого указателя на другом конце соединения служит уведомлением для процесса о том, что отправитель вошел в срочный режим, а ука- затель указывает на последний байт внеполосных (срочных) данных. Но эти дан- ные отсылаются через то же соединение и подчиняются обычным функциям управления потоком данных TCP. В API сокетов срочный режим TCP сопоставляется внеполосным данным. Отправитель входит в срочный режим, задавая флаг MSG_OOB при вызове функции send. Последний байт данных, переданных с помощью этой функции, считается внеполосным байтом. Приемник получает уведомление о том, что его TCP полу- чил новый срочный указатель. Это происходит либо с помощью сигнала SIGURG, либо с помощью функции select, которая указывает, что на сокете возникла ис- ключительная ситуация. По умолчанию TCP извлекает байт с внеполосными данными и помешает его в специальный однобайтовый буфер для внеполосных данных, откуда принимающий процесс считывает его с помощью вызова функ- ции recv с флагом MSG_OOB. Имеется другой вариант — получатель может вклю- чить параметр сокета SO_OOBINLINE, и тогда внеполосный байт остается в потоке обычных данных. Независимо от того, какой метод используется принимающей стороной, уровень сокета поддерживает отметку внеполосных данных в потоке данных, и операция считывания остановится, когда дойдет до этой отметки. Что- бы определить, достигнута ли эта отметка, принимающий процесс использует функцию sockatmark. Внеполосные данные применяются не очень широко. Они используются в про- токолах Telnet и Rlogin, а также FTP. В FTP они находят применение лишь пото- му, что ранние реализации этого протокола не обеспечивали мультиплексирова- ние ввода-вывода. Внеполосные данные были предложены тогда, когда ресурсов (памяти и времени центрального процессора) не хватало. Сейчас при создании новых приложений, для которых требуется второй канал связи между собесед- никами, обладающий высоким приоритетом и не подверженный контролю со сто- роны функций управления потоком, предпочтительнее создать новое соедине- ние, чем использовать внеполосные данные.
640 Глава 21. Внеполосные данные Упражнения 1. Есть ли разница между одним вызовом функции sendtfd. "ab". 2. MSG_OOB) и двумя последовательными вызовами sendtfd "а'. 1 MSG_OOB). sendtfd. "b”. 1. MSG_OOB) 2. Переделайте программу, приведенную в листинге 21.4, так, чтобы использо- вать функцию pol 1 вместо функции select. 3. Переделайте функцию sig_al rm из листингов 21.11 и 21.12 так, чтобы в случае положительного значения счетчика nprobes (то есть если предыдущий запрос остался без ответа) с помощью функции write выводилось бы сообщение об этом. Запустите клиентскую и серверную программы на двух узлах в одной локальной сети и посмотрите, как часто будет выводиться это сообщение. Под- ключите стандартный поток ввода клиента к большому текстовому файлу, а стандартный поток вывода направьте во временный файл. Запустите снова клиентскую программу и сравните входной файл с выходным, чтобы прове- рить, все ли данные были доставлены. Стало ли сообщение выводиться чаще при передаче большого количества данных? Запустите клиентскую и сервер- ную программы на двух узлах в глобальной сети и сравните результаты с пе- редачей по локальной сети. 4. Перепишите клиент-серверные функции проверки пульса для случая, когда используется второе соединение TCP, а не срочные данные. Выполните зада- ния из предыдущего упражнения и сравните результаты.
ГЛАВА 22 Управляемый сигналом ввод-вывод 22.1. Введение Ввод-вывод, управляемый сигналом, подразумевает, что мы указываем ядру про- информировать нас сигналом, если что-либо произойдет с дескриптором. Исто- рически такой ввод-вывод назвали асинхронным вводом-выводом, но в действи- тельности описанный ниже управляемый сигналом ввод-вывод асинхронным не является. Последний обычно определяется как операция ввода-вывода с немед- ленным возвратом управления процессу после инициирования операции в ядре. Процесс продолжает выполняться во время того, как производится ввод-вывод. Когда операция ввода-вывода завершается или обнаруживается некоторая ошиб- ка, процесс некоторым образом оповещается. В разделе 6.2 проводилось сравне- ние всех возможных типов ввода-вывода и было показано различие между вво- дом-выводом, управляемым сигналом, и асинхронным вводом-выводом. Следует отметигь, что неблокируемый ввод-вывод, описанный в главе 15, так- же не является асинхронным. При неблокпруемом вводе-выводе ядро не возвра- щает управление после инициирования операции ввода-вывода. Управление возвращается немедленно, только если операция пе может быть выполнена без блокирования процесса. ПРИМЕЧАЯИЕ ----------------------------------------- Стандарт Posix 1 обеспечивает истинный асинхронный ввод-вывод с помощью функ- ций аю ХХХ Эти функции позволяют процессу решить, генерировать ли при завер- шении ввода-вывода сигнал и какой именно Беркли-реализации поддерживают ввод-вывод, управляемый сигналом, для сокетов и устройств вывода с помощью сигнала SIGIO SVR4 поддерживает ввод- вывод, управляемый сигналом, для потоковых устройств с помощью сигнала SIGPOLL, который в данном случае приравнивается к SIGIO. 22.2. Управляемый сигналом ввод-вывод для сокетов Для использования ввода-вывода, управляемого сигналом, с сокетом (SIGIO) нет обходимо, чтобы процесс выполнил три следующих этапа:
642 Глава 22. Управляемый сигналом ввод-вывод 1. Установка обработчика сигнала SIGIO. 2. Определение владельца сокета. Обычно это выполняется с помощью коман- ды F_SETOWN функции fcntl (см. табл. 7.5). 3. Для сокета должен быть разрешен управляемый сигналом ввод-вывод, что обычно выполняется с помощью команды F_SETFL функции fcntl или путем включения флага O_ASYNC (см. табл. 7.5). ПРИМЕЧАНИЕ --------------------------------------------------------- Флаг O_ASYNC появился в стандарте Posix.lg. Ни в одной из систем, приведенных на рис. 1.7, этот флаг не поддерживается. Для разрешения управляемого сигналом ввода- вывода вместо этого флага мы используем в листинге 22.2 функцию ioctl с фла- гом FIOASYNC. Следует отметить, что разрабогчики Posix.lg выбрали не самое удач- ное имя для нового флага: ему больше подходит имя O_SIGIO. Обработчик сигнала должен быть установлен до того, как будет задан владелец сокета. В Беркли-реализациях порядок вызова этих функций не имеет значения, поскольку по умолчанию сигнал SIGIO игнорируется. Поэтому если изменить порядок вызова функ- ций на противоположный, появится небольшая вероятность того, что сигнал будет сге- нерирован после вызова функции fcntl, но перед вызовом функции signal. Однако если это произойдет, то сигнал просто не будет учитываться. В SVR4 SIGIO определяется в заголовочном файле <sys/signal.h> как SIGPOLL, а действием по умолчанию для SIGPOLL является прерывание процесса. Таким образом, в SVR4 желательно быть уверенным в том, что обработчик сигнала установлен до задания владельца сокета. Перевести сокет в режим ввода-вывода, управляемого сигналом, несложно. Сложнее оказывается определение условий, которые должны приводить к гене- рации сигнала SIGIO для владельца сокета. Это зависит от лежащего в основе про- токола. Сигнал SIGIO и сокеты UDP Использовать ввод-вывод, управляемый сигналом, с сокетами UDP довольно легко. Сигнал генерируется в следующих случаях: на сокет прибывает дейтаграмма; на сокете возникает асинхронная ошибка. Таким образом, когда мы перехватываем сигнал SIGIO для сокета UDP, вызы- вается функция recvfrom как для чтения дейтаграммы, так и для получения асин- хронной ошибки. Асинхронные ошибки, касающиеся UDP-сокетов, обсуждались в разделе 8.9. Напомним, что эти сигналы генерируются, только если сокет UDP является присоединенным (создан с помощью вызова функции connect). ПРИМЕЧАНИЕ --------------------------------------------------------- Сигнал SIGIO генерируется для этих двух условий путем вызова макроса sorwakeup, описываемого в книге [105, с. 775, с. 779 с. 784]. Сигнал SIGIO и сокеты TCP К сожалению, использовать управляемый сигналом ввод-вывод для сокетов TCP почти бесполезно. Проблема состоит в том, что сигнал генерируется слишком
22.2. Управляемый сигналом ввод-вывод для сокетов 643 часто, а само по себе возникновение сигнала не позволяет выяснить, что произош- ло. Как отмечается на с. 439 книги [105], генерацию сигнала SIGIO для ТСР-соке- та вызывают все нижеперечисленные ситуации (притом, что ввод-вывод, управ- ляемый сигналом, разрешен): на прослушиваемом сокете выполнен запрос на соединение; инициирован запрос на отключение; запрос па отключение выполнен; половина соединения закрыта; данные доставлены на сокет; данные отправлены с сокета (то есть в' буфере отправки имеется свободное место); произошла асинхронная ошибка. Например, если одновременно осуществляется и чтение, и запись в ТСР-со- кет, то сигнал SIGIO генерируется, и когда поступают новые данные, и когда под- тверждается прием ранее записанных данных, а обработчик сигнала не имеет возможности их различить. Если используется сигнал SIGIO, то для предотвраще- ния блокирования при выполнении функции read или write TCP-сокет должен находиться в режиме неблокируемого ввода-вывода. Следует использовать сиг- нал SIGIO лишь с прослушиваемым сокетом TCP, поскольку для прослушиваемо- го сокета этот сигнал генерируется только при завершении установления нового соединения. Единственное реальное применение управляемого сигналом ввода-вывода с сокетами, которое удалось обнаружить автору, — это сервер NTP (Network Time Protocol — сетевой протокол синхронизации времени), использующий протокол UDP. Основной цикл этого сервера получает дейтаграмму от клиента и посылает ответ. Е1о обработка клиентского запроса на этом сервере требует некоторого не- нулевого количества времени (больше, чем для нашего тривиального эхо-сервера). Процесс-сервер Рис. 22.1. Два варианта построения UDP-сервера
644 Глава 22. Управляемый сигналом ввод-вывод Серверу важно записать точные отметки времени для каждой принимаемой дей- таграммы, поскольку это значение возвращается клиенту и используется им для вычисления времени обращения к серверу (RTT). На рис. 22.1 показаны два ва- рианта построения такого UDP-сервера. Большинство UDP-серверов (включая эхо-сервер, описанный в главе 8) по- строены так, как показано на рисунке слева. Однако NTP-сервер использует спо- соб, показанный справа: когда прибывает новая дейтаграмма, она читается обра- ботчиком сигнала SIGIO, который также записывает время прибытия дейтаграммы. Далее дейтаграмма помещается в другую очередь внутри процесса, из которой она будет удалена, а затем обработана основным циклом сервера. Это усложняет код сервера, но зато обеспечивает точные отметки времени прибытия дейтаграмм. ПРИМЕЧАНИЕ -------------------------------------------------------- Напомним, что согласно листингу 20.3, процесс может установить параметр сокета IPRECVDSTADDR, чтобы получить адрес получателя пришедшей UDP-дейтаграм- мы. Можно возразить, что вместе с полученной дейтаграммой UDP должны быть воз- вращены два дополнительных фрагмента информации — интерфейс, па котором была получена дейтаграмма (этот интерфейс может отличаться от адреса получателя, если узел использует более типичную модель системы с гибкой привязкой), и время прибы- тия дейтаграммы. Для IPv6 интерфейс, па котором была получена дейтаграмма, можно получить, если включен параметр сокета IPV6 PKTINFO (см. раздел 20.8). Апало! ичный параметр сокета IP_RECVIF для IPv4 описывался в разделе 20 2. В FreeBSD также предусмотрен параметр сокета SOTIMESTAMP, возвращающий время получения дей гаграммы как вспомогательные данные в струю уре timeval. В Linux существует флаг SIOCGSTAMP для функции ioctl, которая возвращает структуру timeval, содержащую время прибытия дейтаграммы. 22.3. Эхо-сервер UDP с использованием сигнала SIGIO В этом разделе мы приведем пример, аналогичный правой части рис. 22.1: UDP- сервер, использующий сигнал SIGIO для получения приходящих дейтаграмм. Этот пример также иллюстрирует использование надежных сигналов стандарта Posix. В данном случае клиент совсем не изменен по сравнению с листингами 8.3 и 8.4, а функция сервера main не изменилась по сравнению с листингом 8.1. Един- ственные внесенные изменения касаются функции dg echo, которая будет приве- дена в следующих четырех листингах. В листинге 22.1* представлены глобаль- ные объявления. Листинг 22.1. Глобальные объявления //sigio/dgechoOl с 1 #include "unp h" 2 static int sockfd 1 Все исходные коды npoipaMM, опубликованные в этой-книге, вы можете найти по адресу irttp:// www.piter.com/download
22.3. Эхо-сервер UDP с использованием сигнала SIGIO 645 3 #define QSIZE 8 /* размер входной очереди */ 4 fdefine MAXDG 4096 /* максимальный размер дейтаграммы*/ 5 typedef struct { 6 void *dg_data. 7 size_t dg_len, 8 struct sockaddr *dg_sa: 9 socklen_t dg_salen 10 } DG. 11 static DG dg[QSIZE] 12 static long cntread[QSIZE + 1]. 13 static int iget 14 static int iput 15 static int nqueue. 16 static socklen_t clilen. 17 static void sig_io(int). 18 static void sigjiup(int): /* указатель на текущую дейтаграмму */ /* длина дейтаграммы */ /* указатель на sockaddr}} с адресом клиента */ /* длина sockaddr}} */ /* очередь дейтаграмм для обработки */ /* диагностический счетчик*/ /* следующий элемент для обработки в основном цикле */ /* следующий элемент для считывания обработчиком сигналов */ /* количество дейтаграмм в очереди на обработку в основном цикле */ /* максимальная длина sockaddr}} */ Очередь принимаемых дейтаграмм 3-12 Обработчик сигнала SIGIO помещает приходящие дейтаграммы в очередь. Эта очередь является массивом структур DG, который интерпретируется как кольце- вой буфер. Каждая структура содержит указатель на принятую дейтаграмму, ее длину и указатель на структуру адреса сокета, содержащую адрес протокола кли- ента и размер адреса протокола. В памяти размещается столько этих структур, сколько указано в QSIZE (в данном случае 8), и в листинге 22.2 будет видно, что функция dg echo для размещения в памяти всех структур дейтаграмм и адресов сокетов вызывает функцию mall ос. Также происходит выделение памяти под ди- агностический счетчик entread, который будет рассмотрен чуть ниже. На рис. 22.2 Рис. 22.2. Структуры данных, используемые для хранения прибывающих дейтаграмм и структур адресов их сокетов
646 Глава 22 Управляемый сигналом ввод-вывод приведен массив структур, при этом предполагается, что первый элемент указы- вает па 150-байтовую дейтаграмму, а длина связанного с ней адреса сокета рав- на 16 Индексы массивов 13 15 Переменная iget является индексом следующего элемента массива для обра- ботки в основном цикле, а переменная i put — это индекс следующего элемента массива, в котором сохраняется результат действия обработчика сигнала Пере- менная nqueue обозначает полное количество дейтаграмм, предназначенных для обработки в основном цикле В листинге 22 2 показан основной цикл сервера — функция dg echo Листинг 22.2. Функция dg_echo основной обрабатывающий цикл сервера 7/sigio/dgechoOl с 19 20 21 22 23 void dg echo(int sockfd arg SA *pcliaddr socklen t clilen arg) { int 1 const int on = 1 24 25 26 sigset_t zeromask newmask oldmask sockfd = sockfd_arg clilen = clilen_arg 27 28 29 30 31 32 for (i = 0 i < QSIZE i++) { /* инициализация очереди */ dg[i] dg_data = Malloc(MAXDG) dg[i] dg_sa = Malloc(clilen) dg[i] dg_salen = clilen iget = iput = nqueue = 0 33 34 35 36 37 Signal(SIGHUP sig_hup) Signal(SIGIO sig_io) Fcntl (sockfd F SETOVJN getpidO) Ioctl(sockfd FIOASYNC &on) Ioctl(sockfd FIONBIO &on) 38 39 40 41 Sigemptyset(&zeromask) /* инициализация трех наборов сигналов */ Sigemptyset(&oldmask) Sigemptyset(&newmask) Sigaddsett&newmask SIGIO) /* сигнал который хотим блокировать*/ 42 43 44 45 Sigprocmask(SIG_BLOCK &newmask &oldmask) for ( ) { while (nqueue == 0) sigsuspend(&zeromask) /* ждем дейтаграмму для обработки */ 46 47 /* разблокирование SIGIO */ Sigprocmask(SIG_SETMASK &oldmask NULL) 48 49 Sendtotsockfd dgtiget] dg_data dgliget] dg_len 0. dgliget] dg_sa dg[iget] dg_salen) 50 51 if (++iget >= QSIZE) iget = 0
22 3 Эхо-сервер UDP с использованием сигнала SIGIO 647 52 /* блокировка SIGIO */ 53 Sigprocmask(SIG_BLOCK &newmask &oldmask) 54 nqueue 55 } 56 } Инициализация очереди принятых дейтаграмм 27 32 Дескриптор сокета сохраняется в глобальной переменной, поскольку он необ- ходим обработчику сигналов Происходит инициализация очереди принятых дей- таграмм Установка обработчиков сигналов и флагов сокетов 33 37 Для сигналов SIGHUP (он используется для диат ностических целей) и SIGIO уста- навливаются обработчики С помощью функции fcntl задается владелец сокета, а с помощью функции i octi устанавливаются флаги ввода-вывода, управляемого сигналом, и неблокируемого ввода-вывода ПРИМЕЧАНИЕ ---------------------------------------------------------- Ранее отмечалось, что для ра трешения ввода-вывода, управляемо! о сигналом, в Posix 1g применяется флаг O_ASYNC функции fcntl, по поскольку большинство систем пока его не поддерживают, мы испочь>уем функцию ioctl Поскольку большинство систем не поддерживают флаг O NONBLOCK для включения деблокируемою ввода-выво- да, здесь также рассмотрен вариант использования функции ioctl Инициализация наборов сигналов 38 41 Инициализируется три набора сигналов zeromask (никогда не изменяется), oldmask (хранит старую маску сигнала, когда SIGIO блокируется) и newmask Функ- ция sigaddset включает в набор newmask бит, соответствующий SIGIO Блокирование SIGIO и ожидание дальнейших действий 42 45 Функция sigprocmask сохраняет текущую маску сигналов процесса в oldmask, а за- тем выполняет логическое сложение, сравнивая newmask с текущей маской сигна- лов Такие действия блокируют сит нал SIGIO и возвращают текущую маску сиг- налов Далее мы заходим в цикл for и проверяем счетчик nqueue Пока этот счетчик равен нулю, ничего делать не нужно, и мы вызываем функцию sigsuspend Эга функция Posix, сохранив в одной из локальных переменных текущую маску сиг- налов, и присваивает текущей маске значение аргумента zeromask Так как zeromask является пустым набором сигналов, то разрешаются любые сигналы Как только перехватывается сигнал и завершается обработчик, функция sigsuspend также завершается (Это необычная функция, поскольку опа всегда возвращает ошибку EINTR ) Прежде чем завершиться, функция sigsuspend всегда устанавливает такое значение маски сигналов, которое предшествовало ее вызову (в данном случае newmask) Таким образом т арантируется, что кот да функция sigsuspend возвращает значение, сигнал SIGIO блокирован Именно поэтому можно проверять счетчик nqueue, поскольку известно, что пока он проверяется, сигнал SIGIO не может быть доставлен
648 Глава 22. Управляемый сигналом ввод-вывод ПРИМЕЧАНИЕ----------------------------------------------------- А что произойдет, если сигнал SIGIO не будет блокирован во время проверки пере- менной nqueue, используемой совмеспюосновным циклом и обработчиком сигналов? Может случиться так, что проверка nqueue покажет нулевое значение, а сразу после проверки возникнет сигнал и nqueue станет равна 1. Далее мы вызовем функцию sigsuspend и перейдем в режим ожидания, в результате чего пропустим сигнал. После вызова функции sigsuspend мы пе выйдем из режима ожидания, пока не нос тупит дру- юй сшнал Это похоже на ситуацию гонок (race condition), описанную в разделе 8 5. Разблокирование SIGIO и отправка ответа 46 51 Разблокируем сигнал SIGIO с помощью вызова sigprocmask, чтобы вернуть маске сигналов процесса значение, сохраненное ранее (oldmask). В этом случае ответ посылается с помощью функции sendto. Индекс iget увеличился на 1, и если его значение совпадает с количеством элементов массива, он снова обнуляется Мас- сив трактуется как кольцевой буфер. Обратите внимание, чго нет необходимости блокировать сигнал SIGIO во время изменения переменной iget, поскольку этот индекс используется только в основном цикле и никогда не изменяется обработ- чиком сигнала. Блокирование SIGIO 52-54 Сигнал SIGIO блокируется, а значение переменной nqueue уменьшается на 1. Во время изменения данной переменной необходимо заблокировать сигнал, по- скольку она используется совместно основным циклом и обработчиком сигнала. Также необходимо, чтобы сигнал SIGIO был заблокирован, когда в начале цикла происходит проверка переменной nqueue. Альтернативным способом является удаление обоих вызовов функции sig- procmask, находящихся внутри цикла for, что предотвращает разблокирование сигнала и его последующее блокирование. Однако проблема состоит в следую- щем. в такой ситуации весь цикл выполняется при блокированном сигнале, что уменьшает быстроту реагирования обработчика сигнала. При этом дейтаграммы не будут теряться (если, конечно, буфер приема сокета достаточно велик), но выдача сигнала процессу будет задерживаться на то время, в течение которого сигнал находится в состоянии блокировки. Одной из задач при создании прило- жений, производящих обработку сигнала, должна быть минимизация времени .блокирования сигнала. Листинг 22.3. Обработчик сигнала SIGIO //sigio/dgechoOl с 57 static void 58 sig_io(int signo) 59 { 60 ssize_t len 61 int nread 62 DG *ptr 63 for (nread = 0. ) { 64 if (nqueue >= QSIZE) 65 err quitCreceive overflow"):
22.3. Эхо-сервер UDP с использованием сигнала SIGIO 649 67 ptr->dg_salen = clilen 68 len = recvfrom(sockfd ptr->dg_data MAXDG 0, 69 ptr->dg_sa &ptr->dg_salen) 70 if (len < 0) { 71 if (errno = EWOULDBLOCK) 72 break /* все сделано очередь на чтение отсутствует */ 73 else 74 err_sys( 'recvfrom error') 75 ! 76 ptr->dg_len = len 77 nread++ 78 nqueue++ 79 if (++iput >= QSIZE) 80 iput = 0 81 } 82 cntread[nread]++, /* гистограмма количества дейтаграмм считанных для каждого сигнала */ 83 } Во время создания этих обработчиков сигналов была обнаружена следующая проблема, в стандарте Posix сигналы обычно не помещаются в очередь Это озна- чает, что если во время пребывания внутри обработчика сигналов (при этом га- рантируется, что сигнал заблокирован) возникает еще два сигнала, то сигнал вы- дается еще один раз. ПРИМЕЧАНИЕ ----------------------------------------------------------- В оандарю Posix 1 предусмотрено несколько сигналов реального времени, для кото- рых обеспечивается буферизация, однако ряд других chi палов, в том числе и SIGIO, обычно не буферизуются, то есть не помещаются в очередь па доставку Рассмотрим следующий сценарий. Прибывает дейтаграмма и выдается сигнал Обработчик сигнала считывает дей гаграмму и помещает ее в очередь к основно- му циклу. Но во время работы обработчика сигнала приходят еще две дейтаграм- мы, вызывая генерацию сигнала еще дважды Поскольку сигнал блокирован, то когда обработчик сигналов возвращает управление после обработки первого сиг- нала, он запустится снова всего лишь один раз. После второго запуска обработ- чик считывает вторую дейтаграмму, а третья будет оставлена в очереди приходя- щих дейтаграмм сокета. Эта третья дейтаграмма будет прочитана, только если (и только когда) придет четвертая Когда придет четвертая дейтаграмма, считана и поставлена в очередь на обработку основным циклом будет именно третья, а не четвертая дейтаграмма. Поскольку сигналы не помещаю гея в очередь, дескриптор, установленный для управляемого сигналом ввода-вывода, обычно переводится в неблокируемый режим Обработчик сигнала SIGIO мы кодируем таким образом, чтобы он считы- вал дейтаграммы в цикле, который прерывается, только когда при считывании возвращается ошибка EWOULDBLOCK. Проверка переполнения очереди 64-65 Если очередь переполняется, происходит завершение работы. Для обработки такой ситуации существуют и другие способы (например, можно размещать в па-
650 Глава 22. Управляемый сигналом ввод-вывод мяти дополнительные буферы), но для данного примера достаточно простого за- вершения. Чтение дейтаграммы 5-76 На неблокируемом сокете вызывается функция recvfrom. Элемент массива, обо- значенный индексом 1 put, — это то место, куда записывается дейтаграмма. Если нет дейтаграмм, которые нужно считывать, мы выходим из цикла for с помощью оператора break. Увеличение счетчиков и индекса на единицу '-80 Переменная nread является диагностическим счетчиком количества дейтаграмм, читаемых за 1 сигнал. Переменная nqueue — это количество дейтаграмм для обра- ботки основным циклом. 82 Прежде чем обработчик сигналов возвращает управление, он увеличивает счет- чик на единицу в соответствии с количеством дейтаграмм, прочитанных за 1 сиг- нал. Этот массив приведен в листинге 22.4 и представляет собой диагностиче- скую информацию для обработки сигнала SIGHUP. Последняя функция (листинг 22.4) представляет собой обработчик сигнала SIGHUP, который выводит массив ent read. Он считает количество дейтаграмм, про- читанных за 1 сигнал. Листинг 22.4. Обработчик сигнала SIGHUP //sigio/dgechoOl с 84 static void 85 sig_hup(int signo) 86 { 87 int 88 for (i =0: i <= QSIZE i++) 89 printft "cntread[W] = £ld\en", i, cntread[i]), 90 } Чтобы проиллюстрировать, что сигналы не буферизуются и что в дополнение к установке флага, указывающего на управляемый сигналом ввод-вывод, необхо- димо перевести сокет в неблокпруемый режим, запустим этот сервер с шестью клиентами одновременно. Каждый клиент посылает серверу 3645 строк (для от- ражения). При этом каждый клиент запускается из сценария интерпретатора (shell script) в фоновом режиме, так что все клиенты стартуют примерно одновремен- но. Когда все клиенты завершены, серверу посылается сигнал SIGHUP, в результате чего сервер выводит получившийся массив entread: bsdi % udpservOl cntread[0] = 2 cntread[l] = 21838 cntread[2] = 12 cntread[3] = 1 cntread[4] = 0 cntread[5] = 1 cntread[6] = 0 cntread[7] = 0 cntread[8] = 0 Большую часть времени обработчик сигналов читает только одну дейтаграм- му, но бывает, что готово больше одной дейтаграммы. Поскольку мы считываем
Упражнение 651 дейтаграммы в цикле обработчика сигнала, дейтаграмма, прибывшая во время считывания других дейтаграмм, будет считана вместе с этими дейтаграммами (в том же вызове обработчика), а сигнал об ее прибытии будет отложен и достав- лен процессу после завершения обработчика. Это приведет к повторному вызову обработчика, но считывать ему будет нечего (отсюда cntread[0]>0). Наконец, можно проверить, что взвешенная сумма элементов массива (21 838 х 1+12 х 2+1 х 3+ + 1x5 = 21 870) равна 6 х 3645 (количество клиентов х количество строк кли- ента). 22.4. Резюме При управляемом сигналом вводе-выводе ядро уведомляет процесс сигналом SIGIO, если «что-нибудь» происходит па сокете. Для присоединенного TCP-сокета существует множество ситуаций, которые вызывают такое уведомление, что делает эту возможность практически бес- полезной. Для прослушиваемого TCP-сокета уведомление приходит процессу в случае готовности принятия нового соединения. Для UDP такое уведомление означает, что либо пришла дейтаграмма, либо произошла асинхронная ошибка: в обоих случаях вызывается recvfrom. С помощью метода, аналогичного применяемому для сервера NTP, был изме- нен эхо-сервер UDP для работы с вводом-выводом, управляемым сигналом: мы стремимся выполнить чтение дейтаграммы как можно быстрее после ее прибы- тия, чтобы получить точную отметку времени прибытия и постановить дейта- грамму в очередь для дальнейшей обработки. Упражнение Ниже приведен альтернативный вариант цикла; рассмотренного в листин- ге 22.2: for ( . ) { Sigprocmask(SIG_BLOCK &newmask &oldmask) while (nqueue == 0) sigsuspend(&zeromask), /* ожидание дейтаграммы для обработки*/, nqueue--. /* разблокирование SIGIO */ Sigprocmask(SIG_SETMASK. &oldmask, NULL). Sendtotsockfd. dg[iget] dg_data, dg[iget].dg_len. 0. dgLiget] dg_sa. dg[iget] dg_salen), if (++iget >= QSIZE) iget = 0, } Верна ли такая модификация?
ГЛАВА 23 Программные потоки 2 3.1. Введение Согласно традиционной модели U nix, когда процессу требуется, чтобы некое дей- ствие было выполнено каким-либо другим объек гом, он порождает дочерний про- цесс, используя функцию fork, и этим порожденным процессом выполняется необходимое действие. Большинство сетевых серверов под Unix устроены имен- но таким образом, как мы видели при рассмотрении примера параллельного (concurrent) сервера: родительский процесс осуществляет соединение с помощью функции accept и порождает дочерний процесс, используя функцию fork, а затем дочерний процесс занимается обработкой клиентского запроса. Хотя эта концепция с успехом использовалась на протяжении многих лет, с функцией fork связаны определенные неудобства. Стоимость функции fork довольно высока, так как при се использовании тре- буется скопировать все содержимое памяти из родительского процесса в до- черний, продублировать все дескрипторы и т. д. Текущие реализации исполь- зуют технологию, называемую копированием при записи {copy-on-write), при которой копирование пространства данных из родительского процесса в до- черний происходит лишь тогда, когда дочернему процессу требуется своя соб- ственная копия. Но несмотря на эту оптимизацию, стоимость функции fork остается высокой. Для передачи данных между родительским и дочерним процессами после вы- зова функции fork требуется использовать средства взаимодействия процес- сов (IPC). Передача информации перед вызовом fork не вызывает затрудне- ний, так как при запуске дочерний процесс получает от родительского копию пространства данных и копии всех родительских дескрипторов. Но возвраще- ние информации из дочернего процесса в родительский требует большей ра- боты. Обе проблемы могут быть разрешены путем использования программных по- токов (threads). Программные потоки иногда называются облегченными процес- сами {lightweight processes), так как поток проще, чем процесс. Это означает, что создание потока требует в 10-100 раз меньше времени, чем создание процесса. Все потоки одного процесса совместно используют его глобальные перемен- ные, поэтому им легко обмениваться информацией, но это приводит к необходи- мости синхронизации. Однако общими становятся не только глобальные пере- менные. Все потоки одного процесса разделяют:
23.2. Основные функции для работы с потоками: создание и завершение 653 • инструкции процесса; большую часть данных; открытые файлы (например, дескрипторы); - обработчики сигналов и вообще настройки для работы с сигналами (действие сигнала); текущий рабочий каталог; идентификаторы пользователя и группы пользователей. У каждого потока имеются собственные: * идентификатор потока; набор регистров, включая счетчик команд и указатель стека; стек (для локальных переменных и адресов возврата); переменная errno; маска сигналов; приоритет. ПРИМЕЧАНИЕ ------------------------------------------------------ Как сказано в разделе 11.14, можно рассматривать обработчик сигнала как некую раз- новидность потока. В традиционной модели Unix у нас имеется основной поток вы- полнения и обработчик сигнала (другой поток). Если в основном потоке в момент воз- никновения сш нала происходит корректировка связного списка и обработчик сигнала также пытается изменить связный список, обычно начинается путаница. Основ- ной поток и обработчик сигнала совместно используют одни и те же глобальные пере- менные, по у каждого из них имеется свой собственный сгек. В этой книге мы рассматриваем потоки Posix, которые также называются Pthreads (Posix threads). Они были стандартизованы в 1995 году как часть Posix. 1с и будут поддерживаться большинством версий Unix. Мы увидим, что все назва- ния функций Pthread начинаются с символов pthread_. Эта глава является введе- нием в концепцию потоков, необходимым для того, чтобы в дальнейшем мы могли использовать потоки в наших сетевых приложениях. Более подробную инфор- мацию вы можете найти в [16]. 2 3.2. Основные функции для работы с потоками: создание и завершение потоков В этом разделе мы рассматриваем пять основных функций для работы с потока- ми, а в следующих двух разделах мы используем эги функции для модификации клиента и сервера TCP таким образом, чтобы в них вместо функции fork исполь- зовались программные потоки. Функция pthread_create Когда программа запускается с помощью функции ехес, создается один поток, называемый начальным (initial) или главным (main). Дополнительные потоки со- здаются функцией pthead_create.
654 Глава 23. Программные потоки #include <pthread h> int pthread_create(pthread__t *tid const pthread_attr_t *dttr, void *(*/wc)(void *) void *arg) Возвращает 0 в случае успешного выполнения положительное значение Еххх в случае ошибки Каждый поток процесса обладает собственным идентификатором потока (thread ID), относящимся к типу данных pthread_t (как правило, это целое число без знака, unsigned int). При успешном создании нового потока его идентифика- тор возвращается через указатель tid. У каждого потока имеется несколько атрибутов-, его приоритет, исходный размер стека, указание па то, должен ли этот поток являться демоном или пет, и т. д. При создании потока мы можем задать эти атрибуты, инициализируя пере- менную pthread_attr_t, что позволяет заменить значение, заданное по умолчанию. Обычно мы используем значение по умолчанию, в этом случае мы задаем аргу- мент attr равным пустому указателю. Наконец, при создании потока мы должны указать, какую функцию будет выполнять этот поток. Выполнение потока начинается с вызова заданной функ- ции, а завершается либо явно (вызовом pthread_exit), либо неявно (когда вы- званная функция возвращает управление). Адрес функции задается аргументом tunc, и она вызывается с единственным аргументом-указателем arg. Если этой функции необходимо передать несколько аргументов, следует поместить их в не- которую структуру и передать адрес этой структуры как единственный аргумент функции. Обратите внимание на объявления tunc и arg. Функции передается один аргу- мент — указатель на неопределенный тип (generic pointer type), vol d *. Это позво- ляет нам передавать потоку с помощью единственного указателя все, что требу- ется, и точно так же поток возвращает любые данные, используя этот указатель. Возвращаемое значение функций Pthread — это обычно 0 в случае успешного выполнения или ненулевая величина в случае ошибки. Но в отличие о г функций сокетов и большинства системных вызовов, для которых в случае ошибки воз- вращается -1 и переменной еггпо присваивается некоторое положительное зна- чение (код ошибки), функции Pthread возвращают сам код ошибки. Например, если функция pthread_create не может создать новый поток, так как мы превыси- ли допустимый системный предел количества потоков, функция возврагитзначе- ние EAGAIN. Функции Pthread не присваивают переменной еггпо никаких значений. Соглашение о том, что 0 является индикатором успешного выполнения, а ненуле- вое значение — индикатором ошибки, не приводит к противоречию, так как все зна- чения Еххх, определенные в заголовочном файле <sys errno h>, являются положи- тельными. Ни одному из имен ошибок Еххх не сопоставлено нулевое значение. Функция pthreadjoin Мы можем приостановить выполнение текущего потока и ждать завершения вы- полнения какого-либо другого потока, используя функцию phtreadjoi п. Сравни- вая потоки и процессы Unix, можно сказать, что функция pthread_create анало- гична функции fork, а функция pthreadjoin — функции waitpid. #include <pthread h> int pthreadjoint pthreadJ tid void **status) Возвращает 0 в случае успешного выполнения положительное значение Еххх в случае ошибки
23.2. Основные функции для работы с потоками: создание и завершение 655 Следует указать идентификатор tт d того потока, завершения которого мы ждем. К сожалению, нет способа указать, что мы ждем завершения любого потока дан- ного процесса (тогда как при работе с процессами мы могли с помощью функции wai tpi d ждать завершения любого процесса, задав аргумент идентификатора про- цесса, равный -1). Мы вернемся к этой проблеме при обсуждении листинга 23.11. Если указатель status непустой, то значение, возвращаемое потоком (указа- тель на некоторый объект), хранится в ячейке памяти, на которую указывает status. Функция pthread_self Каждый поток снабжен идентификатором, уникальным в пределах данного про- цесса. Идентификатор потока возвращается функцией pthread_create и, как мы видели, используется функцией pthreadjoi п. Поток может узнать свой собствен- ный идентификатор с помощью вызова pthread_self. include <pthread h> pthread_t pthread_se’lf(void). Возвращает идентификатор вызывающего потока Сравнивая потоки и процессы Unix, можно отметить, что функция pthread sel f аналогична функции getpid. Функция pthread_detach Поток может быть либо присоединяемым (joinable), каким он является по умол- чанию, либо отсоединенным (detached). Когда присоединяемый поток завершает свое выполнение, его статус завершения и идентификатор сохраняются, пока дру- гой поток данного процесса не вызовет функцию pthread join. В свою очередь, отсоединенный поток напоминает процесс-демон: когда он завершается, все за- нимаемые им ресурсы освобождаются и мы не можем отслеживать его заверше- ние. Если один поток должен знать, когда завершится выполнение другого пото- ка, нам следует оставить последний присоединяемым. Функция pthread detach изменяет состояние потока, превращая его из присо- единяемого в отсоединенный. #include <pthread h> int pthread_detach(pthread_t tid) Возвращает 0 в случае успешного выполнения, положительное значение Еххх в случае ошибки Эта функция обычно вызывается потоком при необходимости изменить соб- ственный статус в следующем формате: pthread_detach (pthread_self()) Функция pthread_exit Одним из способов завершения потока является вызов функции pthread exit. #include <pthread h> void pthread_exit{void ^status) Ничего не возвращает вызвавшему потоку Если поток не является отсоединенным, идентификатор потока и статус за- вершения сохраняются до того момента, пока какой-либо другой поток данного процесса не вызовет функцию pthreadjoi п.
656 Глава 23 Программные потоки Указатель status не должен указывать на объект локальный по отношению к вызывающему потоку так как этот объект будет уничтожен при завершении потока Существуют и другие способы завершения потока Функция, которая была вызвана потоком (третий аргумент функции pthread_ create), может возвратить управление в вызывающий процесс Поскольку со гласно своему объявлению эта функция возвращает указатель void возвра- щаемое ею значение играет роль статуса завершения (exit status) даннш о по- тока Если функция main данного процесса возвращает управление или любой по ток вызывает функцию exit, процесс завершается, в том числе завершается выполнение всех потоков 2 3.3. Использование потоков в функции strcli В качестве первого примера использования потоков мы перепишем нашу функ цию str cl 1 В листинге 15 6 была представлена версия этой функции в которой использовалась функция fork Напомним, что были также представлены и неко торые другие версии этой функции изначально в листинге 5 4 функция блоки- ровалась в ожидании ответа и была как мы показали, далека от оптимальной в случае пакетного ввода в листинге 6 2 применяется блокируемый ввод-вывод и функция select, версии, показанные в листинге 15 1 и далее, используют не блокируемый ввод-вывод На рис 23 1 показана структура очередной версии функции str_cli, на этот раз использующей потоки, а в листинге 23 11 представлен код этой функции Рис. 23.1. Измененная функция str cli использующая потоки Листинг 23.1. Функция str_ch, использующая потоки //threads/strclithread с 1 include unpthread h 2 void *copyto(void *) 3 static int sockfd /* глобальная переменная к которой имеют доступ оба потока */ 1 Вес исходные коды программ опубликованные в этой книге вы можете найти по адресу http // www piter com/download
23 3 Использование потоков в функции str cli 657 4 static FILE *fp 5 void 6 str_cli(FILE *fp_arg int sockfd_arg) 1 { 8 char recvline[MAXLINE] 9 pthread_t tid 10 sockfd - sockfd arg /* копирование аргументов во внешние Иеремённые */ И fp = fp_arg 12 Pthread__create(&tid NULL copyto NULL) 13 while (Readlinetsockfd recvline MAXLINE) > 0) 14 Fputs(recvline stdout) 15 } 16 void * 17 copytotvoid *arg) 18 { 19 char sendline[MAXLINE] 20 while (Fgets(sendline MAXLINE fp) l= NULL) 21 Writentsockfd sendline strlen(sendline)) 22 Shutdown(sockfd SHUT_WR) /* признак конца файла в стандартном потоке ввода отправка сегмента FIN */ 23 return (NULL) 24 /* завершение потока происходит когда в стандартном потоке ввода встречается признак конца файла */ 25 } Заголовочный файл unpthread.h 1 Мы впервые встречаемся с заголовочным файлом unpthread h Он включает наш обычный заголовочный файл Posix 1 unp h, затем — заголовочный файл Pthread <pthread h>, и далее определяет прототипы для наших потоковых функций-обер- ток Pthread XXX (см раздел 1 4), название каждой из которых начинается с Pthread_ Сохранение аргументов во внешних переменных 10 И Для потока, который мы собираемся создать, требуются значения двух аргу- ментов функции str_cl 1 fp — стандартный указатель структуры FILE для входно- го файла и sockfd — сокет TCP связанный с сервером Для простоты мы храним эти два значения во внешних переменных Альтернативой является запись этих двух значений в структуру, указатель на которую затем передается в качестве аргумента создаваемому потоку Создание нового потока 12 Создается поток и значение нового идентификатора ротока сохраняется в tid Функция, выполняемая новым потоком, — это copyto. Никакие аргументы пото- ку не передаются Главный цикл потока: копирование из сокета в стандартный поток вывода 13 14 В основном цикле вызываются функции readl i пе и fputs, которые осуществлй- ют копирование из сокета в стандартный поток вывода
658 Глава 23. Программные потоки Завершение 15 Когда функция str_cl i возвращает управление, функция mam завершается при помощи вызова функции exit (см. раздел 5.4). При этом завершаются все потоки данного процесса. В обычном сценарии другой поток уже должен завершиться в результате считывания признака конца файла из стандартного потока ввода. Но в случае, когда сервер преждевременно завершил свою работу (см. раздел 5.12), при вызове функции exi t завершается также и другой поток, чего мы и добиваемся. Поток copyto 6-25 Этот поток просто осуществляет копирование из стандартного потока ввода в сокет. Когда он считывает признак конца файла из стандартного потока ввода, на сокете вызывается функция shutdown и отсылается сегмент FIN, после чего по- ток возвращает управление. При выполнении оператора return (то есть когда функ- ция, запустившая поток, возвращает управление) поток также завершается. В конце раздела 15.2 мы привели результаты измерений времени выполнения для пяти различных способов реализации функции str_cli. Мы отметили, что версия, с применением потоков выполняется всего 8,5 секунды — немногим быс- трее, чем версия, использующая функцию fork (как мы и ожидали), но медлен- нее, чем версия с неблокируемым вводом-выводом. Тем не менее, сравнивая уст- ройство версии с неблокируемым вводом-выводом (см. раздел 15.2) и версии с использованием потоков, мы заметили, что первая гораздо сложнее. Поэтому мы рекомендуем использовать именно версию с потоками, а не с неблокируемым вводом-выводом. 23.4. Использование потоков в эхо-сервере TCP Теперь мы перепишем эхо-сервер TCP, приведенный в листинге 5.1, используя для каждого клиента по одному потоку вместо одного процесса. Кроме того, с по- мощью нашей функции tcp_l i sten мы сделаем эту версию не зависящей от прото- кола. В листинге 23.2 показан код сервера. Листинг 23.2. Эхо-сервер TCP, использующий потоки //threads/tcpservOl с 1 #include "unpthread h" 2 static void *doit(void *). /* каждый поток выполняет ,эту функцию */ 3 int 4 main(int argc. char **argv) 5 { 6 int listenfd. connfd. 7 pthread_t tid, 8 socklen_t addrlen len. 9 struct sockaddr *cliaddr. 10 if (argc == 2) 11 listenfd = Tcp_listent NULL, argv[l] Saddrlen). 12 else if (argc == 3) 13 listenfd = Tcp_listen(argv[l], argv[2], Saddrlen).
23.4. Использование потоков в эхо-сервере TCP 659 14 else 15 err_quit("usage tcpservOl [ <host> ] <service or port>”), 16 cliaddr = Malloc(addrlen). 17 for ( ,) { 18 len = addrlen. 19 connfd = Accept(1istenfd cliaddr. &len) 20 Pthread_create(&tid. NULL. &doit. (void *) connfd): 21 } 22 } 23 static void * 24 doittvoid *arg) 25 { 26 Pthread_detach(pthread_self()). 27 str_echo((int) arg). /* та же функция, что и раньше */ 28 Closet(int) arg). /* мы закончили с присоединенным сокетом */ 29 return (NULL). 30 } Создание потока 17-21 Когда функция accept возвращает управление, мы вызываем функцию pthread_ create вместо функции fork. Мы передаем функции doт t единственный аргумент — дескриптор присоединенного сокета connfd. ПРИМЕЧАНИЕ-------------------------------------------------------- Мы преобразуем целочисленный дескриптор соке га к указателю па неопределенный тип (void). В ANSI С не гарантируемся, что такое преобразование будет выполнено кор- ректно, — мы можем быть уверены лишь в том, что оно сработает в гех системах, в ко- торых размер целого числа по превышает размера указателя. К счастью, большинство реализаций Unix обладают этим свойством (см. табл. 1 5) Чутьниже мы поговорим об этом подробнее. Функция потока 23-30 doit — это функция, выполняемая потоком. Поток отделяет себя с помощью функции pthread_detach, так как нет причины, по которой для главного потока имело бы смысл ждать завершения каждого созданного им потока. Функция str_echo не изменилась и осталась такой же, как в листинге 5.2. Когда эта функ- ция завершается, следует вызвать функцию cl ose для того, чтобы закрыть присо- единенный сокет, поскольку этот поток использует все дескрипторы совместно с главным потоком. При использовании функции fork дочерний процесс не дол- жен специально закрывать присоединенный сокет, так как при завершении до- чернего процесса все открытые дескрипторы закрываются (см. упражнение 23.5). Обратите также внимание на то, что главный поток не закрывает присоеди- ненный сокет, что всегда происходило, когда параллельный сервер вызывал функ- цию fork. Это объясняется тем, что все потоки внутри процесса совместно ис- пользуют все дескрипторы, поэтому если главному потоку потребуется вызвать функцию cl ose, эго приведет к закрытию соединения. Создание нового потока не влияет на счетчики ссылок для открытых дескрипторов, в отличие от того, что происходит при вызове функции fork.
660 Глава 23 Программные потоки В этой программе имеется одна неявная ошибка о которой рассказывается в разделе 23 5 Можете ли вы ее обнаружить? (См упражнение 23 5 ) Передача аргументов новым потокам Мы уже упомянули, что в листиш е 23 2 мы преобразуем целочисленную пере- менную connfd к указателю на неопределенный тип (void), но этот способ не рабо- тает в некоторых системах Для корректной обработки данной ситуации требу- ются дополнительные усилия В первую очередь, заметим, что мы не можем просто передать адрес connfd но- вого потока, то есть следующий код не будет работать int main(int argc char **argv) int listenfd connfd for ( ) { len = addrlen connfd = Accept(1istenfd cliaddr &len) Pthread_create(&tid NULL &doit Sconnfd) } } static void * doittvoid *arg) int connfd connfd = *((int *) arg) Pthread_detach(pthread_self()) str_echo(connfd) /* та же функция что и прежде */ Close(connfd) /* мы закончили с присоединенным сокетом */ return(NULL) } С точки зрения ANSI С здесь все в порядке мы гарантированно можем преоб- разовать целочисленный указатель к типу void * и затем обратно преобразовать получившийся указатель на неопределенный тип к целочисленному указателю Проблема заключается в другом — на что именно он будет указывать? В главном потоке имеется одна целочисленная переменная connfd, и при каж- дом вызове функции accept значение этой переменной изменяется на новое (в со- ответствии с новым присоединенным сокетом) Может сложиться следующая ситуация Функция accept возвращает управление, записывается новое значение пере- менной connfd (допустим, новый дескриптор равен 5) п в главном потоке вы- зывается функция pthread_create Указатель на connfd (а не фактическое его значение!) является последним аргументом функции pthread_create Создается новый поток, и начинает выполняться функция doi t Готово другое соединение, и главный поток снова начинает выполняться (прежде, чем начнется выполнение вновь созданного потока) Завершается функция accept, записывается новое значение переменной connfd (например,
23 4 Использование потоков в эхо-сервере TCP 661 значение нового дескриптора равно 6) и главный поток вновь вызывает функ- цию pthread_create Хотя созданы два новых потока, оба они будут работать с одним и тем же по- следним значением переменной connfd, которое, согласно нашему предположе- нию, равно 6 Проблема заключается в том, что несколько потоков получают доступ к совместно используемой переменной (целочисленному значению, хра- нящемуся в connfd) при отсутствии синхронизации В листинге 23 2 мы решаем эту проблему, передавая значение переменной connfd функции pthread_create, вместо того чтобы передавать указатель на это значение Этот метод работает бла- годаря тому способу, которым целочисленные значения в С передаются вызыва- емой функции (копия значения помещается в стек вызванной функции) В листинге 23 3 показано более удачное решение описанной проблемы Листинг 23.3. Эхо-сервер TCP, использующий потоки с более переносимой передачей аргументов //threads/tcpserv02 с 1 #include unpthread h 2 static void *doit(void *) /* каждый поток выполняет эту функцию */ 3 int 4 main(int argc char **argv) 5 { 6 int listenfd *iptr 7 thread_t tid 8 socklen_t addrlen len 9 struct sockaddr *cliaddr 10 if (argc == 2) 11 listenfd = Tcp_listen(NULL argv[l] Saddrlen) 12 else if (argc == 3) 13 listenfd = Tcp_listen(argv[l] argv[2] &addrlen) 14 else 15 err_quit( usage tcpservOl [ <host> ] <service or port>“). 16 cliaddr = Ma11 octaddrlen) 17 for ( ) { 18 len = addrlen 19 iptr = Malloctsizeof(int)) 20 *iptr = Accept(1istenfd cliaddr 81en), 21 Pthread_create(&tid NULL &doit iptr) 22 } 23 } 24 static void * 25 doittvoid *arg) 26 { 27 int connfd 28 connfd - *((int *) arg) 29 free(arg) 30 Pthread_detach(pthread_self()) 31 str_echo(connfd) /* та же функция что и раньше */ 32 Close(connfd) /* мы закончили С присоединенным сокетом */ продолжение &
662 Глава 23. Программные потоки Листинг 23.3 (продолжение) 33 return (NULL). 34 } .7-22 Каждый раз перед вызовом функции accept мы вызываем функцию mal 1 ос и вы- деляем в памяти пространство для целочисленной переменной (дескриптора при- соединенного сокета). Таким образом каждый поток получает свою собственную копию этого дескриптора. ’8-29 Поток получает значение дескриптора присоединенного сокета, а затем осво- бождает занимаемую им память с помощью функции free. Исторически функции mal 1 ос и free не допускали повторного вхождения. Это означает, что при вызове той или иной функции из обработчика сигнала в то вре- мя, когда главный поток выполняет одну из них, возникает большая путаница, так как эти функции оперируют статическими структурами данных. Как же мы можем вызывать эти две функции в листинге 23.3? Дело в том, что в Posix. 1 тре- буется, чтобы эти две функции, так же как и многие другие, были безопасными в многопоточной среде (thread-safe). Обычно это достигается с помощью некото- рой разновидности синхронизации, осуществляемой внутри библиотечных функ- ций и являющейся для нас прозрачной (то есть незаметной). Функции, безопасные в многопоточной среде Стандарт Posix. 1 требует, чтобы все определенные в нем функции, а также функ- ции, определенные в стандарте ANSI С, были безопасными в многопоточной сре- де. Исключения из этого правила приведены в табл. 23.1. К сожалению, в Posix. 1 ничего не сказано о безопасности в многопоточной среде по отношению к функциям сетевого API. Последние пять строк в этой таб- лице появились благодаря Unix 98. В разделе 11.14 мы говорили о том, что функ- ции gethostbyname и gethostbyaddr не допускают повторного вхождения. Как уже отмечалось, некоторые производители определяют версии этих функций, обла- дающие свойством безопасности в многопоточной среде (их названия заканчи- ваются на г), но поскольку они не стандартизованы, лучше от них отказаться. Все функции getXXX, не допускающие повторного вхождения, были приведены в табл. 9.2. Таблица 23.1. Функции, безопасные в многопоточной среде Могут не быть безопасными в многопоточной среде Должны быть безопасными в многопоточной среде Комментарии asctime asctinier Безопасна в многопо- точной среде только в случае непустого аргумента ctermid ctune dirtier getcunlocked getcharunlocked getgrid getgnd_r getgrnam getgrnamr getlogin getloginr getpwnam getpwnamr
23 5. Собственные данные потоков 663 Могут не быть безопасными в многопоточной среде Должны быть безопасными Комментарии в многопоточной среде getpwmd gin time localtune putc unlocked putcharunlocked rand readdir strtock getpwuidr gintime г localtnner ran d r readdirr strtockr tmpnam Безопасна в мноюпо- точпои среде только в случае и еп у ст ol о аргумента ttyname gethostXXX getnetXXX getprotoXXX getservXXX met ntoa ttynamer Приведенная таблица позволяет заключить, что общим способом сделать функ- цию допускающей повторное вхождение является определение новой функции с названием, оканчивающимся на _г. Обе функции будут безопасными в много- поточной среде, только если вызывающий процесс выделяет в памяти место для результата и передает соответствующий указатель как аргумент функции. 23.5. Собственные данные потоков При написании программного кода к главе 27 автор совершил обычную програм- мистскую ошибку, характерную для ситуации, когда приложение, пе использую- щее потоки, преобразуется к виду, в котором потоки используются. Эга ошибка была обнаружена только при запуске сервера, представленного в листинге 27.20, но сама ошибка содержится не в этом листиш е, а в функции readl т пе, которая вызывается для обслуживания клиентских запросов. Та же самая функция вы- зывается в листинге 23.2. Как и в случае многих других программных ошибок, связанных с потоками, эта ошибка была недетерминированной. Фактически обе программы в листин- гах 23.2 и 27.20 работали, но ошибка была обнаружена при выполнении тестов синхронизации для версии с потоками в листинге 27.22. Эта версия работала, когда клиент и сервер были на одном узле, но когда клиент и сервер находились на разных узлах, в различных точках происходили сбои и наша функция web_child (листинг 27.5) выдавала ошибку, сообщая, что клиент запросил 0 байт. После нескольких часов безрезультатной отладки выяснилось, что в процессе увеличе- ния быстродействия функции readl те (см. листинг 3.10) были добавлены стати- ческие переменные (см. листинг 3.11). Это увеличение быстродействия и стало причиной сбоев, когда функция вызывалась из различных потоков в пределах одного процесса.
664 Глава 23. Программные потоки Эта проблема часто возникает в ситуациях, когда существующие функции преобразуются к виду, необходимому для работы с потоками. Существует несколь- ко способов решения этой проблемы. 1. Использование собственных данных потоков (thread-specific data). Это нетри- виальная задача, и функция при этом преобразуется к такому виду, что может использоваться только в системах, поддерживающих потоки. Преимущество этого подхода заключается в том, что не меняется вызывающая последователь- ность, и все изменения связаны с библиотечной функцией, а не с приложени- ями, которые вызывают эту функцию. Позже в этом разделе мы покажем безопасную в многопоточной среде версию функции readl i пе, созданную с при- менении собственных данных потоков. 2. Изменение вызывающей последовательности таким образом, чтобы вызыва- ющий процесс упаковывал все аргументы в некую структуру, а также записы- вал в нее статические переменные из листинга 3.11. Это также было сделано, и в листинге 23.4 показана новая структура и новые прототипы функций. Листинг 23.4. Структура данных и прототип функции для версии функции readline, допускающей повторное вхождение typedef struct { int read_fd. /* дескриптор, указывающий, откуда считываются данные */ char *read_ptr; /* буфер, куда передаются данные */ size__t readjnaxlen. /* максимальное количество байтов, которое может быть считано */ /* следующие три элемента для внутреннего использования функцией */ int rl_cnt. /* инициализируется нулем */ char *rl_bufptr. /* инициализируется значением rl_buf */ char rl_buf[MAXLINE]. } Rime. void readline_rinit(int ssize_t readl ine_r(Rl me *) ssize t Readline r(Rline *) void * size_t. Rline *). Эти новые функции могут использоваться как в системах с поддержкой пото- ков, так и в тех, где потоки не поддерживаются, но все приложения, вызываю- щие функцию readline, должны быть изменены. 3. Отказ от увеличения быстродействия, достигнутого в листинге 3.11, и возвра- щение к более старой версии, представленной в листинге 3.10. Использование собственных данных потоков — это распространенный способ сделать существующую функцию безопасной в многопоточной среде. Прежде чем описывать функции Pthread, работающие с такими данными, мы опишем саму концепцию и возможный способ реализации, так как эти функции кажутся более сложными, чем являются на самом деле. ПРИМ ЕЧ АН И Е -------------------------------------------------------- Частично осложнения возникают но том причине, что во всех книгах, где идет речь о потоках, описание собственных данных потоков дается по образцу стандарта Pthread. Пары ключ-значение и ключи рассматриваются в них как непрозрачные объекты. Мы описываем собственные данные потоков в терминах индексов и указателей, так как обычно в реализациях в качестве ключей используются небольшие положи- тельные целые числа (индексы), а значение, ассоциированное с ключом, — это просто указатель на область памяти, выделяемую с помощью функции malloc.
23.5. Собственные данные потоков 665 В каждой системе поддерживается ограниченное количество объектов соб- ственных данных потоков. В Posix. 1 требуется, чтобы этот предел не превышал 128 (на каждый процесс), и в следующем примере мы используем именно это значение. Система (вероятно, библиотека потоков) поддерживает один массив структур (которые мы называем структурами Key) для каждого процесса, как по- казано на рис. 23.2. Рис. 23.2. Возможная реализация собственных данных потока Флаг в структуре Key указывает, используется ли в настоящий момент данный элемент массива. Все флаги инициализируются как указывающие на то, что эле- мент не используется. Когда поток вызывает функцию pthread_key_create для со- здания нового элемента собственных данных потока, система отыскивает в мас- сиве структур Key первую структуру, не используемую в настоящий момент. Индекс этой структуры, который может иметь значение от 0 до 127, называется ключом и возвращается вызывающему потоку как результат выполнения функ- ции. О втором элементе структуры Key, так называемом указателе-деструкторе, мы поговорим чуть позже. В дополнение к массиву структур Key, общему для всего процесса, система хра- нит набор сведений о каждом потоке процесса в структуре Pthread. Частью этой структуры является массив указателей, состоящий из 128 элементов, который мы называем ркеу. Это показано на рис. 23.3. Поток 0 Pthread{} Поток n Pthread{} ркеу [£)] pkeyfl] pkey[127] Другая информация о потоке NULL pkey[0] NULL pkeyfl] NULL pkey[127] Другая информация о потоке null" NULL о 1 Элементы Г собственных данных NULL J потока Указатель Указатель Указатель Указатель Указатель Указатель Рис. 23.3. Информация, хранящаяся в системе для каждого потока Все элементы массива ркеу инициализируются пустыми указателями. Эти 128 указателей являются «значениями», ассоциированными с каждым из 128 «клю- чей» процесса.
666 Глава 23. Программные потоки Когда мы с помощью функции pthread_key_create создаем ключ, система сооб- щает нам фактическое значение ключа (индекс). Затем каждый поток может за- писать значение (указатель), связанное с этим ключом, и как правило, каждый поток получает этот указатель в виде возвращаемого значения функции та 11 ос. Частично путаница с собственными данными потока обусловлена тем, что указа- тель в паре ключ-значение играет роль значения, по сами собственные данные потока — это то, на что указывает данный указатель. Теперь мы перейдем к примеру применения собственных данных потока, пред- полагая, что наша функция readl ire использует их для хранения информации о состоянии каждого потока при последовательных обращениях к ней. Вскоре мы покажем код, выполняющий эту задачу, в котором функция readl те модифи- цирована так, чтобы реализовать представленную ниже последовательность шагов. 1. Запускается процесс, и создается несколько потоков. 2. Один из потоков вызовет функцию readl i пе первой, а та, в свою очередь, вызо- вет функцию phtread key create. Система отыщет первую неиспользуемую структуру Key (см. рис. 23.2) и возвратит вызывающему процессу ее индекс. В данном примере мы предполагаем, что индекс равен 1. Мы будем использовать функцию pthread_once, чтобы гарантировать, что функ- ция pthread_key_create вызывается только первым потоком, вызвавшим функ- цию readl те. 3. Функция readl i пе вызывает функцию pthread_getspeci f iс для получения зна- чения pkey [ 1 ] («указатель» на рис. 23.3 для ключа, имеющего значение 1) для данного потока, и эта функция возвращает пустой указатель. Затем функция readl те вызывает функцию mal 1 ос для выделения памяти, которая необходи- ма для хранения информации о каждом потоке при последовательных вызо- вах функции readl i пе. Функция readl i ne инициализирует эти области памяти по мере надобности и вызывает функцию pthread_setspeci f i с, чтобы установить Системные структуры данных Память, выделенная потоком Элементы собственных данных потока Фактические данные Рис. 23.4. Соответствие между областью памяти, выделенной функцией malloc и указателем собственных данных потока
23.5. Собственные данные потоков 667 указатель собственных данных потока (ркеу[Ц), соответствующий данному ключу, на только что выделенную область памяти. Мы показываем этот про- цесс на рис. 23.4, предполагая, что вызывающий поток — это поток с номером О в данном процессе. На этом рисунке следует обратить внимание на то, что структура Pthread под- держивается системой (вероятно, библиотекой потоков), но фактически соб- ственные данные потока, которые мы размещаем в памяти с помощью функ- ции mall ос, поддерживаются нашей функцией (в данном случае readl i пе). Все, что делает функция pthread_setspeci f 1 с, — это установка указателя для данно- го ключа в структуре Pthread на выделенную область памяти. Аналогично, дей- ствие функции pthread_getspecific сводится к возвращению этого указателя. 4. Другой поток, например поток с номером п, вызывает функцию readl ine, воз- можно, в тот момент, когда поток с номером 0 все еще находится в стадии вы- полнения функции readline. Функция readline вызывает функцию pthread_once, чтобы инициализировать ключ этого элемента собственных данных, но так как эта функция уже была однажды вызвана, то больше она не вызывается. 5. Функция readl i пе вызывает функцию pthread getspeci f i с для получения зна- чения указателя pkey[ 1] для данного потока, в результате чего возвращается пустой указатель. Затем поток вызывает функцию malloc и функцию pthread_ setspecific, как и в случае с потоком номер 0, инициализируя элемент соб- ственных данных потока, соответствующий этому ключу (1). Этот процесс иллюстрирует рис. 23.5. 6. Поток номер п продолжает выполнять функцию readl 1 пе, используя и моди- фицируя свои собственные данные потока. Один вопрос, который мы пока не рассмотрели, заключается в следующем: что происходит, когда поток завершает свое выполнение? Если поток вызвал Поток 0 Поток п Рис. 23.5. Структуры данных после того, как поток п инициализировал свои собственные данные
668 (лава 23. Программные потоки функцию read! i ne, эта функция выделила в памяти область, которая должна быть освобождена по завершении выполнения потока. Для этого используется указа- тель-деструктор, показанный на рис. 23.2. Когда поток, создающий элемент соб- ственных данных потока, вызывает функцию pthread_key_create, одним из аргумен- тов этой функции является указатель на функцию-деструктор Когда выполнение потока завершается, система перебирает массив ркеу для данного потока, вызы- вая соответствующую функцию-деструктор для каждого непустого указателя ркеу. Под «соответствующим деструктором» мы понимаем указатель на функцию, хра- нящийся в массиве Key с рис. 23 2. Таким образом осуществляется освобождение памяти, занимаемой собственными данными потока, когда выполнение потока завершается. Первые две функции, которые обычно вызываются при работе с собственны- ми данными потока, — это pthread_once и pthread_key_create. #include <pthread h> int pthread_once(pthread_once_t *onceptr void (*imt)(void)) int pthread_key_create(pthread_key_t *keyptr void (,*des true tor) (void *velue)) Обе функции возвращают 0 в случае успешного выполнения положительное значение Еххх в случае ошибки Функция pthread once обычно вызывается при вызове функции, манипулиру- ющей с собственными данными потока, но pthread once использует значение пе- ременной, на которую указывает onceptr, чтобы гарантировать, что функция init вызывается для каждого процесса только один раз. Функция pthread_key_create должна вызываться только один раз для данного ключа в пределах одного процесса. Значение ключа возвращается с помощью указателя keyptr, а функция-деструктор (если аргумент является непустым ука- зателем) будет вызываться каждым потоком по завершении его выполнения, если этот поток записывал какое-либо значение, соответствующее этому ключу. Обычно эти две функции используются следующим образом (если игнориро- вать возвращение ошибок): pthread_key_t rl_key pthread_once_t rl_once = PTHREAD_ONCE_INIT void readline_destructor(void *ptr) free(ptr) } void read!ine_once(void) { pthread_key_create(&rl_key, readline_destructor). } ssizet read]inet { pthread_once(&rl_once read!ine_once)
23.5. Собственные данные потоков 669 if ( (ptr = pthread_getspecific(rl_key)) = NULL) { ptr = Malloct ), pthread_setspecific(rl_key ptr) /* инициализация области памяти на которую указывает ptr */ } /* используются значения на которые указывает ptr */ } Каждый раз, когда вызывается функция readl иге, она вызывает функцию pthread_once. Эта функция использует значение, на которое указывает ее аргу- мент-указатель onceptr (содержащийся в переменной rl once), чтобы удостове- риться, что функция 1 m t вызывается только один раз. Функция инициализации readl 1 пе_опсе создает ключ для собственных данных потока, который хранится в rl_key и который функция readl ine затем использует в вызовах функций pthread_getspecific и pthread_setspecific Функции pthread_getspeci f тс и pthread setspeci f тс используются для того, чтобы получать и задавать значение, ассоциированное с данным ключом. Это значение представляет собой тот указатель, который показан на рис 23.3. На что указыва- ет этот указатель — зависит от приложения, но обычно он указывает на динами- чески выделяемый участок памят и. #include <pthread h> void *pthread_getspecific(pthread_key_t key) Возвращает указатель на собственные данные потока (возможно пустой указатель) int pthread_setspecific(pthread_key_t key const void *value) Возвращает 0 в случае успешного выполнения положительное значение Еххх в случае ошибки Обратите внимание на то, что аргументом функции pthread_key_create являет- ся указатель па ключ (поскольку эта функция хранит значение, присвоенное клю- чу), вто время как аргументами функций get и set являются сами ключи (кото- рые, скорее всего, представляют собой небольшие целые числа, как говорилось выше). Пример: функция readline, использующая собственные данные потока В этом разделе мы приводим полный пример использования собственных дан- ных потока, преобразуя оптимизированную версию функции readl 1 пе из листин- га 3.11 к виду, безопасному в многопоточной среде, не изменяя последователь- ность вызовов. В листинге 23 5 показана первая часть функции: переменные pthread_key_t и pthread_once_t, функции readline_destructor и readline_once и наша структура R11 пе, которая содержит всю информацию, нужную нам для каждого потока Листинг 23.5. Первая часть функции readlme, безопасной в многопоточной среде //threads/readline с 1 #include 'unpthread h' 2 static pthread_key_t rl_key 3 static pthread_once_t rl_once = PTHREAO_ONCE_LNLT, продолжение &
670 Глава 23 Программные потоки Листинг 23.5 (продолжение) 4 static void 5 readline_destructor(void *ptr) 6 { 7 free(ptr) 8 } 9 static void 10 readline_once(void) U { 12 Pthread_key_create(&rl_key readline_destructor) 13 } 14 typedef struct { 15 int rl_cnt /* инициализируется нулем */ 16 char *rl_bufptr /* инициализируется значением rl_buf */ 17 char rl_buf[MAXLINE] 18 } Rline Деструктор 4 8 Наша функция-деструктор просто освобождает всю память, которая была вы- делена для дапно! о потока «Одноразовая» функция 9 13 Мы увидим что наша «одноразовая» (то есть вызываемая только один раз) функция вызывается однократно из функции pthread once и просто создает ключ, который затем используется в функции read] i пе Структура Rline 14 18 Наша структура Rline содержит три переменные которые, будучи объявленными как статические (static) переменные в листинге 3 11, привели к возникновению описанных выше проблем Такая структура динамически выделяется в памяти для каждого потока, а по завершении выполнения этого потока она освобождает- ся функцией-деструктором В листин! е 23 6 показана сама функция read 1 з пе а также функция my_read, ко- торую она вызывает Этот листинг является модификацией листинга 311 Листинг 23.6. Вторая часть функции readline, безопасной в многопоточной среде //threads/readline с 19 static ssizet 20 my_read(Rline *tsd int fd char *ptr) 21 { 22 if (tsd >rl_cnt <= 0) { 23 again 24 if ( (tsd >rl_cnt = readtfd tsd >rl_buf MAXLINE)) < 0) { 25 if (errno == EINTR) 26 goto again 27 return ( 1) 28 } else if (tsd >rl_cnt == 0) 29 return (0) 30 tsd >rl_bufptr = tsd >rl_buf 31 } 32 tsd >rl_cnt 33 *ptr = *tsd >rl_bufptr++
23 5 Собственные данные потоков 671 34 35 return (1) } 36 37 38 39 40 41 ssize_t readlinednt fd void *vptr size t maxlen) { int n rc char c *ptr Rime *tsd 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 Pthread_once(&rl_once readline_once) if ( (tsd = pthread_getspecific(rl_key)) == NULL) { tsd = Callocd sizeof(Rline)) /* инициализируется нулём */ Pthread setspecific(rl key tsd) } ptr = vptr for (n = 1 n < maxlen n++) { if ( (rc = my_read(tsd fd &c)) == 1) { *ptr++ = c if (c == \en ) break } else if (rc == 0) { if (n = 1) return (0) /* EOF данные не были считаны *7 else break /* EOF было считано некоторое количество данных */ } else return ( 1) /* ошибка errno устанавливается функцией readO */ 61 62 63 *ptr = 0 return (n) Функция my_read 19 35 Первым аргументом функции является теперь указатель на структуру R11 ne, которая была размещена в памяти для данного потока (и содержит собственные данные этого потока) Размещение собственных данных потока в памяти 42 Сначала мы вызываем функцию pthread_once, так чтобы первый поток, вызыва- ющий функцию read] те в этом процессе, вызвал бы функцию readl we once для создания ключа собственных данных потока Получение указателя на собственные данные потока 43 46 Функция pthread_getspeci f 1 с возвращает указатель на структуру R1 1 пе для дан- ного потока Но если это первый вызов функции readl we данным потоком, то возвращаемым значением будет пустой указатель В таком случае мы выделяем в памяти место для структуры R1 i пе, а элемент rl_cnt этой структуры инициали- зируется нулем с помощью функции cal 1 ос Затем мы записываем этот указатель для данного потока, вызывая функцию pthread setspeci f 1 с Когда этот поток вы- зовет функцию readl i пе в следующий раз, функция pthread_getspeci f 1 с возвратит этот указатель, который был только что записан
672 (лава 23 Программные потоки 2 3.6. Web-клиент и одновременное соединение (продолжение) Вернемся к нашему примеру с web-клиентом из раздела 15 5 и перепишем его с использованием потоков вместо неблокируемой функции connect Мы можем оставить сокеты в их заданном по умолчанию виде — блокируемыми, и создать один поток на каждое соединение Каждый поток может блокироваться в вызове функции connect, так как ядро будет просто выполнять какой-либо другой поток, готовый к работе В листинге 23 7 показана первая часть нашей программы, глобальные пере- менные и начало функции maw Листинг 23.7. Глобальные переменные и начало функции maw //threads/webOl с 1 2 #include unpthread h #include <thread h> /* потоки Solaris */ 3 #define MAXFILES 20 4 #define SERV 80 /* номер порта ипи имя службы */ 5 struct file { 6 char *f_name /* имя файла */ 7 char *f_host /* имя узла или IP адрес */ 8 int f_fd /* дескриптор */ 9 int f_flags /* F_xxx ниже */ 10 pthread_t f_tid /* идентификатор потока */ 11 } file[MAXFILES] 12 #define F_CONNECTING 1 /* функция connectO в процессе выполнения */ 13 #define F READING 2 /* функция connectt) завершена выполняется считывание ♦/ 14 #define F_DONE 4 /* все сделано */ 15 #define GET_CMD GET Xs НТТР/1 0\ег\еп\ег\еп 16 int nconn nfiles nlefttoconn nlefttoread 17 void *do_get_read(void *) 18 void home_page(const char * const char *) 19 void wnte_get_cmd(struct file *) 20 int 21 maintint argc char **argv) 22 { 23 int in maxnconn 24 pthread_t tid 25 struct file *fptr 26 if (argc < 5) 27 err_quit( usage web <#conns> <IPaddr> «homepage* filtl ') 28 maxnconn = atoi(argv[l]) 29 nfiles = mintargc 4 MAXFILES) 30 for (1=0 i < nfiles i++) { 31 file[i] f_name = argv[i + 4] 32 file[i] fjiost = argv[2]
23 6 Web-клиент и одновременное соединение (продолжение) 673 33 file[i] fflags = О 34 } 35 printft nfiles = MXen nfiles) 36 home_page(argv[2] argv[3]) 37 nlefttoread = nlefttoconn = nfiles 38 nconn = 0 Глобальные переменные 16 Мы включаем заголовочный файл <th read h> вдобавок к обычному <pthread h>, так как нам требуется использовать потоки Solans в дополнение к потокам Pthread, как мы вскоре покажем 10 Мы добавили к структуре file один элемент — идентификатор потока f_tid Остальная часть этого кода аналогична коду в листинге 15 9 В этой версии нам не нужно использовать функцию sei ect, а следовательно, не нужны наборы де- скрипторов и переменная maxfd 36 Функция home_page не изменилась относительно листинга 1510 В листинге 23 8 показан основной рабочий цикл потока main Листинг 23.8. Основной рабочий цикл потока main //threads/webOl с 39 while (nlefttoread > 0) { 40 while (nconn < maxnconn && nlefttoconn > 0) { 41 /* находим файл для считывания */ 42 for (1=0 i < nfiles i++) 43 if (filefi] f_flags = 0) 44 break 45 if (i == nfiles) 46 err_quit( nlefttoconn = M but nothing found nlefttoconn) 47 filefi] f_flags = FCONNECTING 48 Pthread_create(&tid NULL &do_get_read &file[i]) 49 file[i] f_tid = tid 50 nconn++ 51 nlefttoconn 52 } 53 1 if ( (n = thr_join(0 &tid (void **) &fptr)) 0) 54 errno = n err_sys( thrjoin error ) 55 nconn 56 nlefttoread 57 printft thread id Xd for 5!s doneXen tid fptr >f_name) 58 } 59 exit(0) 60 } По возможности создаем другой поток ) 52 Если имеется возможность создать другой поток (nconn меньше, чем maxconn), мы так и делаем Функция, которую выполняет каждый новый поток, — это do_get_read, а ее аргументом является указатель на структуру f 11 е
674 Глава 23. Программные потоки Ждем, когда завершится выполнение какого-либо потока 53-54 Мы вызываем функцию потоков thrjoin Solaris с нулевым первым аргумен- том, чтобы дождаться завершения выполнения какого-либо из наших потоков. К сожалению, в Pthread не предусмотрен способ, с помощью которого мы могли бы ждать завершения выполнения любого потока, и функция pthread_joi п требу- ет, чтобы мы точно указали, завершения какого потока мы ждем. В разделе 23.9 мы увидим, что решение этой проблемы в случае применения технологии Pthread оказывается сложнее и требует использования условной переменной для сооб- щения главному потоку о завершении выполнения дополнительного потока. ПРИМЕЧАНИЕ ----------------------------------------------------------------------- Показанное здесь решение, в котором используется функция потоков thr_join Solans, не является, вообще говоря, совместимым со всеми системами. Тем не менее мы при- водим здесь эту версию web-клиента, использующую потоки, чтобы не осложнять об- суждение рассмотрением условных переменных и взаимных исключений (mutex). К счастью, в Solaris допустимо смешивать потоки Pthread и потоки Solaris. Когда автор этой книги выступил в Usenet с замечанием о том, что функция pthread_jom не способна обеспечить ожидание завершения любого потока, некоторые программис- ты, работавшие над стандартом Pthread, утверджали, что такое конструктивное реше- ние оправданно, так как никакая функция, в том числе и pthread_join, не может быть использована во всех ситуациях. Было также высказано утверждение, что в модели процессов существует взаимоотношение типа родительский процесс—дочерний про- цесс, поэтому имело смысл обеспечить возможность ожидания родительским процессом завершения выполнения дочернего процесса (с помощью функций wait или waitpid). Но в случае, когда речь идет о потоках, нет иерархических отношений типа «родитель- потомок», поэтому ожидание завершения любого потока не имеет смысла. Поток, ста- тус которого возвращается функцией, ожидающей завершения любого потока, — это не обязательно поток, созданный вызвавшим указанную функцию потоком. Также было сказано, что если кому-то действительно нужно дождаться завершения выполнения любого потока, это можно сделать (нетривиальным способом), используя условные переменные, как вскоре будет показано. Несмотря на эту аргументацию, автор книги не считает устройство функции pthread join обоснованным. В листинге 23.9 показана функция do_get_read, которая выполняется каждым потоком. Эта функция устанавливает соединение TCP, посылает серверу коман- ду HTTP GET и считывает ответ сервера. Листинг 23.9. Функция do_get_read //threads/webOl с 61 void * 62 do_get_read (void *vptr) 63 { 64 int fd, n. 65 char line [MAXLINE]. 66 struct file *fptr. 67 fptr = (struct file *) vptr, 68 fd = Tcp_connect (fptr->f_host. SERV). 69 fptr->f_fd = fd. 70 printf ("do_get_read for ts. fd £d. thread *d\n", 71 fptr->f_name fd. fptr->f_tid).
23.7. Взаимные исключения 675 72 wnte_get_cmd (fptr). 73 /*Чтение ответа сервера*/ 74 for ( . . ) { 75 Tf ( ( n - Read (fd. line. MAXLINE)) == 0) 76 break. /*сервер закрывает соединение*/ 77 pnntf ("read Xd bytes from £s\n" n, fptr->f_name). 78 } 79 pnntf ("end-of-file on £s\n". fptr->f_name). 80 Close (fd). 81 fptr->f_flags = F_DONE, /*сбрасываем F_READING*/ 82 return (fptr). /*завершение потока*/ 83 } Создание сокета TCP, установление соединения 68-71 Создается сокет TCP, и с помощью функции tcp_connect устанавливается со- единение. В данном случае используется обычный блокируемый сокет, поэтому поток будет блокирован при вызове функции connect, пока не будет установлено соединение. Запись ответа сервером 72 Функция wri te_get_cmd формирует команду HTTP GET и отсылает ее серверу. Мы не показываем эту функцию заново, так как единственным отличием от лис- тинга 15.12 является то, что в версии, использующей потоки, не вызывается мак- рос FD_SET и не используется maxfd. Чтение ответа сервера 73-82 Затем считывается ответ сервера. Когда соединение закрывается сервером, устанавливается флаг F DONE и функция возвращает управление, завершая вы- полнение потока. Мы также не показываем функцию home_page, так как она полностью повторя- ет версию, приведенную в листинге 15.10. Мы вернемся к этому примеру, заменив функцию Solaris thrjoin на более переносимую функцию семейства Pthread, но сначала нам необходимо обсудить взаимные исключения и условные переменные. 23.7. Взаимные исключения Обратите внимание на то, что в листинге 23.8 при завершении выполнения оче- редного потока в главном цикле уменьшаются на единицу и nconn, и nl efttoread. Мы могли бы поместить оба эти оператора уменьшения в одну функцию do_get_ read, что позволило бы каждому потоку уменьшать эти счетчики непосредствен- но перед тем, как выполнение потока завершается. Но это привело бы к возник- новению трудноуловимой серьезной ошибки параллельного программирования. Проблема, возникающая при помещении определенного кода в функцию, ко- торая выполняется каждым потоком, заключается в том, что обе эти переменные являются глобальными, а не собственными переменными потока. Если один по- ток в данный момент уменьшает значение переменной и это действие приоста- навливается, чтобы выполнился другой поток, который также станет уменьшать
676 Глава 23. Программные потоки на единицу эту переменную, может произойти ошибка. Предположим, например, что компилятор С осуществляет уменьшение переменной на единицу в три эта- па: загружает информацию из памяти в регистр, уменьшает значение регистра, а затем сохраняет значение регистра в памяти. Рассмотрим возможный сценарий. 1. Выполняется поток А, который загружает в регистр значение переменной псопп (равное 3). 2 Система переключается с выполнения потока А на выполнение потока В. Регистры потока А сохранены, регистры потока В восстановлены. 3. Поток В выполняет три этапа, составляющие оператор инкремента в языке С (псопп--), сохраняя новое значение переменной псопп, равное 2. 4. Впоследствии в некоторый момент времени система переключается на выпол- нение потока А. Восстанавливаются регистры потока А, и он продолжает вы- полняться с того места, на котором остановился, а именно начиная со второго этапа из трех, составляющих оператор декремента. Значение регистра умень- шается с 3 до 2, и значение 2 записывается в переменную псопп. Окончательный результат таков: значение псопп равно 2, в то время как оно должно быть равным 1. Это ошибка Подобные ошибки параллельного программирования трудно обнаружить по многим причинам. Во-первых, они возникают нечасто Тем не менее это ошибки, которые вызывают сбои в программах Во-вторых, ошибки такого типа возника- ют не систематически, так как зависят от недетерминированного совпадения не- скольких событий. Наконец, в некоторых системах аппаратные команды могут быть атомарными. Это значит, что имеется аппаратная команда уменьшения зна- чения целого числа на единицу (вместо трехступенчатой последовательности, которую мы предположили выше), а аппаратная команда не может быть прерва- на до окончания своего выполнения Но это не гарантировано для всех систем, так что код может работать в одной системе и не работать в другой. Программирование с использованием потоков является параллельным (pa- rallel), или одновременным (concurrent), программированием, так как несколько потоков могут выполняться параллельно (одновременно), получая доступ к од- ним и тем же переменным. Хотя ошибочный сценарий, рассмотренный нами выше, предполагает систему с одним центральным процессором, вероятность ошибки также присутствует, если потоки А и В выполняются в одно и то же время на разных процессорах в многопроцессорной системе В обычном программирова- нии под Unix мы не сталкиваемся с подобными ошибками, так как при использо- вании функции fork родительский и дочерний процессы не используют совмест- но ничего, кроме дескрипторов. Тем не менее мы столкнемся с ошибками этого типа при обсуждении совместного использовании памяти несколькими процес- сами. Эту проблему можно с легкостью продемонстрировать на примере потоков. В листинге 23.11 показана программа, которая создает два потока, после чего каж- дый поток инкрементирует некоторую глобальную переменную 5000 раз. Мы повысили вероятность ошибки за счет того, что потребовали от програм- мы получить текущее значение переменной counter, вывести это значение и запи- сать его. Если мы запустим эту программу, то получим результат, представлен- ный в листинге 23.10.
23 7. Взаимные исключения 677 Листинг 23.10. Результат выполнения программы, приведенной в листинге 23.11 4 1 4 2 4 3 4 4 продолжение выполнения потока номер 4 4 517 4 518 5 518 теперь выполняется поток номер 5 5 519 5 520 продолжение выполнения потока номер 5 5 926 5 927 4 519 теперь выполняется поток номер 4 записывая неверные значения 4 520 Листинг 23.11. Два потока, которые неверно увеличивают значение глобальной переменной //threads/exampleOl с 1 #include "unpthread h' 2 #define NLOOP 5000 3 int counter /* потоки должны увеличивать значение этой переменной */ 4 void *doit(void *). 5 int 6 main(int argc char **argv) 7 { 8 pthread_t tidA tidB 9 Pthread_create(&tidA NULL. &doit NULL) 10 Pthread_create(&tidB NULL &doit NULL). 11 /* ожидание завершения обоих потоков *7 12 Pthread_join(tidA NULL) 13 Pthread_join(tidB. NULL). 14 exit(0) 15 } 16 void * 17 doitlvoid *vptr) 18 { 19 int i val. 20 /* Каждый поток получает выводит и увеличивает на 21 * единицу переменную counter NLOOP раз Значение 22 * переменной должно увеличиваться монотонно 23 */ 24 for (1 = 0 1 < NLOOP 1++) { 25 val = counter 26 printfC'^d 2d\en' pthread self() val +1) 27 counter = val + 1 J продолжение £?
678 Глава 23. Программные потоки Листинг 23.11 (продолжение) 29 return (NULL). 30 } Обратите внимание на то, что в первый раз ошибка происходит при переклю- чении системы с выполнения потока номер 4 на выполнение потока номер 5: каж- дый поток в итоге записывает значение 518. Это происходит множество раз на протяжении 10 000 строк вывода. Недетерминированная природа ошибок такого типа также будет очевидна, если мы запустим программу несколько раз: каждый раз результат выполнения про- граммы будет отличаться от предыдущего. Также, если мы переадресуем вывод результатов в файл на диске, эта ошибка иногда не будет возникать, так как про- грамма станет работать быстрее, что приведет к уменьшению вероятности пере- ключения системы между потоками. Наибольшее количество ошибок возникнет в случае, если программа будет работать интерактивно, записывая результат на медленный терминал, но при этом также сохраняя результат в файл при помощи программы Unix script (которая описана в главе 19 книги [93]). Только что описанная проблема, возникающая, когда несколько потоков из- меняют значение одной переменной, является самой простой из проблем парал- лельного программирования. Для решения этой проблемы используются так на- зываемые взаимные исключения (mutex — mutual exclusion), с помощью которых контролируется доступ к переменной. В терминах Pthread взаимное исключе- ние — это переменная типа pthread_mutex_t, которая может быть заблокирована и разблокирована с помощью следующих двух функций: #include <pthread h> int pthread_mutex_lock(pthread_mutex_t ★aptr'). int pthread_mutex_unlock(pthread_mutex_t *nptr) Обе функции возвращают 0 в случае успешного выполнения, положительное значение Еххх в случае ошибки Если некоторый поток попытается блокировать взаимное исключение, кото- рое уже блокировано каким-либо другим потоком (то есть принадлежит ему в дан- ный момент времени), этот поток окажется заблокированным до освобождения взаимного исключения. Если переменная-исключение размещена в памяти статически, следует ини- циализировать ее константой PTHREAD_MUTEX_INITIALIZER. В разделе 27.8 мы уви- дим, что если мы размещаем исключение в совместно используемой (разделяе- мой) памяти, мы должны инициализировать его во время выполнения программы путем вызова функции pthread_mutex_imt. ПРИМЕЧАНИЕ ------------------------------------------------------------------- Некоторые системы (например, Solaris) определяют константу PTHREAD_MUTEX_ INITIALIZER как 0. Если данная инициализация будет опущена, это ни на что не по- влияет, так как статически размещаемые переменные все равно автоматически иници- ализируются нулем. Но для других систем такой гарантии дать нельзя — например, в Digital Unix константа инициализации ненулевая. В листинге 23.12 приведена исправленная версия листинга 23.11, в которой используется одно взаимное исключение для блокирования счетчика при работе с двумя потоками.
23.7. Взаимные исключения 679 Листинг 23.12. Исправленная версия листинга 23.11, использующая взаимное исключение для защиты совместно используемой переменной //threads/exampleOl с 1 #include "unpthread h" 2 ^define NLOOP 5000 3 int counter. /* переменная, значение которой увеличивают оба потока *7 4 void *doit(void *). 5 int 6 maintint argc. char **argv) 7 { 8 pthread_t tidA tidB. 9 Pthread_create(&tidA, NULL &doit, NULL). 10 Pthread_create(&tidB, NULL &doit NULL). 11 /* идем завершения выполнения обоих потоков *7 12 PthreadjointtidA. NULL). 13 Pthread_join(tidB. NULL). 14 exit(0). 15 } 16 void * 17 doittvoid *vptr) 18 { 19 int i. val 20 /* Каждый поток получает, выводит и увеличивает на 21 * единицу переменную counter NLOOP раз Значение 22 * переменной должно увеличиваться монотонно 23 *7 24 for (1 = 0 1 < NLOOP. i++) { 25 val = counter. 26 printfl"Xd £d\en". pthreadself(). val + 1). 27 counter = val + 1. 28 } 29 return (NULL). 30 } Мы объявляем взаимное исключение с именем counterjnutex. Это исключение должно быть заблокировано потоком на то время, когда он манипулирует пере- менной counter. Когда мы запускали эту программу, результат всегда был пра- вильным: значение переменной увеличивалось монотонно, а ее окончательное значение всегда оказывалось равным 10 000. Насколько серьезной является дополнительная нагрузка, связанная с исполь- зованием взаимных исключений? Мы изменили программы, приведенные в лис- тингах 23.11 и 23.12, заменив значение NLOOP на 50 000 (вместо исходного значе- ния 5000), и засекли время, направив вывод на устройство/dev/nulL Время работы центрального процессора в случае корректной версии, использующей взаимное исключение, увеличилось относительно времени работы некорректной версии без
680 Глава 23. Программные потоки взаимного исключения на 10%. Это означает, что использование взаимного ис- ключения не связано со значительными издержками. 23.8. Условные переменные Взаимное исключение позволяет предотвратить одновременный доступ к совме- стно используемой (разделяемой) переменной, но для того, чтобы перевести поток в состояние ожидания (спящее состояние) до момента выполнения некоторого условия, необходим другой механизм. Продемонстрируем сказанное на следую- щем примере. Вернемся к нашему web-клиенту из раздела 23.6 и заменим функ- цию Solaris thrjow на pthreadjoin. Но мы не можем вызвать функцию Pthread до тех пор, пока не будем знать, что выполнение потока завершилось. Сначала мы объявляем глобальную переменную, которая служит счетчиком количества за- вершившихся потоков, и организуем управление доступом к ней с помощью вза- имного исключения. int ndone /* количество потоков завершивших выполнение */ pthreadjnutex_t ndonejnutex = PTHREAD_MUTEX_INITIALIZER. Затем мы требуем, чтобы каждый поток по завершении своего выполнения увеличивал этот счетчик на единицу, используя соответствующее взаимное ис- ключение. void * do_get_read(void *vptr) { Pthread_mutex_lock(&ndone_mutex) ndone++ Pth readjriutexjjnl ock (&ndone_mutex). return(fptr). /* завершение выполнения потока */ } Но каким при этом получается основной цикл? Взаимное исключение долж- но быть постоянно блокировано основным циклом, который проверяет, какце потоки завершили свое выполнение. while (nlefttoread > 0) { while (nconn < maxnconn && nlefttoconn > 0) { /* находим файл для чтения */ /* Проверяем, не завершен ли поток */ 1 Pthread_mutex_lock(&ndone_mutex). if (ndone > 0) { for (i = 0. i < nfiles i++) { if (file[i] f flags & F DDNE) { Pthread_join(file[i] f_tid (void **) &fptr) /* обновляем file[i] для завершенного потока */
23.8. Условные переменные 681 Pthread_mutex_unlоск(&ndone_mutex). } Это означает, что главный поток никогда не переходит в спящее состояние, а просто входит в цикл, проверяя каждый раз значение переменной ndone. Этот процесс называется опросом (polling) и рассматривается как пустая трата време- ни центрального процессора. Нам нужен метод, с помощью которого главный цикл мог бы входить в состо- яние ожидания, пока один из потоков не оповестит его о том, что какая-либо задача выполнена. Эта возможность обеспечивается использованием условной перемен- ной (conditional variable) вместе с взаимным исключением. Взаимное исключение используется для реализации блокирования, а условная переменная обеспечива- ет сигнальный механизм. В терминах Pthread условная переменная — это переменная типа pthread_cond_t- Эти переменные используются в следующих двух функциях: #include <pthread h> int pthread_cond_wait(pthread_cond_t *cptr pthread_mutex_t *mptr) int pthread_cond_signal(pthread_cond_t *cptr) Обе функции возвращают 0 в случае успешного выполнения положительное значение Еххх в случае ошибки Слово s 1 gna 1 в названии второй функции не имеет отношения к сигналам Unix SIGxxx. Проще всего объяснить действие этих функций на примере. Вернемся к наше- му примеру web-клиента. Счетчик ndone теперь ассоциируется и с условной пере- менной, и с взаимным исключением: 1 nt ndone. pthread_mutex_t ndone_mutex = PTHREAD_MUTEX__INITIALIZER: ptiiread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER. Поток оповещает главный цикл о своем завершении, увеличивая знэдкние счетчика, пока взаимное исключение принадлежит данному потоку (блокирова- но им), и используя условную переменную для сигнализации. Ptiiread_mutex_l ock(&ndone_mutex) ndone++ Pthread_cond_signal(&ndone_cond). Pthread_mutex_unlock(&ndone_mutex) Затем основной цикл блокируется в вызове функции pfhread_con(|_wai.t, о»$й- дая оповещения о завершении выполнения потока: while (nlefttoread > 0) { while (псопп < maxnconn && nlefttoconn > 0) { /* находим файл для чтения */ } /* Ждем завершения выполнения какого-либо потока */ Pthread_mutex_lock(Sridonejriiitex). while (ndone == 0) Pthread_cond_wait(&ndone_cond &ndone_mutex) for (i=0 i < nfiles, i++) {
682 Глава 23. Программные потоки if (file[i] f_flags & F_DONE) { Pthread_join(file[i] f_tid (void **) &fptr) /* обновляем file[i] для завершенного потока */ } } Pthread_mutex_unlock(&ndone_mutex) } Обратите внимание на то, что переменная ndone по-прежнему проверяется, только если потоку принадлежит взаимное исключение. Тогда, если не требуется выполнять какое-либо действие, вызывается функция pthread_cond_wait. Таким образом, вызывающий поток переходит в состояние ожидания, и разблокируется взаимное исключение, которое принадлежало этому потоку. Кроме того, когда управление возвращается потоку функцией pthread_cond_wait (после того, как поступил сигнал от какого-либо другого потока), он снова блокирует взаимное исключение. Почему взаимное исключение всегда связано с условной переменной? «Усло- вие» обычно представляет собой значение некоторой переменной, используемой совместно несколькими потоками Взаимное исключение требуется для того, чтобы различные потоки могли задавать и проверять значение условной пере- менной. Например, если в примере кода, приведенном выше, отсутствовало бы взаимное исключение, то проверка в главном цикле выглядела бы следующим образом: /* Ждем завершения выполнения одного или нескольких потоков */ while (ndone == 0) Pth read__cond_wa 11 (&ndone_cond &ndone_mutex) Но при этом существует вероятность, что последний поток увеличивает зна- чение переменной ndone после проверки ndone == 0, но перед вызовом функции pthread_cond_wait. Если это происходит, то последний «сигнал» теряется, и основ- ной цикл оказывается заблокированным навсегда, так как он будет ждать собы- тия, которое никогда не произойдет По этой же причине при вызове функции pthread_cond_wait поток должен бло- кировать соответствующее взаимное исключение, после чего эта функция раз- блокирует взаимное исключение и помещает вызывающий поток в состояние ожидания, выполняя эти действия как одну атомарную операцию Если бы эта функция не разблокировала взаимное исключение и не заблокировала его снова после своего завершения, то выполнять эти операции пришлось бы потоку, как показано в следующем фрагменте кода: /* Ждем завершения выполнения одного или нескольких потоков */ Pth readjnutех_1ос k(&ndone_mutex) while (ndone == 0) { Pthread_mutex_unlock(&ndone_mutex) Pthread_cond_wai t (&ndone_cond &ndone_mutex) Pthread_mutex_lock(Undonejriut ex) } Существует вероятность того, что по завершении выполнения поток увели- чит на единицу значение переменной ndone и это произойдет между вызовом функ- ций pthread_mutex_unlock и pthread_cond_wait.
23.8. Условные переменные 683 Обычно функция pthread_cond_signal выводит из состояния ожидания один поток, на который указывает условная переменная Существуют ситуации, когда некоторый поток знает, что из состояния ожидания должны быть выведены не- сколько потоков. В таком случае используется функция pthread_cond_broadcast, выводящая из состояния ожидания все потоки, которые блокированы условной переменной. #include <pthread h> int pthread_cond_broadcast(pthread_cond_t *cptr') int pthread_cond_timedwait(pthread_cond_t *cptr pthread_mutex_t *mptr const struct timespec *abstime) Обе функции возвращают 0 в случае успешного выполнения положительное значение Еххх в случае ошибки Функция pthread_cond_timedwa'it позволяет потоку задать предельное время блокирования. Аргумент abstime представляет собой структуру timespec (опреде- ленную в разделе 6 9 при рассмотрении функции pselect), которая задает систем- ное время для момента, когда функция должна возвратить управление, даже если к этому моменту условная переменная не подала сигнал Если возникает такая ситуация, возвращается ошибка ETIME. В данном случае значение времени является абсолютным значением времени, в отличие от относительного значения разницы во времени {time delta) между некоторыми событиями. Иными словами, abstime — это системное время, то есть количество секунд и наносекунд, прошедших с 1 января 1970 года (UTC) до того момента, когда эта функция должна вернуть управление Здесь имеется разли- чие как с функцией pselect, так и с функцией select, задающими количество се- кунд (и наносекунд в случае psel ect) до некоторого момента в будущем, когда функция должна вернуть управление. Обычно для этого вызывается функция gettimeofday, которая выдает текущее время (в виде структуры timeval), а затем оно копируется в структуру timespec и к нему добавляется требуемое значение: struct timeval tv struct timespec ts if (gettimeofday(&tv NULL) < 0) err_sys('gettimeofday error") ts tv_sec = tv tvsec + 5. /*5 секунд в будущем */ ts tv_nsec = tv tv_usec * 1000 /* микросекунды переводим в наносекунды */ pthread_cond_timedwait( Sts) Преимущество использования абсолютного времени (в противоположность относительному) заключается в том, что функция может завершиться раньше (возможно, из-за перехваченного сигнала). Тогда функцию можно вызвать сно- ва, не меняя содержимое структуры timespec. Недостаток этого способа заключа- ется в необходимости вызывать дополнительно функцию gettimeofday перед тем, как в первый раз вызывать функцию pthread_cond_timedwait. ПРИМЕЧАНИЕ--------------------------------------------------------------------------- В Posix 1 определена новая функция clock gcttimc, возвращающая текущее время в виде структуры timespec
684 Глава 23. Программные потоки 23.9. Web-клиент и одновременный доступ Изменим код нашего web-клиента из раздела 23.6: уберем вызов функции Solans thr joi п и заменим его вызовом функции pthreadjoi п. Как сказано в разделе 23.6, теперь нам нужно точно указать, завершения какого потока мы ждем. Для этого мы используем условную переменную, как показано в разделе 23.8. Единственным изменением в отношении глобальных переменных (см. лис- тинг 23.7) является добавление нового флага и условной переменной: #define F_JOINED 8 /* количество потоков */ int ndone. /* количество завершившихся потоков */ pthread_mutex_t ndonejnutex = PTHREAD_MUTEX_INITIALIZER pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER Единственным изменением функции do_get_read (см. листинг 23.9) будет уве- личение на единицу значения переменной ndone и оповещение главного цикла о завершении выполнения потока: printf("end-of-file on $s\en" fptr->f_name). Close(fd) Pthread_mutex_lock(Sndonejnutex) fptr->f_flags = F DQNE. /* сбрасывает флаг F_READING */ ndone++, Pthread_cond_signal( &ndone_cond) 1 Pthread_mutex_unlock(&ndone_mutex). return(fptr). /* завершение выполнения потока */ } Большинство изменений касается главного цикла, представленного в листин- ге 23.8. Новая версия показана в листинге 23.13. Листинг 23.13. Основной рабочий цикл функции main //threads/web03 с 43 while (nlefttoread > 0) { 44 while (nconn < maxnconn && nlefttoconn > 0) { 45 /* находим файл для считывания */ 46 for (i = 0. i < nfiles. 1++) 47 if (file[i] f_flags == 0) 48 break 49 if (i == nfiles) 50 err_quit(“nlefttoconn = 5:d but nothing found” nlefttoconn); 51 file[i] f_flags = F_CONNECTING 52 Pthread_create(&tid NULL. &do_get_read &file[i]). 53 ‘ file[i] f_tid = tid. 54 nconn++. 55 nlefttoconn--. 56 } 57 /* Ждем завершения выполнения одного из потоков */ 58 Pthread_mutex_lock(Sndonejnutex) 59 while (ndone == 0) 60 Pthread_cond_wait(&ndone_cond &ndone_mutex). 61 for (i=0 i < nfiles, i++) {
23.10. Резюме 685 62 if (file[i] fflags & F_00NE) { 63 Pthread_join(file[i] f_tid. (void **) Sfptr). 64 if (&file[i] '= fptr) 65 err_quit('file[i] l= fptr') 66 fptr->f_flags = FJOINED /* clears F_DONE */ 67 ndone-- 68 nconn-- 69 nlefttoread--. 70 pnntf(' thread 2d for 2s done\en' fptr->f_tid. fptr->f_name): 73 Pthread_mutex_unlock(&ndone_mutex). 74 } 75 exit(O) 76 } По возможности создаем новый поток 44-56 Эта часть кода не изменилась. Ждем завершения выполнения потока 57-60 Мы ждем завершения выполнения потоков, отслеживая, когда значение ndone станет равно нулю. Как сказано в разделе 23.8, эта проверка должна быть прове- дена перед тем, как взаимное исключение будет блокировано, а переход потока в состояние ожидания осуществляется функцией pthread_cond_wait. Обработка завершенного потока 51-73 Когда выполнение потока завершилось, мы перебираем все структуры f 11 е, отыс- кивая соответствующий поток, вызываем pthreadjoin, а затем устанавливаем новый флаг F JOINED. В табл. 15.1 показано, сколько времени требует выполнение этой версии web- клиента, а также версии, использующей неблокируемую функцию connect. 23.10. Резюме Создание нового потока обычно требует меньше времени, чем порождение нового процесса с помощью функции fork. Одно это уже является большим преимуще- ством использования потоков на активно работающих сетевых серверах. Много- поточное программирование, однако, представляет собой отдельную технологию, требующую большей аккуратности при использовании. Все потоки одного процесса совместно используют глобальные переменные и дескрипторы, тем самым эта информация становится доступной всем потокам процесса. Но совместное использование информации вносит проблемы, связан- ные с синхронизацией доступа к разделяемым переменным, и поэтому нам сле- дует использовать примитивы синхронизации технологии Pthread — взаимные исключения и условные переменные. Синхронизация доступа к совместно ис- пользуемым данным — необходимое условие почти для любого приложения, ра- ботающего с потоками. При разработке функций, которые могут быть вызваны таким приложением, нужно учитывать требование безопасности в многопоточной среде. Это требова-
686 Глава 23. Программные потоки ние выполнимо при использовании собственных данных потоков (thread-specific data), пример которых мы показали при рассмотрении функции readl i пе в этой главе. К модели потоков мы вернемся в главе 27, где сервер при запуске создает пул (накопитель — pool) потоков. Для обслуживания очередного клиентского запро- са используется очередной свободный поток. Упражнения 1. Сравните использование дескриптора в случае, когда в коде сервера применя- ется функция fork, и в случае, когда используются потоки. Предполагается, что одновременно обслуживается 100 клиентов. 2. Что произойдет в листинге 23.2, если поток при завершении функции str_echo не вызовет функцию close для закрытия сокета? 3. В листингах 5.4 и 6.2 мы выводили сообщение Server terminated prematurely (Сервер завершил работу преждевременно) в случаях, когда мы ждали от сер- вера прибытия отраженной строки, а вместо этого получали признак конца файла (см. раздел 5.12). Модифицируйте листинг 23.1 таким образом, чтобы в соответствующих случаях также выдавалось аналогичное сообщение. 4. Модифицируйте листинги 23.5 и 23.6 таким образом, чтобы программы мож- но было компилировать в системах, не поддерживающих потоки. 5. Чтобы увидеть ошибку в функции readl те, приведенной в листинге 23.2, за- пустите эту программу на стороне сервера. Затем измените эхо-клиент TCP из листинга 6.2, корректно работающий в пакетном режиме. Возьмите какой- либо большой текстовый файл в своей системе и трижды запустите клиент в пакетном режиме, чтобы он считывал текст из этого файла и записывал ре- зультат во временный файл. Если есть возможность, запустите клиенты на другом узле (не на том, на котором запущен сервер). Если все три клиента выполнят работу правильно (часто они зависают), посмотрите на файлы с ре- зультатом и сравните их с исходным файлом. Теперь создайте версию сервера, используя корректную версию функции readl те из раздела 23.5. Повторите тест, используя три эхо-клиента. Теперь все три клиента должны работать исправно. Также поместите функцию pm ntf в функции readl i ne destructor, readl i ne_once и в вызов функции mal 1ос в readl i ne. Это даст вам возможность увидеть, что ключ создается только один раз, но для каждого потока выделяется область памяти и вызывается функция-дест- руктор.
ГЛАВА 24 Параметры IP 24.1. Введение В IPv4 допускается, чтобы после фиксированного 20-байтового заголовка шло до 40 байт, отведенных под различные параметры. Хотя определено 10 параметров, чаще всего используется параметр маршрута от отправителя (source route option). Доступ к этим параметрам осуществляется через параметр сокета IP OPTIONS, что мы покажем на примере использования маршрутизации от отправителя (source routing). В IPv6 допускается наличие расширяющих заголовков (extension headers) между фиксированным 40-байтовым заголовком IPv6 и заголовком транспорт- ного уровня (например, ICMPv6, TCP или UDP). В настоящее время определены 6 различных расширяющих заголовков. В отличие от подхода, использованного в IPv4, доступ к расширяющим заголовкам IPv6 осуществляется через функцио- нальный интерфейс, что не требует от пользователя понимания фактических де- талей того, как именно эти заголовки расположены в пакете IPv6. 24.2. Параметры IPv4 На рис. А.1 мы показываем параметры, расположенные после 20-байтового заго- ловка IPv4. Как отмечено при рассмотрении этого рисунка, 4-разрядное поле длины ограничивает общий размер заголовка IPv4 до 15 32-разрядных слов (что составляет 60 байт), так что на параметры IPv4 отведено 40 байт. Для IPv4 опре- делено 10 различных параметров. 1. NOP (no-operation — нет действий). Этот однобайтовый параметр использу- ется для выравнивания очередного параметра по 4-байтовой границе. 2. EOL (end-of-list — конец списка параметров). Этот однобайтовый параметр обозначает конец списка параметров. Он необходим только в том случае, если конец списка параметров не совпал с окончанием заголовка. Поскольку сум- марный размер параметров IP должен быть кратным 4 байтам, после послед- него параметра добавляются байты EOL. 3. LSRR (Loose Source and Record Route — гибкая1 маршрутизация от отправи- теля с записью) (см. раздел 8.5 [94]). Пример использования этого параметра мы вскоре продемонстрируем. 1 Иногда название этого параметра переводится как «потеря отправителя и запись маршрута», но это, вероятно, следствие путаницы: loose — свободный, несвязанный; loss — потеря. — Примеч. перев
688 Глава 24. Параметры IP 4. SSRR (Strict Source and Record Route — жесткая маршрутизация от отправи- теля с записью) (см. раздел 8.5 [94]). Пример использования этого параметра мы также вскоре продемонстрируем. 5. Отметка времени (timestamp) (см. раздел 7.4 [94]). 6. Запись маршрута (record route) (см. раздел 7.3 [94]). 7. Основной параметр обеспечения безопасности (basic security). 8. Расширенный параметр обеспечения безопасности (extended security). 9. Идентификатор потока (устаревший параметр) (stream identifier). 10. Извещение маршрутизатора (router alert). Этот новый параметр описан в RFC 2113 [52]. Он включается в дейтаграмму IP, для того чтобы все пересы- лающие эту дейтаграмму маршрутизаторы обрабатывали ее содержимое. В главе 9 книги [105] приводится более подробное рассмотрение первых шес- ти параметров, а в указанных выше разделах [94] имеются примеры их использо- вания. В RFC 1108 [53] приведено более подробное рассмотрение двух парамет- ров, связанных с безопасностью (параметры 7 и 8 в расположенном выше списке), которые не слишком широко используются. Функции getsockopt и setsockopt (для которых аргумент level равен IPPROTO_IP, а аргумент optname — IP_OPTIONS) предназначены соответственно для получения и установки параметров IP. Четвертый аргумент функций getsockopt и setsockopt — это указатель на буфер (размер которого не превосходит 44 байт), а пятый аргу- мент — это размер буфера. Причина, по которой размер буфера может на 4 байта превосходить максимальный суммарный размер параметров, заключается в спо- собе обработки параметров маршрута от отправителя, как мы вскоре увидим. Все остальные параметры помещаются в буфер именно в том виде, в котором они потом упаковываются в заголовок дейтаграммы. Когда параметры IP задаются с использованием функции setsockopt, указан- ные параметры включаются во все дейтаграммы, отсылаемые с данного сокета. Этот принцип работает для сокетов TCP, UDP и для символьных сокетов. Для отмены этого параметра следует вызвать функцию setsockopt и задать либо пус- той указатель в качестве четвертого аргумента, либо нулевое значение в качестве пятого аргумента (длина). ПРИМЕЧАНИЕ----------------------------------------------------------- Установка параметров IP для символьного сокета IP не работает во всех реализациях, если уже установлен параметр IP_HDRINCL (который мы обсудим в последующих главах). Многие Беркли-реализации не отсылают параметры, установленные с помощью IP_OPTIONS, если включен параметр IP_HDRINCL, так как приложение может ус- танавливать свои собственные параметры в формируемом им заголовке IP [ 105, с. 1056- 1057] В других системах (например, в FreeBSD) приложение может задавать свои па- раметры IP, либо используя параметр сокета IP_OPTIONS, либо установив параметр IP—HDRINCL и включив требуемые параметры в создаваемый им заголовок IP, но одновременное применение обоих этих способов не допускается. При вызове функции getsockopt для получения параметров IP присоединен- ного сокета TCP, созданного функцией accept, возвращается лишь обращенный параметр маршрута от отправителя, полученный вместе с клиентским сегментом SYN на прослушиваемом сокете [105, с. 931]. TCP автоматически обращает мар-
24 3. Параметры маршрута от отправителя IPv4 689 шрут от отправителя, поскольку маршрут, указанный клиентом, — это маршрут от клиента к серверу, а сервер должен использовать для отсылаемых им дейта- грамм обратный маршрут. Если вместе с сегментом SYN не был получен марш- рут от отправителя, то значение пятого аргумента (этот аргумент типа «значе- ние-результат», как было указано выше, задает длину буфера) при завершении функции getsockopt будет равно нулю. Для прочих сокетов TCP, всех сокетов UDP и всех символьных сокетов IP при вызове функции getsockopt вы просто получи- те копию тех параметров IP, которые были установлены для этих сокетов с по- мощью функции setsockopt. Заметим, что для символьных сокетов IP получен- ный заголовок IP, включая все параметры IP, всегда возвращается всеми входными функциями, поэтому полученные параметры IP всегда доступны. ПРИМЕЧАНИЕ ------------------------------------------------------------- В Беркли-ядрах полученный маршрут от отправителя, так же как и другие параметры IP, никогда пе возвращается для сокетов UDP Показанный на с. 775 [105] код, пред- назначенный для получения параметров IP, существовал со времен 4 3BSD Reno, но так как оп не работал, его всегда приходилось превращать в комментарий Таким обра- зом, для сокетов UDP невозможно использовать обращенный маршрут от отправите- ля полученной дейтаграммы для того, чтобы отослать ответ. Многие Беркли-ядра дают сбой (то есть система прекращает работу) при попытке вы- звать функцию getsockopt или setsockopt на символьном сокете IP Но для создания символьного сокета требуются права привилегированного пользователя, а тот, кто обла- дает такими правами, имеет возможность нанести системе гораздо более серьезный вред 24.3. Параметры маршрута от отправителя IPv4 Маршрут от отправителя (source route) — это список IP-адресов, указанных от- правителем дейтаграммы IP. Если маршрут является жестким (строгим, strict), то дейтаграмма должна передаваться только между указанными узлами и пройти их все Иными словами, все узлы, перечисленные в маршруте от отправителя, должны быть соседними друг для друга. Но если маршрут является свободным, или гибким (loose), дейтаграмма должна пройти все перечисленные в нем узлы, но может при этом пройти и еще какие-то узлы, не входящие в список маршрута. ПРИМЕЧАНИЕ-------------------------------------------------- Маршрутизация от отправителя (source routing) в IPv4 является предметом споров и сомнений. В [19] пропагандируется отказ от поддержки этой функции на всех марш- рутизаторах, и многие организации и провайдеры действительно следуют этому прин- ципу Один из наиболее разумных способов использования маршрутизации от отправи- теля — это обнаружение с помощью программы Traceroute асимметричных маршрутов, как показано на с. 108-109 [94], но в настоящее время даже этот способ становится непопулярен. Тем не менее определение и получение маршрута от отправителя — это часть API сокетов, и поэтому заслуживает описания. Параметры IPv4, связанные с маршрутизацией от отправителя, называются параметрами маршрутизации от отправителя с записью (Loose Source and Record Routes — LSRR в случае свободной маршрутизации и Strict Source and Record
690 Глава 24 Параметры IP Routes — SSRR в случае жесткой маршрутизации), так как при проходе дейта- граммы через каждый из перечисленных в списке узлов происходит замена ука- занного адреса на адрес интерфейса для исходящих дейтаграмм Это позволяет получателю дейтаграммы обратить полученный список, превратив его в марш- рут, по которому будет послан ответ отправителю Примеры этих двух маршру- тов от отправителя вместе с соответствующим выводом программы tcpdump, при- ведены в разделе 8 5 книги [94]. Маршрут от отправителя мы определяем как массив адресов IPv4, которому предшествуют три однобайтовых поля, как показано на рис 24 1 Это формат бу- фера, который передается функции setsockopt |ч 4 байта —н NOP code len Ptr IP-адрес 1 IP-адрес 2 IP-адрес 3 IP-адрес 9 IP-адрес получателя 1 1 1 1 4 байта 4 байта 4 байта 4 байта 4 байта Рис. 24.1. Передача маршрута от отправителя ядру Перед параметром маршрута от отправителя мы поместили параметр NOP (нет действий), чтобы все IP-адреса были выровнены по 4-байтовой границе Это не обязательно, но желательно, поскольку в результате мы выравниваем адреса, не расходуя дополнительно лишней памяти (все IP-параметры обычно выравнива- ются, чтобы в итоге занимать место, кратное 4 байтам) На рис 24 1 показано, что маршрут состоит из 10 адресов, но первый приве- денный адрес удаляется из параметра маршрута от отправителя и становится ад- ресом получателя, когда дейтаграмма IP покидает узел отправителя Хотя в 40-бай- товом пространстве, отведенном под данный параметр IP, хватает места только для 9 адресов (не забудьте о 3-байтовом заголовке параметра, который мы вскоре опишем), фактически в заголовке IPv4 у нас имеется 10 IP-адресов, так как к 9 ад- ресам узлов добавляется адрес получателя Поле code — это либо 0x83 для параметра LSRR, либо 0x89 для параметра SSRR Задаваемое нами значение поля len — это размер параметра в байтах, включая 3-байтовый заголовок и дополнительный адрес получателя, приведенный в кон- це списка Для маршрута, состоящего из одного IP-адреса, это значение будет равно 11, для двух адресов — 15, и так далее вплоть до максимального значения 43 Параметр NOP не является частью обсуждаемого параметра, и его длина не вклю- чается в значение поля 1 еп, но она входит в размер буфера, который мы сообщаем функции setsockopt Когда первый адрес в списке удаляется из параметра марш- рута от отправителя и добавляется в поле адреса получателя в заголовок IP, зна- чение поля len уменьшается на 4 (см рис 932и933 [105]) Поле ptr — это указа- тель, или сдвиг, задающий положение следующего IP-адреса из списка, который должен быть обработан Мы инициализируем это поле значением 4, что соответ- ствует первому адресу IP Значение этого поля увеличивается на 4 каждый раз, когда дейтаграмма обрабатывается одним из перечисленных в маршруте узлов Теперь мы переходим к определению трех функций, с помощью которых мы инициализируем, создаем и обрабатываем параметр маршрута от отправителя Наши функции предназначены для работы только с этим параметром Хотя в прин- ципе возможно объединить параметр маршрута от отправителя с другими пара-
24 3 Параметры маршрута от отправителя IPv4 691 метрами IP (такими, как параметр временной отметки), но все эти параметры, за исключением параметров маршрутизации, используются редко В листинге 24 I1 приведена функция i net_srcrt_i mt, а также некоторые статические переменные, используемые при составлении параметра Листинг 24.1. Функция inet_srcrt_init инициализация перед записью маршрута от отправителя //ipopts/sourceroute с 1 ^include unp h 2 include <netinet/in_systm h> 3 include <netinet/ip h> 4 static u_char *optr /* указатель на формируемые параметры */ 5 static u_char *lenptr /* указатель на поле длины в параметре SRR */ 6 static int ocnt /* счетчик количества адресов */ 7 u_char * 8 inet srcrt imt(void) 9 { 10 optr = Mal1oc(44) /* NOP code len ptr ло 10 адресов */ 11 bzero(optr 44) /* гарантия EOL в конце */ 12 ocnt = 0 13 return (optr) /* указатель для функции setsockoptO */ 14 } Инициализация 10 13 Мы выделяем в памяти буфер, максимальный размер которого — 44 байта, и об- нуляем его содержимое Значение параметра EOL равно нулю, так что тем самым параметр инициализируется как содержащий EOL байт Указатель на параметр возвращается вызывающему процессу, а затем передается как четвертый аргу- мент функции setsockopt Следующая функция, inet_srcrt_add, добавляет один 1Ру4-адрес к создавае- мому маршруту от отправителя Листинг 24.2. Функция inet_srcrt_add добавление одного IPv4-appeca к маршруту от отправителя //ipopts/sourceroute с 15 int 16 inet_srcrt_add(char *hostptr int type) 17 { 18 int len 19 struct addrinfo *ai 20 struct sockaddr_in *sin 21 if (ocnt > 9) 22 err_quit( too many source routes with Xs*, hostptr). 23 if (ocnt == 0) { 24 *optr++ = IPOPT_NOP /* NOP для выравнивания */ 25 *optr++ = type ’ IPOPT_SSRR IPOPT_LSRR 26 lenptr = optr++ /* поле длины заполним позже */ 27 *optr++ = 4 /* сдвиг первого адреса */ __________________ продолжение & 1 Все исходные коды программ, опубликованные в этой книге вы можете найти по адресу http// www piter com/download
692 Глава 24. Параметры IP Листинг24.2 (продолжение) 28 } 29 ат = Host_serv(hostptr. AF_INET 0). 30 sin = (struct sockaddr_in *) ai->ai_addr. 31 memcpytoptr &sin->sin_addr sizeoftstruct in_addr)): 32 freeaddnnfo(ai) 33 optr += sizeof(struct in addr). 34 ocnt++. 35 len = 3 + (ocnt * sizeoftstruct inaddr)), 36 *lenptr = len, 37 return (len + 1), /* размер для функции setsockoptO */ 38 } Аргументы 16 Первый аргумент указывает либо на имя узла, либо на адрес IP в точечно-деся- тичной записи, второй аргумент в случае гибкой маршрутизации равен нулю, а в случае жесткой маршрутизации имеет ненулевое значению. Мы увидим, что от типа первого адреса, добавленного к маршруту, зависит, гибким или жестким является маршрут. Проверка переполнения и последующая инициализация 18 Мы проверяем, не слишком ли много задано адресов, а затем инициализируем статические переменные, если это первый адрес. Как уже было сказано, мы все- гда помещаем параметр NOP перед параметром маршрута от отправителя. Мы сохраняем указатель на поле len и будем записывать в него соответствующее зна- чение при добавлении к списку очередного адреса. Получение двоичного IP-адреса и запись маршрута 37 Наша функция host_serv обрабатывает либо имя узла, либо адрес IP в точечно- десятичной записи, и полученный двоичный адрес мы заносим в список. Мы об- новляем значение поля 1 еп и возвращаем суммарный размер буфера (включая NOP), который вызывающий процесс должен передать функции setsockopt. Когда полученный маршрут от отправителя возвращается приложению функ- цией getsockopt, формат этого параметра отличается от того, что было показано на рис. 24.1. Формат полученного параметра маршрута от отправителя показан на рис. 24.2. |-4-----------------------------------— 4 байта ------------------------------------->• IP адрес #1 NOP code len Ptr IP-адрес 2 IP-адрес 3 IP-адрес 4 IP-адрес получателя 4 байта 11114 байта 4 байта 4 байта Рис. 24.2. Формат параметра маршрута от отправителя, возвращаемого функцией getsockopt В первую очередь, мы можем отметить, что порядок следования адресов изме- нен ядром на противоположный относительно полученного маршрута от отпра- вителя. Имеется в виду следующее: если в полученном маршруте содержались
24.3. Параметры маршрута от отправителя IPv4 693 адреса А, В, С и D в указанном порядке, то под противоположным порядком под- разумевается следующий: D, С, В, А. Первые 4 байта содержат первый IP-адрес из списка, затем следует однобайтовый параметр NOP (для выравнивания), за- тем — 3-байтовый заголовок параметра маршрута от отправителя, и далее осталь- ные IP-адреса. После 3-байтового заголовка может следовать до 9 IP-адресов, и максимальное значение поля 1 еп в возвращенном заголовке равно 39. Посколь- ку параметр NOP всегда присутствует, длина буфера, возвращаемая функцией getsockopt, всегда будет равна значению, кратному 4 байтам. ПРИМЕЧАНИЕ-------------------------------------------------------------------------- Формат, приведенный на рис. 24.2, определен в заголовочном файле <netinet/ip_var.h> в виде следующей структуры: #define MAXJPOPTLEN 40 struct ipoption { struct in_addr ipopt_dst. /* адрес первого получателя */ char ipopt_list[MAX_IPOPTLEN], /* соответствующие параметры */ } В листинге 24.3 мы анализируем эти данные, не используя указанную структуру. Возвращаемый формат отличается от того, который был передан функции setsockopt. Если нам было бы нужно преобразовать формат, показанный на рис. 24.2, к формату, показанному на рис. 24.1, нам следовало бы поменять места- ми первые и вторые 4 байта и изменить значение поля 1 еп, добавив к имеющему- ся значению 4. К счастью, нам не нужно этого делать, так как Беркли-реализации автоматически используют обращенный маршрут от получателя для сокета TCP. Иными словами, данные, возвращаемые функцией getsockopt (представленные на рис. 24.2), носят чисто информативный характер. Нам не нужно вызывать функ- цию setsockopt, чтобы указать ядру на необходимость использования данного маршрута для дейтаграмм IP, отсылаемых по соединению TCP, — ядро сделает это автоматически. Подобный пример с нашим сервером TCP мы вскоре увидим. Следующей из рассматриваемых нами функций, связанных с параметром мар- шрутизации, полученный маршрут от отправителя передается в формате, пока- занном на рис. 24.2. Затем она выводит соответствующую информацию. Эту функ- цию inet_srtcrt_print мы показываем в листинге 24.3. Листинг 24.3. Функция inet_srtcrt_print: вывод полученного маршрута от отправителя //ipopts/sourceroute с 39 void 40 inet_srcrt_print(u_char *ptr, int len) 41 { 42 u_char c. 43 char str[INET_ADDRSTRLEN], 44 struct in_addr hopl, 45 memcpy(&hopl. ptr. sizeof(struct in_addr)); 46 ptr +- sizeof(struct in_addr), 47 while ( (c = *ptr++) == IPOPT NOP) . /* пропускаем pee параметры NOP */ 48 if (c “ IPOPT LSRR) , л _ продолжение тУ
694 Глава 24. Параметры IP Листинг 24.3 (продолжение) 49 printfC received LSRR "). 50 else if (c == IP0PT_SSRR) 51 printfC received SSRR ”). 52 else { 53 printfC received option type td\en". c). 54 return. 55 } 56 printfC^s ". Inet_ntop(AF_INET. &hopl. str. sizeof(str))). 57 len = *ptr++ - sizeof(struct in_addr) /* убираем IP-адрес получателя */ 58 ptr++. /* пропускаем указатель */ 59 while (len > 0) { 60 printfC^s ". Inet_ntop(AF_INET. ptr. str. sizeof(str))) 61 ptr += sizeof(struct in_addr), 62 len -= sizeof(struct in_addr). 63 } 64 printf("\en"). 65 } Сохраняем первый адрес IP, пропускаем все параметры NOP 47 Первый адрес IP в буфере сохраняется, а все следующие за ним параметры NOP мы пропускаем. Проверяем параметр маршрута от отправителя 64 Мы выводим информацию о маршрутизации (гибкая или жесткая) и проверя- ем значение поля code, содержащегося в 3-байтовом заголовке, получаем значе- ние поля len и пропускаем указатель ptr. Затем мы выводим все IP-адреса, следу- ющие за 3-байтовым заголовком, кроме IP-адреса получателя. Пример Теперь мы модифицируем наш эхо-сервер TCP таким образом, чтобы выводить полученный маршрут от отправителя, а эхо-клиент TCP — так, чтобы маршрут от отправителя можно было задавать. В листинге 24.4 показан код для эхо-кли- ента TCP. Листинг 24.4. Эхо-клиент TCP, задающий маршрут от отправителя Z/ipopts/tcpcl101 с 1 #include "unp h" 2 int 3 main(int argc. char **argv) 4 { 5 int c. sockfd. len = 0. 6 u_char *ptr. 7 struct addrinfo *ai. 8 if (argc < 2) 9 err_quit("usage tcpcliOl [ -[gG] <hostname> ... ] <hostname>"); 10 ptr = inet_srcrt_imt(). 11 opterr = 0. /* чтобы функция getoptO не записывала сообщения об ошибках в поток stderr */ 12 while ( (с = getopt(argc. argv. "g G ")) '= -1) {
24.3. Параметры маршрута от отправителя IPv4 695 13 switch (с) { 14 case 'д' /* гибкая маршрутизация от отправителя */ 15 len = inet_srcrt_add(optarg. 0). 16 break. 17 case 'G' /* жесткая маршрутизация от отправителя */ 18 len = inet_srcrt_add(optarg. 1). 19 break. 20 case '?' 21 err_quit("unrecognized option. £c". c), 22 } 23 ) 24 if (optind '= argc - 1) 25 err_quit("missing <hostname>"). 26 ai = Host_serv(argv[optind] SERV_PORT_STR. AFJNET SOCK_STREAM). 27 sockfd = Socket(ai->ai family ai->ai_socktype ai->ai_protocol) 28 if (len > 0) { 29 len = inet_srcrt_add(argv[optind], 0). /* адрес получателя в конц§ *,/ 30 Setsockopt(sockfd. IPPROTOJP. IP_OPTIONS ptr len) 31 free(ptr). 32 } 33 Connect(sockfd ai->ai_addr, ai->ai_addrlen). 34 str_cli(stdin, sockfd). /* обработка в цикле */ 35 exit(0). 36 } Обработка аргументов командной строки 8-23 Мы вызываем нашу функцию inet_srcrt j ni t, чтобы инициализировать марш- рут от отправителя. Каждому узлу соответствует либо параметр -д (гибкая марш- рутизация), либо параметр -G (жесткая маршрутизация). От типа первого IP-ад- реса (гибкий или жесткий) зависит тип всего маршрута от отправителя. Такое решение мы выбрали из-за его простоты. Конечно, мы могли бы добавить код для проверки того, что все узлы относятся к одному типу. Наша функция i net srcrt add добавляет очередной адрес к маршруту. ПРИМЕЧАНИЕ----------------------------------------------------------------------------- Здесь мы впервые встречаемся с функцией Posix.2 getopt. Третьим ее аргументом яв- ляется символьная строка, определяющая, какие символы допускаются в качестве аргументов командной строки — в данном случае «g» и «G». После каждого символа следует двоеточие. Эта функция работает с четырьмя глобальными переменными, определенными в заголовочном файле <unistd.h>: extern char *optarg extern int optind. opterr, optopt Перед вызовом функции getopt мы обнуляем глобальную переменную opterr, так как хотим избежать записи сообщений об ошибках в стандартный поток сообщений об ошибках, поскольку мы будем обрабатывать ошибки самостоятельно. В Posix.2 ука- зано, что если первым символом третьего аргумента функции getopt является двоето- чие, то это автоматически предотвращает запись функцией сообщений в stderr, но не все реализации поддерживают это соглашение.
696 Глава 24 Параметры IP Обработка адреса получателя и создание сокета 27 Последний аргумент командной строки — это имя узла или адрес сервера в то- чечно-десятичной записи, который обрабатывается нашей функцией host_serv. Мы не можем вызвать функцию tcp_connect, так как мы должны задать маршрут от отправителя между вызовом функций socket и connect. Последняя инициирует трехэтапное рукопожатие, а нам нужно, чтобы сегмент SYN отправителя и все последующие пакеты проходили по одному и тому же маршруту. 34 Если маршрут от отправителя задан, следует добавить IP-адрес сервера в конец списка адресов (см рис. 24.1) Функция setsockopt устанавливает маршрут от от- правителя для данного сокета. Затем мы вызываем функцию connect, а потом — нашу функцию str ci 1 (см. листинг 5.4). Наш TCP-сервер имеет много общего с кодом, показанным в листинге 5 9, и со- держит следующие изменения Во-первых, мы выделяем место для параметров: int len u_char *opts opts = Malloc(44) Во-вторых, мы получаем параметры IP после вызова функции accept, но пе- ред вызовом функции fork- len = 44 Getsockopt(connfd IPPROTOJP IP_OPTIONS opts Men). if (len > 0) { printfCreceived IP options len = W\en' len), inet_srcrt_print(opts len) } Если сегмент SYN, полученный от клиента, не содержит никаких параметров IP, переменная len по завершении функции getsockopt будет иметь нулевое зна- чение (эта переменная относится к типу «значение-результат»). Как упомина- лось выше, нам не нужно предпринимать какие-либо шаги для того, чтобы на стороне сервера использовался бы обращенный маршрут от отправителя, это делается автоматически без нашего участия [105, с. 931]. Вызывая функцию getsockopt, мы просто получаем копию обращенного маршрута от отправителя Если мы не хотим, чтобы TCP использовал этот маршрут, то после завершения функции accept следует вызвать функцию setsockopt и задать нулевую длину (по- следний аргумент), тем самым удалив все используемые в текущий момент пара- метры IP Но маршрут от отправителя, тем не менее, уже был использован в про- цессе трехэтапного рукопожатия при пересылке второго сегмента, а если мы уберем параметры маршрутизации, IP составит и будет использовать для пере- сылки последующих пакетов какой-либо другой маршрут. Теперь мы покажем пример клиент-серверного взаимодействия при заданном маршруте от отправителя. Мы запускаем наш клиент на узле sol am s следующим образом: solans £ tcpcliOl -g gw -g sunos5 bsdi Тем самым дейтаграммы IP отсылаются с узла solans на маршрутизатор gw (см. рис. 1 7), далее на узел sunos5, а затем на узел bsdi, где запущен наш сервер. Две промежуточные системы, gw и sunos5, должны переправить дейтаграммы по маршруту от отправителя, чтобы этот пример работал.
24 3 Параметры маршрута от отправителя IPv4 697 Когда соединение установлено, на стороне сервера выдается следующий ре- зультат: bsdi X tcpservOl received IP options len = 16 received LSRR 206 62 226 36 206 62 226 62 206 62 226 33 Первый выведенный IP-адрес — это первый узел обратного маршрута (sunos5, как показано на рис. 24 2), а следующие два адреса идут в том порядке, который используется сервером для отправки дейтаграмм обратно клиенту. Если мы по- наблюдаем за процессом взаимодействия клиента и сервера с помощью програм- мы tcpdump, мы увидим, как используется параметр маршрутизации для каждой дейтаграммы в обоих направлениях. ПРИМЕЧАНИЕ ---------------------------------------------------------------- К сожалению, действие параметра сокета IP_OPTIONS никогда не было документиро- вано, поэтому в различных системах вы можете увидеть различные вариации, не име- ющие отношения к исходному коду Беркли Например, в системе Solans 2 5 первый адрес, возвращаемый функцией getsockopt (см рис 24 2), — это не первый адрес в об- ращенном маршруте, а адрес собеседника Тем не менее обратный маршрут, использу- емый TCP, будет корректен Кроме того, в Solans 2 5 всем параметрам маршрутизации предшествует четыре параметра NOP, что ограничивает параметр маршрутизации во- семью IP-адресами, а не девятью, которые реально могли бы поместиться Уничтожение маршрута, полученного от отправителя К сожалению, использование параметра маршрутизации образует брешь $ систе- ме обеспечения безопасности. Начиная с выпуска Net/1 (1989), серверы r^ogind и rshd использовали код, аналогичный следующему: u_char buf[44] char lbuf[BUFSIZ] int optsize optsize = sizeof(buf). if (getsockopt(0 IPPROTOJP IP_OPTIONS buf &optsize) = 0 && optsize '= 0) { /* форматируем параметры как шестнадцатеричные числа для записи в lbuf[] */ syslog(LOG_NOTICE 'Connection received using IP options (ignored) Xs Ibuf) setsockopt(t) ipproto IP_OPTIONS NULL 0) } Если устанавливается соединение с какими-либо параметрами IP (значение переменной optsize, возвращенное функцией getsockopt, не равно нулю), то с по- мощью функции sysl од делается запись соответствующего сообщения и вызывается функция setsockopt для очистки всех параметров Таким образом предотвраща- ется отправка последующих сегментов TCP для данного соединения по обращен- ному маршруту от отправителя. Сейчас уже известно, что этой технологии недо- статочно, так как к моменту установления соединения трехэтапное рукопожатие TCP будет уже завершено и второй сегмент (сегмент SYN-АСК на рис. 2 4) будет уже отправлен по обращенному маршруту от отправителя к клиенту. Даже если этот сегмент не успеет дойти до клиента, то во всяком случае он дойдет до неко-
698 Глава 24. Параметры!? торого промежуточного узла, входящего в маршрут от отправителя, где, возмож- но, затаился хакер. Так как предполагаемый хакер видел порядковые номера TCP в обоих направлениях, даже если никаких других пакетов по маршруту от отпра- вителя послано не будет, хакер по-прежнему может отправлять клиенту сообще- ния с правильным порядковым номером. Единственным решением этой возможной проблемы является запрет на при- ем любых соединений TCP, приходящих по обращенному маршруту от отправи- теля, когда вы используете IP-адрес от отправителя для какой-либо формы под- тверждения (как, например, в случае с г login или rshd). Вместо вызова функции setsockopt во фрагменте кода, приведенном выше, закройте только что принятое соединение и завершите только что порожденный процесс сервера. Второй сег- мент трехэтапного рукопожатия отправится, но соединение не останется откры- тым и не будет использоваться далее. 24.4. Заголовки расширения IPv6 Мы не показываем никаких параметров в заголовке IPv6 на рис. А.2 (который всегда имеет длину 40 байт), но следом за этим заголовком могут идти заголовки расширения1 (extension headers). 1. Параметры для транзитных узлов1 2 (hop-by-hop options) должны следовать непосредственно за 40-байтовым заголовком IPv6. В настоящее время не опре- делены какие-либо параметры для транзитных узлов, которые могли бы ис- пользоваться в приложении. 2. Параметры получателя (destination options). В настоящее время не определе- ны какие-либо параметры получателя, которые могли бы использоваться в приложении. 3. Заголовок маршрутизации. Этот параметр маршрутизации от отправителя аналогичен по своей сути тем, которые мы рассматривали в случае IPv4. 4. Заголовок фрагментации. Этот заголовок автоматически генерируется узлом при фрагментации дейтаграммы IPv6, а затем обрабатывается получателем при сборке дейтаграммы из фрагментов. 5. Заголовок аутентификации (АН — authentication header). Использование этого заголовка документировано в RFC 1826 [3] и [54]. 6. Заголовок шифрования3 (ESH — encapsulating security pay load header). Ис- пользование этого заголовка документировано в RFC 1827 [4] и в [55]. Мы уже говорили о том, что заголовок фрагментации целиком обрабатывает- ся ядром, а предложения по обработке заголовков АН и ESP изложены в [64]. Остаются еще три параметра, которые мы обсудим в следующем разделе. 1 Используются также термины «расширенные заголовки» и «дополнительные заголовки» — Примеч перев 2 Встречается также термин «заголовок переходов» — Примеч перев. 3 Используются также термины «заголовок защиты» и «дополнительный заголовок аутентификации». Он применятся для обеспечения конфиденциальности при передаче зашифрованных данных. — Примеч перев
24.5. Параметры транзитных узлов и параметры получателя IPv6 699 24.5. Параметры транзитных узлов и параметры получателя IPv6 Параметры для транзитных узлов и параметры получателя IPv6 имеют одинако- вый формат, показанный на рис. 24.3.8-разрядное поле следующий заголовок (next header) идентифицирует следующий заголовок, который следует заданным заго- ловком. 8-разрядное поле длина заголовка расширения (header extension length) со- держит длину заголовка расширения в условных единицах (1 у. е. = 8 байт), но не учитывает первые 8 байт заголовка. Например, если заголовок занимает всего 8 байт, то значение поля длины будет равно нулю. Если заголовок занимает 16 байт, то соответственно значение этого поля будет равно 1, и т. д. Оба заголов- ка заполняются таким образом, чтобы длина каждого была кратна 8 байтам. Это достигается либо с помощью параметра padl, либо с помощью параметра padN, ко- торые мы вскоре рассмотрим. О 78 15 16 23 24 31 Следующий заголовок Длина заголовка расширения Параметры транзитных узлов или параметры получателя Рис. 24.3. Формат параметра для транзитных узлов и параметра получателя Заголовок параметра транзитных узлов и заголовок параметра получателя могут содержать произвольное количество отдельных параметров, как показано на рис. 24.4. Тип Длина Значение параметра 1 1 Рис. 24.4. Формат отдельных параметров, входящих в заголовок параметра транзитных узлов и заголовок параметра получателя Этот формат иногда называется TLV, так как для каждого отдельного пара- метра указывается его тип, длина и значение (type, length, value). 8-разрядное поле типа (type) указывает тип параметра. В дополнение к этому 2 старших разряда указывают, что именно узел IPv6 будет делать с этим параметром в том случае, если он не сможет в нем разобраться: & 00 — пропустить параметр и продолжить обработку заголовка. Ж 01 — игнорировать пакет. Ж 10 — игнорировать пакет и отослать отправителю сообщение об ошибке ICMP Parameter problem (Проблема с параметром) типа 2 (табл. А.4), независимо от того, является ли адрес получателя пакета групповым адресом.
700 Глава 24. Параметры IP 8 11 — игнорировать пакет и отослать отправителю сообщение об ошибке ICMP Parameter problem (Проблема с параметром) типа 2 (табл. А.4), но только в том случае, если адрес получателя пакета не является адресом многоадресной пе- редачи. Следующий разряд указывает, могут ли меняться данные, входящие в этот параметр. 8 0 — данные параметра не могут быть изменены. » 1 — данные параметра могут быть изменены. Оставшиеся пять младших разрядов задают сам параметр. 8-разрядное поле длины задает длину данных этих параметров в байтах. Дли- на поля типа и длина самого поля длины не входят в это значение. Два параметра заполнения (pad options) определены в RFC 1883 [25] и могут быть использованы как в заголовке параметров для транзитных узлов, так и в за- головке параметров получателя. Один из параметров транзитных узлов — пара- метр размера увеличенного поля данных (jumbo payload length option) — также определен в RFC 1883. Ядро генерирует этот параметр по мере необходимости и обрабатывает при получении. Новый параметр увеличенного объема данных для IPv6, аналогичный параметру извещения маршрутизатора (router alert), пред- ложен в [52]. Эти параметры изображены на рис. 24.5. Рас11: 0 PadN: 1 Длина Нулевые байты Количество байтов, заданное в поле длины Размер увеличенного поля данных 194 4 Размер увеличенного поля данных 4 байта Извещение маршрутизатора Тип 2 Значение 2 байта Рис. 24.5. Параметры IPv6 для транзитных узлов Параметр padl — это единственный параметр, для которого не указывается длина и значение. Его назначение — вставка одного пустого байта для заполне- ния. Параметр padN используется, когда требуется вставить 2 или более байтов заполнения. Для 2 байт заполнения длина параметра будет иметь нулевое значе- ние, а сам параметр будет состоять из поля типа и поля длины. В случае 3 байт заполнения длина будет равна 1, а следом за полем длины будет стоять один ну- левой байт. Параметр размера увеличенного поля данных допускает увеличение поля размера дейтаграмм до 32 бит и используется, когда 16-разрядное поле раз- мера, показанное на рис. А.2, оказывается недостаточно большим.
24.5. Параметры транзитных узлов и параметры получателя IPv6 701 Мы показываем эти параметры схематически, потому что для всех парамет- ров получателя и транзитных узлов действует так называемое условие выравни- вания (alignment requirement), записываемое как хп + у. Это означает, что сдвиг данного параметра относительно начала заголовка равен числу, п раз кратному х байтам, к которому добавлено у байт (то есть величина сдвига в байтах равна хп + у). Например, условие выравнивания для параметра размера увеличенного поля данных записывается как 4п + 2. Это означает, что 4-байтовое значение па- раметра (длина размера увеличенного поля данных) будет выровнено по 4-бай- товой границе. Причина, по которой значение у для этого параметра равно 2, за- ключается в том, что параметры транзитных узлов и получателя начинаются именно с двух байтов — один байт используется для указания типа, другой — для указания длины (см. рис. 24.4). Параметры транзитных узлов и параметры получателя обычно задаются как вспомогательные данные в функции sendmsg и возвращаются функцией recvmsg также в виде вспомогательных данных. От приложения не требуется никаких специальных действий для отправки этих параметров — нужно только задать их при вызове функции sendmsg. Но для получения этих параметров должен быть включен соответствующий параметр сокета: IPV6_H0P0PTS для параметра транзит- ных узлов и IPV6 DST0PTS для параметров получателя. Например, чтобы можно было получить оба параметра, нужен следующий код: const int on = 1. setsockopt(sockfd. IPPR0T0_IPV6. IPV6JH0P0PTS. &on. sizeof(on)). setsockopt(sockfd. IPPR0T0JPV6. IPV6_DST0PTS. &on, sizeof(on)). На рис. 24.6 показан формат объектов вспомогательных данных, используе- мый для отправки и получения параметров транзитных узлов и параметров по- лучателя. cmsghdr{} cmsghdr{} cmsg_len cmsg_len cmsg_level IPPROTO_IPV6 cmsg_level IPPROTO_IPV6 cmsg_type IPV6_HOPO₽TS cmsg_type IPV6_DSTOPTS Параметры транзитных узлов Параметры получателя Рис. 24.6. Объекты вспомогательных данных, используемые для параметров транзитных узлов и параметров получателя В отличие от других объектов вспомогательных данных IPv6 (см. рис. 20.5), в данном случае каждая реализация определяет, что передается между пользова- телем и ядром в той части этих объектов, которая обозначена как cmsg_data. Вме- сто того чтобы определять содержимое этой части, определены шесть функций, которые создают и обрабатывают эти вспомогательные объекты данных. Следу- ющие четыре функции формируют отправляемый параметр. #include <netinet/in h> int inet6_option_space(int nbytes).
702 Глава 24. Параметры IP Возвращает количество байтов, занимаемых данным параметром int inet6_option_imt(void *buf. struct cmsghdr ★★cmsgp int type): Возвращает 0 в случае успешного выполнения, -1 в случае ошибки int inet6_option_append(struct cmsghdr *cmsg. const uint8_t ★typep. int multx. int plusy): Возвращает 0 в случае успешного выполнения. -1 в случае ошибки urnt8_t *inet6_option_allос(struct cmsghdr *ansg int data len. int multx. int plusy): Возвращает указатель на поле типа параметра в случае успешного выполнения, NULL в случае ошибки Функция inet6_option_space возвращает количество байтов, необходимое для данного параметра, включая структуру cmsghdr в начале и произвольное количе- ство байтов заполнения в конце. Аргумент nbytes — это размер структуры, опре- деляющей параметр, который должен включать произвольное количество бай- тов заполнения в начале (значение у из суммы хп + у), тип параметра, длину и данные. Функция 1 net6_opti on_i m t вызывается один раз для каждого объекта вспомо- гательных данных, который будет содержать либо параметр транзитных узлов, либо параметр получателя. Аргумент buf указывает на буфер, который будет со- держать объект вспомогательных данных. Аргумент cmsgp — это адрес указателя на структуру cmsghdr. Эта функция инициализирует данную структуру в буфере, на который указывает аргумент buf, и возвращает указатель на эту структуру в аргументе *cmsgp. Аргумент type может принимать значение IPV6_H0P0PTS либо IPV6_DST0PTS и хранится в поле cmsg_type формируемой структуры cmsghdr. Функция 1 net6_opti on append добавляет либо параметр транзитных узлов, либо параметр получателя к вспомогательному объекту данных, инициализированно- му функцией 1 net6_opt 1 on_i mt. Аргумент cmsg — это указатель на структуру cmsghdr, которая была инициализирована функцией inet6_option_imt. Аргумент typep — это указатель на 8-разрядное поле, задающее тип параметра, за которым должно следовать 8-разрядное поле длины параметра и данные TLV. Вызывающий про- цесс устанавливает эти значения перед вызовом данной функции. Аргументы mul tx и р] usy — это два слагаемых х и у, фигурирующие в условии выравнивания для данного параметра. Описанная выше функция требует, чтобы вызывающий процесс составил дан- ные TLV и передал указатель как аргумент typep, после чего параметр копируется в объект вспомогательных данных. Другой вариант — функция i net6_opt i on_a 11 ос возвращает указатель на объект вспомогательных данных, и вызывающий про- цесс затем сохраняет данные TVL для параметра как вспомогательные данные. Аргумент cmsg — это указатель на структуру cmsghdr, инициализированную функ- цией wet6_option_im t. Аргумент datalen — это значение поля длины для данного параметра. Этот аргумент необходим для расчета количества байтов заполнения, если таковые добавляются к параметру. Аргументы mul tx и pl usy — это два слага- емых х и у, фигурирующие в условии выравнивания для данного параметра. Оставшиеся две функции обрабатывают полученный параметр. #include <netinet/in h>
24,6. Заголовок маршрутизации IPv6 703 int inet6_option_next(const struct cmsghdr *cmsg, uint8_t **tptrp). Возвращает 0 в случае если имеется параметр для обработки. -1 в случае ошибки или если нет параметров для обработки int inet6_option_find(const struct cmsghdr *cmsg. uint8_t *tptrp int type). Возвращает 0 в случае успешного выполнения. -1 в случае ошибки Функция Tnet6_option_next обрабатывает следующий параметр в буфере. Ар- гумент cmsgdr указывает на структуру cmsgdr, в которой поле cmsg_level должно иметь значение IPPR0T0_IPV6, а поле cmsg_type — либо 1Р\/6_Н0Р0РТ5,либо IPV6_DST0PTS. Когда эта функция впервые вызывается для конкретного объекта вспомогатель- ных данных, аргумент *tptrp должен быть пустым указателем. Затем каждый раз при завершении этой функции аргумент *tptrp указывает на 8-разрядное поле типа для параметра, который будет обработан следующим. Значение аргумента *tptrp используется данной функцией для того, чтобы в промежутках между по- следовательными вызовами хранить информацию о том, в каком месте объекта вспомогательных данных она находилась. Когда обработан последний параметр, функция возвращает значение -1, a *tptrp становится пустым указателем. Если происходит ошибка, возвращаемое значение также -1, но *tptrp является непус- тым указателем. Функция inet6_option_find аналогична предыдущей функции, но позволяет вызывающему процессу задать тип параметра, который следует искать (аргумент type), вместо того чтобы каждый раз возвращать следующий параметр. 24.6. Заголовок маршрутизации IPv6 Заголовок маршрутизации IPv6 используется для маршрутизации от отправите- ля в IPv6. Первые два байта заголовка маршрутизации такие же, как показанные на рис. 24.3: поле следующего заголовка (next header) и поле длины заголовка рас- ширения {header extension length). Следующие два байта задают тип маршрути- зации (routing type) и количество оставшихся сегментов (number of segments left) (то есть сколько из перечисленных узлов еще нужно пройти). Определен только один тип заголовка маршрутизации, обозначаемый как тип 0. Формат заголовка маршрутизации показан на рис. 24.7. В заголовке маршрутизации IPv6 может появиться до 23 адресов, а количе- ство оставшихся сегментов может варьироваться от 1 до 23. 24 бита, которые мы нумеруем от 0 до 23, формируют так называемую разрядную карту жесткой/гиб- кой маршрутизации: единичный бит означает, что соответствующий адрес отно- сится к «жесткому» узлу (этот узел должен непосредственно соседствовать с пре- дыдущим узлом в списке), а нулевой бит означает, что соответствующий адрес «гибкий», то есть он не обязан быть непосредственным соседом предыдущего узла. Бит, помеченный номером 1, соответствует Адресу 1 (рис. 24.7), бит под номе- ром 2 соответствует Адресу 2 и т. д. Бит под номером 0 соответствует последне- му узлу. В RFC 1883 [25] приводятся детали обработки заголовка по мере про- движения пакета к получателю, а также имеется подробный пример. Заголовок маршрутизации обычно задается как вспомогательные данные в функции sendmsg и возвращается в виде вспомогательных данных функцией recvmsg. Для отправки заголовка приложению не требуется выполнять какие-либо
704 Глава 24. Параметры IP специальные действия — достаточно просто указать его при вызове функции sendmsg. Но для получения заголовка маршрутизации требуется, чтобы был вклю- чен параметр I PV6_RTHDR: socket option must be enabled as in const int on = 1 setsockopt(sockfd IPPR0T0_IPV6 IPV6_RTHDR &on sizeof(on)). На рис. 24.8 показан формат объекта вспомогательных данных, используемый для отправки и получения заголовка маршрутизации. Аналогично объектам вспо- могательных данных для параметров транзитных узлов и параметров получателя (см. рис. 24.6), каждая реализация определяет самостоятельно, что передается между пользователем и ядром в качестве той части этих объектов, которая обо- значена как cmsg_data. Для создания и обработки заголовка маршрутизации опре- делены восемь функций. Следующие четыре функции используются для созда- ния отправляемого параметра: #include <netinet/in h> size_t inet6_rthdr_space(int type, int segments') Возвращает положительное число равное количеству байтов'в случаё успешного выполнения в случае ошибки struct cmsghdr *inet6_rthdr_imt(void ★buf int type). Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки
24.6. Заголовок маршрутизации IPv6 705 int inet6_rthdr_add(struct cmsghdr *cmsg const struct in6_addr *addr unsigned int flags'). Возвращает 0 в случае успешного выполнения -1 в случае ошибки int inet6_rthdr_lasthop(struct cmsghdr *cmsg unsigned int flags). Возвращает 0 в случае успешного выполнения. -1 в случае ошибки Функция inet6_rthdr_space возвращает количество байтов, необходимое для размещения объекта вспомогательных данных, содержащего заголовок маршру- тизации указанного типа (обычно это IPV6_RTHDR_TYPE_0) с заданным количеством сегментов. Этот размер включает структуру cmsghdr. cmsghdr{} cmsg_len cmsg__level cmsg_type IPPROTO_IPV6 IPV6_RTHDR Заголовок маршрутизации Рис. 24.8. Объект вспомогательных данных для заголовка маршрутизации IPv6 ПРИМЕЧАНИЕ------------------------------------------------------------------- Функции inet6_rthdr_space и inet6_option_space обе возвращают количество байтов, необходимое для размещения объекта вспомогательных данных заданного типа, но ни одна из них не занимается фактическим его размещением Это сделано потому, что вызывающему процессу, возможно, потребуется разместить в памяти буфер большего размера для размещения в нем еще и других объектов вспомогательных данных. Функция inet6_trhdr_imt инициализирует буфер, на который указывает ар- гумент buf, как содержащий объект вспомогательных данных с заголовком типа type. Возвращаемое значение этой функции — указатель на структуру cmsghdr, которая встраивается в буфер, после чего этот указатель используется как ар- гумент при вызове следующих двух функций. Инициализируются элементы cmsg_l evel и cmsg_type. Функция inet6_rthdr_add добавляет адрес IPv6, на который указывает аргу- мент addr, к концу составляемого заголовка маршрутизации. Аргумент fl ags — это либо IPV6_RTHDR_L00SE, либо IPV6_RTHDR_STRICT. В случае успешного выполне- ния обновляется значение элемента cmsg_l еп, чтобы учесть добавленный новый адрес. Функция 1 net6_rthdr_l asthop задает значение аргумента fl ags (I PV6_RTHDR_L00SE или IPV6_RTHDR_STRICT) для последнего узла. Учтите, что заголовок маршрутиза- ции с N адресами реально включает в себя N+1 узел. Это подразумевает N вызо- вов функции inet6_rthdr_add и один вызов функции inet6_rthdr_1 asthop. Следующие четыре функции манипулируют полученным заголовком марш- рутизации: #include <netinet/in h> int inet6_rthdr_reverse(const struct cmsghdr *in struct cmsghdr *out)
706 Глава 24. Параметры IP Возвращает 0 в случае успешного выполнения. -1 в случае ошибки int inet6_rthdr_segments(const struct cmsghdr *cmsg) Возвращает количество сегментов в заголовке маршрутизации в случае успешного выполнения, -1 в случае ошибки struct in6_addr *inet6_rthdr_getaddr(struct cmsghdr *cmsg mt index). Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки int inet6_rthdr_getflags(const struct cmsghdr *cmsg int index). Возвращает флаг жесткой/гибкой маршрутизации в случае успешного выполнения. -1 в случае ошибки Функция 1 net6_rthdr_reverse принимает в качестве аргумента заголовок мар- шрутизации, полученный в виде объекта вспомогательных данных (на который указывает аргумент i п), и создает новый заголовок маршрутизации (в буфере, на который указывает аргумент out), отправляющий дейтаграммы по обратному маршруту. Указатели in и out могут указывать на один и тот же буфер. Функция 1 net6_rthdr_segments возвращает количество сегментов в заголовке маршрутизации, обозначенном как cmsg. В случае успешного выполнения функ- ции возвращаемое значение лежит в диапазоне от 0 до 23 (включительно). Функция inet6_rthdr_getaddr возвращает указатель на адрес IPv6, заданный через 1 ndex в заголовке маршрутизации cmsg. Аргумент i ndex должен лежать в пре- делах от 1 до значения, возвращенного функцией inet6_rthdr_segments, включи- тельно. Функция 1 net6_rthdr_getf 1 ags возвращает либо флаг I PV6_RTHDR_L00SE, либо флаг IPV6_RTHDR_STRICT, соответствующий адресу, на который указывает аргумент i ndex (его значение лежит в пределах от 1 до значения, возвращенного функцией inet6_rthdr_segments, включительно) в заголовке маршрутизации cmsg. ПРИМЕЧАНИЕ -------------------------------------------------------------------------- Адреса индексируются начиная с 1, а флаги жесткой/гибкой маршрутизации индекси- руются начиная с 0, как показано на рис. 24.7. Такой способ индексации согласован с системой обозначений, описанной в RFC 1883 [25] 24.7. «Закрепленные» параметры IPv6 Мы рассмотрели использование вспомогательных данных с функциями sendmsg и recvmsg для отправки и получения следующих шести различных типов объек- тов вспомогательных данных: 1. Информация о пакете IPv6: структура i n6_pkti nfo, содержащая адрес получа- теля и индекс интерфейса для исходящих дейтаграмм либо адрес отправителя и индекс интерфейса для приходящих дейтаграмм (индекс принимающего интерфейса) (рис. 20.5). 2. Предельное количество транзитных узлов для исходящих или приходящих дейтаграмм (рис. 20.5). 3. Адрес следующего транзитного узла (рис. 20.5).
24.8. Резюме 707 4. Параметры транзитных узлов (рис. 24.6). 5. Параметры получателя (рис. 24.6). 6. Заголовок маршрутизации (рис. 24.8). В табл. 13.4 приведены значения полей cmsg level и cmsg_type для этих объек- тов, а также значения для других объектов вспомогательных данных. Вместо того чтобы отсылать эти параметры при каждом вызове функции sendmsg, мы можем установить параметр сокета IPV6_PKT0PTI0NS. При установке этого параметра четвертый аргумент функции указывает на буфер, содержащий все объекты вспомогательных данных, которые должны быть отправлены в каждом пакете, посылаемом с данного сокета. Формат этого буфера в точности совпадает с форматом буфера, используемого функцией sendmsg для хранения объектов вспо- могательных данных. Заданные таким образом параметры называются закреп- ленными' {sticky), поскольку, если они заданы один раз, они остаются действи- тельными, пока не будут явным образом отменены (для отмены требуется задать данный параметр сокета с нулевой длиной). Но закрепленные параметры могут быть заменены для конкретного пакета в случае сокета UDP или символьного сокета IPv6, если при вызове функции sendmsg задать какие-либо другие парамет- ры в качестве объектов вспомогательных данных. Если при вызове функции sendmsg указаны какие-либо вспомогательные данные, ни один из закрепленных параметров не будет послан с этим пакетом. Концепция закрепленных параметров также может быть использована и в слу- чае TCP, поскольку вспомогательные данные никогда не отсылаются и не прини- маются с помощью функции sendmsg или recvmsg на сокете TCP. Вместо этого приложение TCP может установить параметр сокета IPV6_PKT0PTI0NS и указать любой из упомянутых в начале этого раздела шести объектов вспомогательных данных. Тогда эти параметры будут относиться ко всем пакетам, отсылаемым с данного сокета. Приложение TCP может также вызвать функцию getsockopt, чтобы с помощью параметра сокета I PV6_PKT0PTI0NS получить эти объекты вспомогательных данных. В таком случае ядро будет хранить только те параметры из последнего получен- ного сегмента, которые явным образом запросило приложение (включив соот- ветствующий параметр сокета). 24.8. Резюме Из десяти определенных в IPv4 параметров наиболее часто используются пара- метры маршрутизации от отправителя, но в настоящее время их популярность падает из-за проблем, связанных с безопасностью. Доступ к параметрам заголов- ков IPv4 осуществляется с помощью параметра сокета IP OPTIONS. В IPv6 определены шесть заголовков расширения, хотя в настоящее время их поддержка минимальна. Доступ к заголовкам расширения IPv6 осуществляется с помощью функционального интерфейса, что освобождает нас от необходимо- сти углубляться в детали фактического формата внутри пакета. Эги заголовки 1 Sticky — клейкий, липкий В данном случае подразумевается, что параметры «приклеиваются» к сокету. — Примеч перев.
708 Глава 24 Параметры IP расширения записываются как вспомогательные данные функцией sendmsg и воз- вращаются функцией recvmsg также в виде вспомогательных данных. Упражнения 1. Что изменится на стороне сервера, если в нашем примере маршрута от отпра- вителя, приведенном в конце раздела 24.3, мы вместо указания клиенту име- ни узла gw зададим другой IP-адрес этого маршрутизатора — 206.85.40.74, по- казанный на рис. 1.7? 2. Что изменится, если в нашем примере, приведенном в конце раздела 24.3, мы зададим каждый промежуточный узел с параметром -G вместо -д? 3. Размер буфера, указываемый в качестве аргумента функции setsockopt для параметра сокета IP OPTIONS, должен быть кратен 4 байтам. Что бы нам при- шлось делать, если бы мы не поместили параметр NOP в начало буфера, как показано на рис. 24. Р 4. Каким образом программа рт ng получает маршрут от отправителя, когда ис- пользуется параметр IP Record Route (запись маршрута), описанный в разде- ле 7.3 [105]? 5. Почему в примере кода для сервера г 1 ogi nd, приведенном в конце раздела 24.3, который предназначен для удаления полученного маршрута от отправителя, дескриптор сокета (первый аргумент функций getsockopt и setsockopt) имеет нулевое значение? 6. В течение долгого времени для удаления маршрута использовался код, не- сколько отличающийся от приведенного в конце раздела 24.3. Он выглядел следующим образом: optsize = О setsockopt(0 ipproto IP_OPTIONS, NULL &optsize). Что в этом фрагменте неправильно? Имеет ли это значение?
ГЛАВА 25 Символьные сокеты 25.1. Введение Символьные, или неструктурированные, сокеты1 (raw sockets) обеспечивают три возможности, не предоставляемые обычными сокетами TCP и UDP. 1. Символьные сокеты позволяют читать и записывать пакеты ICMPv4, IGMPv4 и ICMPv6. Например, программа Ping посылает эхо-запросы ICMP и получа- ет эхо-ответы ICMP. (Наша оригинальная версия программы Ping приведена в разделе 25.5.) Демон маршрутизации многоадресной передачи mrouted посы- лает и получает пакеты IGMPv4 Эта возможность также позволяет реализовывать как пользовательские про- цессы те приложения, которые построены с использованием протоколов ICMP и 1GMP, вместо того чтобы помещать большее количество кода в ядро. На- пример, подобным образом построен демон отыскания маршрута (in rdi sc в си- стеме Solaris 2.x. В приложении F книги [94] показано, как получить исход- ный код открытой версии). Этот демон обрабатывает два типа сообщений ICMP, о которых ядро ничего не знает (извещение маршрутизатора и запрос маршрутизатору). 2. С помощью символьных сокетов процесс может читать и записывать IPv4- дейтаграммы с полем протокола IPv4, которое не обрабатывается ядром. На- помним, что 8-разрядное поле протокола IPv4 изображено на рис. А 1. Большин- ство ядер обрабатывают дейтаграммы, содержащие значения поля протокола 1 (ICMP), 2 (IGMP), 6 (TCP) и 17 (UDP). Но для этого поля определено гораздо большее количество значений, полный список которых приведен в RFC 1700 [87]. Например, протокол маршрутизации OSPF не использует протоколы TCP или UDP, а работает напрямую с протоколом IP, устанавли- вая в поле протокола значение 89 для IP-дейтаграмм. Программа gated, реали- зующая OSPF, должна использовать для чтения и записи таких 1Р-дейтаграмм символьный сокет, поскольку они содержат значение поля протокола, о кото- ром ничего не известно ядру. Эта возможность также переносится в версию IPv6. 3. С помощью символьных сокетов процесс может построить собственный заго- ловок IPv4 при помощи параметра сокета IP_HDRINCL. Такую возможность мож- но использовать, например, для построения собственного пакета UDP или TCP. Подобный пример приведен в разделе 26.6. Как уже отмечалось ранее, используется также термин «сырой сокет» — Примеч перво
710 Глава 25. Символьные сокеты В данной главе описывается создание символьных сокетов, а также их ввод и вывод. Далее приводятся версии программ Ping и Traceroute, работающие как с версией IPv4, так и с версией IPv6. 25.2. Создание символьных сокетов При создании символьных сокетов выполняются следующие шаги: 1. Символьный сокет создается функцией socket со вторым аргументом SOCK RAW. Третий аргумент (протокол) обычно ненулевой. Например, для создания сим- вольного сокета IPv4 следует написать: int sockfd. sockfd = socket(AF_INET. SOCK_RAW. protocol) где protocol — одна из констант IPPROTO_xxx, определенных в подключенном заголовочном файле <neti net/1 n h>, например IPPROTO_ICMP. Имейте в виду, что хотя имя протокола может быть определено в данном заголовочном файле (например, IPPROTO_EGP), это не означает, что ядро его поддерживает. Только привилегированный пользователь может создать символьный сокет. Такой подход предотвращает отправку IP-дейтаграмм в сеть обычными пользо- вателями. 2. Параметр сокета IP_HDRINCL может быть установлен следующим образом: const int on = 1. if (setsockopt(sockfd. IPPROTO_IP. IP_HDRINCL, &on. sizeof(on)) < 0) error В следующем разделе описывается действие этого параметра. 3. На символьном сокете можно вызвать функцию bind, но это делается редко. Эта функция устанавливает только локальный адрес: на символьном сокете нет понятия порта. Что касается вывода, вызов функции bind устанавливает IP-адрес отправителя, который будет использоваться для дейтаграмм, отправ- ляемых на символьном сокете (только если не установлен параметр сокета IP HDRINCL). Если функция bind не вызывается, ядро использует в качестве IP- адреса отправителя IP-адрес исходящего интерфейса. 4. На символьном сокете можно вызвать функцию connect, но это делается ред- ко. Эта функция устанавливает только внешний адрес, так как на символьном сокете нет понятия порта. О выводе можно сказать, что вызов функции connect позволяет нам вызвать функцию write или send вместо sendto, поскольку IP- адрес получателя уже определен. 25.3. Вывод на символьном сокете Вывод на символьном сокете регулируется следующими правилами: 1. Стандартный вывод выполняется путем вызова функции sendto или sendmsg и определения IP-адреса получателя. Функции write, writev и send также мож- но использовать, если сокет уже присоединен. 2. Если не установлен параметр сокета IP_HDRINCL, то начальный адрес данных, предназначенных для записи ядром, указывает на первый байт, следующий за
25.3. Вывод на символьном сокете 711 IP-заголовком, поскольку ядро будет строить IP-заголовок и добавлять его к началу данных из процесса. Ядро устанавливает значение третьего аргумен- та функции socket равным полю протокола создаваемого заголовка IPv4. 3. Если параметр сокета IP_HDRINCL установлен, то начальный адрес данных, пред- назначенных для записи ядром, указывает на первый байт IP-заголовка. Раз- мер данных для записи должен включать размер IP-заголовка вызывающего процесса. Процесс полностью формирует IP-заголовок, за исключением того, что, во-первых, значение поля идентификации IPv4 может быть нулевым (что ука- зывает ядру на необходимость самостоятельно установить это значение), и во- вторых, ядро всегда вычисляет и сохраняет контрольную сумму заголовка IPv4. 4. Ядро фрагментирует символьные пакеты, превышающие значение MTU ис- ходящего интерфейса. ПРИМЕЧАНИЕ---------------------------------------------------------- К сожалению, параметр сокета IPHDRINCL никогда не документ ировался, в том числе и в отношении порядка байтов для полей в заголовке IPv4. В Беркли-ядрах все поля имеют порядок байтов сети, за исключением полей ip_len и ip_off, имеющих порядок байтов узла [105, с. 233, с. 1057]. В системе Linux, однако, все поля имеют порядок бай- тов сети. Параметр сокета IP HDRINCL впервые был представлен в системе 4.3BSD Reno. До этого приложение имело единственную возможность определить свой собственный IP- заголовок в пакетах, отсылаемых на символьный сокет, — использовать заплату ядра (kernel patch), которая была представлена в 1988 году Ван Якобсоном (Van Jacobson) для поддержки программы Traceroute. Эта заплата позволяла приложению создавать символьный IP-сокет, определяя протокол как IPPROTO_RAW, что соответствовало значению 255 (это значение является зарезервированным и никогда не должно появ- ляться в поле протокола IP-заголовка). Функции, осуществляющие ввод-вывод на символьном сокете, являются одними из простейших функций в ядре. Например, в книге [105, с. 1054-1057] каждая такая функ- ция занимает около 40 строк кода на языке С. Для сравнения: функция ввода TCP содержит около 2000 строк, а функция вывода TCP около 700 строк. Данное описание параметра сокета IP_HDRINCL относится к системе 4.4BSD. В более ранних версиях, таких как Net/2, при использовании данного параметра заполнялось большее количество полей заголовка IP. В протоколе IPv4 пользовательский процесс отвечает за вычисление и уста- новку контрольной суммы любого заголовка, следующего за заголовком IPv4. Например, в нашей программе Ping (см. листинг 25.9), прежде чем вызывать функ- цию sendto, мы должны вычислить контрольную сумму ICMPv4 и сохранить ее в заголовке ICMPv4. Особенности символьного сокета версии IPv6 Для символьного сокета IPv6 существует несколько отличий [96]: Все поля в заголовках протоколов, отсылаемых или получаемых на символь- ном сокете IPv6, должны находиться в сетевом порядке байтов. В IPv6 не существует параметров, подобных параметру IP HDRINCL сокета IPv4. Полные пакеты IPv6 (включая дополнительные заголовки) не могут быть про-
712 Глава 25. Символьные сокеты читаны или записаны через символьный сокет IPv6. Приложения имеют до- ступ почти ко всем полям заголовка IPv6 и дополнительных заголовков через параметры сокета или вспомогательные данные (см. упражнение 25.1). Если приложению все же необходимо полностью считать или записать IPv6-дей- таграмму, необходимо использовать канальный доступ (о нем речь пойдет в главе 26). Как вскоре будет показано, на символьном сокете IPv6 по-другому обрабаты- ваются контрольные суммы. Параметр сокета IPv6_CHECKSUM Для символьного сокета IPv6 ядро всегда вычисляет и сохраняет контрольную сумму в заголовке ICMPv6, тогда как для символьного сокета ICMPv4 приложе- ние должно выполнять данную операцию самостоятельно (сравните листинги 25.9 и 25.11). И ICMPv4, и ICMPv6 требуют от отправителя вычисления контрольной суммы, но ICMPv6 включает в свою контрольную сумму псевдозаголовок (поня- тие псевдозаголовка обсуждается при вычислении контрольной суммы UDP в листинге 26.9). Одно из полей этого псевдозаголовка представляет собой IPv6- адрес отправителя, и обычно приложение оставляет ядру возможность выбирать это значение. Чтобы приложение не пыталось отыскать этот адрес для вычисле- ния контрольной суммы, проще разрешить вычислять контрольную сумму ядру. В случае других символьных сокетов IPv6 (при создании которых третий ар- гумент функции socket отличен от I PPROTQ_ICMPV6) параметр сокета сообщает ядру, вычислять ли контрольную сумму и сохранять ли ее в исходящих пакетах, а так- же следует ли проверять контрольную сумму в приходящих пакетах. По умолча- нию этот параметр выключен, а включается он путем присваивания неотрица- тельного значения параметра, как в следующем примере: int offset - 2 if (setsockopt(sockfd IPPRDTDJPV6 IPV6_CHECKSUM. &offset sizeof(offset)) < 0) error Здесь не только разрешается вычисление контрольной суммы на данном со- кете, но и сообщается ядру смещение 16-разрядной контрольной суммы в байтах: в данном примере оно составляет 2 байта от начала данных приложения. Чтобы отключить данный параметр, ему нужно присвоить значение -1. Если он вклю- чен, ядро будет вычислять и сохранять контрольную сумму для исходящих паке- тов, посланных на данном сокете, а также проверять контрольную сумму для па- кетов, получаемых данным сокетом. 25.4. Ввод через символьный сокет Первый вопрос, на который следует ответить, говоря о символьных сокетах, сле- дующий: какие из полученных IP-дейтаграмм ядро передает символьному соке- ту? Применяются следующие правила: 1. Получаемые пакеты UDP и TCP никогда не передаются на символьный сокет. Если процесс хочет считать IP-дейтаграмму, содержащую пакеты UDP или TCP, пакеты должны считываться на канальном уровне, как показано в гла- ве 26.
25.4. Ввод через символьный сокет 713 2. Большинство ICMP-пакетов передаются на символьный сокет, после того как ядро заканчивает обработку ICMP-сообщения. Беркли-реализации посыла- ют все получаемые ICMP-пакеты на символьный сокет, кроме эхо-запроса, запроса отметки времени и запроса маски адреса [105, с. 302-303]. Эти три типа ICMP-сообщений полностью обрабатываются ядром. 3. Все IGMP-пакеты передаются на символьный сокет, после того как ядро за- канчивает обработку IGMP-сообщения. 4. Все IP-дейтаграммы с таким значением поля протокола, которое не понимает ядро, передаются на символьный сокет. Для этих пакетов ядро выполняет толь- ко минимальную проверку некоторых полей IP-заголовка, таких как версия IP, контрольная сумма 1Р\’4-заголовка, длина заголовка и IP-адрес получате- ля [105, с. 213-220]. 5. Если дейтаграмма приходит фрагментами, символьному сокету ничего не пе- редаются, до тех пор, пока все фрагменты не прибудут и не будут снова собра- ны вместе. Если у ядра есть IP-дейтаграмма для пересылки символьному сокету, в поис- ках подходящих сокетов проверяются все символьные сокеты всех процессов. Копия IP-дейтаграммы доставляется каждому подходящему сокету. Для каждо- го символьного сокета выполняются три перечисленных ниже теста, и только в том случае, если все три теста дают положительный результат, дейтаграмма направ- ляется данному сокету. 1. Если при создании символьного сокета определено ненулевое значение про- токола (третий аргумент функции socket), то значение поля протокола полу- ченной дейтаграммы должно совпадать с этим ненулевым значением, иначе дейтаграмма не будет доставлена на данный сокет. 2. Если локальный IP-адрес связан с символьным сокетом функцией bind, IP- адрес получателя в полученной дейтаграмме должен совпадать с этим адре- сом, иначе дейтаграмма не посылается данному сокету. 3. Если для символьного сокета был определен внешний адрес с помощью функ- ции connect IP-адрес отправителя в полученной дейтаграмме должен совпа- дать с этим адресом, иначе дейтаграмма не посылается данному сокету. Следует отметить, что если символьный сокет создан с нулевым полем прото- кола и не вызывается ни функция bind, ни функция connect, то сокет получает копии всех дейтаграмм, которые ядро направляет символьным сокетам. Полная дейтаграмма, включая IP-заголовок, передается процессу всегда, ког- да полученная дейтаграмма направляется символьному сокету IPv4. В версии IPv6 символьному сокету передается все, кроме дополнительных заголовков (см., на- пример, рис. 25.4 и рис. 25.6). ПРИМЕЧАНИЕ------------------------------------------------------------- В заголовке IPv4, передаваемом приложению, для ip_len, ipoff и ip_id установлен по- рядок байтов узла, а все остальные ноля имеют порядок байтов сети. В системе Linux все поля остаются в сетевом порядке байтов. В предыдущем разделе уже отмечалось, что все поля символьного сокета IPv6 остают- ся в сетевом порядке байтов
714 Глава 25. Символьные сокеты Фильтрация ICMPv6 Символьный сокет ICMPv4 получает большинство сообщений ICMPv4, полу- ченных ядром. Но ICMPv6 является расширением ICMPv4, включающим функ- циональные возможности ARP и IGMP (см. раздел 2.2). Следовательно, символь- ный сокет ICMPv6 потенциально может принимать гораздо больше пакетов по сравнению с символьным сокетом ICMPv4. Но большинство приложений, ис- пользующих символьные сокеты, заинтересованы только в небольшом подмно- жестве всех ICMP-приложений. Для уменьшения количества пакетов, передаваемых от ядра к приложению через символьный ICMPv6-cokct, предусмотрен фильтр, связанный с приложе- нием. Фильтр объявляется с типом данных struct icmp6_filter, который опреде- ляется путем подключения заголовочного файла <net т net/1 стрб. h>. Для установки и получения текущего 1СМР\’6-фильтра для символьного сокета ICMPv6 исполь- зуются функции setsockopt и getsockopt с аргументом 1 evel, равным IPPR0T0_ICMPV6, и аргументом optname, равным ICMP6_FILTER. Со структурой icmp6_filter работают шесть макросов. #include <netinet/1стрб h> void ICMP6_FILTER_SETPASSALL(struct icmp6_filter *filt). void ICMP6_FILTER_SETBL0CKALL(struct icmp6_filter *filt). void ICMP6_FILTER_SETPASS(int msgtype, struct icmp6_filter void ICMP6_FILTER_SETBL0CK(int msgtype, struct icmp6_filter *filt). int ICMP6_FILTER_WILLPASS(int msgtype, const struct icmp6_filter *filt). int ICMP6_FILTER_WILLBLOCK(int msgtype const struct icmp6_filter *filt). Обе возвращают 1. если фильтр пропускает (блокирует) сообщение данного типа 0 в противном случае Аргумент fi 11 всех макрокоманд являет ся указателем на переменную i cmp6_f 11ter, изменяемую первыми четырьмя макрокомандами и проверяемую последними двумя. Аргумент msgtype является значением в интервале от 0 до 255, определяю- щим тип ICMP-сообщения. Макрокоманда SETPASSALL указывает, что все типы сообщений должны пере- сылаться приложению, а макрокоманда SETBLOCKALL — что никакие сообщения не должны посылаться приложениям. По умолчанию при создании символьного сокета ICMPv6 подразумевается, что все типы ICMP-сообщений пересылаются приложению. Макрокоманда SETPASS определяет конкретный тип сообщений, который дол- жен пересылаться приложению, а макрокоманда SETBLOCK блокирует один конк- ретный тип сообщений. Макрокоманда WILLPASS возвращает значение 1, если опре- деленный тип пересылается фильтром. Макрокоманда WILLBLOCK возвращает значение 1, если определенный тип блокирован фильтром, и нуль в противном случае. В качестве примера рассмотрим приложение, которое будет получать только 1СМР\’6-извещения маршрутизатора: struct icmp6_filter myfilt. fd = Socket(AF_INET6. SOCK_RAW. IPPR0T0JCMPV6).
25.5. Программа Ping 715 ICMP6_FILTER_SETBL0CKALL(&niyfi It): ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT. &myfi1t). Setsockopt(fd, IPPR0T0_ICMPV6. ICMP6_FILTER. &myfilt. sizeof(myfilt)). Сначала мы блокируем все типы сообщений (поскольку по умолчанию все типы сообщений пересылаются), а затем разрешаем пересылать только извеще- ния маршрутизатора. 25.5. Программа Ping В данном разделе приводится версия программы Ping, работающая как с IPv4, так и с IPv6. Вместо того чтобы представить известный доступный исходный код, мы разработали оригинальную программу, и сделано это по двум причинам. Во- первых, свободно доступная программа Ping страдает общей болезнью програм- мирования, известной как «ползучий улучшизм» (стремление к постоянным ненужным усложнениям программы в погоне за мелкими улучшениями): она под- держивает 12 различных параметров. Наша цель при исследовании программы Ping в том, чтобы понять концепции и методы сетевого программирования и не быть при этом сбитыми с толку ее многочисленными параметрами. Наша версия программы Ping поддерживает только один параметр и занимает в пять раз мень- ше места, чем общедоступная версия. Во-вторых, общедоступная версия работа- ет только с IPv4, а нам хочется показать версию, поддерживающую также и IPv6. Действие программы Ping предельно просто: по некоторому IP-адресу посы- лается эхо-запрос ICMP, и этот узел отвечает эхо-ответом ICMP. Оба эти сооб- щения поддерживаются в обеих версиях — и в IPv4, и в IPv6. На рис. 25.1 приве- ден формат ICMP-сообщений. Рис. 25.1. Формат сообщений эхо-запроса и эхо-ответа ICMPv4 и ICMPv6 В табл. А.З и А.4 приведены значения поля тип (type) для этих сообщений и показано, что значение поля код (code) равно нулю. Далее будет показано, что в поле идентификатор (identifier) указывается идентификатор процесса Ping, а значение поля порядковый номер (sequence number) увеличивается на 1 для каж- дого отправляемого пакета. В поле дополнительные данные (optional data) сохра- няется 8-байтовая отметка времени отправки пакета. Правила ICMP-запроса тре- буют, чтобы идентификатор, порядковый номер и все дополнительные данные возвращались в эхо-ответе. Сохранение отметки времени отправки пакета позво- ляет вычислить RTT при получении ответа. В листинге 25.1 приведены примеры работы нашей программы. В первом ис- пользуется версия IPv4, а во втором IPv6. Обратите внимание па приглашение системы, заканчивающееся знаком #, обозначающим привилегированного пользе-
716 Глава 25. Символьные сокеты вателя, поскольку для создания символьного сокета нужны права привилегиро- ванного пользователя. Листинг 25.1 Примеры вывода программы Ping solans # ping gemim.tuc.noao.edu PING gemim tuc noao edu (140 252 4 54) 56 data bytes 64 bytes from 140 252 4 54 seq=O tt1=248 rtt=37 542 ms 64 bytes from 140 252 4 54 seq=l tt1=248 rtt=34 596 ms 64 bytes from 140 252 4 54 seq=2 ttl=248. rtt=29 204 ms 64 bytes from 140 252 4 54 seq=3 ttl=248, rtt=52 630 ms solans # ping 6bone-router cisco.com PING 6bone-router cisco com (5f00 6d00 cOlf 700 1 60 3ell 6770) 56 data bytes 64 bytes from 5f00 6d00 cOlf 700 1 60 3ell 6770 seq=0 hlim=255 rtt=116 802 ms 64 bytes from 5f00 6d00 cOlf 700 1 60 3ell 6770 seq=l hlim=255 rtt=129 321 ms 64 bytes from 5f00 6d00 cOlf 700 1 60 3ell 6770 seq=2 hlim=255 rtt=109 297 ms 64 bytes from 5f00 6d00 cOlf 700 1 60 3ell 6770 seq=3 hlim=255 rtt=78 216 ms На рис. 25 2 приведен обзор функций, составляющих программу Pmg. or Бесконечный цикл получения Рис. 25.2. Обзор функций программы Ping Отправка эхо-ответа раз в секунду Данная программа состоит из двух частей: одна половина читает все, что при- ходит на символьный сокет, и выводит эхо-ответы ICMP, а другая половина раз в секунду посылает эхо-запросы ICMP. Вторая половина запускается раз в се- кунду сигналом SIGALRM. В листинге 25.2 приведен заголовочный файл ping.h, подключаемый во всех файлах программы. Листинг 25.2. Заголовочный файл ping.h //ping/ping h 1 include unp h 2 #include <netinet/in_systm h> 3 #include <netinet/ip h> 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу nrtp:// www piter com/download
25.5. Программа Ping 717 4 #include <netinet/ip_icmp h> 5 #define BUFSIZE 1500 6 7 8 char char /* глобальные переменные */ recvbuf[BUFSIZE] sendbuf[BUFSIZE] 9 10 11 12 13 14 int char int pid_t int int datalen /* размер данных в байтах следующих за ICMP-заголовком */ *host nsent /* добавляет 1 для каждого вызова sendtoO */ pid /* идентификатор нашего процесса РЮ */ sockfd verbose 15 16 17 18 19 20 21 22 void void void void void void void /* прототипы функций */ proc_v4(char * ssize_t struct timeval *) proc_v6(char * ssize_t struct timeval *) send_v4(void) send_v6(void) readloop(void) sig_alrm(int) tv_sub(struct timeval * struct timeval *) 23 24 25 26 27 28 29 30 struct proto { void (*fproc) (char * void (*fsend) (void) struct sockaddr *sasend struct sockaddr *sarecv socklen_t salen int icmpproto } *pr ssize_t struct timeval *) /* структура sockaddr{) для отправки, получена из getaddrinfo */ /* sockaddr{} для получения*/ /* длина sockaddr{} */ /* значение IPPR0T0_xxx для ICMP-сообщений*/ 31 #i fdef IPV6 32 33 #include 'ip6 h" /* #include "icmp6 h" /* должно быть <netinet/ip6 h> */ должно быть <netinet/icmp6 h> */ 34 #endif Подключение заголовочных файлов IPv4 и ICMPv4 1-22 Подключаются основные заголовочные файлы IPv4 и ICMPv4, определяются некоторые глобальные переменные и прототипы функций. Определение структуры proto 23-30 Для обработки различий между IPv4 и IPv6 используется структура proto. Дан- ная структура содержит два указателя на функции, два указателя на структуры адреса сокета, размер структуры адреса сокета и значение протокола для ICMP. Глобальный указатель рг будет указывать на одну из этих структур, которая бу- дет инициализироваться для IPv4 или IPv6. Подключение заголовочных файлов IPv6 и ICMPv6 31-34 Подключаются два заголовочных файла, определяющие структуры и констан- ты IPv6 и ICMPv6 [96].
718 Глава 25. Символьные сокеты Функция main приведена в листинге 25.3. Листинг 25.3. Функция main //ping/main с 1 #include "ping h" 2 struct proto proto_v4 = 3 {proc_v4. send_v4. NULL. NULL 0. IPPROTO_ICMP). 4 #ifdef IPV6 5 struct proto proto_v6 “ 6 {proc_v6. send_v6 NULL. NULL. 0. IPPROTOJCMPV6}. 7 #endif 8 int datalen = 56. /* данные передаваемые с эхо-запросом ICMP */ 9 int 10 mainlint argc. char **argv) И { 12 int c. 13 struct addrinfo *ai, 14 opterr - О /* чтобы функция getoptO не записывала в stderr */ 15 while ( (с = getoptlargc. argv. "v")) != -1) { 16 switch (c) { 17 case V 18 verbose++. 19 break. 20 case '7' 21 err_quit("unrecogmzed option %c". c) 22 } 23 } 24 if (optind != argc - 1) 25 err_quit("usage ping [ -v ] <hostname>"). 26 host - argv[optindj. 27 pid - getpidO . 28 Signal(SIGALRM. sigalrm). 29 ai = Host_serv(host. NULL. 0. 0). 30 printfCPING 2 s (Xs) 2d data bytes\en". ai->ai_canonname. 31 Sock_ntop_host(ai->aiaddr. ai->ai_addrlen). datalen). 32 /* инициализация в зависимости от протокола */ 33 if (ai->ai_family == AF_INET) { 34 pr - &proto_v4. 35 #ifdef IPV6 36 } else if (ai->ai_famly == AF INET6) { 37 pr = &proto_v6. 38 if (IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_in6 *) 39 ai->ai_addr)->sin6_addr))) 40 err_quit("cannot ping IPv4-mapped IPv6 address”). 41 #endif 42 } else 43 err_quit("unknown address family 2d". ai->ai_family).
25.5. Программа Ping 719 44 pr->sasend = ai->ai_addr. 45 pr->sarecv = Callocd. ai->ai_addrlen), 46 pr->salen = ai->ai_addrlen. 47 readloop(), 48 exit(O). 49 } Определение структуры proto для IPv4 и IPv6 2-7 Определяется структура proto для IPv4 и IPv6. Указатели структуры адреса сокета инициализируются как нулевые, поскольку еще не известно, какая из вер- сий будет использоваться — IPv4 или IPv6. Длина дополнительных данных 8 Устанавливается количество дополнительных данных (56 байт), которые бу- дут посылаться с эхо-запросом ICMP. При этом полная 1Р\’4-дейтаграмма будет иметь размер 84 байта (20 байт на 1Р\’4-заголовок и 8 байт на ICMP-заголовок), а 1Р\’6-дейтаграмма будет иметь длину 104 байта. Все данные, посылаемые с эхо- запросом, должны быть возвращены в эхо-ответе. Время отправки эхо-запроса будет сохраняться в первых 8 байтах области данных, а затем, при получении эхо- ответа, будет использоваться для вычисления и вывода времени RTT. Обработка параметров командной строки 14-28 Единственный параметр командной строки, поддерживаемый в нашей версии, это параметр -v, в результате использования которого большинство ICMP-сооб- щений будут выводиться на консоль. (Мы не выводим эхо-ответы, принадлежа- щие другой запущенной копии программы Ping.) Для сигнала SIGALRM установлен обработчик, и мы увидим, что этот сигнал генерируется раз в секунду и вызывает отправку эхо-запросов ICMP. Обработка аргумента, содержащего имя узла '9-46 Строка, содержащая имя узла или IP-адрес, является обязательным аргумен- том и обрабатывается функцией host_serv. Возвращаемая структура addrinfo со- держит семейство протоколов — либо AF_INET, либо AF_INET6. Глобальный указа- тель рг устанавливается на требуемую в конкретной ситуации структуру proto. Также с помощью вызова функции IN6_IS_ADDR_V4MAPPED мы убеждаемся, что ад- рес IPv6 на самом деле не является адресом IPv4, преобразованным к виду IPv6, поскольку даже если возвращаемый адрес является адресом IPv6, узлу будет от- правлен пакет IPv4. (Если такая ситуация возникнет, можно переключиться и ис- пользовать I Pv4.) Структура адреса сокета, уже размещенная в памяти с помощью функции getaddri nfo, используется для отправки, а другая структура адреса соке- та того же размера размещается в памяти для получения. 47 Как показано в листинге 25.4, обработка происходит в функции readl оор. Создание сокета 0-11 Создается символьный сокет, соответствующий выбранному протоколу. В вы- зове функции setuid нашему эффективному идентификатору пользователя при-
720 Глава 25. Символьные сокеты сваивается фактический идентификатор пользователя. Для создания символь- ных сокетов программа должна иметь права привилегированного пользователя, но когда символьный сокет уже создан, от этих прав можно отказаться. Всегда разумнее отказаться от лишних прав, если в них нет необходимости, например на тот случай, если в программе есть скрытая ошибка, которой кто-либо может вос- пользоваться. Листинг 25.4. Функция readloop //ртng/readlоор с 1 #include 'ping h” 2 void 3 readloop(void) 4 { 5 int size 6 char recvbuf[BUFSIZE]: 7 socklen_t len. 8 ssize_t n 9 struct timeval tval 10 sockfd = Socket(pr->sasend->sa_family. SOCK_RAW. pr->icmpproto). 11 setuid(getuidO) /* специальные права больше не требуются */ 12 size = 60 * 1024 /* ОК. если setsockopt не достигает успеха */ 13 setsockopttsockfd SOL_SOCKET SO_RCVBUF &size sizeof(size)) 14 sig_alrm(SIGALRM) /* отправка первого пакета */ 15 for ( ) { 16 « len = pr->salen. 17 n = recvfrom(sockfd recvbuf. sizeof(recvbuf). 0 pr->sarecv &len). 18 if (n < 0) { 19 if (errno = EINTR) 20 continue 21 else 22 err_sys("recvfrom error"). 23 } 24 Gettimeofday(&tval. NULL). 25 (*pr->fproc) (recvbuf n &tval): 26 } 27 } Установка размера приемного буфера сокета 13 Пытаемся установить размер приемного буфера сокета, равный 61 440 байт (60x1024) — этот размер больше задаваемого по умолчанию. Это делается в рас- чете на случай, когда пользователь проверяет качество связи с помощью програм- мы Ping, используя либо широковещательный адрес IPv4, либо групповой адрес, каждый из которых генерирует большое количество ответов. Увеличивая размер буфера, мы уменьшаем вероятность того, что приемный буфер переполнится. Отправка первого пакета : Запускаем обработчик сигнала, который, как мы увидим, посылает пакет и со- здает сигнал SIGALRM раз в секунду. Обычно обработчик сигналов не запускается
25.5. Программа Ping 721 напрямую, как у нас, но это можно делать. Обработчик сигналов является функ- цией языка С, хотя обычно он асинхронно запускается ядром. Бесконечный цикл для считывания всех ICMP-сообщений 15-26 Основной цикл программы является бесконечным циклом, считывающим все пакеты, возвращаемые на символьный сокет ICMP. Вызывается функция gettimeofday для регистрации времени получения пакета, а затем вызывается со- ответствующая функция протокола (proc_v4 или proc_v6) для обработки ICMP- сообщения. В листинге 25.6 приведена функция proc_v4, обрабатывающая все принимае- мые сообщения ICMPv4. Можно также обратиться к рис. А.1, на котором изобра- жен формат заголовка IPv4. Кроме того, следует осознавать, что к тому моменту, когда процесс получает на символьном сокете ICMP-сообщение, ядро уже про- верило, что основные поля в заголовке IPv4 и в сообщении ICMPv4 действитель- ны [105, с. 214, с. 311]. Извлечение указателя на ICMP-заголовок 10-14 Значение поля длины заголовка IPv4, умноженное на 4, дает размер заголовка IPv4 в байтах. (Следует помнить, что IPv4-заголовок может содержать парамет- ры.) Это позволяет нам установить указатель icmp так, чтобы он указывал на на- чало ICMP-заголовка. На рис. 25.3 приведены различные заголовки, указатели и длины, используемые в коде. ip icmp Рис. 25.3. Заголовки, указатели и длина при обработке 1СМРу4-ответов Проверка эхо-ответа ICMP 15-19 Если сообщение является эхо-ответом ICMP, то необходимо проверить поле идентификатора, чтобы выяснить, относится ли этот ответ к посланному данным процессом запросу. Если программа Ping запущена на одном узле несколько раз, каждый процесс получает копии всех полученных ICMP-сообщений. 20-25 Путем вычитания времени отправки сообщения (содержащегося в части ICMP- ответа, отведенной под дополнительные данные) из текущего времени (на кото- рое указывает аргумент функции tvrecv) вычисляется значение RTT. Время RTT преобразуется из микросекунд в миллисекунды и выводится на экран вместе с полем порядкового номера и полученным значением TTL. Поле порядкового номера позволяет пользователю проследить, не были ли пакеты пропущены, пе- реупорядочены или дублированы, а значение TTL показывает количество пере- сылок между двумя узлами.
722 Глава 25 Символьные сокеты Вывод всех полученных ICMP-сообщений при включении параметра verbose 30 Если пользователем указан параметр командной строки v, также выводятся поля типа и кода из всех других полученных ICMP-сообщений В листинге 25 5 приведена функция tv_sub, вычисляющая разность двух струк- тур timeval и сохраняющая результат в первой из них Листинг 25.5. Функция tvsub вычитание двух структур timeval //lib tv_sub с 1 #include unp h 2 void 3 tv_sub(struct timeval *out struct timeval *in) 4 { 5 if ( (out >tv_usec = in >tv_usec) < 0) { /* out -= in */ 6 out >tv_sec 7 out >tv_usec += 1000000 8 } 9 out >tv_sec = in >tv_sec 10 } Обработка сообщений ICMPv6 управляется функцией proc_v6, приведенной в листинге 25 7 Она аналогична функции proc_v4, представленной в листинге 25 6 Листинг 25.6. Функция proc_v4 обработка сообщений ICMPv4 //ping/prov_v4 с 1 #include ping h 2 void 3 proc_v4(char *ptr ssize_t len struct timeval *tvrecv) 4 { 5 int hlenl icmplen 6 double rtt 7 struct ip *ip 8 struct icmp *icmp 9 struct timeval *tvsend 10 ip = (struct ip *) ptr /* начало IP заголовка */ 11 nienl = ip >ipjil « 2 /* длина IP заголовка */ 12 icmp = (struct icmp *) (ptr + hlenl) /* начало заголовка ICMP1*/ 13 if ( (icmplen = len hlenl) < 8) 14 err_quit( icmplen (M) < 8 icmplen) 15 if (icmp >icmp_type == ICMP_ECHOREPLY) { 16 if (icmp >icmp_id '= pid) 17 / return /* ответ не на наш запрос ECHO__REQUEST */ 18 if (icmplen « 16) ~ 19 err_quit( icmplen (^d) < 16 icmplen) 20 tvsend = (struct timeval *) icmp >icmp_data 21 tv_sub(tvrecv tvsend) 22 rtt = tvrecv >tv_sec * 1000 0 + tvrecv >tv_usec / 1000 0 23 printf( fcd bytes from ^s seq=^u ttWd rtt=$ 3f ms\en 24 icmplen Sock_ntop_host(pr >sarecv pr >salen) 25 icmp >icmp_seq ip >ip_ttl rtt)
25 5 Программа Ping 723 26 } else if (verbose) { 27 printf( bytes from ^s type = M code = W\en 28 icmplen Sock_ntop_host(pr >sarecv pr >salen) 29 icmp >icmp_type icmp >icmp_code) 30 } 31 } Листинг 25.7. функция proc_v6 обработка сообщений ICMPv6 //ping/proc_v6 c 1 #include ping h 2 void 3 proc_v6(char *ptr ssize_t len struct timeval *tvrecv) 4 { 5 #ifdef IPV6 6 int hlenl icmp61en 7 double rtt 8 struct ip6_hdr *ip6 9 struct icmp6_hdr *icmp6 10 struct timeval *tvsend 11 ip6 = (struct ip6_hdr *) ptr /* начало заголовка IPvt ♦/ 12 hlenl = sizeoftstruct ip6_hdr) 13 if (ip6 >ip6_nxt '= IPPR0T0JCMPV6) 14 err_quit( next header not IPPR0T0_ICMPV6 ) 15 icmp6 = (struct icmp6_hdr *) (ptr + hlenl) 16 if ( (icmp61en = len hlenl) < 8) 17 err_quit( icmp61en (M) < 8 icmp61en) 18 if (icmp6 >icmp6_type == ICMP6_ECH0_REPLY) { 19 if (icmp6 >icmp6_id '= pid) 20 return /* ответ не на наш запрос ECHO_REQUEST */ 21 if (icmp61en < 16) 22 err_quit( icmp61en (^d) < 16 icmp61en) 23 tvsend = (struct timeval *) (icmp6 + 1) 24 tv_sub(tvrecv tvsend) 25 rtt = tvrecv >tv_sec * 1000 0 + tvrecv >tv_usec / 1000 0 26 printf( W bytes from Sts seq=lu hlim=M rtt=^ 3f ms\en 27 icmp61en Sock_ntop_host(pr >sarecv pr >salen) 28 icmp6 >icmp6_seq ip6 >ip6_hlim rtt) 29 } else if (verbose) { 30 printft ^d bytes from type = W code = M\er. 31 icmp61en Sock_ntop_host(pr >sarecv pr >salen) 32 icmp6 >icmp6_type icmp6 >icmp6_code) 33 } 34 #endif /* IPV6 */ 35 } Извлечение указателя на заголовок ICMPv6 11 17 Размер заголовка фиксирован IPv6 (40 байт), и при этом известно, что следую- щий заголовок будет заголовком ICMPv6 (Напомним, что дополнительные заголовки, если они присутствуют, всегда возвращаются не как стандартные дан- ные, а как вспомогательные ) На рис 25 4 приведены различные заголовки, ука- затели и длины, используемые в коде 1 .11*-
724 Глава 25. Символьные сокеты Рис. 25.4. Заголовки, указатели и длина при обработке ответов ICMPv6 Проверка эхо ответа ICMP 1-28 Если ICMP-сообщение является эхо-ответом, то чтобы убедиться, что ответ предназначен для нас, мы проверяем поле идентификатора. Если это подтверж- дается, то вычисляется значение RTT, которое затем выводится вместе с поряд- ковым номером и предельным количеством транзитных узлов IPv4. Вывод всех полученных ICMP-сообщений при включении параметра verbose -33 Если пользователь указал параметр командной строки -v, выводятся также поля типа и кода всех остальных получаемых ICMP-сообщений. ПРИМЕЧАНИЕ ------------------------------------------------ Если параметр -у не включен, можно установить фильтр ICMPv6 (параметр сокета ICMP6_FILTER, описанный в разделе 25.4), чтобы ядро передавало нашему сокету только эхо-ответы. Обработчиком сигнала SIGALRM является функция sig_alгш, приведенная в ли- стинге 25.8. В листинге 25.4 видно, что функция readloop вызывает обработчик сигнала один раз для отправки первого пакета. Эта функция в зависимости от протокола вызывает функцию send_v4 или send_v6 для отправки эхо-запроса ICMP и далее программирует запуск другого сигнала SIGALRM через 1 секунду. Листинг 25.8. функция sig_alrm: обработчик сигнала SIGALRM //ping/sig_alrm с 1 include "ping h" 2 void 3 sig_alrm(int signo) 4 { 5 (*pr->fsend) (): 6 alarm(l): 7 return; /* вероятно, прерывает выполнение функции recvfromO */ 8 } Функция send_v4, приведенная в листинге 25.9, строит 1СМРу4-сообщенйёйЕбМ запроса и записывает его в символьный сокет. Формирование ICMP-сообщения 12 1СМРу4-сообщение сформировано. В поле идентификатора установлен иден- тификатор нашего процесса, а порядковый номер установлен как глобальная пе-
25.5. Программа Ping 725 ременная nset, которая затем увеличивается на 1 для следующего пакета. Теку- щее время сохраняется в части данных ICMP-сообщения. Вычисление контрольной суммы ICMP 13-15 Для вычисления контрольной суммы ICMP-значение поля контрольной сум- мы устанавливается равным 0, а затем вызывается функция i n cksum, а результат сохраняется в поле контрольной суммы. Контрольная сумма ICMPv4 вычисля- ется по 1СМРу4-заголовку и всем следующим за ним данным. Листинг 25.9. Функция send_v4: построение эхо-запроса ICMPv4 и его отправка //ping/send_v4 с 1 #include "ping.h" 2 void 3 send_v4(void) 4 { 5 int len. 6 struct icmp *icmp. 7 icmp = (struct icmp *) sendbuf: 8 icmp->icmp_type = ICMP_ECHO: 9 icmp->icmp_code = 0. 10 icmp->icmp_id = pid. 11 icmp->icmp_seq = nsent++, 12 Gettimeofday((struct timeval *) icmp->icmp_data. NULL). 13 len = 8 + datalen, /* контрольная сумма ICMP-заголовка'и данных */ 14 icmp->icmp_cksum = 0. 15 icmp->icmp_cksum = in_cksum((u_short *) icmp. len). 16 Sendto(sockfd. sendbuf. len, 0, pr->sasend, pr->salen). 17 } Отправка дейтаграммы 16 ICMP-сообщение отправлено на символьный сокет. Поскольку параметр соке- та I P_HDR INCL не установлен, ядро составляет заголовок I Pv4 и добавляет его в на- чало нашего буфера. Контрольная сумма Интернета является суммой обратных кодов 16-разряд- ных значений. Если длина данных является нечетным числом, то для вычисле- ния контрольной суммы используется логическое сложение одного нулевого байта и данных, расположенных в конце. Такой алгоритм применяется для вычисле- ния контрольных сумм IPv4, ICMPv4, IGMPv4, ICMPv6, UDP и TCP. В RFC 1071 [14] содержится дополнительная информация и несколько числовых примеров. В разделе 8.7 книги [105] более подробно рассказывается об этом алгоритме, а так- же приводится более эффективная его реализация. В нашем случае контрольную сумму вычисляет функция in cksum, приведенная в листинге 25.10. Листинг 25.10. Функция in cksum: вычисление контрольной суммы Интернета //libfree/in_cksum с 1 unsigned short 2 in_cksum(unsigned short *addr int len) 3 { 4 int nleft - len. продолжение &
726 Глава 25. Символьные сокеты Листинг 25 Л О (продолжение) 5 int sum = 0. 6 unsigned short *w = addr. 7 unsigned short answer = 0. 8 /* В нашем простом алгоритме используется 32-разрядный 9 * сумматор накапливающего типа, мы добавляем 10 * в него последовательно 16-разрядные слова. 11 * а в конце переносим старшие 16 бит в младшие 12 */ 13 while (nleft > 1) { 14 sum += *w++. 15 nleft -= 2. 16 } 17 /* если необходимо убираем добавочный байт */ 18 if (nleft = 1) { 19 *(unsigned char *) (&answer) = *(unsigned char *) w; 20 sum += answer. 21 } 22 /* переносим старшие 16 бит в младшие */ 23 sum = (sum » 16) + (sum & Oxffff). /* добавляем старшие 16бит -к нладшин */ 24 sum += (sum » 16). /* перенос */ 25 answer = -sum. /* усекаем до 16 бит */ 26 return (answer). 27 } Алгоритм вычисления контрольной суммы Интернета 27 Первый цикл while вычисляет сумму всех 16-битовых значений. Если длина нечетная, то к сумме добавляется конечный байт. Алгоритм, приведенный в лис- тинге 25.10, является простым алгоритмом, подходящим для программы Ping, но неудовлетворительным для больших объемов вычислений контрольных сумм, производимых ядром. ПРИМЕЧАНИЕ-------------------------------------------------------------- Эта функция взята из общедоступной версии программы Ping. Последней функцией нашей программы Ping является функция send_v6, при- веденная в листинге 25.11, которая строит и посылает эхо-запросы ICMPv6. Функция send_v6 аналогична функции send_v4, но обратите внимание, что она не вычисляет контрольную сумму. Как отмечалось ранее, поскольку для вычис- ления контрольной суммы ICMPv6 используется адрес отправителя из IPv6-3a- головка, данная контрольная сумма вычисляется для нас ядром, после того как ядро выяснит адрес отправителя. Листинг 25.11. Функция send_v6: построение и отправка !СМРу6-сообщения эхо- запроса //рing/sendv6 с 1 #include "ping h” 2 void 3 send_v6() 4 { 5 #ifdef IPV6
25.6. Программа Traceroute 727 6 int len, 7 struct icmp6_hdr *icmp6 8 icmp6 = (struct icmp6_hdr *) sendbuf. 9 icmp6->icmp6_type = ICMP6_ECH0_REQUEST. 10 icmp6->icmp6_code = 0. 11 icmp6->icmp6_id = pid, 12 icmp6->icmp6_seq = nsent++. 13 Gettimeofday((struct timeval *) (icmp6 + 1). NULL): 14 len = 8 + datalen. /* 8-байтовый заголовок ICMPv6 */ 15 Sendto(sockfd, sendbuf. len, 0. pr->sasend, pr->salen). 16 /* ядро вычисляет и сохраняет контрольную сумму */ 17 #endif /* IPV6 */ 18 } 25.6. Программа Traceroute В этом разделе мы приведем собственную версию программы Traceroute. Как и в случае с программой Ping, приведенной в предыдущем разделе, мы представ- ляем нашу собственную, а не общедоступную версию. Это делается для того, что- бы, во-первых, получить версию, поддерживающую как IPv4, так и IPv6, а во- вторых, не отвлекаться на множество параметров, не относящихся к обсуждению сетевого программирования. Программа Traceroute позволяет нам проследить путь IP-дейтаграмм от на- шего узла до получателя. Ее действие довольно просто, а в главе 8 книги [94] оно детально описано со множеством примеров. В версии IPv6 программа Traceroute использует поле TTL (в версии IPv4) или поле предельного количества транзитных узлов (называемое также полем ограничения пересылок), а также два типа ICMP-сообщений. Эта программа на- чинает свою работу с отправки UDP-дейтаграммы получателю, причем полю TTL (или полю ограничения пересылок) присваивается значение 1. Такая дейтаграм- ма вынуждает первый маршрутизатор отправить ICMP-сообщение об ошибке Time exceeded (Превышено время передачи). Затем значение TTL увеличивается на 1, и посылается следующая UDP-дейтаграмма, которая достигает следующего мар- шрутизатора. Когда UDP-дейтаграмма достигает конечного получателя, необхо- димо заставить узел вернуть ICMP-ошибку Port unreachable (Порт недоступен). Для этого UDP-дейтаграмма посылается на случайный порт, который (как мож- но надеяться) не используется на данном узле. Ранние версии программы Traceroute могли устанавливать поле TTL в заго- ловке IPv4 только с помощью параметра сокета I P_HDRINCL путем построения своего собственного заголовка. Однако современные системы поддерживают параметр сокета IP TTL, позволяющий определить значение TTL для исходящих дейтаграм. (Данный параметр сокета впервые был представлен в выпуске 4.3BSD Reno.) Проше установить данный параметр сокета, чем полностью формировать IPv4- заголовок (хотя в разделе 26.6 показано, как строить собственные заголовки IPv4 и UDP). Параметр сокета IPv6 IPV6_UNICAST_HOPS позволяет контролировать поле предельного количества транзитных узлов (ограничения пересылок) в дейта- граммах IPv6.
728 Глава 25. Символьные сокеты В листинге 25.12 приведен заголовочный файл trace, h, подключаемый ко всем файлам нашей программы. Листинг 25.12. Заголовочный файл trace.h //traceroute/trace h 1 2 3 4 5 include "unp h” include <netinet/in_systm h> #include <netinet/ip h> include <netinet/ip_icmp h> #include <netinet/udp h> 6 #define BUFSIZE 1500 7 struct rec { 7* структура данных UDP */ 8 u_short rec_seq. /* порядковый номер */ 9 u_short rec_ttl. /* значение TTL. с которым пакет отправляется */ 10 struct timeval rec tv. /* время отправки пакета */ 11 }• 12 /* глобальные переменные */ 13 char recvbuf[BUFSIZE]; 14 char sendbuf[BUFSIZE], 15 int datalen. /* размер данных в байтах после заголовка ICMP */ 16 char *host. 17 u_short sport, dport: 18 int nsent. /* добавляет 1 для каждого вызова sendto!) */ 19 pid_t pid. /* идентификатор нашего процесса PID */ 20 int probe, nprobes: 21 int sendfd recvfd; /* посылает на сокет UDP. читает на символьном сокете ICMP */ 22 int ttl. max_ttl. 23 int verbose. 24 /* прототипы функций */ 25 char *icmpcode_v4(int). 26 char *icmpcode_v6(int). 27 int recv_v4(int. struct t'fnfcWl 28 int recv_v6(int struct*timeval>*)i 29 void sig_alrm(int). 30 void traceloop(void). 31 void tv_sub(struct timeval *. strict timeVtfl 32 struct proto { 33 char *(*icmpcode) (int). 34 int (*recv) (int. struct timeval *). 35 struct sockaddr *sasend. /* структура sockaddr{} для отправки, получена ИЗ getaddrinfo */ 36 struct sockaddr *sarecv. /* структура sockaddr{} для получения */ 37 struct sockaddr *salast, /* последняя структура sockaddr{} для получения */ 38 struct sockaddr *sabind, /* структура sockaddr{} для связывания поруа отправителя*/ 39 socklen_t salen /* длина структур sockaddr{}s */ 40 int icmpproto. /* значение IPPR0T0_xxx для ICMP */ 41 int ttllevel. /* значение аргумента level функции setsockoptO для задания TTL */ 42 int ttloptname /* значение аргумента name функции setsockoptO для задания TTL */
25.6. Программа Traceroute 729 43 } *pr. 44 #ifdef IPV6 45 #uiclude "ip6 h’ /* должно быть <netinet/ip6 h> */ 46 include "icmp6 h” /* должно быть <netinet/icmp6 h> */ 47 #endif 1-11 Подключаются стандартные заголовочные файлы IPv4, определяющие струк- туры и константы IPv4, ICMPv4 и UDP. Структура гес определяет часть посыла- емой UDP-дейтаграммы, содержащую собственно данные, но как мы увидим даль- ше, нам никогда не придется исследовать эти данные. Они отсылаются в основном для целей отладки. Определение структуры proto 32-43 Как и в программе Ping, описанной в предыдущем разделе, мы обрабатываем различие между протоколами IPv4 и IPv6, определяя структуру proto, которая содержит указатели на функции, указатели на структуры адресов сокетов и дру- гие константы, различные для двух версий IP. Глобальная переменная рг будет установлена как указатель на одну из этих структур, инициализированных либо для IPv4, либо для IPv6, после того как адрес получателя будет обработан функ- цией main (поскольку именно адрес получателя определяет, какая версия исполь- зуется — IPv4 или IPv6). Подключение заголовочных файлов IPv6 44-47 Подключаются заголовочные файлы, определяющие структуры и константы IPv6 и ICMPv6. Функция main приведена в листинге 25.13. Она обрабатывает аргументы ко- мандной строки, инициализирует указатель рг либо для IPv4, либо для IPv6 и вы- зывает нашу функцию tracel оор. Листинг 25.13. Функция main программы Traceroute //traceroute/main с 1 include "trace h" 2 struct proto proto_v4 = 3 {icmpcode_v4 recv_v4 NULL. NULL NULL. NULL. 0. 4 IPPROTOJCMP. IPPROTOJP. IPJTL}. 5 #ifdef IPV6 6 struct proto proto_v6 = 7 {icmpcode_v6. recv_v6 NULL. NULL. NULL. NULL. 0. 8 IPPROTOJCMPV6 IPPR0T0JPV6. IPV6_UNICAST_HOPS}. 9 #endif 10 int datalen = sizeof(struct rec). /* значения по уиолчаййю *? 11 int max_ttl = 30. 12 int nprobes = 3. 13 u_short dport = 32768 + 666. 14 int 15 main(int argc. char **argv) продолжение &
730 Глава 25. Символьные сокеты Листинг 25.13 (продолжение) 17 18 int с. struct addrinfo *ai. 19 20 21 22 23 24 25 opterr = 0. /* чтобы функция getoptO не записывала в stderr */ while ( (с = getoptlargc. argv, "m v")) != -1) { switch (c) { case 'm' if ( (max_ttl = atoi(optarg)) <= 1) err_qint("invalid -m value"). break. 26 27 28 case V verbose++. break. 29 30 31 32 case '?' err quit("unrecognized option £c". c). } } 33 34 35 if (optind '= argc - 1) err_quit(”usage traceroute [ -m <maxttl> -v ] <hbstname>*) host = argv[optind]. 36 37 pid = getpidO, Signal(SIGALRM. sig_alrm) 38 ai = Host_serv(host. NULL. 0. 0): 39 40 41 42 printfC"traceroute to Is ds) £d hops max. 2d data bytes\en". ai->ai_canonname. Sock_ntop_host(ai->ai_addr. ai->ai_addrlen). max_ttl. datalen). 43 44 45 46 47 48 49 50 51 52 53 /* инициализация в зависимости от протокола*/ if (ai->ai_family = AF_INET) { pr = &proto_v4, #ifdef IPV6 } else if (ai->ai_fannly == AF_INET6) { pr = &proto_v6. if (IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_in6 *) ai->ai_addr)->sin6_addr))) err_quit("cannot traceroute IPv4-mapped IPv6 address"). #endif } else err_quit("unknown address family 2d". ai->ai_family). 54 55 56 57 58 pr->sasend = ai->ai_addr. /* содержит адрес получателя */ pr->sarecv = Callocd. ai->ai_addrlen). pr->salast = Callocd. ai->ai_addrlen): pr->sabind - Callocd. ai->ai_addrlen); pr->salen = ai->ai_addrlen. 59 traceloopO: 60 61 exit(0).
25.6. Программа Traceroute 731 Определение структуры proto 2-9 Определяются две структуры proto, одна для IPv4 и другая для IPv6, хотя ука- затели на структуры адреса сокета не размещаются в памяти до окончания вы- полнения данной функции. Установка значений по умолчанию 10-13 Максимальное значение поля TTL, или поля предельного количества транзит- ных узлов, используемое в программе, по умолчанию равно 30. Предусмотрен параметр командной строки -ш, чтобы пользователь мог поменять это значение. Для каждого значения TTL посылается три пробных пакета, но их количество также может быть изменено с помощью параметра командной строки. Изначаль- но используется номер порта получателя 32 768+666, и каждый раз, когда посы- лается новая дейтаграмма UDP, это значение увеличивается на 1. Мы можем на- деяться, что порты с такими номерами не используются на узле получателя в тот момент, когда приходит дейтаграмма, однако гарантии здесь нет. Обработка аргументов командной строки 19-37 Параметр командной строки -V позволяет вывести все остальные 1СМР-сооб- щения. Обработка имени узла или IP-адреса и завершение инициализации 38-58 Имя узла получателя или IP-адрес обрабатывается функцией host_serv, воз- вращающей указатель на структуру addri nfo. В зависимости от типа возвращен- ного адреса (IPv4 или IPv6) заканчивается инициализация структуры proto, со- храняется указатель в глобальной переменной рг, а также размещается в памяти дополнительная структура адреса сокета соответствующего размера. 59 Функция tracel оор, приведенная в листинге 25.14, отправляет дейтаграммы и чи- тает вернувшиеся ICMP-сообщения. Это основной цикл программы. Листинг 25.14. Функция traceloop: основной цикл обработки //traceroute/traceloop с 1 include "trace, h” 2 void 3 traceloop(void) 4 { 5 int seq. code, done: 6 double rtt. 7 struct rec *rec, 8 struct timeval tvrecv. 9 recvfd = Socket(pr->sasend->sa_family. SOCK_RAW pr->icmpproto). 10 setuid(getuidO) /* специальные права больше не требуются */ И sendfd = Socket(pr->sasend->sa_family, SOCK_DGRAM. 0). 12 pr->sabind->sa_family = pr->sasend->sa_family 13 sport = (getpidO & Oxffff) | 0x8000. /* номер порта UDP отправителя */ 14 sock_set_port(pr->sabind. pr->salen, htons(sport)). 15 Bind(sendfd. pr->sabind, pr->salen) продолжение &
732 Глава 25. Символьные сокеты Листинг 25.14 (продолжение) 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 sig_alrm(SIGALRM), seq = 0. done = 0 for (ttl = 1. ttl <= max_ttl && done “ 0. ttl++) { Setsockopt(sendfd. pr->ttllevel. pr->ttloptname. &ttl. sizeof(int)): bzero(pr->salast, pr->salen), printfC"X2d ". ttl). fflush(stdout). for (probe = 0. probe < nprobes. probe++) { rec = (struct rec *) sendbuf. rec->rec_seq = ++seq. rec->rec_ttl = ttl. Gettimeofday(&rec->rec_tv. NULL). sock_set_port(pr->sasend pr->salen htons(dport + seq)) Sendto(sendfd. sendbuf datalen. 0 pr->sasend pr->salen). if ( (code = (*pr->recv) (seq &tvrecv)) = -3) printfC *") /* истечение времени ожидания, нет ответа */ else { char str[NI_MAXHOST], if (sock_cmp_addr(pr->sarecv. pr->salast pr->salen) != 0) { if (getnameinfo(pr->sarecv pr->salen str. sizeof(str). NULL. 0. 0) = 0) printfC Xs (Xs)". str. Sock_ntop_host(pr->sarecv. pr->salen)). else printfC Xs". Sock_ntop_host(pr->sarecv pr->salen)). memcpy(pr->salast. pr->sarecv. pr->salen), } tv_sub(&tvrecv. &rec-=rec_tv) rtt = tvrecv tv_sec * 1000 0 + tvrecv tv_usec / 1000.0: printfC X 3f ms" rtt). if (code = -1) /* порт получателя недоступен */ done++ else if (code >= 0) printfC (ICMP Xs)". (*pr->icmpcode) (code)) } fflush(stdout). } printf("\en"). } } Создание двух сокетов 11 Нам необходимо два сокета: символьный сокет, на котором мы читаем все вер- нувшиеся ICMP-сообщения, и UDP-сокет, на который мы посылаем пробные пакеты с увеличивающимся значением поля TTL. После создания символьного сокета мы заменяем наш эффективный идентификатор пользователя на факти-
25.6. Программа Traceroute 733 ческий, поскольку более нам не понадобятся права привилегированного пользо- вателя. Связывание порта отправителя UDP-сокета 12-15 Осуществляется связывание порта отправителя с UDP-сокетом, который ис- пользуется для отправки пакетов. При этом берется 16 младших битов из иден- тификатора нашего процесса, а старшему биту присваивается 1. Поскольку несколько копий программы Traceroute могут работать одновременно, нам необ- ходима возможность определить, относится ли поступившее ICMP-сообщение к одной из наших дейтаграмм, или оно пришло в ответ на дейтаграмму, послан- ную другой копией программы. Мы используем порт отправителя в UDP-заго- ловке для определения отправляющего процесса, поскольку возвращаемое ICMP- сообщение всегда содержит UDP-заголовок дейтаграммы, вызвавшей ICMP- ошибку. Установка обработчика сигнала SIGALARM 16 Мы устанавливаем нашу функцию sig_alarm в качестве обработчика сигнала SIGALRM, поскольку каждый раз, когда мы посылаем UDP-дейтаграмму, мы ждем 3 секунды, прежде чем послать следующий пробный пакет. Основной цикл: установка TTL или предельного количества транзитных узлов и отправка трех пробных пакетов 17-28 Основным циклом функции является двойной вложенный цикл for. Внешний цикл стартует со значения TTL или предельного количества транзитных узлов, равного 1, и увеличивает это значение на 1, в то время как внутренний цикл по- сылает три пробных пакета (UDP-дейтаграммы) получателю. Каждый раз, когда изменяется значение TTL, мы вызываем setsockopt для установки нового значе- ния, используя параметр сокета IP_TTL или IPV6_UNICAST_H0PS. Каждый раз во внешнем цикле мы инициализируем нулем структуру адреса сокета, на которую указывает salast. Данная структура будет сравниваться со структурой адреса сокета, возвращенной функцией recvfrom, при считывании ICMP-сообщения, и если эти две структуры будут различны, на экран будет вы- веден IP-адрес из новой структуры. При использовании этого метода для каждо- го значения TTL выводится IP-адрес, соответствующий первому пробному паке- ту, а если для данного значения TTL IP-адрес изменится (то есть во время работы программы изменится маршрут), то будет выведен новый 1Р-адрес. Установка порта получателя и отправка UDP-дейтаграммы 29-30 Каждый раз, когда посылается пробный пакет, порт получателя в структуре адреса сокета sasend меняется с помощью вызова функции sock_set_port. Причи- на, по которой порт меняется для каждого пробного пакета, заключается в том, что когда мы достигаем конечного получателя, все три пробных пакета посыла- ются на разные порты, и есть надежда, что по крайней мере один из этих портов не используется. Функция sendto посылает UDP-дейтаграммы.
734 Глава 25. Символьные сокеты Чтение ICMP-сообщения -54 Одна из функций recv_v4 или recv_v6 вызывает функцию recvfrom для чтения и обработки вернувшихся ICMP-сообщений. Обе эти функции возвращают зна- чение -3 в случае истечения времени ожидания (сообщая, что следует послать следующий пробный пакет, если для данного значения TTL еще не посланы все три пакета), значение -2, если приходит ICMP-ошибка о превышении времени передачи, и значение -1, если получена ICMP-ошибка о недоступности порта (port unreachable) (что означает, что достигнут конечный получатель). Если же прихо- дит какая-либо другая ICMP-ошибка недоступности получателя (destination unreachable), эти функции возвращают неотрицательный 1СМР-код. Вывод ответа ;-53 Как отмечалось выше, в случае первого ответа для данного значения TTL, а так- же если для данного TTL меняется IP-адрес узла, посылающего ICMP-сообще- ние, выводится имя узла и IP-адрес (или только IP-адрес, если вызов функции getnameinfo не возвращает имени узла). Время RTT вычисляется как разность между временем отправки пробного пакета и временем возвращения и вывода ICMP-сообщения. Функция recv_v4 приведена в листинге 25.15. Листинг 25.15. Функция recv_v4: чтение и обработка сообщений ICMPv4 //traceroute/recv_v4 1 #include "trace h" 2 /* Возвращает 3 * -3 в случае истечения времени ожидания. 4 * -2 в случае превышения времени передачи * (вызывающая программа продолжает работать): 5 * -1 в случае недоступности порта 1 * (вызывающая программа завершена). 6 * >= 0 означает другой ICMP-код недоступности 7 */ 8 int 9 recv_v4(int seq. struct timeval *tv) 10 { il int hlenl, hlen2, icmplen. 12 socklen_t len. 13 ssizet n: 14 struct ip *ip. *hip, 15 struct icmp *icmp, 16 struct udphdr *udp. 17 alarm(3). 18 for (..) { 19 len = pr->salen; 20 n = recvfrom!recvfd. recvbuf. sizeof(recvbuf). 0. pr->sarecv. &len); 21 if (n < 0) { 22 if (errno == EINTR) 23 return (-3). /* время ожидания истекло */ 24 else 25 err_sys("recvfrom error"), 26 }
25.6. Программа Traceroute 735 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 Gettimeofday(tv. NULL): /* получает время прибытия пакета*/ ip = (struct тр *) recvbuf. /* начало IP-заголовка */ hlenl - ip->ip_hl « 2. /* длина IP-заголовка */ icmp = (struct icmp *) (recvbuf + hlenl): /* начало ICMP-заголовка */ if ( (icmplen = n - hlenl) < 8) err_quit("icmplen (&j) < 8", icmplen). if (icmp->icmp_type == ICMP_TIMXCEED && icmp->icmp_code == ICMP_TIMXCEED_INTRANS) { if (icmplen <8 + 20 + 8) err_quit("icmplen Kd) < 8 + 20 + 8”. icmplen). hip = (struct ip *) (recvbuf + hlenl + 8). hlen2 = hip->ip_hl « 2. udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2). if (hip->ip_p == IPPROTOJJDP && udp->uh_sport == htons(sport) 88 udp->uh_dport = htons(dport + seq)) return (-2). /* попали на промежуточный маршрутизатор */ } else if (icmp->icmp_type == ICMPJJNREACH) { if (icmplen <8 + 20 + 8) err_quit("icmplen (£d) < 8 + 20 + 8". icmplen); hip = (struct ip *) (recvbuf + hlenl + 8). hlen2 = hip->ip_hl « 2. udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2): if (hip->ip_p == IPPROTOJJDP 88 udp->uh_sport == htons(sport) && udp->uh_dport == htons(dport + seq)) { if (icmp->icmp_code = ICMP_UNREACH_PORT) return (-1). /* достигли получателя */ ! else return (icmp->icmp_code). /* 0. 1. 2 ... */ } } else if (verbose) { printfC' (from fcs type = %d. code = 2d)\en". Sock_ntop_host(pr->sarecv. pr->salen). icmp->icmp_type. icmp->icmp_code)• /* какая-либо другая ICMP-ошибка, снова вызываем recvfromO*/ } 1 Установка таймера и прочтение каждого ICMP-сообщения 17-27 Таймер устанавливается на 3 секунды, и функция входит в цикл, вызывающий recvfrom, считывая каждое 1СМРу4-сообщение, возвращаемое на символьный сокет. ПРИМЕЧАНИЕ ------------------------------------------------- Данная функция приводит к той же ситуации гонок, которая была описана в разде- ле 18.5, когда обсуждалось, что сигнал SIGALRM прерывает операцию считывания.
736 Глава 25. Символьные сокеты Извлечение указателя на ICMP-заголовок 28-32 У казатель 1 р указывает на начало IPv4-заголовка (напомним, что операция чте- ния на символьном сокете всегда возвращает IP-заголовок), а указатель icmp ука- зывает на начало ICMP-заголовка. На рис. 25.5 показаны различные заголовки, указатели и длины, используемые в данном коде. п icmplen hlenl hlen2 icmp ip hip udp I----►Дейтаграмма IPv4, которая сгенерировала ошибку ICMP Рис. 25.5. Заголовки, указатели и длины при обработке ошибки ICMPv4 Обработка ICMP-сообщения о превышении времени передачи 33-43 Если ICMP-сообщение является сообщением Time exceeded (Превышено время передачи), вероятно, оно является ответом на один из наших пробных пакетов. Указатель hi р указывает на заголовок IPv4, который возвращается в 1СМР-сооб- щении и следует сразу за 8-байтовым ICMP-заголовком. Указатель udp указывает на следующий далее UDP-заголовок. Если ICMP-сообщение было сгенерирова- но UDP-дейтаграммой и если порты отправителя и получателя этой дейтаграм- мы совпадают с теми значениями, которые мы посылали, то тогда это ответ от промежуточного маршрутизатора на наш пробный пакет. Обработка ICMP-сообщения о недоступности порта 44-57 Если ICMP-сообщение является сообщением Desti nation unreachabl е (Получа- тель недоступен), тогда, чтобы узнать, является ли это сообщение ответом на наш пробный пакет, мы смотрим на UDP-заголовок, возвращенный в данном ICMP- сообщении. Если это так и код означает сообщение Port unreachable (Порт недо- ступен), то возвращается значение -1, поскольку достигнут конечный получа- тель. Если же ICMP-сообщение является ответом на один из наших пробных пакетов, но не является сообщением типа Destination unreachable (Получатель недоступен), то тогда возвращается значение ICMP-кода. Обычным примером такого случая является ситуация, когда брандмауэр возвращает какой-либо дру- гой код недоступности для получателя, на который посылается пробный пакет. Обработка других ICMP-сообщений 58-62 Все остальные ICMP-сообщения выводятся, если был задан параметр -V. Следующая функция, recv_v6, приведена в листинге 25.16 и является эквива- лентом для IPv6 ранее описанной функции. Эта функция почти идентична функ-
25.6. Программа Traceroute 737 ции recv_v4, за исключением различий в именах констант и элементов структур. Кроме того, размер заголовка IPv6 является фиксированным и составляет 40 байт, в то время как для получения IP-параметров в заголовке IPv4 необходимо полу- чить поле длины заголовка и умножать его на 4. На рис. 25.6 приведены различ- ные заголовки, указатели и длины, используемые в коде. п гстрбГеп hlen2 hlenl Заголовок IPv6 Заголовок ICMPv6 Заголовок IPv6 Параметры UDP k 40 байт f s 1 k 40 i k 8 icmp6 ip6 hip6 udp |----►Дейтаграмма IPv6, которая сгенерировала ошибку ICMP Рис. 25.6. Заголовки, указатели и длины, используемые при обработке ошибки ICMPv6 Мы определяем две функции, icmpcode_v4 и icmpcode_v6, которые можно вызы- вать в конце функции traceloop для вывода строки описания, соответствующей ICMP-ошибке недоступности получателя. В листинге 25.17 приведена IPvG-функ- ция. 1Р\'4-функция аналогична, хотя и длиннее, поскольку существует большее количество 1СМРу4-кодов недоступности получателя (табл. А.З). Последней функцией в нашей программе Traceroute является обработчик сиг- нала SIGALRM — функция signal rm, приведенная в листинге 25.18. Эта функция лишь возвращает ошибку EINTR из функции recvfrom, как в случае функции recv_v4, так и в случае recv_v6. Листинг 25.16. Функция recv_v6; чтение и обработка сообщений )CMPv6 //traceroute/recvj/6 1 #include "trace h" 2 /* Возвращает 3 * -3 в случае истечения времени ожидания 4 * -2 в случае превышения времени передачи, * (вызывающая программа продолжает работать) 5 * -1 в случае недоступности порта * (вызывающая программа завершена). 6 * >= 0 означает другой ICMP-код недоступности 7 */ 8 int 9 recv_v6(int seq. struct timeval *tv) 10 { 11 #ifdef IPV6 12 int hlenl. hlen2. icmp61en, 13 ssize_t n 14 socklen_t len, 15 struct ip6_hdr *ip6, *hip6. 16 Struct !Cmp6_hdr *icmp6. продолжение J
738 Глава 25. Символьные сокеты Листинг 25.16 (продолжение) 17 18 19 20 21 22 23 24 25 26 27 28 struct udphdr *udp. alarm(3). for (..) { len = pr->salen n - recvfrom!recvfd recvbuf sizeof(recvbuf). 0. pr->sarecv &len) if (n < 0) { if (errno == EINTR) return (-3). /* время ожидания истекло */ else err sys("recvfrom error"). } Gettimeofday(tv. NULL). /* получаем время прибытия пакета */ 29 30 трб - (struct ip6_hdr *) recvbuf. /* начало заголовка IPv6 */ hlenl = sizeof(struct ip6_hdr) 31 32 33 icmp6 = (struct icmp6_hdr *) (recvbuf + hlenl). /* заголовок ICMP */ if ( (icmp61en = n - hlenl) < 8) err_quit("icmp61en Cd) < 8". icmp61en). 34 35 36 37 if (icmp6->icmp6_type == ICMP6JTMEEXCEEDED && icmp6->icmp6code == ICMP6JTIME_EXCEED_TRANSIT) { if (icmp61en <8 + 40 + 8) err_quit("icmp61en (Xd) < 8 + 40 + 8" icmp61en). 38 39 40 41 42 43 44 hip6 = (struct ip6_hdr *) (recvbuf + hlenl + 8), ' hlen2 = sizeoftstruct ip6_hdr), udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2). if (hip6->ip6_nxt “ IPPROTOJJDP && , udp->uh_sport “ htons(sport) && udp->uh_dport == htons(dport + seq)) return (-2) /* попали на промежуточный маршрутизатор */ 45 46 47 } else if (icmp6->icmp6_type == ICMP6_DST_UNREACH) { if (icmp61en <8 + 40 + 8) err_quit("icmp61en (JKd) <8 + 40 + 8" icmp61en) 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 hip6 = (struct ip6_hdr *) (recvbuf + hlenl + 8). hlen2 = 40. udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2). if (hip6->ip6_nxt == IPPROTOJJDP &8 udp->uh_sport == htons(sport) && udp->uh_dport == htons(dport + seq)) { if (icmp6->icmp6_code = ICMP6_DSTJINREACHJJOPORT) return (-1). /* достигли получателя */ else return (icmp6->icmp6 code). /* 0 1. 2 */ } } else if (verbose) { printfC (from type = &j. code = Xd)\en". Sock_ntop_host(pr->sarecv pr->salen). icmp6->icmp6 type. icmp6->icmp6 code). } /* какая-либо другая ICMP-ошибка, снова вызываем recvfromO */ } #endif }
25.6. Программа Traceroute 739 Листинг 25.17. Возвращение строки, соответствующей коду недоступности ICMPv6 //traceroute/icmpcode_v6 с 1 #include "trace h" 2 char * 3 icr!ipcode_v6( int code) 4 { 5 switch (code) { 6 case ICMP6_DST_UNREACH_N0R0UTE 7 return ("no route to host') 8 case ICMP6_DST_UNREACH_ADMIN 9 return ("administratively prohibited’); 10 case ICMP6_DST_UNREACH_N0TNEIGHB0R 11 return ('not a neighbor") 12 case ICMP6_DST_UNREACH_ADDR 13 return ("address unreachable'). 14 case ICMP6_DST_UNREACH_N0P0RT 15 return ("port unreachable"). 16 default 17 return ("[unknown code]"), 18 } 19 } Листинг 25.18. Функция sigalrm //traceroute/sig_alrm c 1 #include "trace h" 2 void 3 sig_alrm(int signo) 4 { 5 return /* прерывается работа функции recvfromO */ 6 } Пример Сначала приведем пример, использующий IPv4: solans # traceroute gemini.tuc.noao.edu traceroute to gemini tuc noao edu (140 252 3 54) 30 hops max 12 data bytes 1 gw kohala com (206 62 226 62) 3 839 ms 3 595 ms 3 722 ms 2 tuc-l-sl-9 rtd net (206 85 40 73) 42 014 ms 21 078 ms 18 826 ms 3 frame-gw ttn ep net (198 32 152 9) 39 283 ms 24 598 ms 50 037 ms 4 tucson-nap-1 anzona edu (198 32 152 248) 44 350 ms 78 109 ms 47 003 ms 5 Butch-ENET-BONE Tel com Arizona EDU (128 196 11 5 ) 29 849 ms 46 664 ms 83 571 ms 6 gateway tuc noao edu (140 252 104 1) 37 376 ms 36 430 ms 30 555 ms 7 gemini tuc noao edu (140 252 3 54) 70 476 ms 43 555 ms 88.716 ms Ниже приведен пример с IPv6. Для лучшей читаемости длинные строки раз- биты. solans # traceroute ipng9.ipng.nist.gov traceroute to ipng9 ipng nist gov (5f00 3100 8106 3300 0 cO 3302 5a) 30 hops max. 12 data bytes 1 6bone-router cisco inner net (5f00 6d00 cOlf 700 1 60 3ell 6770) 185 869 ms * 127 082 ms 2 buzzcut ipv6 nrl navy mil (5f00 3000 84fa 5a00 5) 187 736 ms 199 455 ms 172 839 ms 3 ipng9 ipng mst gov (5f00 3100 8106 3300 0 cO 3302 5a) 206 762 ms * 441 081 ms
740 Глава 25. Символьные сокеты В данном примере время второй попытки с предельным количеством тран- зитных узлов, равным 1, истекло, так же как и во второй попытке, когда это зна- чение было равно 3. 25.7. Демон сообщений ICMP Получение асинхронных ошибок ICMP на сокет UDP всегда было и продолжает оставаться проблемой. Ядро получает сообщения об ошибках ICMP, но они ред- ко доставляются приложениям, которым необходимо о них знать. Мы видели, что для получения этих ошибок в API сокетов требуется присоединение сокета UDP к одному IP-адресу (см. раздел 8.2). Причина такого ограничения заключа- ется в том, что единственная ошибка, возвращаемая функцией recvfrom, является целым кодом еггпо, а если приложение посылает дейтаграммы по нескольким адресам, а затем вызывает recvfrom, то данная функция не может сообщить при- ложению, какая из дейтаграмм вызвала ошибку. В разделе 31.4 мы увидим, что XTI усовершенствует такой подход (незначи- тельно) путем возвращения ошибки своим эквивалентом функции recvfrom. Пос- ле этого приложение должно вызвать другую функцию (t_rcvuderr) для получе- ния фактической ошибки, адреса получателя и номера порта из дейтаграммы, вызвавшей эту ошибку. Однако проблема данного решения заключается в том, что ядро, вероятно, хранит одновременно информацию не более чем об одной из этих асинхронных ошибок. Если приложение посылает три дейтаграммы и две из них вызывают ICMP-ошибки, приложению возвращается только одна ошибка. В данном разделе предлагается другое решение, не требующее никаких изме- нений в ядре Мы предлагаем демон ICMP-сообщений icmpd, который создает символьный сокет ICMPv4 и символьный сокет ICMPv6 и получает все ICMP- сообщения. направляемые к ним ядром. Он также создает потоковый сокет доме- на Unix, связывает его (при помощи функции bind) с полным именем /tmp/icmpd и прослушивает входящие соединения (устанавливаемые при помощи функции connect) клиента с этим сокетом. Этот пример приведен на рис. 25.7. Прослушиваемый потоковый сокет домена Unix, связанный с /tcp/icmpd Приложение UDP (являющееся клиентом для демона) сначала создает сокет UDP — сокет, на который оно хочет получать асинхронные ошибки. Приложе- ние должно связать (функция Ы nd) динамически назначаемый порт с этим соке- том; для чего это делается, будет пояснено ниже. Далее оно создает потоковый
25.7. Демон сообщений ICMP 741 доменный сокет Unix и присоединяется (функция connect) к заранее известному полному имени файла демона. Это показано на рис. 25.8. Прослушиваемый потоковый сокет домена Unix, связанный с /tcp/icmpd Рис. 25.8. Приложение создает свой сокет UDP и доменный сокет Unix Далее приложение «передает» свой UDP-сокет демону через соединение до- мена Unix, используя передачу дескрипторов, как показано в разделе 14.7. Такой подход позволяет демону получить копию сокета, так что он может вызвать функ- цию getsockname и получить номер порта, связанный с сокетом. На рис. 25.9 по- казана передача сокета. Прослушиваемый потоковый сокет домена Unix, связанный с /tcp/icmpd Рис. 25.9. Пересылка сокета UDP демону через доменный сокет Unix После того как демон получает номер порта, связанный с UDP-сокетом, он закрывает свою копию сокета и мы возвращаемся обратно к схеме, приведенной на рис. 25.8. ПРИМЕЧАНИЕ------------------------------------------------------- Если узел поддерживает передачу данных, идентифицирующих отправителя (см раз- дел 14 8), приложение также может послать эти данные демону. Затем демон может проверить, можно ли допускать данного пользователя к данному устройству. В таком случае в результате любой ошибки ICMP, полученной демоном в от- вет на UDP-дейтаграмму, посланную с порта, который связан с UDP-сокетом приложения, демон посылает приложению сообщение (о котором мы рассказы- ваем чуть ниже) через доменный сокет Unix. Тогда приложение должно исполь- зовать функцию sei ect или pol 1, чтобы обеспечить ожидание прибытия данных либо на UDP-сокет, либо на доменный сокет Unix
742 Глава 25. Символьные сокеты Сначала рассмотрим исходный код приложения, использующего данный де- мон, а затем и сам демон. В листинге 25.19 приведен заголовочный файл, подклю- чаемый и к приложению, и к демону. Листинг 25.19. Заголовочный файл unpicmpd.h //icmpd/unpicmpd h 1 2 #ifndef unpicmpji #define unpicmp_h 3 include "unp h” 4 #define ICMPD_PATH "/tmp/ictnpd” /* известное полное имя сервера */ 5 6 7 8 9 10 11 12 struct icmpd err { int icmpd_errno. /* EHOSTUNREACH. EMSGSIZE. ECONNREFUSED */ char icmpd_type. /* действительный тип ICMPv[46] */ char TCtnpd_code. /* действительный код ICMPv[46] */ socklen_t icmpd_len. /* длина следующей ниже структуры адреса сокета */ struct sockaddr icmpd_dest. /* может быть больше */ char icmpd fi11[MAXSOCKADDR - sizeoftstruct sockaddr)]; } 13 #endif /* ____unpicmp_h */ 12 Определяются известное полное имя сервера и структура i cmpd_err, передавае- мая от сервера приложению сразу, как только получено ICMP-сообщение, кото- рое должно быть передано данному приложению. -8 Проблема в том, что типы сообщений ICMPv4 отличаются численно (а иногда и концептуально) от типов сообщений ICMPv6 (табл. А.З и А.4). Возвращаются реальные значения типа (type) и кода (code), но мы также отображаем соответ- ствующие им значения errno (i cmpd_errno), взятые из последнего столбца табл. А.З и А.4. Приложение может использовать эти значения вместо зависящих от про- токола значений ICMPv4 и ICMPv6. В табл. 25.1 показаны обрабатываемые со- общения ICMP и соответствующие им значения еггпо. Демон возвращает пять типов ошибок ICMP. Таблица 25.1. Значения переменной icmpd_errno, сопоставляющей ошибки ICMPv4 и ICMPV6 icmpd_errno Ошибка ICMPv4 Ошибка ICMPv6 ECONNREFUSED Port unreachable (Порт недоступен) Port unreachable (Порт недоступен) EMSGSIZE Fragmentation needed but DF bit set (Необходима фра! ментация, но установлен бит DF) Packet too big (Слишком большой пакет) EHOSTUNREACH EHOSTUNREACH Time exceeded (Превышено время передачи) Source quench (Отключение отправителя) Time exceeded (Превышено время передачи) EHOSTUNREACH Все другие сообщения о недоступности получателя (Destination unreachable) Все другие сообщения о недоступности получателя (Destination unreachable) 1. Port unreachabl е (Порт недоступен) означает, что сокет не связан с портом по- лучателя на IP-адресе получателя.
25.7. Демон сообщений ICMP 743 2. Packet too bi g (Слишком большой пакет) используется при определении транс- портной MTU. В настоящее время нет определенного API, позволяющего UDP-приложениям осуществлять поиск транспортной MTU. Если ядро под- держивает поиск транспортной MTU для UDP, то обычно получение данной ошибки ICMP заставляет ядро записать новое значение транспортной MTU в таблицу маршрутизации ядра, но UDP-приложение, пославшее дейтаграм- му, не извещается. Вместо этого приложение должно завершиться по превы- шению времени ожидания и повторно послать дейтаграмму, и тогда ядро найдет новое (меньшее) значение MTU в своей таблице маршрутизации и фраг- ментирует дейтаграмму. Передача этой ошибки приложению позволяет ему ускорить повторную передачу дейтаграммы, и, возможно, приложение смо- жет уменьшить размер посылаемой дейтаграммы. 3. Ошибка Time exceeded (Превышено время передачи) обычно возникает с ко- дом 0 и означает, что либо значение поля TTL (в случае IPv4), либо предель- ное количество транзитных узлов (в случае IPv6) достигло нуля. Обычно это свидетельствует о зацикливании маршрута, что, возможно, является времен- ной ошибкой. 4. Ошибка Source quench (Отключение отправителя) ICMPv4 хотя и рассматри- вается в RFC 1812 [5] как устаревшая, может быть послана маршрутизаторами (или неправильно сконфигурированными узлами, действующими как мар- шрутизаторы). Такие ошибки означают, что пакет отброшен, и поэтому обра- батываются как ошибки недоступности получателя. Следует отметить, что в версии IPv6 нет ошибки отключения отправителя. 5. Все остальные ошибки недоступности получателя (Destination unreachble) означают, что пакет отброшен. Элемент icmpd_dest является структурой адреса сокета, содержащей IP-адрес получателя и порта дейтаграммы, сгенерировавшей ICMP-ошибку. Этот элемент может быть структурой sockaddr in для ICMPv4 либо структурой sockaddr_in6 для ICMPv6. Если приложение посылает дейтаграммы по нескольким адресам, оно, вероятно, имеет по одной структуре адреса сокета на каждый адрес. Возвра- щая эту информацию в структуре адреса сокета, приложение может сравнить ее со своими собственными структурами для поиска той, которая вызвала ошибку. Элемент i cmpd_f111 заполняет структуру i cmpd_err таким образом, чтобы мож- но было разместить структуру адреса сокета максимального размера. Эхо-клиент UDP, использующий демон icmpd Теперь модифицируем наш эхо-клиент UDP (функцию dg_cl 1) для использова- ния нашего демона i cmpd. В листинге 25.20 приведена первая половина функции. Листинг 25.20. Первая часть приложения dg cli //icmpd/dgcl101 с 1 #include "unpicmpd h" 2 void 3 dg_cli(FILE *fp. int sockfd. const SA *pservaddr. socklen_t servlen) 4 { 5 int icmpfd, maxfdpl. 6 char sendline[MAXLINE], recvline[MAXLINE + 1). продолжение
744 Глава 25. Символьные сокеты Листинг 25.20 (продолжение) 7 fd_set rset. 8 ssize_t n. 9 struct timeval tv. 10 struct icmpd_err icmpd_err. 11 Sock_bind_wild(sockfd. pservaddr->sa_family). 12 icmpfd = Tcp_connect("/umx”. ICMPD_PATH). 13 Write_fd(icmpfd. "1". 1. sockfd). 14 n = Read(icmpfd. recvline. 1); 15 if (n ’= 1 || recvlinelO] != ’Г) 16 err_quit("error creating icmp socket, n = ^d, char « Же”. 17 n. recvline[O]); 18 FD_ZERO(&rset). 19 maxfdpl = max(sockfd. icmpfd) + 1. !-3 Аргументы функции те же, что и во всех ее предыдущих версиях. Связывание с универсальным адресом и динамически назначаемым портом И Вызываем функцию sock_bi nd_wi 1 d для связывания при помощи функции bi nd универсального IP-адреса и динамически назначаемого порта с UDP-сокетом. Таким образом копия сокета, который пересылается демону, оказывается связа- на с портом, поскольку демону необходимо знать этот порт. ПРИМЕЧАНИЕ---------------------------------------------------------------------- Демон также может произвести подобное связывание, если локальный порт не был связан с сокетом, который был передан демону, но это работает пе во всех системах. В реализациях SVR4, таких как Solaris 2.5, сокеты не являются частью ядра, и когда один процесс связывает (bind) порт с совместно используемым соке!ом, другой процесс при попытке использовать копию этого сокета получает ошибки. Простейшим реше- ние — потребовать, чтобы приложение связывало локальный порт прежде, чем переда- вать сокет демону. Установление соединения домена Unix с демоном 12 Вызываем функцию tcp connect для создания потокового доменного сокета Unix и соединения при помощи функции connect с заранее известным полным именем файла демона. (Как вы помните, в разделе 11.5 говорилось, что наша реализация getaddrinfo поддерживает потоковые доменные сокеты Unix.) Отправка UDP-сокета демону, ожидание ответа от демона 5-17 Вызываем функцию write_fd, приведенную на рис. 14.11 для отправки копии UDP-сокета демону. Мы также посылаем одиночный байт данных — символ "1", поскольку некоторые реализации не передают дескриптор без данных. Демон посылает обратно одиночный байт данных, состоящий из символа "1", для обо- значения успешного выполнения. Любой другой ответ означает ошибку.
25.7. Демон сообщений ICMP 745 18-19 Инициализируем набор дескрипторов и вычисляем первый аргумент для функ- ции sei ect (максимальный из двух дескрипторов, увеличенный на единицу). Последняя часть нашего клиента приведена в листинге 25.21. Это цикл, кото- рый считывает данные из стандартного ввода, посылает строку серверу, считыва- ет ответ сервера и записывает ответ в стандартный вывод. Листинг 25.21. Вторая часть приложения dg_cli //icmpd/dgcl101 с 20 while (Fgets(sendline. MAXLINE, fp) '= NULL) { 21 Sendtotsockfd. sendline. strlen(sendiine). 0. pservaddr. servlen): 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 tv tv_sec = 5. tv tv_usec = 0. FD_SET(sockfd. &rset). FD_SET(icmpfd. &rset). if ( (n = Seiect(maxfdpl. &rset. NULL. NULL. &tv)) = 0) { fprintftstderr, "socket timeoutXen"): continue. } if (FDISSETtsockfd, &rset)) { n = Recvfromtsockfd. recvline, MAXLINE. 0. NULL. NULL). recvline[n] = 0. /* завершающий нуль */ Fputs(recvline. stdout). } if (FD_ISSET(icmpfd. &rset)) { if ( (n = Readticmpfd. &icmpd_err. sizeof(icmpd_err))) == 0) err_quit("ICMP daemon terminated"). else if (n != sizeof(icmpd_err)) err_quit(”n = W. expected M". n sizeof(icmpd_err)). printfCICMP error dest = Is Is type = W. code = MXen". Sock_ntop(&icmpd_err icmpd_dest. icmpd_err icmpdjen). strerror(icmpd_err icmpd_errno). icmpd_err icmpd_type. icmpd_err.icmpd_code). } Вызов функции select 22-29 Поскольку мы вызываем функцию sei ect, мы можем легко установить время ожидания ответа от эхо-сервера. Задаем его равным 5 секундам, открываем оба дескриптора для чтения и вызываем функцию sei ect. Если происходит превыше- ние времени, выводится соответствующее сообщение и осуществляется переход в начало цикла. Вывод ответа сервера 30-34 Если дейтаграмма возвращается сервером, она выводится на стандартное уст- ройство вывода. Обработка ICMP-ошибки 35-44 Если наше соединение домена Unix с демоном icmpd готово для чтения, мы пы- таемся прочитать структуру i cmpd_err. Если это удается, ВЫВОДИТСЯ сойтвОТСТНу ющая информация, возвращаемая демоном.
746 Глава 25. Символьные сокеты ПРИМЕЧАНИЕ----------------------------------------------------------------- Функция strerror является примером простой, почти тривиальной функции, которая должна быть более переносимой, чем она есть. В ANSI С и стандарте Posix. 1 ничего не говорится об ошибках, возвращаемых функцией. В руководстве по операционной сис- теме Solaris 2.5 говорится, что функция возвращает пустой указатель, если ее аргумент выходит за пределы допустимых значений. Это означает, что код наподобие следую- щего: printft"^s". strerrortarg)). является некорректным, поскольку strerror может вернуть пустой указатель. Однако реализации UnixWare 2.1, так же как и все реализации исходного кода, которые автор смог найти, обрабатывают неправильный аргумент, возвращая указатель на строку типа «Неизвестная ошибка». Это имеет смысл и означает, что приведенный выше код пра- вильный. Но в документации к операционной системе Unix 98 говорится, что посколь- ку не предусмотрено значение, сигнализирующее об ошибке, связанной с выходом аргу- мента за допустимые пределы, функция присваивает переменной errno значение EIVAL. (Ничего не сказано об указателе, возвращаемом в случае ошибки.) Это означа- ет, что полностью правильный код должен обнулить errno, вызвать функцию strerror, проверить, не равняется ли значение errno величине EINVAL, и в случае ошибки выве- сти некоторое сообщение. Примеры эхо-клиента UDP Приведем несколько примеров работы данного клиента, прежде чем рассматри- вать исходный код демона. Сначала посылаем дейтаграмму на IP-адрес, связан- ный с Интернетом: solans % udpcliOl 192.3.4.5 echo hi there socket timeout and hello socket timeout Мы считаем, что демон icmpd запущен, и ждем возвращения каким-либо мар- шрутизатором ICMP-ошибок недоступности получателя. Вместо этого наше при- ложение завершается по превышению времени ожидания. Мы показываем это, чтобы повторить, что время ожидания все еще необходимо, а генерация ICMP- сообщения о недоступности узла может и не произойти. Мы запустили данный при- мер примерно через 30 секунд и получили ожидаемое сообщение об ошибке ICMP: solans % udpcliOl 192.3.4.5 echo hello ICMP error dest = 192 3 4 5 7. No route to host, type = 3 code = 1 В следующем примере посылается дейтаграмма стандартному эхо-серверу на узел, где этот сервер не запущен. Как и ожидалось, получаем сообщение ICMPv4 о недоступности порта: solans % udpcliOl gemim.tuc.noao.edu echo hello, world ICMP error dest = 140 252 4 54 7. Connection refused type = 3 code = 3 Демон icmpd Начинаем описание нашего демона icmpd с заголовочного файла icmpd.h, приве- денного в листинге 25.22.
25.7. Демон сообщений ICMP 747 Листинг 25.22. Заголовочный файл icmpd.h для демона icmpd //icmpd/icmpd h 1 #include "unpicmpd h" 2 struct client { 3 int connfd /* соединение потокового доменного сокета Unix с клиентом */ 4 int family. /* AFJNET или AF_INET6 */ 5 int Iport. /* локальный порт связан с клиентским сокетом UDP*/ 6 /*сетевой порядок байтов */ 7 } client[FD_SETSIZE]. 8 /* глобальные переменные */ 9 int fd4. fd6 listenfd. maxi, maxfd. nready; 10 fd_set rset all set. 11 socklen_t addrlen. 12 struct sockaddr *cliaddr 13 /* прототипы функций */ 14 int readable_conn(int) 15 int readable_listen(void). 16 int readable_v4(void). 17 int readable v6(void). Массив client 2-17 Поскольку демон может обрабатывать любое количество клиентов, для сохра- нения информации о каждом клиенте используется массив структур client. Они аналогичны структурам данных, которые использовались в разделе 6.8. Кроме дескриптора для доменного сокета Unix, через который осуществляется связь с клиентом, сохраняется также семейство адресов клиентского UDP-сокета AF INET или AF_I NET6 и номер порта, связанного с сокетом. Далее объявляются прототипы функций и глобальные переменные, совместно используемые этими функциями. В листинге 25.32 приведена первая часть функции main. Листинг 25.23. Первая часть функции main: создание сокетов //icmpd/icmpd с 1 #include "icmpd h" 2 int 3 main(int argc char **argv) 4 { 5 int i, sockfd. 6 if (argc '= 1) 7 err_quit("usage icmpd"), 8 maxi = -1. /* индекс массива client[] */ 9 for (i = 0 i < FD_SETSIZE. i++) 10 client[i] connfd = -1. /* -1 обозначает доступный элемент */ 11 FD_ZERO(&allset). 12 fd4 = Socket (AFJNET SOCK_RAW. IPPROTOJCMP). 13 FD_SET(fd4 &allset). 14 maxfd = fd4, 15 #ifdef IPV6 16 fd6 = Socket(AFJNET6 SOCK_RAW IPPR0T0_ICMPV6). продолжение &
748 (лава 25 Символьные сокеты Листинг 25.23 (продолжение) 17 FD_SET(fd6 Sallset) 18 maxfd = max(maxfd fd6) 19 #endif 20 listenfd = Tcp listenC /mix ICMPD_PATH &addrlen) 21 FD_SET(listenfd &allset) 22 maxfd = max(maxfd listenfd) 23 cliaddr = Malloc(addrlen) Инициализация массива client 10 Инициализируется массив client путем присваивания значения -1 элементу присоединенного сокета Создание сокетов 2 23 Создаются три сокета символьный сокет ICMPv4, символьный сокет ICMPv6 и потоковый доменный сокет Unix Для создания последнего вызывается функ- ция tcp_l 1 sten, которая также связывает при помощи функции bi nd свое заранее известное полное имя с сокетом и вызывает функцию 11 sten Это сокет, к которо- му клиенты присоединяются с помощью функции connect Для функции sei ect также вычисляется максимальный дескриптор, а для вызовов функции accept в па- мяти размещается структура адреса сокета В листинге 25 24 приведена вторая часть функции main Она содержит беско- нечный цикл, вызывающий функцию sei ect в ожидании, когда будет готов к чте- нию какой-либо из дескрипторов демона Листинг 25.24. Вторая часть функции main обработка готового к чтению дескриптора //icmpd/icmpd с 24 for ( ) { 25 rset = all set 26 nready = Select(maxfd + 1 &rset NULL. NULL, NULL);; 27 if (FD_ISSET(listenfd &rset)) 28 if (readable_listen() <= 0) 29 continue 30 if (FD_ISSET(fd4 &rset)) 31 if (readable_v4() <= 0) 32 continue 33 tfifdef IPV6 34 if (FD_ISSET(fd6 &rset)) 35 if (readable_v6() <= 0) 36 continue 37 #endif 38 for (i = 0 i <- maxi i++) { /* все клиенты проверяются на наличие данных */ 39 if ( (sockfd = clientfi] connfd) < 0) 40 continue 41 if (FD_ISSET(sockfd &rset)) 42 if (readable_conn(i) <- 0) 43 break /* больше нет дескрипторов готовых для чтения */ 44 }
25 7 Демон сообщений ICMP 749 45 } 46 exit(O) 47 } Проверка прослушиваемого доменного сокета Unix 27 29 Прослушиваемый доменный сокет Unix проверяется в первую очередь, и в слу- чае, если он готов, запускается функция readable_listen Переменная nready — количество дескрипторов, которое функция sei ect возвращает как готовые к чте- нию — является глобальной Каждая из наших функций readabl е_ХХХ уменьшает ее значение на 1 и новое значение этой переменной является возвращаемым зна- чением функции Когда ее значение достигает нуля, это говорит о том, что все готовые к чтению дескрипторы обработаны, и поэтому функция sei ect вызывает- ся снова Проверка символьных сокетов ICMP 30 37 Проверяется символьный сокет ICMPv4, а затем символьный сокет ICMPv6 Проверка присоединенных доменных сокетов Unix 38 44 Затем проверяется, готов ли для чтения какой-нибудь из присоединенных до- менных сокетов Unix Готовность для чтения какого-либо из таких сокетов обо- значает, что клиент отослал дескриптор или завершился В листинге 25 25 приведена функция readabl е_1 i sten, вызываемая, когда про- слушиваемый сокет готов для чтения Это указывает на новое клиентское соеди- нение Листинг 25.25. Обработка нового соединения клиента //icmpd/readable_listen с 1 include icmpd h 2 int 3 readable_listen(void) 4 { 5 int i connfd 6 socklen_t clilen 7 clilen = addrlen 8 connfd = Accept!!istenfd cliaddr &clilen) 9 /* найдена первая доступная структура clientE] */ 10 for (i = 0 1 < FDSETSIZE 1++) 11 if (clientEi] connfd < 0) { 12 clientEi] connfd = connfd /* сохраняется дескриптор */ 13 break 14 } 15 if (i == FDJSETSIZE) 16 err_quit( too many clients ) 17 printf( new connection i = M connfd = W\en i connfd) 18 FD_SET(connfd &allset) /* в набор добавляется новый дескриптор */ 19 if (connfd > maxfd) 20 maxfd = connfd /* для функции select!) */ 21 if (i > maxi) 22 maxi = i /* максимальный индекс в массиве clientE] */ •, продолжение
750 Глава 25. Символьные сокеты Листинг 25.25 {продолжение) 23 return (--nready). 24 } 23 Принимается соединение и используется первый доступный вход в массив cl 1 ent. Код данной функции скопирован из начала кода, приведенного в листин- ге 6.4. Когда присоединенный сокет готов для чтения, вызывается функция readabl е_ conn (листинг 25.26), а ее аргументом является индекс данного клиента в массиве client. Листинг 25.26. Считывание данных и, возможно, дескриптора от клиента //icmpd/readable_conn с 1 #include "icmpd h" 2 int 3 readable_conn(int i) 4 { 5 int umxfd. recvfd: 6 char c. 7 ssize_t n. 8 socklen t len: 9 union { 10 char buf[MAXSOCKADDR]. 11 struct sockaddr sock. 12 } un. 13 umxfd = client[i] connfd. 14 recvfd = -1. 15 if ( (n = Read_fd(umxfd. &c. 1. &recvfd)) == 0) { 16 err_msg("client !Kd terminated, recvfd = W". i, recvfd); 17 goto clientdone. 7* вероятно, клиент завершил выполнение *7 18 } 19 /* данные от клиента, должно быть, дескриптор */ 20 if (recvfd < 0) { 21 err_msg("read_fd did not return descriptor"). 22 goto clienterr, 23 } Считывание данных клиента и, возможно, дескриптора -18 Вызываем функцию read fd, приведенную в листинге 14.9, для считывания дан- ных и, возможно, дескриптора. Если возвращаемое значение равно нулю, клиент закрыл свою часть соединения, вероятно, завершив свое выполнение. ПРИМЕЧАНИЕ ------------------------------------------------------------------------ При написании кода пришлось выбирать, что использовать для связи между приложе- нием и демоном — либо потоковый доменный сокет Unix, либо дейтаграмнный домен- ный сокет Unix. Дескриптор сокета UDP может быть передан через любой доменный соке г Unix. Причина, по которой предпочтение было отдано потоковому сокету, за- ключается в том, что он позволяет определить момент отключения клиента. Все его дескрипторы автоматически закрываются, когда клиент отключается, в том числе и до- менный сокет Unix, используемый для связи с демоном, в результате чего данный кли- ент удаляется демоном из массива client. Если бы мы использовали сокет дейтаграмм, мы бы не узнали, когда клиент отключился.
25.7. Демон сообщений ICMP 751 19-23 Если клиент не закрыл соединение, мы ждем получения дескриптора. Вторая часть функции readable_conn приведена в листинге 25.27. Листинг 25.27. Получение номера порта, который клиент связал с UDP-сокетом //icmpd/readable_conn.c 24 len = sizeof(un buf). 25 if (getsockname!recvfd. (SA *) un buf. &len) < 0) { 26 err_ret("getsockname error"). 27 goto clienterr, 28 } 29 clientli] family = un sock.sa_family. 30 if ( (client[ij Iport = sock_get_port(&un sock, len)) == 0) { 31 clientfi] Iport = sock_bind_wild(recvfd. client[i] family). 32 if (client[i] Iport <= 0) { 33 err_ret("error binding ephemeral port"). 34 goto clienterr: 35 } 36 } 37 Write(umxfd, "1", 1). /* сообщение клиенту, что все OK */ 38 FD_SET(umxfd. &allset). 39 if (umxfd > maxfd) 40 maxfd = umxfd. 41 if (i > maxi) 42 maxi = 1. 43 Close(recvfd). /* все сделано с UDP-сокетом клиента */ 44 return (--nready). 45 clienterr 46 Write(unixfd. "0". 1). /* сообщение клиенту о возникновении ошибки */ 47 clientdone 48 Close! umxfd). 49 if (recvfd >= 0) 50 Close(recvfd): 51 FD_CLR( umxfd. &allset); 52 client[i] connfd = -1. 53 return (--nready). 54 } Получение номера порта, связанного с сокетом UDP 24-28 Вызывается функция getsockname, так что демон может получить номер порта, связанного с сокетом. Поскольку неизвестно, каков размер буфера, необходимо- го для размещения структуры адреса сокета, мы объявляем объединение символь- ного массива и универсальной структуры адреса сокета. Такой подход, в отличие от отдельного объявления символьного массива, гарантирует, что символьный массив надлежащим образом выравнивается в соответствии со структурой адре- са сокета. Эта проблема проиллюстрирована в листинге 11.3, а в данной програм- ме для обеспечения выравнивания вызывается функция mall ос. 29-36 Семейство адресов сокета вместе с номером порта сохраняется в структуре client. Если номер порта равен нулю, мы вызываем функцию sock_bind_wild для связывания универсального адреса и динамически назначаемого порта с соке- том, но, как отмечалось ранее, такой подход не работает в реализациях SVR4. Сообщение клиенту, что все в порядке 37-42 Один байт, содержащий символ “1", отправляется обратно клиенту. Новый де- скриптор добавляется к набору дескрипторов для функции sei ect, а значения maxfd и maxi при необходимости обновляются.
752 Глава 25. Символьные сокеты Закрытие UDP-сокета клиента 43 Заканчиваем работу с UDP-сокетом клиента и закрываем его с помощью функ- ции close. Дескриптор был переслан нам клиентом и, таким образом, является копией, следовательно, UDP-сокет все еще открыт на стороне клиента. Обработка ошибок и завершение работы клиента г5-53 Если происходит ошибка, клиент получает нулевой байт. Когда клиент завер- шается, наша часть доменного сокета Unix закрывается, и соответствующий де- скриптор удаляется из набора дескрипторов для функции sei ect. Полю connfd структуры cl i ent присваивается значение -1, что является указанием на ее недо- ступность. Функция readabl e_v4 вызывается, когда символьный сокет ICMPv4 открыт для чтения. Первая часть данной функции приведена в листинге 25.28. Этот код аналогичен коду для ICMPv4, приведенному ранее в листингах 25.6 и 25.15. Листинг 25.28. Обработка полученных дейтаграмм ICMPv4, первая часть //icmpd/readable_v4 с 1 include "icmpd h" 2 #include <netinet/in_systm h> 3 #include <netinet/ip h> 4 #include <netinet/ip_icmp h> 5 #include <netinet/udp h> 6 int 7 readable_v4(void) 8 { 9 int i hlenl hlen2. icmplen. sport 10 char buf[MAXLINE]. 11 char srcstr[INET_ADDRSTRLEN], dststr[INET_ADDRSTRLEN]; 12 ssize_t n 13 socklen_t len 14 struct ip *ip. *hip 15 struct icmp *icmp 16 struct udphdr *udp. 17 struct sockaddrin from. dest. 18 struct icmpd_err icmpd_err. 19 len = sizeof(from). 20 n = Recvfrom(fd4, buf. MAXLINE. 0 (SA *) &from. &len): 21 printfC'M bytes ICMPv4 from fc 22 n Sock_ntop_host((SA *) &from, len)) 23 ip = (struct ip *) buf. /* начало IP-заголовка */ 24 hlenl = ip->ip_hl « 2. /* длина IP-заголовка */ 25 icmp = (struct icmp *) (buf + hlenl), /* начало ICMP-заголовка */ 26 if ( (icmplen = n - hlenl) < 8) 27 err_quit("icmplen Ud) < 8' icmplen). 28 printfC type = £d, code = W\er", icmp->icmp_type icmp->icmp_code). Функция выводит некоторую информацию о каждом получаемом сообщении ICMP. Это было сделано для отладки при разработке демона, и вывод управляет- ся аргументом командной строки. В листинге 25.29 приведена вторая часть функции readabl e_v4.
25.7. Демон сообщений ICMP 753 Листинг 25.29. Обработка полученных дейтаграмм ICMPv4, вторая часть //icmpd/readable_v4 с 29 if (icmp->icmpjype == ICMPJJNREACH || 30 icmp->icmpjype == ICMPJIMXCEED || 31 icmp->icmp_type = ICMP_SOURCEQUENCH) { 32 if (icmplen <8 + 20 + 8) 33 err_quit("icmplen Ud) < 8 + 20 + 8". icmplen); 34 hip = (struct ip *) (buf + hlenl + 8), 35 hlen2 = hip->ip_hl « 2. 36 pnntfCXetsrcip = £s. dstip = £s. proto = ld\en". 37 I net_ntop( AFJNET. &hip->ip_src. srcstr sizeof(srcstr)). 38 Inet_ntop(AF_INET, &hip->ip_dst. dststr. sizeof(dststr)). 39 hip->ip_p), 40 if (hip->ip_p == IPPROTOJJDP) { 41 udp = (struct udphdr *) (buf + hlenl + 8 + hlen2). 42 sport = udp->uh_sport, 43 /* найден доменный сокет клиента, отсылаем заголовки */ 44 for (1=0 1 <= maxi. 1++) { 45 if (client[i] connfd >= 0 && 46 client[i] family == AFJNET && 47 clientli] Iport == sport) { 48 bzero(&dest. sizeof(dest)). 49 dest sin_family = AFJNET. 50 #ifdef HAVE_SOCKADDR_SA_LEN 51 dest sinjen = sizeof(dest). 52 #endif 53 memcpy(&dest sin_addr. &hip->ip_dst. 54 sizeof(struct in_addr)) 55 dest sinjaort = udp->uh_dport. 56 icmpd_err icmpdJype = icmp->icmpjype 57 icmpd_err icmpd_code = icmp->icmp_code. 58 icmpd_err icmpdjen = sizeof(struct sockaddrjn). 59 memcpy(&icmpd_err icmpd_dest. &dest. sizeof(dest)). 60 /* тип и код преобразуются в корректное значение еггпо */ 61 icmpd_err icmpd_errno = EHOSTUNREACH /* по умолчанию */ 62 if (icmp->icmpjype == ICMPJJNREACH) { 63 if (icmp->icmp_code == ICMP_UNREACH_PORT) 64 icmpd_err icmpd_errno = ECONNREFUSED 65 else if (icmp->icmp_code — ICMPJJNREACHJIEEDFRAG) 66 icmpd_err icmpd_errno = EMSGSIZE 67 } 68 Write(client[i] connfd. &icmpd_err, sizeof(icmpd_err)); 69 } 70 } 73 return (--nready); 74 } Проверка типа сообщения, уведомление приложения 29-31 ICMP-сообщения, которые посылаются приложениям, — это сообщения о не- доступности порта, превышения времени и завершении клиента (см. табл. 25.1).
754 Глава 25 Символьные сокеты Проверка ошибки UDP, поиск клиента 42 Указатель hip указывает на IP-заголовок, который возвращается сразу после заголовка ICMP Это IP-заголовок дейтаграммы, вызвавшей ICMP-ошибку Мы проверяем, что эта IP-дейтаграмма является UDP-дейтаграммой, а затем извле- каем номер UDP-порта из UDP-заголовка, следующего за IP-заголовком 55 По всем структурам cl i ent осуществляется поиск подходящего семейства адре- сов и порта Если соответствие найдено, строится структура адреса сокета IPv4, которая содержит IP-адрес получателя и порт из UDP-дейтаграммы, вызвавшей ошибку Построение структуры icmpd_err 70 Строится структура i cmpd_err, посылаемая клиенту через доменный сокет Unix Тип и код сообщения ICMP сначала отображаются в значение errno, как показа- но в табл 25 1 Ошибки ICMPv6 обрабатываются функцией readable_v6, первая часть кото- рой приведена в листинге 25 30 Обработка ошибок ICMPv6 аналогична коду, приведенному в листингах 25 7 и 25 16 Листинг 25.30. Обработка полученной дейтаграммы ICMPv6, первая часть //icmpd/readab1e_v6 с 1 include icmpd h 2 #include <netinet/in_systm h> 3 #i nd tide <neti net/i p h> 4 ^include <netinet/ip_icmp h> 5 #include <netinet/udp h> 6 #ifdef IPV6 7 #include ip6 h /* должно быть <netinet/ip6 h> */ 8 include icmp6 h /* должно быть <netinet/iстрб h> */ 9 #endif 10 int 11 readable_v6(void) 12 { 13 #ifdef IPV6 14 int i hlenl hlen2 icmp61en sport 15 char buf[MAXLINE] 16 char srcstr[INET6_ADDRSTRLEN] dststr[INET6_ADDRSTRLEN] 17 ssize_t n 18 socklen_t len 19 struct ip6_hdr *ip6 *hip6 20 struct icmp6_hdr *icmp6 21 struct udphdr *udp 22 struct sockaddr_in6 from dest 23 struct icmpd__err icmpd_err 24 len = sizeof(from) 25 n = Recvfrom(fd6 buf MAXLINE 0 (SA *) &from &len) 26 printf( Xd bytes ICMPv6 from £s 27 n Sock_ntop_host((SA *) &from len)) 28 ip6 = (struct ip6_hdr *) buf /* начало 1Руб-заголовка */ 29 hlenl = sizeof(struct ip6_hdr)
25 7 Демон сообщений ICMP 755 30 if (ip6 >ip6_nxt IPPRDTD_ICMPV6) 31 err_quit( next header not IPPR0T0_ICMPV6 ) 32 icmp6 = (struct icmp6_hdr *) (buf + hlenl) 33 if ( (icmp61en = n hlenl) < 8) 34 err_quit( icmp61en (Xd) <8 icmp61en) 35 printf( type = Xd code = Xd\en icmp6 >icmp6_type icmp6 >icmp6_code) Вторая часть функции readable_v6 приведена в листинге 25 31 Код аналоги- чен приведенному в листинге 25 29 мы проверяем тип ICMP-ошибки, убежда- емся, что дейтаграмма, вызвавшая ошибку, является UDP-дейтаграммой, а затем строим структуру 1 cmpd_err, которую отсылаем клиенту Листинг 25.31. Обработка полученной дейтаграммы ICMPv6, вторая часть //icmpd/readable_v6 с 36 if (тстрб >icmp6_type == ICMP6_DST_UNREACH || 37 тстрб >icmp6_type == ICMP6_PACKET_T00_BIG || 38 тстрб >icmp6_type == ICMP6_TIME_EXCEEDED) { 39 if (icmp61en <8 + 40 + 8) 40 err_quit( icmp6len (Xd) <8 + 40 + 8 icmp6len). 41 hip6 = (struct ip6_hdr *) (buf + hlenl + 8) 42 hlen2 = sizeof(struct ip6_hdr) 43 printf( \etsrcip = Xs dstip = Xs next hdr = Xd\en 44 Inet_ntop(AF_INET6 &hip6 >ip6_src srcstr sizeof(srcstr)) 45 Inet_ntop(AF_INET6 &hip6 >ip6_dst dststr sizeof(dststr)) 46 hip6 >ip6_nxt) 47 if (hip6 >ip6_nxt “ IPPROTOJJDP) { 48 udp = (struct udphdr *) (buf + hlenl + 8 + hlen2) 49 sport = udp >uh_sport 50 /* найден доменный сокет клиента посылаем заголовки*/ 51 for (1 = 0 1 <= maxi i++) { 52 if (clientEiJ connfd >= 0 && 53 clientEi] family == AFJNET6 && 54 clientEi] Iport == sport) { 55 bzero(&dest sizeof(dest)) 56 dest Sin6_fami1y = AF_INET6 57 #lfdef HAVE_SDCKADDR_SA_LEN 58 dest sin6Jen = sizeof(dest) 59 #endif 60 memcpy(Sdest sin6_addr &hip6 >ip6 <lst, 61 sizeof(struct in6_addr)) 62 dest sin6_port = udp >uh_dport 63 icmpd_err icmpd_type = icmp6 >icmp6_type 64 icmpd_err icmpd_code = icmp6 >icmp6_code 65 icmpd_err icmpdjen = sizeof(struct sockaddr_in6) 66 memcpy(&icmpd_err icmpd_dest &dest sizeof(dest)) 67 /* тип и код преобразуются в корректное значение еггпо */ 68 icmpd_err icmpd_errno = EHOSTUNREACH /* по умолчанию */ 69 if (тстрб >icmp6_type == ICMP6_DST_UNREACH) { 70 if (тстрб >icmp6_code == ICMPJJNREACH_PORT) 71 icmpd_err icmpd_errno = ECONNREFUSED 72 else if (тстрб >1Cmp6_code == ICMPJJNREACHJiEEDFRAG) птАплжет1е &
756 Глава 25. Символьные сокеты Листинг 25.31 (продолжение} 73 icmpd_err icmpd_errno = EMSGSIZE, 74 } 75 Write(client[i] connfd, &icmpd_err, sizeof(icmpd_err)). 76 } 77 } 78 } 79 } 80 return (--nready). 81 #endif 82 } 25.8. Резюме Символьные сокеты обеспечивают три возможности: 1. Чтение и запись пакетов ICMPv4, IGMPv4 и ICMPv6. 2. Чтение и запись IP-дейтаграммы с полем протокола, которое не обрабатыва- ется ядром. 3. Формирование своих собственных заголовков IPv4, обычно используемых в диагностических целях (или, к сожалению, хакерами). Два традиционных диагностических средства — программы Ping и Traceroute — используют символьные сокеты. Мы разработали наши собственные версии этих программ, поддерживающих обе версии протокола — и IPv4, и IPv6. Также нами разработан наш собственный демон i cmpd, который обеспечивает доступ к сооб- щениям об ошибках ICMP через сокет UDP. Данный пример также иллюстриру- ет передачу дескриптора через доменный сокет Unix между неродственными кли- ентом и сервером. Упражнения 1. В этой главе говорилось, что почти все поля заголовка IPv6 и все дополни- тельные заголовки доступны приложению через параметры сокета или вспо- могательные данные. Какая информация из дейтаграммы IPv6 недоступна приложению? 2. Что произойдет в листинге 25.29, если по какой-либо причине клиент пере- станет производить считывание из своего соединения домена Unix в свой де- мон icmpd, и множество ошибок ICMP поступят к клиенту? В чем заключается простейшее решение данной проблемы? 3. Если задать нашей программе Ping адрес широковещательной передачи, на- правленный в подсеть, она будет работать, то есть широковещательный эхо- запрос ICMP посылается как широковещательный запрос канального уров- ня, даже если мы не установим параметр сокета SO_BROADCAST. Почему? 4. Что произойдет с программой Ping, если мы запустим ее на узле с нескольки- ми интерфейсами, а в качестве аргумента имени узла возьмем групповой ад- рес 224.0.0.1?
ГЛАВА 26 Доступ к канальному уровню 26.1. Введение В настоящее время большинство операционных систем позволяют приложению получать доступ к канальному уровню. Это свойство подразумевает следующие возможности: 1. Отслеживание пакетов, принимаемых на канальном уровне, что, в свою оче- редь, позволяет запускать такие программы, как tcpdump, на обычных компью- терных системах (а не только на специальных аппаратных устройствах для отслеживания пакетов). Если добавить к этому способность сетевого интер- фейса работать в смешанном режиме (promiscuous mode), это позволит прило- жению отслеживать все пакеты, проходящие по локальному кабелю, а не только предназначенные для того узла, на котором работает эта программа. 2. Возможность запуска определенных программ как обычных приложений, а не как частей ядра. Например, большинство версий Unix сервера RARP — это обычные приложения, которые считывают запросы RARP с канального уров- ня (запросы RARP не являются дейтаграммами IP), а затем передают ответы также на канальный уровень. Три наиболее распространенных средства получения доступа к канальному уровню в Unix — это пакетный фильтр BSD (BPF, BSD Packet Filter), DLPI в SVR4 (Datalink Provider Interface — интерфейс поставщика канального уров- ня) и интерфейс пакетных сокетов Linux (SOCK_PACKET). Мы приводим в этой гла- ве обзор перечисленных средств, а затем описываем 1 1 Ьсар — открытую для сво- бодного доступа библиотеку, содержащую функции для захвата пакетов. Эта библиотека работает со всеми тремя перечисленными средствами, и использова- ние библиотеки позволяет сделать наши программы не зависящими от фактичес- кого способа обеспечения доступа к канальному уровню, применяемому в данной операционной системе. Мы описываем эту библиотеку, разрабатывая програм- му, которая посылает запросы серверу имен DNS (мы составляем свои собствен- ные дейтаграммы UDP и записываем их в символьный сокет) и считывает ответ при помощи 1т Ьсар, чтобы определить, добавляет ли сервер имен контрольную сумму в дейтаграммы UDP. 26.2. BPF: пакетный фильтр BSD 4.4BSD и многие другие Беркли-реализации поддерживают BPF — пакетный фильтр BSD (BSD Packet Filter). Реализация BPF описана в главе 31 [105].
758 Глава 26. Доступ к канальному уровню История BPF, описание псевдопроцессора BPF и сравнение с пакетным фильт- ром SunOs 4.1.x NIT приведены в [63]. Каждый канальный уровень вызывает BPF сразу после получения пакета и не- посредственно перед его передачей выше, как показано на рис. 26.1. Рис. 26.1. Захват пакета с использованием BPF Примеры подобных вызовов для интерфейса Ethernet приведены на рис. 4.11 и 4.19 в [105]. Вызов BPF должен произойти как можно скорее после получения пакета и как можно позже перед его передачей, так как это увеличивает точность временных отметок. Организовать само по себе перехватывание пакетов из канального уровня не очень сложно, однако преимущество BPF заключается в возможности их фильт- рации. Каждое приложение, открывающее устройство BPF, может загрузить свой собственный фильтр, который затем BPF применяет к каждому пакету. В то вре- мя как некоторые фильтры достаточно просты (например, при использовании фильтра udp or tcp принимаются только пакеты UDP и TCP), другие фильтры позволяют исследовать значения определенных полей в заголовках пакетов. Например, фильтр tcp and port 80 and tcp[13 1] & 0x7 =0 использовался в главе 14 [105] для отбора сегментов TCP, направлявшихся к пор- ту 80 или от него и содержащих флаги SYN, FIN или RST. Выражение tcp [13:1] соответствует однобайтовому значению, начинающемуся с 13-го байта от начала заголовка TCP. В BPF реализован основанный на регистрах механизм фильтрации, который применяет специфические для приложений фильтры к каждому полученному пакету. Хотя можно написать свою программу фильтрации на машинном языке псевдопроцессора (он описан в руководстве по использованию BPF), проще все- го будет компилировать строки ASCII (такие, как только что показанная строка,
26.2, BPF: пакетный фильтр BSD 759 начинающаяся с tcp) в машинный язык с помощью функции рсар_согпрт 1 е, о кото- рой мы рассказываем в разделе 26.6. В технологии BPF применяются три метода, позволяющие уменьшить наклад- ные расходы на ее использование. 1. Фильтрация BPF происходит внутри ядра, за счет чего минимизируется ко- личество данных, которые нужно копировать из ядра в приложение. Копиро- вание из пространства ядра в пользовательское пространство является доволь- но дорогостоящим. Если бы приходилось копировать каждый пакет, у BPF могли бы возникнуть проблемы при попытке взаимодействия с быстрыми ка- налами. 2. BPF передает приложению только часть каждого пакета. Здесь речь идет о длине захвата (capture length). Большинству приложений требуется только заголовок пакета, а не содержащиеся в нем данные. Это также уменьшает количество данных, которые BPF должен скопировать в приложение. В про- грамме tcpdump, например, по умолчанию это значение равно 68 байт, и этого достаточно для размещения 14-байтового заголовка Ethernet, 20-байтового за- головка IP, 20-байтового заголовка TCP и 14 байт данных. Но для вывода до- полнительной информации по другим протоколам (например, DNS или NFS) требуется, чтобы пользователь увеличил это значение при запуске програм- мы tcpdump. 3. BPF буферизует данные, предназначенные для приложения, и этот буфер пе- редается приложению только когда он заполнен или когда истекает заданное время ожидания для считывания (read timeout). Это время может быть задано приложением. Программа tcpdump, например, устанавливает время ожидания 1000 миллисекунд, а демон RARP задает нулевое время ожидания (посколь- ку пакетов RARP немного, сервер RARP должен послать ответ сразу, как толь- ко он получает запрос). Назначением буферизации является уменьшение количества системных вызовов. При этом между BPF и приложением проис- ходит обмен тем же количеством пакетов, но за счет того, что уменьшается количество системных вызовов, каждый из которых связан с дополнитель- ными накладными расходами, уменьшается и общий объем этих расходов. Например, на рис. 3.1 [93] сравниваются накладные расходы, возникающие при системном вызове read, когда файл считывается в несколько приемов, причем размер фрагментов варьируется от 1 байта до 131 072 байт. Хотя на рис. 26.1 мы показываем только один буфер, BPF поддерживает по два буфера для каждого приложения и заполняет один, пока другой копиру- ется в приложение. Эта стандартная технология носит название двойной бу- феризации (double buffering). На рис. 26.1 мы показываем только получение пакетов фильтром BPF: паке- ты, приходящие на канальный уровень снизу (из сети) и сверху (IP). Приложе- ние также может записывать в BPF, в результате чего пакеты будут отсылаться по канальному уровню, но большая часть приложений только считывает пакеты из BPF. У нас нет оснований использовать BPF для отправки дейтаграмм IP, поскольку параметр сокета IP HDRINCL позволяет нам записывать дейтаграммы IP любого типа, включая заголовок IP. (Подобный пример мы показываем в раз- деле 26.6.) Записывать в BPF можно только с одной целью — чтобы отослать
760 Глава 26. Доступ к канальному уровню наши собственные сетевые пакеты, не являющиеся дейтаграммами IP. Напри- мер, демон RARP делает это для отправки ответов RARP, которые не являются дейтаграммами IP. Для получения доступа к BPF необходимо открыть (вызвав функцию open) еще не открытое каким-либо другим процессом устройство BPF. Например, можно попробовать /dev/bpf О, и если будет возвращена ошибка EBUSY, то нужно попробо- вать /dev/bpf 1, и т. д. Когда устройство будет открыто, потребуется выполнить примерно 12 команд ioctl для задания характеристик устройства, таких как за- грузка фильтра, время ожидания для считывания (read timeout), размер буфера, присоединение канального уровня к устройству BPF, включение смешанного режима и т. д. Затем с помощью функций read и write осуществляется ввод и вывод. 26.3. DLPI: интерфейс поставщика канального уровня SVR4 обеспечивает доступ к канальному уровню через DLPI (Data Link Provider Interface — интерфейс поставщика канального уровня). DLPI — это не завися- щий от протокола интерфейс, разработанный в AT&T, который служит средством связи с сервисами, обеспечиваемыми канальным уровнем [101]. Доступ к DLPI осуществляется посредством отправки и получения сообщений через потоки. Для подсоединения к канальному уровню приложение просто открывает устройство (например, I еО) с помощью команды open и использует запрос DL_ATTACH_REQ. Но для эффективной работы используются два дополнительных модуля: pfmod, который осуществляет фильтрацию внутри ядра, и bufmod, буфери- зующий данные, предназначенные для приложения. Это показано на рис. 26.2. Рис. 26.2. Захват пакета с использованием DLPI, pfmod и bufmod
26.4. Linux: SOCK PACKET 761 Концептуально DLPI аналогичен BPF. pfmod поддерживает фильтрацию внутри ядра, используя псевдопроцессор, a bufmod сокращает количество данных и системных вызовов, поддерживая длину захвата и время ожидания для считы- вания. Одно интересное различие, тем не менее, заключается в том, что для BPF и фильтров pfmod используются разные типы псевдопроцессоров. Фильтр BPF — это ориентированный ациклический граф управления потоком (acyclic control flow graph, CFG), в то время как pfmod использует дерево булевых выражений. В первом случае естественным является отображение в код для вычислительной машины с регистровой организацией, а во втором — в код для машины со стеко- вой организацией [63]. В статье [63] показано, что реализация CFG, используе- мая в BPF, обычно работает быстрее, чем дерево булевых выражений, в 3-20 раз в зависимости от сложности фильтра. 26.4. Linux: SOCK_PACKET Для получения пакетов с канального уровня в Linux мы создаем сокет SOCK PACKET. Для этого мы должны обладать правами привилегированного пользователя (ана- логичные необходимым для создания символьного сокета), а третий аргумент функции socket должен быть ненулевым значением, задающим тип кадра Ethernet. Например, для получения всех кадров канального уровня мы пишем: fd = socket(AF_INET SOCK_PACKET htons(ETH_P_ALL)) В результате этого будут возвращены кадры для всех протоколов, получае- мые канальным уровнем. Если нам нужны кадры IPv4, то вызов будет таким: fd = socket(AF_INET SOCK_PACKET htons(ETH_P_IP)) Другие константы, которые могут использоваться в качестве последнего аргу- мента, — это, например, ETH_P_ARP и ETH_P_IPV6. Указывая протокол ЕТН_Р_ххх, мы тем самым сообщаем канальному уровню, какой тип из получаемых канальным уровнем кадров передавать сокету. Если канальный уровень поддерживает смешанный режим (например, Ehtemet), то устройство тоже должно работать в смешанном режиме. Для этого нужно вызвать функцию ioctl с запросом SIOCGIFFLAGS для получения флагов, установить флаг IFF_PROMISC и далее сохранить флаг с помощью SIOCSIFFLAGS. Сравнивая это средство Linux с BPF и DLPI, мы можем отметить некоторые различия. 1. В Linux не обеспечивается буферизация и фильтрация для ядра. Существует обычный буфер приема сокета, но отсутствует возможность буферизации и от- правки приложению нескольких кадров с помощью одной операции считыва- ния. Это увеличивает накладные расходы, связанные с копированием потен- циально возможных больших объемов данных из ядра в приложение. 2. В Linux не предусмотрена фильтрация на уровне устройства. Если в вызове функции socket указан аргумент ЕТН_Р_1Р, то все пакеты IPv4 со всех устройств (например, Ethernet, каналы РРР, каналы SLIP и закольцовка) будут переда- ны на сокет. Функция recvfrom возвращает общую структуру адреса сокета, а элемент sa_data содержит имя устройства (например, ethO). Тогда приложе- ние само должно игнорировать данные с тех устройств, которые не представ-
762 Глава 26. Доступ к канальному уровню ляют для него интереса. Здесь мы сталкиваемся, фактически, с той же пробле- мой: возможно, что приложение будет получать слишком много данных, осо- бенно в случае наблюдения за высокоскоростной сетью. 26.5. Libcap: библиотека для захвата пакетов Библиотека захвата пакетов libcap обеспечивает не зависящий от реализации доступ к средствам операционной системы, с помощью которых осуществляется этот захват. В настоящее время поддерживается только чтение пакетов (хотя до- бавление нескольких строк кода позволяет также записывать пакеты). Сейчас осуществляется поддержка BPF для Беркли-ядер, DLPI для Solaris 2.x, NIT для SunOS 4.1.x, пакетных сокетов (SOCK PACKET) в Linux и нескольких дру- гих операционных системах. Библиотека libcap используется программой tcpdump. Всего в библиотеке насчитывается порядка 25 функций, но вместо того чтобы просто описывать их, мы продемонстрируем их фактическое использование на примере, рассматриваемом в следующем разделе. Названия всех функций начи- наются с рсар_. В руководстве по использованию функций рсар они описаны бо- лее подробно. ПРИМЕЧАНИЕ------------------------------------------------------- Библиотека libcap находится в свободном доступе по адресу ftp://ftp.ee.lbl.gov/ libcap tar.z. 26.6. Анализ поля контрольной суммы UDP Теперь мы приступаем к рассмотрению примера, в котором отсылается дейта- грамма UDP, содержащая запрос UDP к серверу имен, а затем считывается ответ с помощью библиотеки захвата пакетов. Цель данного примера — установить, вычисляется на сервере имен контрольная сумма UDP или нет. В случае IPv4 вычисление контрольной суммы не является обязательным. В большинстве сис- тем в настоящее время вычисление контрольных сумм по умолчанию включено, но, к сожалению, в более старых системах, в частности SunOS 4.1.x, вычисление контрольных сумм по умолчанию отключено. В настоящее время все системы, а особенно система, в которой работает сервер имен, всегда должны работать с включенными контрольными суммами UDP, поскольку поврежденные (содер- жащие ошибки) дейтаграммы могут повредить базу данных сервера. ПРИМЕЧАНИЕ -------------------------------------------------- Включение и выключение контрольных сумм обычно осуществляется сразу для всей системы, как показано в приложении Е [94]. Мы формируем дейтаграмму UDP (запрос DNS) и записываем ее в символь- ный сокет. Для отправки запроса мы могли бы использовать обычный сокет UDP, но мы хотим показать, как использовать параметр сокета IP HDRINCL для создания полной дейтаграммы IP. Нет возможности получить контрольную сумму UDP при чтении из обычного сокета UDP, а также считывать пакеты UDP или TCP,
26,6. Анализ поля контрольной суммы UDP 763 используя символьный сокет (см. раздел 25.4). Следовательно, путем захвата па- кетов нам нужно получить целую дейтаграмму UDP, содержащую ответ сервера имен. Затем мы исследуем поле контрольной суммы UDP в заголовке UDP, и если оно равно нулю, это означает, что на сервере отключено вычисление контрольной суммы. Рисунок 26.3 иллюстрирует действие нашей программы. Мы записываем наши собственные дейтаграммы UDP в символьный сокет и считываем ответы, исполь- зуя библиотеку 11 Ьсар. Обратите внимание, что UDP также получает ответ серве- ра имен и отвечает сообщением о недоступности порта ICMP, так как ничего не знает о номере порта, выбранном нашим приложением. Сервер имен игнорирует эту ошибку ICMP. Также можно отметить, что написать подобную тестовую про- грамму, использующую TCP, было бы сложнее, даже несмотря на то, что мы с легкостью можем записывать свои собственные сегменты TCP. Дело в том, что любой ответ на сегмент TCP, который мы генерируем, обычно инициирует от- правка протоколом TCP ответного сегмента RST туда, куда был послан первый сегмент. (ответ с сервера имен) Рис. 26.3. Приложение, определяющее, включено ли на сервере вычисление контрольных сумм UDP ПРИМЕЧАНИЕ ------------------------------------------------------ Указанную проблему можно обойти. Для этого нужно посылать сегменты TCP с IP- адресом отправителя, который принадлежит присоединенной подсети, но в настоящий момент не присвоен никакому другому узлу. Нужно также добавить данные ARP на посылающем узле для этого нового IP-адреса, чтобы узел отвечал на запросы ARP для него. В результате стек IP на посылающем узле будет игнорировать пакеты, приходя- щие на этот IP-адрес, в предположении, что посылающий узел не является маршрути- затором.
764 Глава 26. Доступ к канальному уровню Рис. 26.4. Функции, которые используются в программе udpcksum На рис. 26.4 приведены функции, используемые в нашей программе. В листинге 26.11 показан заголовочный файл udpcksum.h, в который включен наш базовый заголовочный файл unp.h, а также различные системные заголовки, необходимые для получения доступа к определениям структур для заголовков пакетов IP и UDP. Листинг 26.1. Заголовочный файл udpcksum.h //udpcksum/udpcksum h 1 #include "unp h” 2 #include <pcap h> 3 #i nclude <netinet/Tn_systm h> 4 #include <netinet/in h> 5 #include <netinet/ip h> 6 #i nclude <netinet/ip_var h> 7 #include <netinet/udp h> 8 #include <netinet/udp_var h> 9 include <net/if h> 10 include <netinet/if_ether h> /* требуется ip h */ 11 #defTne LOCALPORT "39123” /* порт отправителя (заданный по умолчанию) */ 12 #define TTL_OUT 64 /* исходящее TTL */ 13 /* объявление глобальных переменных */ 14 extern struct sockaddr *dest *local, 15 extern socklen_t destlen. locallen. 16 extern int datalink. 17 extern char *device. 18 extern pcap_t *pd. 19 extern int rawfd, 20 extern Tnt snaplen, 21 extern Tnt verbose 22 extern Tnt zerosum. 23 /* прототипы функций */ 24 voTd cleanup(Tnt) Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http// www piter com/download
26.6. Анализ поля контрольной суммы UDP 765 25 char *next_pcap(int *), 26 void open_pcap(void). 27 void test_udp(void). 28 void udp_write(char *. int) 29 struct udpiphdr *udp_read(void). 3-10 Для работы с полями заголовков IP и UDP требуются дополнительные заголо- вочные файлы Интернета. 11-29 Мы определяем некоторые глобальные переменные и прототипы для своих соб- ственных функций, которые вскоре покажем. Первая часть функции main показана в листинге 26.2. Листинг 26.2. Функция mam: определения //udpcksum/main с 1 #include "udpcksum h" 2 3 /* определения глобальных переменных */ struct sockaddr *dest. *local. 4 socklen_t destlen locallen. 5 int data!ink. /* из pcap_datalink(). в <net/bpf h> */ 6 7 char *device. int fddipad. /* устройство pcap */ /* HACK для 11 bpcap если определено FDDI */ 8 pcap_t *pd. /* указатель на структуру устройства для захвата пакетов */ 9 10 int rawfd. int snaplen = 200: /* символьный сокет для записи */ /* количество данных, которые будут захвачены */ 11 12 int verbose. int zerosum. /* отправка запроса UDP без контрольной суммы */ 13 static void usage(const char *). 14 int 15 main(int argc. char *argv[J) 16 { 17 int c. on = 1. 18 char *ptr localname[1024], *localport; 19 struct addrinfo *aip 20 if (argc < 2) 21 usageC"). 22 /* Нужен локальный IP-адрес для задания IP-адреса 23 * отправителя дейтаграммы UDP Нельзя задать его 24 * равным 0 и оставить выбор за IP. так как он 25 * нужен для вычисления контрольной суммы * учитывающей псевдозаголовок 26 * Локальное имя и локальный порт можно заменить * с помощью параметра -1 27 */ 28 if (gethostnamedocalname sizeofClocal name)) < 0) 29 err_sys("gethostname error"). 30 localport = LOCALPORT. Проверка количества аргументов командной строки 20-21 Программа требует как минимум двух аргументов: имя или IP-адрес узла, на котором запущен сервер DNS, и имя службы (doma i п) или номер порта (53) серве-
766 Глава 26. Доступ к канальному уровню ра. Мы не показываем функцию usage — она просто выводит данные о формате команд и после этого завершается. Получение локального имени узла 8-30 Поскольку мы будем создавать свои собственные заголовки IP и UDP, нам нужно знать IP-адрес отправителя при записи дейтаграммы UDP. Мы не можем оставить его нулевым и позволить IP выбирать адрес, поскольку этот адрес явля- ется частью псевдозаголовка UDP (его мы вскоре опишем), который использует- ся для вычисления контрольной суммы UDP. Поэтому вы вызываем функцию gethostname для получения имени узла. Мы также определяем порт отправителя, присваивая ему значение из заголовочного файла udpcksum.h. Следующая часть функции main, показанная в листинге 26.3, обрабатывает аргументы командной строки. Листинг 26.3. Функция main: обработка аргументов командной строки //udpcksum/main с 31 opterr = 0 /* чтобы getoptO не записывала сообщения в стандартный поток сообценй об ошибках */ 32 while ( (с = getopttargc. argv. "01 1 v")) != -1) { 33 switch (c) { 34 case 'O' 35 zerosum =1. 36 break 37 case i' 38 device = optarg; /* устройство pcap */ 39 break, 40 case ' Г /* локальный IP-адрес и порт a b c d p */ 41 if ( (ptr = strrchr(optarg, ' ')) == NULL) 42 usageCinvalid -1 option”). 43 *ptr++ = 0 /*0 заменяет последнюю точку */ 44 local port = ptr. /* имя службы или номер порта */ 45 strncpy(local name, optarg sizeof(localname)). 46 break, 47 case 'v' 48 verbose = 1. 49 break. 50 case 51 usage("unrecognized option"): 52 } 53 } Обработка аргументов командной строки 11-36 Мы вызываем функцию getopt для обработки аргументов командной строки. С помощью параметра -0 мы посылаем запросы UDP без контрольной суммы UDP, чтобы выяснить, обрабатываются ли эти дейтаграммы сервером иначе, чем дейтаграммы с контрольной суммой. 7-39 Параметр -1 позволяет нам задать интерфейс, на котором будут приниматься ответы сервера. Если этот интерфейс не будет задан, библиотека для захвата па-
26.6. Анализ поля контрольной суммы UDP 767 кетов выберет какой-либо интерфейс самостоятельно, но в случае узла с несколь- кими сетевыми интерфейсами этот выбор может оказаться некорректным. В этом заключается одно из различий между считыванием из обычного сокета и из уст- ройства для захвата пакетов: в первом случае мы можем указать универсальный локальный адрес, что позволяет получать пакеты, прибывающие на любой из сете- вых интерфейсов. Но во втором случае при работе с устройством для захвата паке- тов мы можем получать пакеты, прибывающие только на конкретный интерфейс. ПРИМЕЧАНИЕ ---------------------------------------------------------------------------- Можно отметить, что для пакетных сокетов Linux захват пакетов не ограничен одним устройством. Тем не менее библиотека libcap обеспечивает фильтрацию либо по умол- чанию, либо согласно заданному нами параметру -1. 40-46 Параметр -1 позволяет нам задать IP-адрес отправителя и номер порта. В каче- стве номера порта (или названия службы) берется строка, следующая за послед- ней точкой, а IP-адресом является все, что расположено перед последней точкой. Последняя часть функции main показана в листинге 26.4. Листинг 26.4. Функция main: преобразование имен узлов и названий служб, создание сокета //udpcksum/main с 54 if (optind ’= argc - 2) 55 usage!"missing <host> and/or <serv>"), 56 /* * Преобразование имени узла получателя и названия службы */ 57 aip = host_serv(argv[optind] argv[optind + 1]. AF_INET. SOCK_DGRAM). 58 dest = aip->ai_addr /* не вызываем функцию freeaddrinfoO */ 59 destlen = aip->ai_addrlen. 60 /* Преобразуем локальное имя и название службы */ 61 aip = host_serv(localname localport AF_INET. SOCK_DGRAM). 62 local = aip->ai_addr /* не вызываем функцию freeaddrinfoO */ 63 locallen = aip->ai_addrlen. 64 /* 65 * Нужен символьный сокет для записи наших * дейтаграмм IP 66 * Процесс должен иметь права привилегированного * пользователя для создания этого сокета 67 * Нужно установить параметр IP_HDRINCL чтобы * записывать свои собственные заголовки IP 68 */ 69 rawfd = Socket(dest->sa_farmly. SOCK_RAW. 0). 70 Setsockopt(rawfd. IPPROTO_IP. IP_HDRINCL Son. sizeof(on)): 71 open_pcap(). /* открываем устройство для захвата пакетов */ 72 setuid(getuidO) /* больше не нужны права привилегированного Пользователя 73 Signal(SIGTERM cleanup) 74 Signal(SIGINT. cleanup). 75 Signal(SIGHUP. cleanup), 76 test udp(). n тродолжение
768 Глава 26. Доступ к канальному уровню Листинг 26.4 (продолжение) 77 cleanup(O) 78 } Обработка имени узла и порта получателя, затем локального имени узла и порта 54 63 Мы убеждаемся, что остается ровно два аргумента командной строки: имя узла получателя и название службы. После этого вызываем функцию host_serv для преобразования их в структуру адреса сокета, указатель на которую сохраняем в переменной dest. Затем выполняем такое же преобразование локального имени узла и номера порта, сохраняя указатель на структуру адреса сокета в перемен- ной local. Создаем символьный сокет и открываем устройство для захвата пакетов 64 71 Мы создаем символьный сокет и включаем параметр сокета IP_HDRINCL. Этот параметр позволяет нам записывать полные дейтаграммы IP, включая заголо- вок IP. Функция ореп_рсар открывает устройство для захвата пакетов. Она пока- зана в следующем листинге. Изменение прав и установка обработчиков сигналов 72 75 Для создания символьного сокета необходимо иметь права привилегированно- го пользователя. Обычно такие привилегии нужны нам для того, чтобы открыть устройство для захвата пакетов, но это зависит от реализации. Например, в слу- чае BPF администратор может установить разрешения для устройств /dev/bpf лю- бым способом в зависимости от того, что требуется для данной системы. Здесь мы не используем эти дополнительные разрешения, предполагая, что для файла программы установлен бит SUID. Процесс выполняется с правами привилегиро- ванного пользователя, а когда они становятся не нужны, при вызове функции setuid фактический идентификатор пользователя (real user ID), эффективный идентификатор пользователя (effective user ID) и сохраненный SUID принима- ют значение фактического идентификатора пользователя (getuid). Мы устанав- ливаем обработчики сигналов на тот случай, если пользователь завершит про- грамму раньше, чем будут изменены права. Выполнение теста и очистка 76-77 Функция test udp (см. листинг 26.6) выполняет тестирование и возвращает управление. Функция cleanup (см. листинг 26.13) выводит итоговую статистику захвата пакетов, а затем завершает процесс. В листинге 26.5 показана функция ореп_рсар, которую мы вызвали из функ- ции main, чтобы открыть устройство для захвата пакетов. Листинг 26.5. Функция ореп_рсар. открытие и инициализация устройства для захвата пакетов //udpcksum/pcap с 1 #include "udpcksum h” 2 #define CMD "udp and src host Xs and src port Xd"
26.6. Анализ поля контрольной суммы UDP 769 4 open_pcap(void) 5 { 6 uint32_t localnet netmask. 7 char cmdIMAXLINE]. errbuf[PCAP ERRBUF SIZE], strl[lNET_ADDRSTRLEN]. 8 Str2[INET_ADDRSTRLEN] 9 struct bpf_program fcode. 10 if (device == NULL) { 11 if ( (device = pcap_lookupdev(errbuf)) == NULL) 12 err_quit("pcap_lookup Xs". errbuf), 13 } 14 printfCdevice = Xs\en". device). 15 /* жестко задано promisc=0 to_ms=500 */ 16 if ( (pd = pcap_open_live(device, snaplen. 0. 500 errbuf)) •= NULL) 17 err_quit(”pcap_open_live Xs", errbuf) 18 if (pcap_lookupnet(device. &localnet &netmask, errbuf) < 0) 19 err_quit(”pcap_lookupnet Xs", errbuf) 20 if (verbose) 21 printfClocalnet = Xs netmask = Xs\en". 22 Inet_ntop(AF_INET &localnet strl. sizeof(strl)), 23 Inet_ntop(AF_INET. &netmask str2. sizeof(str2))). 24 snprintf(cmd sizeof(cmd). CMD 25 Sock_ntop_host(dest destlen). 26 ntohs(sock_get_port(dest destlen))); 27 if (verbose) 28 printfCcmd = Xs\en" cmd). 29 if (pcap_compile(pd &fcode cmd. 0 netmask) < 01 30 err_quit("pcap_compile Xs" pcap_geterr(pd)), 31 if (pcap_setfilter(pd &fcode) < 0) 32 err_quit(‘pcap_setfilter Xs” pcap_geterr(pd)) 33 if ( (datalink = pcap_datalink(pd)) < 0) 34 err_quit("pcap_datalink Xs" pcap_geterr(pd)). 35 if (verbose) 36 printfCdatalink = Xd\en" datalink): 37 } Выбор устройства для захвата пакетов 10-14 Если устройство для захвата пакетов не было задано (с помощью параметра командной строки -1), то выбор этого устройства осуществляется с помощью функции рсар_1ookupdev. С помощью запроса SIOCGIFCONF функции ioctl выбира- ется включенное устройство с минимальным порядковым номером, но только не устройство обратной связи. Многие из библиотечных функций рсар возвра- щают сообщения об ошибках в виде строк. Единственным аргументом функ- ции рсар_1ookupdev является массив, в который записывается строка с сооб- щением об ошибке. Открываем устройство .5-17 Функция pcap_open_live открывает устройство. Слбво ]^еприсугству««В ЙМА звании функции потому, что здесь имеется в виду фактическое устройство'ДОШ
770 Глава 26. Доступ к канальному уровню захвата пакетов, а не файл, содержащий предыдущие сохраненные пакеты. Пер- вым аргументом функции является имя устройства, вторым — количество бай- тов, которое нужно сохранять для каждого пакета (значение shaplen, которое мы инициализировали числом 200 в листинге 26.2), а третий аргумент — это флаг, указывающий на смешанный режим. Четвертый аргумент — это значение време- ни ожидания в миллисекундах, а пятый — указатель на массив, содержащий со- общения об ошибках. Если установлен флаг смешанного режима, интерфейс переходит в этот ре- жим, в результате чего он принимает все пакеты, проходящие по кабелю. Для программы tcpdump это нормальный режим. Тем не менее в нашем примере отве- ты сервера DNS будут посланы непосредственно на наш узел (то есть можно обой- тись без смешанного режима). Четвертый аргумент (время ожидания) — это аргумент для считывания. Вме- сто того чтобы возвращать пакет процессу каждый раз, когда приходит очеред- ной пакет (что может быть весьма неэффективно, так как в этом случае потребу- ется выполнять множество операций копирования отдельных пакетов из ядра в процесс), это делается, либо когда считывающий буфер устройства оказывает- ся заполненным, либо когда истекает время ожидания. Если время ожидания рав- но нулю, то каждый пакет будет переправляться процессу, как только будет по- лучен. Получение сетевого адреса и маски подсети 18-23 Функция pcap l ookupnet возвращает сетевой адрес и маску подсети для устрой- ства захвата пакетов. При вызове функции pcap_compi 1 е, которая будет вызвана следующей, нужно задать маску подсети, поскольку с помощью маски фильтр пакетов определяет, является ли IP-адрес адресом широковещательной переда- чи для данной подсети. Компиляция фильтра пакетов 24-30 Функция pcap compi 1 е получает строку, построенную нами как массив cmd, и ком- пилирует ее, создавая тем самым программу для фильтрации (записывая ее в code). Эта программа будет отбирать те пакеты, которые мы хотим получить. Загрузка программы фильтрации 31-32 Функция pcap_setf 11 ter получает только что скомпилированную программу фильтрации и загружает ее в устройство для захвата пакетов. Таким образом ини- циируется захват пакетов, выбранных нами путем настройки фильтра. Определение типа канального уровня 33-36 Функция pcap_datalink возвращает тип канального уровня для устройства зах- вата пакетов. Эта информация нужна нам при захвате пакетов для того, чтобы определить размер заголовка канального уровня, который будет добавлен в нача- ло каждого считываемого нами пакета (листинг 26.10). После вызова функции ореп_рсар функция main вызывает функцию test_udp, показанную в листинге 26.6. Эта функция посылает запрос DNS и считывает от- вет сервера.
26.6 Анализ поля контрольной суммы UDP 771 Листинг 26.6. Функция test udp: отправка запросов и считывание ответов //udpcksum/udpcksum с 47 void 48 test_udp(void) 49 { 50 volatile int nsent = 0, timeout = 3. 51 struct udpiphdr *ui. 52 Signal(SIGALRM, sig_alrm). 53 if (sigsetjmp(jmpbuf. 1)) { 54 if (nsent >= 3) 55 errquitCno response"). 56 printfCtimeoutXen"). 57 timeout *= 2. /* экспоненциальное увеличение задержки 3 6. 12 */ 58 } 59 canjump =1. /* теперь siglongjmp OK */ j«. 60 send_dns_query(), 61 nsent++ 62 alarm(timeout). 63 ui = udp_read(). 64 canjump = 0. 65 alarm(O). 66 if (ui->ui_sum == 0) 67 printf('UDP checksums offXen"): 68 else 69 pnntfCUDP checksums on\en"). 70 if (verbose) 71 printfCrecevied UDP checksum = XxXen". ntohs(ui->ui sura)): 72 } Переменные volatile 50 Нам нужно, чтобы две динамические локальные переменные nsent и timeout сохраняли свои значения после возвращения siglongjmp из обработчика сигнала в нашу функцию. Реализация допускает восстановление значений динамических локальных переменных, предшествовавших вызову функции sigset jump [93, с. 178], но добавление спецификатора vol ati 1 е предотвращает это восстановление. Установление обработчика сигналов и буфера перехода 52-53 Для сигнала SIGALRM устанавливается обработчик сигнала, а функция sigsetjmp устанавливает буфер перехода для функции siglongjmp. (Эти две функ- ции подробно описаны в разделе 10.15 [93].) Значение 1 во втором аргументе функции sigsetjmp указывает, что требуется сохранить текущую маску сигнала, так как мы будем вызывать функцию siglongjmp из нашего обработчика сигнала. Функция siglongjmp >4-58 Этот фрагмент кода выполняется, только когда функция siglongjmp вызывает- ся из нашего обработчика сигнала. Это указывает на возникновение условий, при которых мы входим в состояние ожидания: мы отправили запрос, на который не пришло никакого ответа. Если после того, как мы отправим три запроса, ответа
772 Глава 26. Доступ к канальному уровню не будет, мы прекращаем выполнение кода. По истечении времени ожидания, отведенного на получение ответа, мы выводим соответствующее сообщение и уве- личиваем значение времени ожидания в два раза, то есть задаем экспоненциаль- ное смещение {exponential backoff), которое также описано в разделе 20 5. Первое значение времени ожидания равно 3 секундам, затем — 6 секундам и 12 секундам. Причина, по которой в этом примере мы используем функции sigsetjmp и siglongjmp, вместо того чтобы просто перехватывать ошибку EINTR (как мы по- ступили в листинге 13.1), заключается в том, что библиотечные функции захвата пакетов (которые вызываются из нашей функции udp_read) заново запускают опе- рацию чтения в случае возвращения ошибки EINTR. Поскольку мы не хотим моди- фицировать библиотечные функции, единственным решением для нас является перехватывание сигнала SIGALRM и выполнение нелокального перехода (операто- ра goto), который возвращает управление в наш код, а не в библиотечную функцию. Отправка запроса DNS и считывание ответа 60-65 Функция send_dns_query (см. листинг 26.8) отправляет запрос DNS на сервер имен. Функция dns_read считывает ответ. Мы вызываем функцию alarm для пре- дотвращения «вечной» блокировки функции read. Если истекает заданное (в се- кундах) время ожидания, генерируется сигнал SIGALRM, и наш обработчик сигнала вызывает функцию siglongjmp. Анализ полученной контрольной суммы UDP 66-71 Если значение полученной контрольной суммы UDP равно нулю, это значит, что сервер не вычислил и не отправил контрольную сумму. В листинге 26.7 показана наша функция si gal rm — обработчик сигнала SIGALRM. Листинг 26.7. Функция sig_alrm: обработка сигнала SIGALRM //udpcksum/udpcksum с 1 #include "udpcksum h" 2 #include <setjmp h> 3 static sigjmp_buf jmpbuf, 4 static int canjump. 5 void 6 sig_alrm(int signo) 7 { 8 if (canjump == 0) 9 return. 10 siglongjmp(jmpbuf 1). И } 8-10 Флаг canjump был установлен в листинге 26.6 после инициализации буфера пе- рехода функцией sigsetjmp. Если флаг был установлен, в результате вызова функ- ции siglongjmp управление осуществляется таким образом, как если бы функция sigsetjmp из листинга 26.6 возвратила значение 1. В листинге 26.8 показана функция send_dns_query, посылающая запрос UDP на сервер DNS. Эта функция формирует запрос DNS. Листинг 26.8. Функция send dns query: отправка запроса UDP на сервер DNS //udpcksum/udpcksum с 16 void 17 send_dns_query(void)
26.6. Анализ поля контрольной суммы UDP 773 18 { 19 size_t nbytes. 20 char buf[sizeof(struct udpiphdr) + 100] *ptr, 21 short one. 22 ptr = buf + sizeoftstruct udpiphdr). /* оставляем место для заголовков IP/tIQP */ 23 *((u_short *) ptr) = htons(1234). /* идентификация */ 24 ptr += 2 25 *((u_short *) ptr) = htons(OxO). /* флаги */ 26 ptr += 2. 27 *((u_short *) ptr) = htons(l). /* количество запросов */ 28 ptr += 2 29 *((u_short *) ptr) = 0. /* количество записей о ресурсах посланных в ответ */ 30 ptr += 2 31 *((u_short *) ptr) = 0 • /* количество записей о ресурсах, определяющих полномочия */ 32 ptr += 2. 33 *((u_short *) ptr) = 0. /* количество дополнительных записей о ресурсах */ 34 ptr <-= 2 35 memcpytptr "\e001a\e014root-servers\e003net\e000" 20). 36 ptr += 20 37 one = htons(l) 38 memcpytptr. tone. 2). /* тип запроса = A */ 39 ptr += 2. 40 memcpytptr tone 2). /* класс запроса = 1 (IP addr) */ 41 ptr += 2 42 nbytes = 36 43 udp_write(buf. nbytes). 44 if (verbose) 45 printfC'sent to bytes of dataXen". nbytes). 46 } Инициализация указателя на буфер 20-22 В буфере buf имеется место для 20-байтового заголовка IP, 8-байтового зато* ловка UDP и еще 100 байт для пользовательских данных. Указатель ptr установ- лен на первый байт пользовательских данных. Формирование запроса DNS 23-34 Для понимания деталей устройства дейтаграммы UDP требуется понимание формата сообщения DNS. Эту информацию можно найти в разделе 14.3 [94]. Мы присваиваем полю идентификации значение 1234, сбрасываем флаги, задаем количество запросов — 1, а затем обнуляем количество записей ресурсов (RR, resource records), получаемых в ответ, количество RR, определяющих полномо- чия, и количество дополнительных RR. ПРИМЕЧАНИЕ-------------------------------------------------- В заголовочном файле <arpa/nameser.h> определен тип данных HEADER, кото- рые располагаются в первых 12 байтах заголовка запроса. Мы решили, что в случае простых запросов, которые мы здесь рассматриваем, этими 12 байтами данных мож- но манипулировать столь же легко, не прибегая к использованию специального типа данных.
774 Глава 26. Доступ к канальному уровню 35-41 Затем мы формируем простой запрос, который располагается после заголовка: запрос типа А IP-адреса узла a root-servers net. Это доменное имя занимает 20 байт и состоит из 4 фрагментов: однобайтовая часть а, 12-байтовая часть root- servers, 3-байтовая часть net и корневая часть, длина которой занимает 0 байт. Тип запроса 1 (так называемый запрос типа А), и класс запроса также 1. Запись дейтаграммы UDP 42-45 Это сообщение состоит из 36 байт пользовательских данных (восемь 2-байто- вых полей и 20-байтовое доменное имя). Мы вызываем нашу функцию udp_write для формирования заголовков UDP и IP и последующей записи дейтаграммы UDP в наш символьный сокет. В листинге 26.9 показана наша функция udp_write, которая формирует заго- ловки IP и UDP, а затем записывает дейтаграмму в символьный сокет. Листинг 26.9. Функция udp_write: формирование заголовков UDP и IP и запись дейтаграммы IP в символьный сокет 7/udpcksum/udpwnte с 6 void 7 udp_wnte(char *buf. int userlen) 8 { 9 struct udpiphdr *ui. 10 struct ip *ip 11 /* Заполняем контрольную сумму UDP */ 12 ip = (struct ip *) buf. 13 ui = (struct udpiphdr *) buf: 14 /* добавляем 8 к userlen - длину псевдозаголовка *7 15 ui->ui_len = htons((u_short) (sizeof(struct udphdr) + userlen)); 16 /* затем добавляем 28 - длину дейтаграммы IP */ 17 userlen += sizeof(struct udpiphdr). 18 ui->ui_next = 0. 19 ui->ui_prev = 0. 20 ui->ui_xl = 0. 21 ui->ui_pr = IPPRDTDJJDP. 22 ui->ui_src saddr = ((struct sockaddrin *) local)->sin_addr saddr;- 23 ui->ui_dst s_addr = ((struct sockaddr_in *) dest)->sin_addr saddr. 24 ui->ui_sport = ((struct sockaddr_in *) local)->sin_port. 25 ui->ui_dport = ((struct sockaddr_in *) dest)->sin_port. 26 ui->ui_ulen = ui->ui_len. 21 ui->ui_sum = 0. 28 if (zerosum == 0) { 29 #ifdef notdef /* изменяем ifndef для Solans 2 x. x < 6 */ 30 if ( (ui->ui_sum = in_cksum((u_short *) ui. userlen)) — 0) 31 ui->ui_sum = Oxffff, 32 #else 33 ui->ui_sum = ui->ui_len 34 #endif 35 } 36 /* Заполняем остальную часть заголовка IP */ 37 7* ip_output() вычисляет и записывает контрольную сумму заголовка IP */ 38 ip->ip_v = IPVERSION. 39 ip->ip_hl = sizeof(struct ip) » 2. 40 ip->ip_tos = 0, 41 #ifdef linux
26,6. Анализ поля контрольной суммы UDP 775 42 тр->тр_1еп - htons(userlen) /* сетевой порядок байтов */ 43 #else 44 ip->ip_len = userlen /* порядок байтов узла */ 45 #endif 46 ip->ip_id = 0. /* пусть IP устанавливает зто значение */ 47 ip->ip_off = 0. /* сдвиг, флаги MF и DF */ 48 ip->ip_ttl = fTL_OUT, 49 Sendtolrawfd. buf userlen. О dest. destlen), 50 } Инициализация указателей на заголовки пакетов 11-13 Указатель тр указывает на начало заголовка IP (структуру тр), а указатель in указывает на то же место, но структура udpi phdr является объединением заголов- ков IP и UDP. Обновление значений длины 14-17 Переменная mien — это длина дейтаграммы UDP: количество байтов пользо- вательских данных плюс размер заголовка UDP (8 байт). Переменная userl еп (ко- личество байтов пользовательских данных, которые следуют за заголовком UDP) увеличивается на 28 (20 байт на заголовок IP и 8 байт на заголовок UDP), для того чтобы соответствовать настоящему размеру дейтаграммы IP. Заполнение заголовка UDP и вычисление контрольной суммы UDP 18-35 При вычислении контрольной суммы UDP учитывается не только заголовок и данные UDP, но и поля заголовка IP. Эти дополнительные поля заголовка IP образуют то, что называется псевдозаголовком (pseudoheader). Включение псев- дозаголовка обеспечивает дополнительную проверку того, что если значение контрольной суммы верно, то дейтаграмма была доставлена на правильный узел и с правильным кодом протокола. В указанных строках располагаются операто- ры инициализации полей в IP-заголовке, формирующих псевдозаголовок. Дан- ный фрагмент кода несколько запутан, но его объяснение приводится в разде- ле 23.6 [105]. Конечным результатом является запись контрольной суммы UDP в поле ui_sum, если не установлен флаг zerosum (что соответствует наличию аргу- мента командной строки -0). Если при вычислении контрольной суммы получается 0, вместо него записы- вается значение Oxffff. В обратном коде эти числа совпадают, но протокол UDP устанавливает контрольную сумму в нуль, чтобы обозначить, что она вовсе не была вычислена. Обратите внимание, что в листинге 25.9 мы не проверяем, равно ли значение контрольной суммы нулю: дело в том, что в случае ICMPv4 нулевое значение контрольной суммы не означает ее отсутствия. ПРИМЕЧАНИЕ ----------------------------------------------------- Следует отметить, что в Solaris 2.x, где х<6, в случаях, когда дейтаграммы UDP или сегменты TCP отправляются с символьно! о сокета при установленном параметре IP_HDRINCL, возникает ошибка. Ядро вычисляет контрольную сумму, и мы должны установить поле ui sum равным длине дейтаграммы UDP.
776 Глава 26. Доступ к канальному уровню Заполнение заголовка IP 36-49 Поскольку мы установили параметр сокета IP_HORINCL, нам следует заполнить большую часть полей в заголовке IP. (В разделе 25.3 обсуждается запись в сим- вольный сокет при включенном параметре IP HDRINCL.) Мы присваиваем полю идентификации нуль (ip_id), что указывает IP на необходимость задания значе- ния этого поля. IP также вычисляет контрольную сумму IP, а функция sendto записывает дейтаграмму IP. Следующая функция — это udp_read, показанная в листинге 26.10. Она вызы- вается из кода, представленного в листинге 26.6. Листинг 26.10. Функция udp_read: чтение очередного пакета из устройства захвата пакетов //udpcksum/udpread с 7 struct udpiphdr * 8 udp_read(void) 9 { 10 int len 11 char *ptr 12 struct ether_header *eptr. 13 for (..) { 14 ptr = next_pcap(&len) 15 switch (datalink) { 16 case DLT_NULL /* заголовок обратной петли - 4байта */ 17 return (udp_check(ptr + 4. len - 4)). 18 case DLTJNIOMB 19 eptr = (struct ether_header *) ptr, 20 if (ntohs(eptr->ether_type) '= ETHERTYPE_IP) 21 err_quit("Ethernet type Jfcx not IP" ntohs(eptr->ether_type)): 22 return (udp_check(ptr + 14 len - 14)) 23 case DLT_SLIP /* заголовок SLIP = 24 байта */ 24 return (udp_check(ptr + 24 len - 24)) 25 case DLT_PPP /* заголовок PPP = 24 байта */ 26 return (udp_check(ptr + 24, len - 24)). 27 default 28 err_quit("unsupported datalink (*d)"_ datalink) 29 } 30 } 31 } .4-29 Наша функция next_pcap (см. листинг 26.11) возвращает следующий пакет из устройства захвата пакетов. Поскольку заголовки канального уровня различают- ся в зависимости от фактического типа устройства, мы применяем ветвление в зависимости от значения, возвращаемого функцией pcap datal ink. ПРИМЕЧАНИЕ------------------------------------------------------------------------- Сдвиги на 4, 14 и 24 байта объясняются на рис. 31.9 [105] Сдвиг, равный 24 байтам, показанный для заголовков SLIP и РРР, применяется в BSD/OS 2.1. Несмотря на то что в названии DLT_EN10MB фигурирует обозначение «10МВ», этот тип канального уровня используется для сетей Ethernet, в которых скорость передачи данных равна 100 Мбит/с.
26.6. Анализ поля контрольной суммы UDP 777 Наша функция udp_check (см. листинг 26.12) исследует пакет и проверяет поля в заголовках IP и UDP. В листинге 26.11 показана функция next_pcap, возвращающая следующий па- кет из устройства захвата пакетов. Листинг 26.11. Функция next_pcap: возвращает следующий пакет //udpcksum/pcap с 38 char * 39 next pcapOnt *len) 40 { 41 char *ptr. 42 struct pcap_pkthdr hdr. 43 /* продолжаем следить, пока пакет не будет готов */ 44 while ( (ptr = (char *) pcap_next(pd &hdr)) == NULL) : 45 *len = hdr caplen, /* длина захваченного пакета */ 46 return (ptr) 47 } 43-44 Мы вызываем библиотечную функцию pcap_next, возвращающую следующий пакет. Указатель на пакет является возвращаемым значением данной функции, а второй аргумент указывает на структуру pcap_pkthdr, которая тоже возвращает- ся заполненной: struct pcap_pkthdr { struct timeval ts bpf_u_int32 caplen bpf_u_int32 len. } /* временная метка */ /* длина захваченного фрагмента */ /* полная длина пакета находящегося в канале */ Временная отметка относится к тому моменту, когда пакет был считан уст- ройством захвата пакетов, в противоположность моменту фактической передачи пакета процессу, которая может произойти чуть позже Переменная caplen содер- жит длину захваченных данных (вспомним, что в листинге 26.2 нашей перемен- ной shaplen было присвоено значение 200 и она являлась вторым аргументом функции pcap open l i ve в листинге 26.5). Назначение устройства захвата пакетов состоит в захвате заголовков, а не всего содержимого каждого пакета. Перемен- ная 1 еп — это полная длина пакета, находящегося в канале. Значение саpl еп будет всегда меньше или равно значению 1 еп. 45-46 Перехваченная часть пакета возвращается через указатель (аргумент функции), и возвращаемым значением функции является указатель на пакет. Следует пом- нить, что указатель на пакет указывает фактически на заголовок канального уров- ня, который представляет собой 14-байтовый заголовок Ethernet в случае кадра Ethernet или 4-байтовый псевдоканальный (pseudo-link) заголовок в случае за- кольцовки на себя. Если мы посмотрим на библиотечную реализацию функции pcapjiext, мы уви- дим, что между различными функциями существует некоторое «разделение тру- да», схематически изображенное на рис. 26.5. Наше приложение вызывает функ- ции рсар_, среди которых есть как зависящие, так и не зависящие от устройства захвата пакетов. Например, мы показываем, что реализация BPF вызывает функ- цию read, в то время как реализация DLPI вызывает функцию getmsg, а реализа- ция Linux вызывает resvfгош.
778 Глава 26 Доступ к канальному уровню udp_read next_pcap I Приложение pcap__next Зависит от устройства pcap_dispatch Не зависит от устройства _ Библиотека захвата пакетов libpcad Процвсс Ядро read getmsg recvfrim (BPF) (DLPI) (Linux) Рис. 26.5. Организация вызовов функций для чтения из библиотеки захвата пакетов Наша функция udp_check проверяет различные поля в заголовках IP и UDP Она показана в листинге 26 12 Эту проверку необходимо выполнить, так как при получении пакета от устройства захвата пакетов уровень IP не замечает этого пакета Для символьного сокета это не так 4 61 Длина пакета должна включать хотя бы заголовки IP и UDP Версия IP прове- ряется вместе с длиной и контрольной суммой заголовка IP Если поле протоко- ла указывает на дейтаграмму UDP, функция возвращает указатель на объеди- ненный заголовок IP/UDP В противном случае программа завершается, так как фильтр захвата пакетов, заданный при вызове функции pcap_setf i 1 ter в листин- ге 26 5, не должен возвращать пакеты никакого другого типа Листинг 26.12. Функция udp_check проверка полей в заголовках IP и UDP //udpcksum/udpread с 38 struct udpiphdr * 39 udp_check(char *ptr int len) 40 { 41 int hlen 42 struct ip *ip 43 struct udpiphdr *ui 44 if (len < sizeof(struct ip) + sizeoftstruct udphdr)) 45 err_quit( len = Xd len) 46 /* минимальная проверка заголовка IP */ 47 ip = (struct ip *) ptr 48 if (ip >ip_v i- IPVERSION) 49 err_quit( ip_v = %d ip >ip_v) 50 hlen = ip >ip_hl « 2 51 if (hlen < sizeoftstruct ip)) 52 err_quit( ip_hl = Xd ip >ip_hl) 53 if (len < hlen + sizeoftstruct udphdr))
26 6 Анализ поля контрольной суммы UDP 779 54 err_quit( len = *d hlen = *d len hlen) 55 if ( (ip >ip_sum = in_cksum((u_short *) ip hlen)) - 0) 56 err_quit( ip checksum error ) 57 if (ip >ip_p == IPPROTOJJDP) { 58 ui = (struct udpiphdr *) ip 59 return (ui) 60 } else 61 err_quit( not a UDP packet ) 62 } Функция cleanup, показанная в листинге 26 13, вызывается из функции main непосредственно перед тем, как программа завершается, а также вызывается в качестве обработчика сигнала в случае, если пользователь прерывает выполне- ние программы (см листинг 26 4) Листинг 26.13. Функция cleanup //udpcksum/cleanup с 2 void 3 cleanup!int signo) 4 { 5 struct pcap_stat stat 6 fflush(stdout) 7 putc( \en stdout) 8 if (verbose) { 9 if (pcap_stats(pd Sstat) < 0) 10 err_quit( pcap_stats £s\en pcap geterr(pd)) 11 printf! W packets received by filter\en stat ps_nec^>J 12 printf! Xd packets dropped by kernel\en stat ps drop). 13 } 14 exit(0) 15 } Получение и вывод статистики по захвату пакетов -13 Функция pcap_stats получает статистику захвата пакетов общее количество полученных фильтром пакетов и количество пакетов, переданных ядру Пример Сначала мы запустим нашу программу с аргументом командной строки 0 и убе- димся, что сервер имен отвечает на приходящие дейтаграммы, не содержащие контрольной суммы Мы также задаем флаг -v. solans # udpcksum 0 v conmx com domain device = leO local net = 206 62 226 32 netmask = 255 255 255 224 cmd = udp and src host 198 69 10 4 and src port 53 datalink = 1 sent 36 bytes of data UDP checksums on recevied UDP checksum = ad39 2 pacKets received by filter 0 packets dropped by kernel
780 Глава 26 Доступ к канальному уровню Затем мы запускаем нашу программу на сервере имен, в котором отключен подсчет контрольных сумм solans # udpcksum v gw pacbell com domain device = leO localnet = 206 62 226 32 netmask = 255 255 255 224 cmd = udp and src host 192 150 170 2 and src port 53 datalink = 1 sent 36 bytes of data UDP checksums off recevied UDP checksum = 0 1 packets received by filter 0 packets dropped by kernel 26.7. Резюме Символьные сокеты предоставляют возможность записывать и считывать IP-дей- таграммы, которые могут быть не поняты ядром, а доступ к канальному уровню позволяет считывать и записывать кадры канального уровня любых типов (не только дейтаграммы IP) Программа tcpdump — это, вероятно, наиболее широко используемая программа, предоставляющая непосредственный доступ к каналь- ному уровню В различных операционных системах применяются различные способы до- ступа к канальному уровню Мы рассмотрели пакетный фильтр Беркли, DLPI SVR4 и пакетные сокеты Linux (SOCK_PACKET) Но у нас имеется возможность, не вникая в различия перечисленных способов, использовать находящуюся в сво- бодном доступе переносимую библиотеку захвата пакетов libcap Упражнения 1 Каково назначение флага canjump в листинге 26 7? 2 При работе программы udpcksum наиболее распространенным сообщением об ошибке является сообщение о недоступности порта ICMP (в пункте назначе- ния не работает сервер имен) или недоступности узла ICMP В обоих случаях нам не нужно ждать истечения времени ожидания, заданного функцией udp read в листинге 26 6, так как сообщение о подобной ошибке фактически является ответом на наш запрос DNS Модифицируйте программу таким образом, что- бы она перехватывала эти ошибки ICMP
ГЛАВА 27 Альтернативное устройство клиента и сервера 27.1. Введение При написании сервера под Unix мы можем выбирать из следующих вариантов управления процессом Наш первый сервер, показанный в листинге 1 5, был последовательным (iterative), но количество сценариев, для которых этот вариант является пред- почтительным, весьма ограничено, поскольку последовательный сервер не может начать обработку очередного клиентского запроса, не закончив полно- стью обработку текущего запроса W В листинге 5 1 показан первый в данной книге параллельный (concurrent) сер- вер, который для обработки каждого клиентского запроса порождал дочерний процесс с помощью функции fork Традиционно большинство серверов, рабо- тающих под Unix, попадают в эту категорию В разделе 6 8 мы разработали другую версию сервера TCP, в котором имеется только один процесс, обрабатывающий любое количество клиентских запро- сов с помощью функции sel ect В листинге 23 2 мы модифицировали параллельный сервер, создав для каж- дого клиента по одному потоку вместо одного процесса В этой главе мы рассмотрим два других способа модификации устройства па- раллельного сервера Предварительное создание дочерних процессов (preforking) В этом случае при запуске сервера выполняется функция fork, которая создает определенное количество (пул) дочерних процессов Обработкой очередного клиентского запроса занимается процесс, взятый из этого набора Предварительное создание потоков (prethreading) При запуске сервера созда- ется некоторое количество (пул) потоков, и для обработки каждого клиента используется поток из данного набора В данной главе мы будем рассматривать множество вопросов, связанных с предварительным созданием потоков и процессов Например, что произойдет, если в пуле окажется недостаточное количество процессов или потоков? А если их будет слишком много? Как родительский и дочерние процессы (или потоки) синхоонизиоуют свои действия?
782 Глава 27. Альтернативное устройство клиента и сервера Обычно написать клиент легче, чем сервер, за счет простоты управления про- цессом клиента. Тем не менее мы уже исследовали различные способы написа- ния простого эхо-клиента, которые вкратце изложены в разделе 27.2. В этой главе мы рассматриваем девять различных способов устройства серве- ра и взаимодействие каждого из этих серверов с одним и тем же клиентом. Кли- ент-серверный сценарий типичен для Web: клиент посылает небольшой по объ- ему запрос, а сервер отвечает ему, отсылая соответствующие запросу данные. Некоторые из этих серверов мы уже достаточно подробно обсуждали (например, параллельный сервер, вызывающий функцию fork для обработки каждого кли- ентского запроса), в то время как предварительное создание процессов и потоков являются новыми для нас концепциями, которые и будут подробно рассмотрены в этой главе. Мы запускали различные экземпляры клиента с каждым сервером, измеряя время, которое центральный процессор тратит на обслуживание определенного количества клиентских запросов. Чтобы информация об этом не оказалась рассе- янной по всей главе, мы свели все полученные результаты в табл. 27.1, на кото- рую в этой главе будем неоднократно ссылаться. Следует отметить, что значения времени, указанные в этой таблице, соответствуют времени центрального про- цессора, затраченному только на управление процессом, так как из фактического значения времени центрального процессора мы вычитаем время, которое тратит на выполнение того же задания последовательный сервер, не имеющий наклад- ных расходов, связанных с управлением процессом. Иными словами, нулевой точкой отсчета в данной таблице для нас является время, затраченное последова- тельным сервером. Для большей наглядности мы включили в таблицу строку для последовательного сервера, с нулевыми значениями времени. В этой главе тер- мином время центрального процессора на управление процессом (process control CPU time) мы обозначаем разность между фактическим значением времени цент- рального процессора и временем, затраченным последовательным сервером, для каждой конкретной системы. Таблица 27.1. Сравнительные значения времени, затраченного каждым из обсуждаемых в данной главе сервером Описание сервера Время центрального процессора на упрааление процессом: Solaris DUnix BSD/OS 0 Последовательный (точка отсчета; затраты на управление процессом отсутствуют) 0,0 0,0 0,0 1 Параллельный сервер, один вызов функции fork для обработки одного клиента 504,2 168,9 29,6 2 Предварительное создание дочерних процессов, каждый из которых вызывает функцию accept 6,2 1,8 3 Предварительное создание дочерних процессов с блокировкой для защиты accept 25,2 10,0 2,7 4 Предварительное создание дочерних процессов с использованием взаимного исключения для защиты accept 21,5 5 Предварительное создание дочерних процессов, родительский процесс передает дочернему 36,7 10,9 6,1 дескриптор сокета
27.1. Введение 783 Описание сервера Время центрального процессора на управление процессом: Solaris DUnix BSD/OS 6 Параллельный сервер, создание одного потока на каждый клиентский запрос 18,7 4,7 7 Предварительное создание потоков с использованием взаимного исключения для защиты accept 8,6 3,5 8 Предварительное создание потоков, главный поток вызывает accept 14,5 5,0 Мы запускали различные серверы на трех узлах: sunos5 (Solaris 2.5.1), alpha (Digital Unix 4.0b) и bsdi (BSD/OS 3.0). Обратите внимание, что не все серверы могут работать на всех трех узлах. Например, серверы, указанные в строке 2, не могут работать на большинстве узлов SVR4 (об этом более подробно рассказано в разделе 27.7), а серверы, использующие потоки, не могут работать под BSD/OS (так как ядро не поддерживает потоки). Все три узла имеют различную архитек- туру, так что нет смысла сравнивать значения времени между ними. Назначение данной таблицы в том, чтобы продемонстрировать разницу между различными серверами на одном и том же узле, а не в сравнении различных аппаратных архи- тектур и операционных систем. Например, из строки с номером 7 видно, что сервер, на котором предусмотрено предварительное создание потоков с блокированием взаимного исключения для защиты accept, является самым быстродействующим как для Solaris, так и для Digital Unix, а из строки 2 видно, что под BSD/OS са- мым быстродействующим является сервер с предварительным созданием дочер- них процессов, каждый из которых вызывает функцию accept. Таблица 27.2. Влияние дополнительных дочерних процессов на время централь- ного процессора на узле сервера Количе- ство дочер- них процес- сов или потоков Время центрального процессора на управление процессом (точкой отсчета является время, затраченное последовательным сервером) Предварительное со- здание дочерних процессов, отсут- ствие блокировки (строка 2 табл. 27.1) Предварительное создание до- черних процессов с блокировкой файла для защиты accept (строка 3 табл. 27.1) Предварительное создание потоков с использованием взаимного исклю- чения для защиты accept (строка 7 табл. 27.1) DUnix BSD/OS Solaris DUnix BSD/OS Solaris 15 30 45 60 75 90 105 120 6,2 7,8 8,9 10,1 11,4 12,6 13,2 15,7 1,8 3,5 5,5 6,9 8,7 10,9 12,0 13,5 25,2 27,3 29,7 34,2 39,8 130,1 10,0 11,2 13,1 14,3 16,0 17,6 19,7 22,0 2,7 5,6 8,7 11,2 13,7 15,5 17,6 19,2 8,6 10,0 19.6 28,6 29,3 28,6 30,4 29,4
784 Глава 27. Альтернативное устройство клиента и сервера Все приведенные выше значения времени были получены путем запуска кли- ента, показанного в листинге 27.1, на двух различных узлах в той же подсети, что и сервер. Во всех тестах оба клиента порождали пять дочерних процессов для создания пяти одновременных соединений с сервером; таким образом, максималь- ное количество одновременных соединений с сервером было равно 10. Каждый клиент запрашивал 4000 байт данных от сервера по каждому соединению. В слу- чае, когда тест подразумевает предварительное создание дочерних процессов или потоков при запуске сервера, их количество равно 15. Как мы уже отметили, устройством некоторых серверов подразумевается со- здание накопителя дочерних процессов или потоков. Другим важным вопросом является эффект слишком большого количества дочерних процессов или пото- ков. В табл. 27.2 приводятся результаты проведенных тестов. Мы обсудим их в последующих разделах данной главы. Другой темой обсуждения является распределение клиентских запросов по потокам или дочерним процессам, находящимся в накопителе. В табл. 27.3 пока- заны варианты этого распределения, которые также будут обсуждаться в соот- ветствующих разделах. Таблица 27.3. Количество клиентов, обслуживаемых каждым из 15 дочерних процессов или потоков Номер Количество обслуживаемых клиентов дочерне- го про- цесса или потока Предваритель- ное создание дочерних про цессов,отсут- ствие блоки- ровки (строка 2 табл. 27.1) Предварительное созда- ние дочерних процессов с блокировкой файла для защиты accept (строка 3 табл. 27.1) Предварительное созда- ние дочерних процессов, передача дескриптора (строка 5 табл. 27.1) Предваритель- ное создание по- токов с блоки- ровкой взаимно- го исключения для защиты accept (строка 7 табл. 27.1) DUnix BSD/OS Solans DUnix BSD/OS Solaris DUnix BSD/OS Solaris DUnix 0 318 333 347 335 335 1006 718 530 333 335 1 343 340 328 334 335 960 647 529 323 337 2 326 335 332 334 332 720 589 509 333 338 3 317 335 335 333 333 582 554 502 328 311 4 309 332 338 333 331 485 526 501 329 345 5 344 331 340 335 335 457 501 495 322 332 6 340 333 335 330 332 385 447 488 324 355 7 337 333 343 334 333 250 389 484 360 322 8 340 332 324 333 334 105 314 460 341 336 9 309 331 315 333 336 32 208 443 348 337 10 356 334 326 333 331 14 62 59 358 334 11 354 333 340 334 338 9 18 0 331 340 12 356 334 330 333 333 4 14 0 321 317 13 302 332 331 333 331 1 12 0 329 326 14 349 332 336 333 331 0 1 0 320 335 5000 5000 5000 5000 5000 5000 5000 5000 5000 5000
27.3. Тестовый клиент ТОР 785 27.2. Альтернативы для клиента TCP Мы уже обсуждали различные способы устройства клиентов, но стоит тем не менее еще раз обратить внимание на относительные достоинства и недостатки этих спо- собов. 1. В листинге 5.4 показан основной способ устройства клиента TCP. С этой про- граммой были связаны две проблемы. Во-первых, когда она блокируется в ожи- дании ввода пользователя, она не замечает происходящих в сети событий, на- пример отключения собеседника от соединения. Во-вторых, она действует в режиме остановки и ожидания, что неэффективно в случае пакетной обра- ботки. 2. Листинг 6.1 содержит следующую, модифицированную версию клиента. С по- мощью функции sei ect клиент получает информацию о событиях в сети во время ожидания ввода пользователя. Однако проблема этой версии заключа- ется в том, что программа не способна корректно работать в пакетном режиме. В листинге 6.2 эта проблема решается путем применения функции shutdown. 3. С листинга 15.1 начинается рассмотрение клиентов, использующих неблоки- руемый ввод-вывод. 4. Первым из рассмотренных нами клиентов, вышедшим за пределы ограниче- ний, связанных с наличием единственного процесса или потока для обслужи- вания всех запросов, является клиент, изображенный на рис. 15.4. В этом слу- чае использовалась функция fork, и один процесс обрабатывал передачу данных от клиента к серверу, а другой — передачу данных в обратном направлении. 5. В листинге 23.1 используются два потока вместо двух процессов. В конце раздела 15.2 мы резюмируем различия между перечисленными вер- сиями. Как мы отметили, хотя версия с неблокируемым вводом-выводом являет- ся самой быстродействующей, ее код слишком сложен, а применение двух пото- ков или двух процессов упрощает код. 27.3. Тестовый клиент TCP В листинге 27.1* показан клиент, который будет использоваться для тестирова- ния всех вариаций нашего сервера. Листинг 27.1. Код клиента TCP для проверки различных версий сервера //server/client с 1 include "unp h" 2 #define MAXN 16384 /* максимальное количество байтов которые могут быть запрошены клиентом от сервера */ 3 int 4 main(int argc. char **argv) { продолжение & 1 Все исходные коды программ, опубликованное,в ЭТО# КНИТ?,,ДЫ ЦОЖ)е%е Найти по адресу http// www piter com/download
786 Глава 27. Альтернативное устройство клиента и сервера Листинг 27.1 (продолжение) 6 int 1. j fd nchildren. nloops. nbytes: 7 pid_t pid. 8 ssize_t n. 9 char request[MAXLINE]. reply[MAXN] 10 if (argc '= 6) 11 err_quit("usage client <hostname or IPaddr> <port> <#children> 12 "<#loops/child> <#bytes/request>“). 13 nchildren = atoi(argv[3J) 14 nloops = atoi(argv[4]). 15 nbytes = atoi(argv[51). 16 snprintf(request, sizeof(request). "Xd\en" nbytes). /* в конце символ новой стооки */ 17 for (i = 0. i < nchildren. i++) { 18 if ( (pid = ForkO) == 0) { /* дочерний процесс */ 19 for (j •= 0. j < nloops. j++) { 20 fd “ Tcp_connect(argv[l], argv[2]), 21 Write(fd request strlen(request)). 22 if ( (n = Readn(fd. reply, nbytes)) nbytes) 23 err_quit("server returned И bytes" n) 24 Close(fd). /* состояние TIME WAIT на стороне клиента, а не сервера */ 25 } 26 printf("child Й done\en" i). 27 exit(0) 28 } 29 /* родительский процесс снова вызывает функцию fork */ 30 } ... 31 while (wait(NULL) > 0) /* теперь родитель ждет завершения все* Дочерних прбцессОв */ 32 33 if (errno |= ECHILD) 34 err_sys("wait error''): 35 exit(0). 36 } )-12 Каждый раз при запуске клиента мы задаем имя узла или IP-адрес сервера, порт сервера, количество дочерних процессов, порождаемых функцией fork (что по- зволяет нам инициировать несколько соединений с одним и тем же сервером од- новременно), количество запросов, которое каждый дочерний процесс должен посылать серверу, и количество байтов, отправляемых сервером в ответ на каж- дый запрос. -30 Родительский процесс вызывает функцию fork для порождения каждого до- чернего процесса, и каждый дочерний процесс устанавливает указанное количе- ство соединений с сервером. По каждому соединению дочерний процесс посыла- ет запрос, задавая количество байтов, которое должен вернуть сервер, а затем дочерний процесс считывает это количество данных с сервера. Родительский про- цесс просто ждет (wait) завершения выполнения всех дочерних процессов. Обра- тите внимание, что клиент закрывает каждое соединение TCP, таким образом состояние TCP TIME_WAIT имеет место на стороне клиента, а не на стороне сервера. Это отличает наше клиент-серверное соединение от обычного соедине- ния HTTP. При тестировнии различных серверов из этой главы мы запускали клиент сле- дующим образом:
27.5. Параллельный сервер TCP: один процесс для каждого клиента 787 X client 206.62.226.36 8888 5 500 4000 Таким образом создается 2500 соединений TCP с сервером: по 500 соедине- ний от каждого из 5 дочерних процессов. По каждому соединению от клиента к серверу посылается 5 байт, а от сервера клиенту передается 4000 байт ("4000\п"). Мы запускаем клиент на двух различных узлах, соединяясь с одним и тем же сервером, что дает в сумме 5000 соединений TCP, причем максимальное количе- ство одновременных соединений с сервером в любой момент времени равно 10. ПРИМЕЧАНИЕ ------------------------------------------------------------ Для проверки различных web-серверов существуют изощренные контрольные тесты Один из них называется WebStone Информация о нем находится в свободном досту- пе по адресу http //ww mindcraft Для общего сравнения различных альтернатив- ных устройств сервера, которые мы рассматриваем в этой главе, нам не нужны столь сложные тесты. Теперь мы представим девять различных вариантов устройства сервера. 27.4. Последовательный сервер TCP Последовательный сервер TCP полностью обрабатывает запрос каждого клиен- та, прежде чем перейти к следующему клиенту. Последовательные серверы ред- ко используются, но один из них, простой сервер для определения времени и даты, мы показали в листинге 1.5. Тем не менее у нас имеется область, в которой желательно применение имен- но последовательного сервера — это сравнение характеристик других серверов. Если мы запустим клиент следующим образом: X client 206.62.226.36 8888 1 5000 4000 и соединимся с последовательным сервером, то получим такое же количество соединений TCP (5000) и такое же количество данных, передаваемых по одному соединению. Но поскольку сервер является последовательным, на нем не осуще- ствляется никакого управления процессами. Это дает нам возможность получить базовое значение времени, затрачиваемого центральным процессором на обра- ботку указанного количества запросов, которое потом мы можем вычесть из ре- зультатов измерений для других серверов. С точки зрения управления процесса- ми последовательный сервер является самым быстрым, поскольку он вовсе не занимается этим управлением. Взяв последовательный сервер за точку отсчета, мы можем сравнивать результаты измерений быстродействия различных других серверов, показанные в табл. 27.1. Мы не приводим код для последовательного сервера, так как он представляет собой тривиальную модификацию параллельного сервера, показанного в следу- ющем разделе. 27.5. Параллельный сервер TCP: один дочерний процесс для каждого клиента Традиционно параллельный сервер TCP вызывает функцию fork для порожде- ния нового дочернего процесса, который будет выполнять обработку очередного тепиентотсого зяппос.я Это позволяет ср пир n v обпябятыиять нрокопько зяппосои
788 Глава 27 Альтернативное устройство клиента и сервера одновременно, выделяя по одному дочернему процессу для каждого клиента Единственным ограничением на количество одновременно обрабатываемых кли- ентских запросов является ограничение операционной системы на количество дочерних процессов, допустимое для пользователя, в сеансе которого работает сервер Листинг 5 9 содержит пример параллельного сервера, и большинство сер- веров TCP написаны в том же духе Проблема с параллельными серверами заключается в количестве времени, которое тратит центральный процессор на выполнение функции fork для порож- дения нового дочернего процесса для каждого клиента Еще десять лет назад, в конце 80-х, когда наиболее загруженные серверы обрабатывали сотни или ты- сячи клиентов за день, это было приемлемо Но расширение Сети изменило тре- бования Теперь загруженными считаются серверы, обрабатывающие миллионы соединений TCP в день Сказанное относится лишь к одиночным узлам, но наи- более загруженные сайты используют несколько узлов, распределяя нагрузку между ними (в разделе 14 2 [95] рассказывается об общепринятом способе рас- пределения этой нагрузки, называемом циклическим обслуживанием DNS — DNS round robin) В последующих разделах описаны различные способы, позволяю- щие избежать вызова функции fork для каждого клиентского запроса, но тем не менее параллельные серверы остаются широко распространенными В листинге 27 2 показана функция mai п для нашего параллельного сервера ТСР Листинг 27.2. Функция main для параллельного сервера TCP //server/servOl с 1 #include unp h 2 int 3 main(int argc char **argv) 4 { 5 int listenfd connfd 6 pid_t childpid 7 void sig chld(int) sigint(lnt). web childCinU 8 socklen_t clilen addrlen 9 struct sockaddr *cliaddr 10 if (argc == 2) 11 listenfd = Tcpjisten (NULL argv[l] &addrlen) 12 else if (argc == 3) 13 listenfd = Tcp_listen(argv[l] argv[2] &addrlen) 14 else 15 err_quit( usage servOl [ <host> ] <port#> ) 16 cliaddr = Malloc(addrlen) 17 Signal(SIGCHLD sig_chld) 18 Signal(SIGINT sig_int) 19 for ( ) { 20 clilen = addrlen 21 if ( (connfd = accept(listenfd cliaddr &clilen)) < 0) 22 if (errno == EINTR) 23 continue /* назад к for() */ 24 else 25 err_sys( accept error ) 26 } 27 i f ( frhilrlmrl == FnrkfH == (H I /* nnupnnuu nnciicrr */
27 5 Параллельный сервер TCP один процесс для каждого 789 28 Closed istenfd) /* закрываем прослушиваемым сокет */ 29 web_chiId(connfd) /* обрабатываем запрос */ 30 exit(0) 31 } 32 Close(connfd) /* родительским прцесс закрывает присоединенный сокет */ 33 } 34 } Эта функция аналогична функции, показанной в листинге 5 9 она вызывает функцию fork для каждого клиентского соединения и обрабатывает сигналы SIGCHLD, приходящие от закончивших свое выполнение дочерних процессов Тем не менее мы сделали эту функцию не зависящей от протокола за счет вызова функ- ции tcp_listen Мы не показываем обработчик сигнала sig_chld он совпадает с показанным в листинге 5 8, но только без функции printf Мы также перехватываем сигнал SIGINT, который генерируется при вводе сим- вола прерывания Мы вводим этот символ после завершения работы клиента, чтобы было выведено время, потраченное центральным процессором на выпол- нение данной программы В листинге 27 3 показан обработчик сигнала Это при- мер обработчика сигнала, который никогда не возвращает управление Листинг 27.3. Обработчик сигнала SIGINT //server/servOl с 35 void 36 sig_int(int signo) 37 { 38 void pr_cpu_time(void) 39 pr_cpu_time() 40 exit(0) 41 } В листинге 27 4 показана функция pr_cpu_time, вызываемая из обработчика сигнала Листинг 27.4. Функция pr_cpu_time вывод полного времени центрального процессора //server/pr_cpu_time с 1 #include unp h 2 #include <sys/resource h> 3 #ifndef HAVE__GETRUSAGE_PROTO 4 int getrusagednt struct rusage *), 5 #endif 6 void 7 pr_cpu_time(void) 8 { 9 double user sys 10 struct rusage myusage childusage 11 if (getrusage(RUSAGE_SELF toyusage) < 0) 12 err_sys( getrusage error ) 13 if (getrusage(RUSAGE_CHILDREN &childusage) < Q) 14 err_sys( getrusage error ) 15 user = (double) myusage ru_utime tv_sec + 16 myusage ruutime tv_usec / 1000000 0 „„одолжение #
790 Глава 27. Альтернативное устройство клиента и сервера Листинг 27.4 (продолжение) 17 user += (double) childusage.ru_utime.tv_sec + 18 childusage ru_utime tv_usec / 1000000 0. 19 sys = (double) myusage ru_stime tv_sec + 20 myusage.ru_stime tv_usec / 1000000.0. 21 sys += (double) childusage.ru_stime tv_sec + 22 childusage.ru_stime tv_usec I 1000000 0: 23 prnTtf("\enuser time = Яд. sys time = Яд\еп". user. sys). 24 } Функция getrusage вызывается дважды: она позволяет получить данные об использовании ресурсов вызывающим процессом (RUSAGE_SELF) и всеми его до- черними процессами, которые завершили свое выполнение (RUSAGE_CHILDREN). Выводится время, затраченное центральным процессором на выполнение пользо- вательского процесса (общее пользовательское время, total user time), и время, которое центральный процессор затратил внутри ядра на выполнение задач, за- данных вызывающим процессом (общее системное время, total system time). Возвращаясь к листингу 27.2, мы видим, что функция web child вызывается для обработки каждого клиентского запроса. Эта функция показана в листин- ге 27.5. Листинг 27.5. Функция web_chiid: обработка каждого клиентского запроса 7/server/web_child с 1 #include "unp h” 2 #define MAXN 16384 /* максимальное количество байтов, которое клиент может запросить */ 3 void 4 web_child(int sockfd) 5 { 6 int ntowrite. 7 ssize_t nread. 8 char 1ine[MAXLINE], resultfMAXN]. 9 for (..) { 10 if ( (nread = Readline(sockfd. line. MAXLINE)) == 0) 11 return. /* соединение закрыто другим концом */ 12 /* line задает, сколько байтов следует отправлять обратно */ 13 ntowrite = atol(line). 14 if ( (ntowrite <= 0) || (ntowrite > MAXN)) 15 err_quit("client request for Xd bytes", ntowrite): 16 Writen(sockfd, result, ntowrite). 17 1 18 } Установив соединение с сервером, клиент записывает одну строку, задающую количество байтов, которое сервер должен вернуть. Это отчасти похоже на HTTP: клиент отправляет небольшой запрос, а сервер в ответ отправляет требуемую информацию (часто это файл HTML или изображение GIF). В случае HTTP сер- вер обычно закрывает соединение после отправки клиенту затребованных дан- ных, хотя более новые версии используют постоянные соединения (persistent connection), оставляя соединения TCP откоытыми лля дополнительных клиент-
27.6. Сервер TCP без блокировки для вызова accept 791 ских запросов. В нашей функции web chi 1 d сервер допускает дополнительные за- просы от клиента, но, как мы видели в листинге 24.1, клиент посылает серверу только по одному запросу на каждое соединение, а по получении ответа от серве- ра это соединение закрывается. В строке с номером 1 табл. 27.1 показаны результаты измерения времени, затра- ченного параллельным сервером. При сравнении со следующими строками этой таблицы видно, что параллельный сервер тратит больше процессорного времени, чем все другие типы серверов — то, что мы и ожидали при вызове функции fork. ПРИМЕЧАНИЕ---------------------------------------------------------- Один из способов устройства сервера, который мы не рассматриваем в этой главе, — это сервер, инициируемый демоном inetd (см. раздел 12.5). С точки зрения управле- ния процессами такой сервер подразумевает использование функций fork и ехес, так что затраты времени центрального процессора будут еще больше, чем показанные в стро- ке 1 для параллельного сервера. 27.6. Сервер TCP с предварительным порождением процессов без блокировки для вызова accept В первом из рассматриваемых нами «усовершенствованных» серверов использу- ется технология, называемая предварительным созданием процессов (preforking). Вместо того чтобы вызывать функцию fork каждый раз при поступлении очеред- ного запроса от клиента, сервер создает при запуске некоторое количество дочер- них процессов, и впоследствии созданные они обслуживают клиентские запросы по мере установления соединений с клиентами. На рис. 27.1 показан сценарий, при котором родитель предварительно создал N дочерних процессов, и в настоя- щий момент имеется два соединения с клиентами. Рис. 27.1. Предварительное создание дочерних процессов сервером
792 Глава 27. Альтернативное устройство клиента и сервера Преимущество этой технологии заключается в том, что обслуживание нового клиента не требует вызова функции fork родительским процессом, тем самым стоимость этого обслуживания понижается. Недостатком же является необходи- мость угадать, сколько дочерних процессов нужно создать при запуске. Если в некоторый момент времени количество имеющихся дочерних процессов будет равно количеству обслуживаемых клиентов, то дополнительные клиентские за- просы будут игнорироваться до того момента, когда освободится какой-либо до- черний процесс. Но, как сказано в разделе 4.5, клиентские запросы в такой ситу- ации игнорируются не полностью. Для каждого из этих дополнительных клиентов ядро выполнит трехэтапное рукопожатие (при этом общее количество соедине- ний не может превышать значения аргумента backlog функции 1 isten), и при вы- зове функции accept установленные соединения будут переданы серверу. При этом, однако, приложение-клиент может заметить некоторое ухудшение в скоро- сти получения ответа, так как, хотя функция connect может быть выполнена сра- зу же, запрос может не поступать на обработку еще некоторое время. За счет некоторого дополнительного усложнения кода всегда можно добить- ся того, что сервер справится со всеми клиентскими запросами. От родительско- го процесса требуется постоянно отслеживать количество свободных дочерних процессов, и если это количество падает ниже некоторого минимального преде- ла, родитель должен вызвать функцию fork и создать недостающее количество дочерних процессов Аналогично, если количество свободных дочерних процес- сов превосходит некоторую максимальную величину, некоторые из этих процес- сов могут быть завершены родителем, так как, согласно табл. 27.2, излишнее количество свободных дочерних процессов также отрицательно влияет на произ- водительность. Но прежде чем углубляться в детали, исследуем основную структуру этого типа сервера. В листинге 27.6 показана функция main для первой версии нашего сервера с предварительным порождением дочерних процессов. Листинг 27.6. Функция main сервера с предварительным порождением дочерних процессов И server/serv02 с 1 include "unp h" 2 static int nchildren 3 static pid t *pids 4 int 5 main(int argc char **argv) 6 { 7 int listenfd i. 8 socklen_t addrlen. 9 void sig_int(int). 10 pid_t child_make(int int int) 11 if (argc == 3) 12 listenfd = Tcp_listen(NULL argvfl] &addrlen) 13 else if (argc == 4) 14 listenfd = Tcp_listen(argv[l] argv[2], &addrlen) 15 else 16 err_quit("usage serv02 [ <host> ] <port#> <#children>"). 17 nchildren = atoi(argv[argc - 1])
27.6. Сервер TCP без блокировки для вызова accept 793 18 pids = Calloc(nchildren sizeof(pid_t)) 19 for (i=0 i < nchildren. i++) 20 pids[i] = childmaked listenfd. addrlen) /* возвращение родительского процесса */ 21 Signal(SIGINT sig_int) 22 for (. ) 23 paused. /* дочерние процессы завершились */ 24 } 11-18 Дополнительный аргумент командной строки указывает, сколько требуется создать дочерних процессов. В памяти выделяется место для размещения массива, в который записываются идентификаторы дочерних процессов, используемые функцией main при окончании работы программы для завершения этих про- цессов. 19-20 Каждый дочерний процесс создается функцией child_make, которую мы пока- зываем в листинге 27.8. Код обработчика сигнала SIGINT, представленный в листинге 27.7, отличается от кода, приведенного в листинге 27.3. Листинг 27.7. Обработчик сигнала SIGINT //server/serv02 с 25 void 26 sig_int(int signo) 27 { 28 int i 29 void pr_cpu_time(void) 30 /* завершаем все дочерние процессы */ 31 for (i = 0 i < nchildren i++) 32 kill(pids[i] SIGTERM). 33 while (wait(NULL) > 0) /* ждем завершения всех дочерних процессов */ 34 35 if (errno '= ECHILD) 36 err_sys("wait error") 37 pr_cpu_time() 38 exit(0) 39 } 30-34 Функция getrusage сообщает об использовании ресурсов всеми дочерними про- цессами, завершившими свое выполнение, поэтому мы должны завершить все до- черние процессы к моменту вызова функции pr_cpu_time. Для этого дочерним процессам посылается сигнал SIGTERM, после чего мы вызываем функцию wait и ждем завершения выполнения дочерних процессов. В листинге 27.8 показана функция child_make, вызываемая из функции main для порождения очередного дочернего процесса. Листинг 27.8. Функция child_make: создание очередного дочернего процесса //server/child02 с 1 #include "unp h" 2 pid_t 3 child makednt i int listenfd int addrlen) . л продолжение
794 Глава 27. Альтернативное устройство клиента и сервера Листинг 27.8(продолжение) 4 { 5 pid_t pid. 6 void child_main(int. int, int). 7 if ( (pid = Fork») > 0) 8 return (pid) /* родительский процесс */ 9 child_main(i. listenfd. addrlen). /* никогда не завершается */ Ю } -9 Функция fork создает очередной дочерний процесс и возвращает родителю иден- тификатор дочернего процесса. Дочерний процесс вызывает функцию chi 1 d_mai n, показанную в листинге 27.9, которая представляет собой бесконечный цикл. Листинг 27.9. Функция child_main: бесконечный цикл, выполняемый каждым дочерним процессом //server/child02 с 11 void 12 chi 1djnain(int i. int listenfd int addrlen) 13 { 14 int connfd. 15 void web_child(int). 16 socklen_t clilen, 17 struct sockaddr *cliaddr. 18 cliaddr = Malloc(addrlen). 19 printfC’child $ld starting\en”. (long) getpidO) 20 for ( .) { 21 clilen = addrlen 22 connfd = Acceptdistenfd cliaddr &clilen). 23 web_child(connfd): /* обработка запроса */ 24 Close(connfd). 25 } 26 } -25 Каждый дочерний процесс вызывает функцию accept, и когда она завершается, функция web chi 1 d (см. листинг 27.5) обрабатывает клиентский запрос. Дочерний процесс продолжает выполнение цикла, пока родительский процесс не завершит его. Реализация 4.4BSD Если вы никогда ранее не сталкивались с таким типом устройства сервера (не- сколько процессов, вызывающих функцию accept на одном и том же прослуши- ваемом сокете), вас, вероятно, удивляет, что это вообще может работать. Пожа- луй, здесь уместен краткий экскурс, описывающий реализацию этого механизма в Беркли-ядрах (более подробную информацию вы найдете в [105]). Родитель сначала создает прослушиваемый сокет, а затем — дочерние процес- сы. Напомним, что каждый раз при вызове функции fork происходит копирова- ние всех дескрипторов в каждый дочерний процесс. На рис. 27.2 показана орга- низация структур proc (по одной структуре на процесс), одна структура f11 е для прослушиваемого дескриптора и одна структура socket.
27,6. Сервер TCP без блокировки для вызова accept 795 ргос{} ргос{} ргос{} ргос{} Рис. 27.2. Организация структур proc, file и socket Дескрипторы — это просто индексы в массиве, содержащемся в структуре proc, который ссылается на структуру file. Одна из целей дублирования дескрипто- ров в дочерних процессах, осуществляемого функцией fork, заключается в том, чтобы данный дескриптор в дочернем процессе ссылался на ту же структуру file, на которую этот дескриптор ссылается в родительском процессе. Каждая струк- тура fi 1 е содержит счетчик ссылок, который начинается с единицы, когда откры- вается первый файл или сокет, и увеличивается на единицу при каждом вызове функции fork и при каждом дублировании дескриптора (с помощью функции dup). В нашем примере с N дочерними процессами счетчик ссылок в структуре fi 1 е будет содержать значение N+1 (учитывая родительский процесс, у которого по-прежнему открыт прослушиваемый дескриптор, хотя родительский процесс никогда не вызывает функцию accept). При запуске программы создается N дочерних процессов, каждый из которых может вызывать функцию accept, и все они переводятся родительским процес- сом в состояние ожидания [105, с. 458J. Когда от клиента прибывает первый зап- рос на соединение, все N дочерних процессов «просыпаются», так как все они были переведены в состояние ожидания по одному и тому же «каналу ожидания» — полю so timeo структуры socket, как совместно использующие один и тот же про- слушиваемый дескриптор, указывающий на одну и ту же структуру socket. Хотя «проснулись» все N дочерних процессов, только один из них будет связан с кли- ентом. Остальные N-1 снова перейдут в состояние ожидания, так как длина оче- реди клиентских запросов снова станет равна нулю, после того как первый из дочерних процессов займется обработкой поступившего запроса. Такая ситуация иногда называется thundering herd — более или менее дослов- ный перевод будет звучать как «общая побудка», так как все N процессов долж- ны быть выведены из спящего состояния, хотя нужен всего один процесс, и ос- тальные потом снова «засыпают». Тем не менее этот код работает, хотя и имеет побочный эффект — необходимость «будить» слишком много дочерних процес- сов каждый раз, когда требуется принять (accept) очередное клиентское соедине- ние. В следующем разделе мы исследуем, как это влияет на производительность в целом.
796 Глава 27. Альтернативное устройство клиента и сервера Эффект наличия слишком большого количества дочерних процессов В табл 27 1 (строка 2) указано время (1,8), затрачиваемое центральным процес- сором в случае наличия 15 дочерних процессов, облуживающих не более 10 кли- ентов. Мы можем оценить эффект «общей побудки», увеличивая количество дочерних процессов и оставляя то же максимальное значение количества обслу- живаемых клиентов (10). В табл. 27.2 показаны значения времени, затрачивае- мого центральным процессором для данного примера и для двух других приме- ров, которые обсуждаются далее. Здесь мы обсудим только блокировку функции accept, а значения, приведенные в остальных четырех колонках, рассматривают- ся в следующих разделах. Каждый раз при добавлении очередных (неиспользуемых) 15 дочерних про- цессов мы отмечаем увеличение времени, затрачиваемого центральным процес- сором Итак, чтобы избежать эффекта «общей побудки», нам следует отказаться от избыточного количества дочерних процессов. ПРИМЕЧАНИЕ ----------------------------------------------------- Некоторые ядра Unix снабжены функцией, часто с именем wakeup one, которая выво- дит из состояния ожидания только один процесс для обработки одного клиентского ’ запроса [91] Ядра BSD/OS не имеют такой функции Распределение клиентских соединений между дочерними процессами Следующей темой обсуждения является распределение клиентских соединений между свободными дочерними процессами, блокированными в вызове функции accept. Для получения этой информации мы модифицируем функцию main, раз- мещая в совместно используемой области памяти массив счетчиков, которые пред- ставляют собой длинные целые числа (один счетчик на каждый дочерний про- цесс) Это делается следующим образом. long *cptr *meter(int) /* для подсчета количества клиентов на один дочерний процесс */ cptr = meter!nchildren) /* перед порождением дочернего процесса */ В листинге 27 10 показана функция meter. Листинг 27.10. Функция meter, которая размещает массив в совместно используемой памяти //server/meter с 1 #i ncl ude unp h' 2 #include <sys/mman h> 3 /* Размещаем массив 'nchildren" длинных целых чисел 4 * в совместно используемой области памяти 5 * Эти числа используются как счетчики количества * клиентов обслуженных данным дочерним процессом. 6 * см с 467-470 книги [93]" 7 */ 8 long * 9 meter(int nchildren)
27.6. Сервер TCP без блокировки для вызова accept 797 Ю { 11 int fd 12 long *ptr 13 #ifdef MAPJWN 14 ptr = Mmap(0. nchildren * sizeof(long). PROT_READ | PROT_WRITE. 15 MAP_ANON | MAP_SHARED -1 0) 16 #else 17 fd = Open("/dev/zero" O_RDWR 0) 18 ptr = MmapfO nchildren * sizeof(long) PROT_READ | PROT_WRITE, 19 MAP_SHARED. fd. 0). 20 Close(fd). 21 #endif 22 return (ptr) 23 } Мы используем неименованное отображение в память, если оно поддержива- ется (например, в 4 4BSD), или отображение файла /dev/zero (например, SVR4). Поскольку массив создается функцией гитар до того, как родительский процесс порождает дочерние, этот массив затем используется совместно родительским и всеми дочерними процессами, созданными функцией fork. Затем мы модифицируем нашу функцию chiIdjtain (см. листинг 27.9) таким образом, чтобы каждый дочерний процесс увеличивал значение соответствую- щего счетчика на единицу при завершении функции accept, а после завершения выполнения всех дочерних процессов обработчик сигнала SIGINT выводил бы упо- мянутый массив счетчиков. В табл. 27.3 показано распределение нагрузки по дочерним процессам. Когда свободные дочерние процессы блокированы вызовом функции accept, имеющий- ся в ядре алгоритм планирования равномерно распределяет нагрузку, так что в результате все дочерние процессы обслуживают примерно одинаковое количе- ство клиентских запросов. Коллизии при вызове функции select Рассматривая данный пример в 4 4BSD, мы можем исследовать еще одну про- блему, которая встречается довольно редко и поэтому часто остается непонятой до конца. В разделе 16 13 [105] говорится о коллизиях (collisions), возникающих при вызове функции sel ect несколькими процессами на одном и том же дескрип- торе, и о том, каким образом ядро решает эту проблему. Суть проблемы в том, что в структуре socket предусмотрено место только для одного идентификатора про- цесса, который выводится из состояния ожидания по готовности дескриптора. Если же имеется несколько процессов, ожидающих, когда будет готов данный дескриптор, то ядро должно вывести из состояния ожидания все процессы, бло- кированные в вызове функции select, так как ядро не знает, какие именно про- цессы ожидают готовности данного дескриптора. Коллизии при вызове функции sel ect в нашем примере можно форсировать, предваряя вызов функции accept из листинга 27.9 вызовом функции select в ожи- дании готовности к чтению на прослушиваемом сокете. Дочерние процессы бу- дут теперь блокированы в вызове функции select, а не в вызове функции accept. В листинге 27 11 показана изменяемая часть функции chi 1 d_mai п, при этом изме- ненные по отношению к листингу 27.9 строки отмечены знаками
798 Глава 27 Альтернативное устройство клиента и сервера Листинг 27.11. Модификация листинга 27 9 блокирование в вызове select вместо блокирования в вызове accept printf( child $ld startingXen (long) getpidO) + FD_ZERO(&rset) for ( ) { + FD_SET(listenfd &rset) + Select(listenfd+1 &rset NULL NULL NULL) + if (FD_ISSET(listenfd &rset) = 0) + err_quit( listenfd readable ) + clilen = addrlen connfd = Accept(listenfd cliaddr &clilen) web_child(connfd) /* обработка запроса */ Close(connfd) 1 Если, проделав это изменение, мы проверим значение счетчика ядра BSD/OS nsel col 1, мы увидим, что в первом случае при запуске сервера произошло 1814 кол- лизий, а во втором случае — 2045 Так как при каждом запуске сервера два клиен- та создают в сумме 5000 соединений, приведенные выше значения указывают, что примерно в 35-40% случаев вызовы функции select приводят к коллизиям Если сравнить значения времени, затраченного центральным процессором в этом примере, то получится, что при добавлении вызова функции sei ect это значение увеличивается с 1,8 до 2,9 Частично это объясняется, вероятно, добав- лением системного вызова (так как теперь мы вызываем не только accept, но еще и sei ect), а частично — накладными расходами, связанными с коллизиями Из этого примера следует вывод, что, когда несколько процессов блокируют- ся на одном и том же дескрипторе, лучше, чтобы эта блокировка была связана с функцией accept, а не с функцией sei ect 27.7. Сервер TCP с предварительным порождением процессов и защитой вызова accept блокировкой файла Описанная выше реализация, позволяющая нескольким процессам вызывать функцию accept на одном и том же прослушиваемом дескрипторе, возможна толь- ко для систем 4 4BSD, в которых функция accept реализована внутри ядра Ядра системы SVR4, в которых accept реализована как библиотечная функция, не до- пускают этого В самом деле, если мы запустим сервер из предыдущего раздела, в котором имеется несколько дочерних процессов, в Solans 2 5 (система SVR4), то вскоре после того, как клиенты начнут соединяться с сервером, вызов функ- ции accept в одном из дочерних процессов вызовет ошибку ЕPROTO, что свидетель- ствует об ошибке протокола ПРИМЕЧАНИЕ----------------------------------------------------------------- Причины возникновения этой проблемы с библиотечной версией функции accept в SVR4 связаны с реализацией потоков и тем фактом что библиотечная функция accept не является атомарной операцией В Solans 2 6 эта проблема решена, но в большинстве реализаций SVR4 она остается
27 7 Сервер TCP с защитой вызова accept блокировкой файла 799 Решением этой проблемы является зашита вызова функции accept при помо- щи блокировки (lock), так что в данный момент времени только один процесс может быть блокирован в вызове этой функции Другие процессы также будут блокированы, так как они будут стремиться установить блокировку для вызова функции accept Существует несколько способов реализации защиты вызова функции accept, о которых рассказывается во втором томе1 данной серии В этом разделе мы ис- пользуем блокировку файла функцией fcntl согласно стандарту Posix Единственным изменением в функции main (см листинг 27 6) будет добавле- ние вызова функции my_lock_init перед началом цикла, в котором создаются до- черние процессы + my_lock_imt( /tmp/lock ХХХХХХ ) /* один файл для всех дочерних процессов */ for (т=0 1 < nchildren i++) pidsfi] = child_make(i listenfd addrlen) /* возвращение родительского процесса */ Функция child_make остается такой же, как в листинге 27 8 Единственным изменением функции chi 1 d_mai п (см листинг 27 9) является блокирование перед вызовом функции accept и снятие блокировки после завершения этой функции for ( ) { clilen = addrlen + my_lock_wait() connfd = Accept!listenfd cliaddr &clilen) + my_lock_release() web child(connfd) /* обработка запроса */ Close(connfd) В листинге 27 12 показана только функция my_lock_imit, в которой использу- ется блокировка файла согласно стандарту Posix Листинг 27.12. Функция myjockjnit блокировка файла согласно стандарту Posix 1 //server/lock_fcntl с 1 #include unp h 2 static struct flock lock_it unlock_it 3 static mt lock_fd = 1 4 /* fcntl() не выполнится если не будет вызвана функция my_lock_1n1t() */ 5 void 6 my_lock_init(char *pathname) 7 { 8 char lock_file[1024] 9 /* нужно скопировать строку вызывающего процесса на случай, если нонстйнга'1*/ 10 strncpy(lock_file pathname sizeof(lock_file)) 11 Mktemp(lock_file) 12 lock_fd = Opendockfile O_CREAT | O_WRONLY FILE_MODE) 13 Unlink(lock_file) /* но lock_fd остается открытый */ 14 lockjt l_type = F_WRLCK продолжение £$> 1 У Стивенс UNIX взаимодействие процессов — СПб Питер, 2002
800 Глава 27. Альтернативное устройство клиента и сервера Листинг 27.12{продолжение) 15 lock_it l_whence = SEEK_SET; 16 lockjit l_start = 0. 17 lock_it l_len = 0. 18 unlock_it l_type = FJJNLCK. 19 unlock_it l_whence = SEEK_SET? 20 unlock_it 1 start = 0. 21 unlockjt lien = 0. 22 } -13 Вызывающий процесс задает шаблон для имени файла в качестве аргумента функции my_lock_imt, и функция mktemp на основе этого шаблона создает уни- кальное имя файла. Затем создается файл с этим именем и сразу же вызывается функция unlink, в результате чего имя файла удаляется из каталога. Если в про- грамме впоследствии произойдет сбой, то файл исчезнет безвозвратно. Но пока он остается открытым в одном или нескольких процессах (иными словами, пока счетчик ссылок для этого файла больше нуля), сам файл не будет удален. (Отме- тим, что между удалением имени файла из каталога и закрытием открытого фай- ла существует фундаментальная разница.) 4-21 Инициализируются две структуры fl ock: одна для блокирования файла, другая для снятия блокировки. Блокируемый диапазон начинается с нуля (]_whence= SEEK SET, l_start=0). Значение 1_1еп равно нулю, то есть блокирован весь файл. В этот файл ничего не записывается (его длина всегда равна нулю), но такой тип блокировки в любом случае будет правильно обрабатываться ядром. ПРИМЕЧАНИЕ ------------------------------------------------------------------ Сначала автор инициализировал эти структуры при объявлении: static struct flock lock_it = { F WRLCK, 0. 0. 0. 0 }. static struct flock unlock_it = { FJJNLCK, 0 0. 0 0 }. но тут возникли две проблемы: у нас нет гарантии, что константа SEEK_SET равна нулю, но, что более важно, стандарт Posix не регламентирует порядок расположения полей этой структуры. В Solans и Digital Unix поле I type всегда расположено первым, но в случае BSD/OS это не так. Posix гарантирует только то, что требуемые поля при- сутствуют в структуре. Posix не гарантирует какого-либо порядка следования полей структуры, а также допускает наличие в ней полей, не относящихся к стандарту Posix. Поэтому когда требуется инициализировать эту структуру (если только пе нужно ини- циализировать все поля нулями), это приходится делать через фактический код С, а не с помощью инициализатора при объявлении структуры. Исключением из этого правила является ситуация, когда инициализатор структуры обеспечивается реализацией. Например, при инициализации взаимного исключения в Posix в главе 23 мы писали: pthread_mutex_t mlock = PTHREADJIUTEXJNITIALIZER. Тип данных pthread_mutex_t — это некая структура, но инициализатор предоставля- ется реализацией и может быть различным для разных реализаций. В листинге 27.13 показаны две функции, которые устанавливают и снимают блокировку с файла. Они представляют собой вызовы функции fcntl, использу- ющие структуры, инициализированные в листинге 27.12.
27.7. Сервер TCP с защитой вызова accept блокировкой файла 801 Листинг 27.13. функции my_lock_wait (установление блокировки файла) и my_lock_release (снятие блокировки файла) //server/lock_fcntl с 23 void 24 my_lock_wait О 25 { 26 int rc. 27 while ( (rc = fcntl (lock_fd. F_SETLKW. &lock_it )) < 0 { 28 if (errno == EINTR) 29 continue. 30 else 31 errsys ("fcntl error for rny_lock_wait"), 32 } 33 } 34 void 35 my_lock_release () 36 { 37 if (fcntl (lock_fd F_SETLKW &unlock_it )) < 0) 38 errsys ("fcntl error for my_lock_release’). 39 } Новая версия нашего сервера с предварительным порождением процессов работает теперь под SVR4, гарантируя, что в данный момент времени только один дочерний процесс блокирован в вызове функции accept. Сравнивая строки 2 и 3 в табл. 27.1 (результаты для серверов Digital Unix и BSD/OS), мы видим, что та- кой тип блокировки увеличивает время, затрачиваемое центральным процессо- ром на узле сервера. ПРИМЕЧАНИЕ---------------------------------------------------------- В выпуске 1.1 wcb-сервера Apache (http’//www apache org) использована технология предварительного порождения процессов, причем если позволяет реализация, все до- черние процессы блокируются в вызове функции accept, иначе используется блоки- ровка файла для защиты вызова accept Эффект наличия слишком большого количества дочерних процессов Мы можем проверить, возникает ли в данной версии сервера эффект «общей по- будки», рассмотренный в предыдущем разделе. В табл. 27.2 показаны результаты излишнего увеличения количества дочерних процессов. В колонке, отведенной под данную версию сервера в системе Solaris, мы видим, что имеет смысл увели- чивать количество дочерних процессов до 75, так как при следующем увеличе- нии (до 90) происходит нечто, приводящее к значительному увеличению време- ни центрального процессора. Возможно, причина в том, что система исчерпывает свои ресурсы памяти из-за слишком большого количества дочерних процессов и начинается интенсивная работа с файлом подкачки (swapping). Распределение клиентских соединений между дочерними процессами Используя функцию, показанную в листинге 27.10, мы можем исследовать распределение клиентских запросов между свободными дочерними процес-
802 Глава 27. Альтернативное устройство клиента и сервера сами. Результат показан в табл. 27.3. Для всех трех систем оказывается, что все клиентские запросы распределяются в пуле дочерних процессов равно- мерно. 27.8. Сервер TCP с предварительным порождением процессов и защитой вызова accept при помощи взаимного исключения Как мы уже говорили, существует несколько способов синхронизации процессов путем блокирования. Блокировка файла по стандарту Posix, рассмотренная в преды- дущем разделе, переносится на все Posix-совместимые системы, но она подразу- мевает некоторые операции с файловой системой, которые могут потребовать времени. В этом разделе мы будем использовать блокировку при помощи взаим- ного исключения, обладающую тем преимуществом, что ее можно применять для синхронизации не только потоков внутри одного процесса, но и потоков, относя- щихся к различным процессам. Функция mat п остается такой же, как и в предыдущем разделе, то же относится к функциям сМ Idjnake и cfnldjnain. Меняются только три функции, осуществля- ющие блокировку. Чтобы использовать взаимное исключение между различны- ми процессами, во-первых, требуется хранить это взаимное исключение в раз- деляемой процессами области памяти, а во-вторых, библиотека потоков должна получить указание о том, что взаимное исключение совместно используется раз- личными процессами. ПРИМЕЧАНИЕ------------------------------------------------------------ Требуется также, чтобы библиотека потоков поддерживала атрибут PTHREAD_PRO- CESS_SHARED Например, в Digital Unix 4 0b этот атрибут пе поддерживается, так что мы не можем запустить этот сервер в данной операционной системе. Существует несколько способов разделения памяти между различными про- цессами, что мы подробно описываем во втором томе1 данной серии. В этом при- мере мы используем функцию mmap с устройством /dev/zero, которое работает с яд- рами Solaris и другими ядрами SVR4. В листинге 27.14 показана только функция my_] ock_imt. Листинг 27.14. Функция myjockjnit: использование взаимного исключения потоками, относящимися к различным процессам (технология Pthread) //server/lock_pthread с 1 #include unpthread h 2 #include <sys/mman h> 3 static pthread_mutex_t *mptr /* фактически взаимное исключениеЛаиез. вчадвместно используемой памяти */ 4 void 1 У Стивенс. UNIX взаимодействие процессов — СПб Питер, 2002
“21 8. Сервер TCP с защитой вызова accept с взаимным исключением 803 5 my_lock imt(char *pathname) 6 { 7 int fd, 8 pthread_mutexattr_t mattr, 9 fd = OpenC'/dev/zero". O_RDWR. 0): 10 mptr « Mmap(0, sizeof(pthread_mutex_t) PR0T_READ | PROT WRITE. 11 MAP SHARED, fd. 0) 12 CloseCfd). 13 Pthreadjnutexattr_ini1(&ma 11 r) 14 Pthread_mutexattr_setpshared(&mattr PTHREAD_PROCESS_SHARED) 15 Pthreadjnutex imt(mptr &mattr) 16 } 9-12 Мы открываем (open) файл I dev I zero, а затем вызываем mmap. Количество бай- тов (второй аргумент этой функции) — это размер переменной pthread_mutex_t- Затем дескриптор закрывается, но для нас это пе имеет значения, так как файл уже отображен в память. 13-15 В приведенных ранее примерах взаимных исключений Pthread мы инициали- зировали глобальные статические взаимные исключения, используя константу PTHREAD_MUTEX_INITIALIZER (см., например, листинг 27.13). Но располагая взаимное исключение в совместно используемой памяти, мы должны вызвать некоторые библиотечные функции Pthread, чтобы сообщить библиотеке о наличии семафо- ра в совместно используемой памяти и о том, что он будет применяться для син- хронизации потоков, относящихся к различным процессам. Мы должны инициа- лизировать структуру pthread_mutexattr_t задаваемыми по умолчанию атрибутами взаимного исключения, а затем установить значение атрибута PTHREAD_PROCESS_ SHARED. (По умолчанию значением этого атрибута должно быть PTHREAD_PROCESS_ PRIVATE, что подразумевает использование взаимного исключения только в пре- делах одного процесса.) Затем вызов pthread_mutex_imt инициализирует взаим- ное исключение указанными атрибутами. В листинге 27.15 показаны только функции my_l ock_wait и ггу_ 1 ock_rel ease. Они содержат вызовы функций Pthread, предназначенных для блокирования и раз- блокирования взаимного исключения. Листинг 27.15. Функции myjock_wait и my lock release: использование блокировок Pthread //server/lock_pthread с 17 void 18 my_lock_wait() 19 { 20 Pthread_mutex_lock(mptr): 21 } 22 void 23 my_lock_rel easel) 24 { 25 Pthread_mutex_unlock(mptr) 26 } Сравнивая строки 3 и 4 табл. 27 1 для сервера Solaris, можно заметить, что вер- сия, использующая синхронизацию процессов при помощи взаимного исключе- ния, характеризуется более высоким быстродействием, чем версия с блокиров- кой файла
804 Глава 27. Альтернативное устройство клиента и сервера 27.9. Сервер TCP с предварительным порождением процессов: передача дескриптора Последней модификацией нашего сервера с предварительным порождением про- цессов является версия, в которой только родительский процесс вызывает функ- цию accept, а затем «передает» присоединенный сокет какому-либо одному до- чернему процессу. Это помогает обойти необходимость защиты вызова accept, по требует некоторого способа передачи дескриптора между родительским и дочер- ним процессами. Эта техника также несколько усложняет код, поскольку роди- тельскому процессу приходится отслеживать, какие из дочерних процессов заня- ты, а какие свободны, чтобы передавать дескриптор только свободным дочерним процессам. В предыдущих примерах сервера с предварительным порождением процессов родительскому процессу не приходилось беспокоиться о том, какой дочерний процесс принимает соединение с клиентом. Этим занималась операционная систе- ма, организуя вызов функции accept одним из свободных дочерних процессов или блокировку файла или взаимного исключения. Из первых пяти строк табл. 27.3 видно, что все три рассматриваемые нами операционные системы осуществляют равномерную циклическую загрузку свободных процессов клиентскими соеди- нениями. В данном примере для каждого дочернего процесса нам нужна некая структу- ра, содержащая информацию о нем. Заголовочный файл child.h, в котором опре- деляется структура Ch 11 d, показан в листинге 27.16. Листинг 27.16. Структура Child //server/chi1d h 1 typedef struct { 2 pid_t child_pid /* ID noouecca */ 3 int childjnpefd; /* программный (неименованный) канал между родительским и дочерним процессами */ 4 int child_status; /* 0 = готово */ 5 long child_count. /* количество обрабатываемых соединений */ 6 } Child. 7 Child *cptr. /* массив структур Child */ Мы записываем идентификатор дочернего процесса, дескриптор программ- ного канала (pipe) родительского процесса, связанного с дочерним, статус дочер- него процесса и количество обрабатываемых дочерним процессом клиентских соединений. Это количество выводится обработчиком сигнала SIGINT и позволя- ет нам отслеживать распределение клиентских запросов между дочерними про- цессами. Рассмотрим сначала функцию chi 1 djnake, которая приведена в листинге 27.17. Мы создаем канал и доменный сокет Unix (см. главу 14) перед вызовом функции fork. После того как создан дочерний процесс, родительский процесс закрывает один дескриптор (sockfd[ 1]), а дочерний процесс закрывает другой дескриптор (sockfd[0]). Более того, дочерний процесс подключает свой дескриптор канала (sockfd[l]) к стандартному потоку сообщений об ошибках, так что каждый до-
27.9. Сервер TCP с порождением процессов: передача дескриптора 805 черный процесс просто использует это устройство для связи с родительским про- цессом. Этот механизм проиллюстрирован схемой, приведенной на рис. 27.3. Листинг 27.17. Функция childjnake: передача дескриптора в сервере с предварительным порождением дочерних процессов //server/child05 с 1 include "unp h" 2 include "child h" 3 pid_t 4 child_make(int i. int listenfd. int addrlen) 5 { 6 int sockfd[2]. 7 pid_t pid. 8 void childjnain(int. int, int), 9 Socketpair(AF_LOCAL. SOCK_STREAM 0. sockfd): 10 if ( (pid = ForkO) > 0) { 11 Close(sockfd[l]). 12 cptr[i] child_pid = pid 13 cptr[i] child_pipefd = sockfd[0]. 14 cptr[i] child_status = 0. 15 return (pid). /* родительский процесс */ 16 } 17 Dup2(sockfd[l], STDERR_FILENO). /* канал от дочернего процесса к родительскому */ 18 Close(sockfd[0]). 19 Close(sockfd[l]). 20 Closedistenfd). /* дочернему процессу не требуется, чтобы он был открыт */ 21 child_main(i listenfd. addrlen). /* никогда не завершается */ 22 } Родительский процесс Дочерний процесс 1 Рис. 27.3. Канал после того, как дочерний и родительский процесс закрыли один конец После создания всех дочерних процессов мы получаем схему, показанную на рис. 27.4. Мы закрываем прослушиваемый сокет в каждом дочернем процессе, поскольку только родительский процесс вызывает функцию accept. Мы показы- ваем на рисунке, что родительский процесс должен обрабатывать прослушивае- мый сокет, а также все доменные сокеты. Как можно догадаться, родительский процесс использует функцию sei ect для мультиплексирования всех дескрипторов. В листинге 27.18 показана функция main. В отличие от предыдущих версий этой функции, в данном случае в памяти размещаются все наборы дескрипторов и в каждом наборе включены все биты, соответствующие прослушиваемому со- кету и каналу каждого дочернего процесса. Вычисляется также максимальное значение дескриптора и выделяется память для массива структур Child. Основ- ной цикл запускается при вызове функции select.
806 Глава 27, Альтернативное устройство клиента и сервера Рис. 27.4. Каналы после создания всех дочерних процессов Листинг 27.18. Функция main, использующая передачу дескриптора 7/server/serv05 с 1 2 #include #include "unp h" "child h" 3 static int nchildren. 4 int 5 maintint argc. char **argv) 6 { 7 int listenfd. i navail maxfd. nsel, connfd. rc. 8 void sig_int(int) 9 pid_t child_make(int int. int) 10 ssize_t n 11 fd_set rset masterset. 12 socklen _t addrlen, clilen. 13 struct sockaddr *cliaddr. 14 if (argc == 3) 15 listenfd = Тср_1isten(NULL argv[l], baddrlen). 16 else if (argc == 4) 17 listenfd = Tcp_listen(argv[l], argv[2] &addrlen), 18 el se 19 err_quit("usage serv05 [ <host> ] <port#> <#chi1dren>”). 20 FD_ZERO(&masterset) 21 FD_SET(listenfd. taasterset). 22 maxfd = listenfd. 23 cliaddr = Malice(addrlen) 24 nchildren = atoi(argv[argc - 1]). 25 navail = nchildren. 26 cptr = Calloc(nchildren sizeof(Child)); 27 /* предварительное создание дочерних процессов */ 28 for (i = 0. i < nchildren, i++) { 29 child_make(i. listenfd, addrlen). /* родительский процесс завершается */
27.9. Сервер TCP с порождением процессов: передача дескриптора 807 30 FD_SET(cptr[i] child_pipefd. fciasterset), 31 maxfd = max(maxfd cptr[i] child_pipefd). 32 } 33 Signal(SIGINT sig_int). 34 for (..) { 35 rset = masterset. 36 if (navai1 <= 0) 37 FD_CLR(listenfd. Srset) /* выключаем если нет свободных дочерних процессов */ 38 nsel = Select(maxfd + 1. &rset NULL NULL. NULL). 39 /* проверка новых соединений */ 40 if (FD_ISSET(1istenfd. &rset)) { 41 clilen = addrlen. 42 connfd = Accept(listenfd cliaddr. &clilen). 43 for (i = 0 i < nchildren i++) 44 if (cptr[i] child_status = 0) 45 break. /* свободный */ 46 if (i == nchildren) 47 err_quit("no available children"), 48 cptr[TJ child status = 1 /* отмечаем этот дочерний процесс как занятый *1 49 cptr[i] child_count++ 50 navai1-- 51 п = Write_fd(cptr[i] child_pipefd. "" 1 connfd). 52 Close(connfd) 53 if (--nsel -= 0) 54 continue. /* с результатами selectO закончено */ 55 } 56 /* поиск освободившихся дочерних процессов */ 57 for (i = 0. i < nchildren. i++) { 58 if (FD_ISSET(cptr[iJ child_pipefd Srset)) { 59 if ( (n = Read(cptr[i] child_pipefd &rc D) == 0) 60 err_quit(“child Xd terminated unexpectedly” i) 61 cptrfi] child_status - 0. 62 navai1++ 63 if (--nsel == 0) 64 break. /* с результатами selectO закончено */ 65 } 66 } 67 } 68 } Отключение прослушиваемого сокета в случае отсутствия свободных дочерних процессов 36-37 Счетчик navai 1 отслеживает количество свободных дочерних процессов. Если его значение становится равным пулю, прослушиваемый сокет в наборе дескрип- торов функции select выключается. Это предотвращает прием нового соедине- ния в тот момент, когда нет ни одного свободного дочернего процесса. Ядро по-прежнему устанавливает эти соединения в очередь, пока их количество не пре- высит значения аргумента back] од функции 11 sten, заданного для прослушивае- мого сокета, но мы пе хотим их принимать, пока у нас не появится свободный дочерний процесс, готовый обрабатывать клиентский запрос.
808 Глава 27. Альтернативное устройство клиента и сервера Прием нового соединения 39-55 Если прослушиваемы» сокет готов для считывания, можно принимать (accept) новое соединение. Мы находим первый свободный дочерний процесс и передаем ему присоединенный сокет с помощью функции write_fd, приведенной в листин- ге 14.11. Вместе с дескриптором мы передаем 1 байт, но получатель пе интересу- ется содержимым этого байта. Родитель закрывает присоединенный сокет. Мы всегда начинаем поиск свободного дочернего процесса с первого элемен- та массива структур Child. Это означает, что новое соединение для обработки по- ступившего клиентского запроса всегда получает первый элемент этого массива. Этот факт мы проверим при рассмотрении табл. 27.3 и значения счетчика child_ count после завершения работы сервера. Если мы не хотим оказывать такое пред- почтение первому элементу массива, мы можем запомнить, какой дочерний про- цесс получил последнее клиентское соединение, и каждый раз начинать поиск свободного дочернего процесса со следующего за ним, а по достижении конца массива переходить снова к первому элементу. В этом нет особого смысла (на самом деле все равно, какой дочерний процесс обрабатывает очередное соедине- ние, если имеется несколько свободных дочерних процессов), если только пла- нировочный алгоритм операционной системы не накладывает санкций на про- цессы, которые требуют относительно больших временных затрат центрального процессора. Обработка вновь освободившихся дочерних процессов 56-66 Когда дочерний процесс заканчивает обработку клиентского запроса, наша функция child_main записывает один байт в капал для родительского процесса. Тем самым родительский конец канала становится доступным для чтения. Упо- мянутый байт считывается (но его значение при этом игнорируется), а дочерний процесс помечается как свободный. Если же дочерний процесс завершит свое выполнение неожиданно, его конец канала будет закрыт, а операция чтения (read) возвратит нулевое значение. Это значение перехватывается и дочерний процесс завершается, но более удачным решением было бы записать ошибку и создать новый дочерний процесс для замены завершенного. Функция chi ld_main показана в листинге 27.19. Листинг 27.19. Функция childjnain: передача дескриптора в сервере с предварительным порождением дочерних процессов //server/child05 с 23 void 24 child_main(int i. int listenfd int addrlen) 25 { 26 char c. 27 int connfd. 28 ssize_t n. 29 void web_child(int), 30 printfCchild Sid startinglen". (long) getpidO) 31 for ( .) { 32 if ( (n = Read_fd(STDERR_FILENO. &c 1 &connfd)) == 0) 33 err_quit("read_fd returned 0"). 34 if (connfd < 0) 35 err_quit("no descriptor from read_fd"). 36 web_child(connfd). /* обработка запроса */
27.10, Параллельный сервер TCP: один поток для каждого клиента 809 37 Close(connfd). 38 Write(STDERR_FILENO 1). /* сообщаем родительскому процессу о том. что дочерний освободился */ 39 } 40 } Ожидание дескриптора от родительского процесса 32-33 Эга функция отличается от аналогичных функций из двух предыдущих разде- лов, так как дочерний процесс не вызывает более функцию accept. Вместо этого дочерний процесс блокируется в вызове функции read_fd, ожидая, когда роди- тельский процесс передаст ему дескриптор присоединенного сокета. Сообщение родительскому процессу о готовности дочернего к приему новых запросов 38 Закончив обработку очередного клиентского запроса, мы записываем (write) 1 байт в канал, чтобы сообщить, что данный дочерний процесс освободился. В табл. 27.1 при сравнении строк 4 и 5 для сервера Solaris мы видим, что дан- ный сервер медленнее, чем версия, рассмотренная нами в предыдущем разделе, которая использовала блокировку потоками взаимного исключения. Сравнивая строки 3 и 5 для серверов Digital Unix и BSD/OS, мы приходим к тому же заклю- чению: передача дескриптора по каналу от родительского процесса к дочернему и запись одного байта в капал для сообщения родительскому процессу о завер- шении обработки клиентского запроса занимает больше времени, чем блокиро- вание и разблокирование взаимного исключения или файла. В табл. 27.3 показаны значения счетчиков chi ld_count из структуры Child, ко- торые выводятся обработчиком сигнала SIGINT по завершении работы сервера. Дочерние процессы, расположенные ближе к началу массива, обрабатывают боль- шее количество клиентских запросов, как было указано при обсуждении лис- тинга 27.18. 27.10. Параллельный сервер TCP: один поток для каждого клиента Предыдущие пять разделов были посвящены рассмотрению серверов, в которых для обработки клиентских запросов используются дочерние процессы, либо за- ранее порождаемые с помощью функции fork, либо требующие вызова этой функ- ции для каждого вновь поступившего клиентского запроса. Если же сервер под- держивает потоки, мы можем применить потоки вместо дочерних процессов. Наша первая версия сервера с использованием потоков показана в листин- ге 27.20. Это модификация листинга 27.2: в ней создается один поток для каждо- го клиента вместо одного дочернего процесса для каждого клиента. Эта версия во многом похожа на сервер, представленный в листинге 23.2. Листинг 27.20. Функция main для сервера TCP, использующего потоки //server/serv06 с 1 #include "unpthread h“ 2 int продолжение &
810 Глава 27. Альтернативное устройство клиента и сервера Листинг 27.20 (продолжение) 3 main(int argc. char **argv) 4 { 5 int listenfd. connfd. 6 void sig_int(int) 7 void *doit(void *). 8 pthread_t tid, 9 socklen_t clilen. addrlen: 10 struct sockaddr *cliaddr. 11 if (argc == 2) 12 listenfd = Tcp_listen(NULL, argv[l]. &addrlen). 13 else if (argc == 3) 14 listenfd = Tcp_listen(argv[l] argv[2], &addrlen): 15 else 16 err_quit("usage serv06 [ <host> ] <port#>”), 17 cliaddr = Mailoc(addrlen). 18 Signal(SIGINT. sig_int) 19 for (. ) { 20 clilen = addrlen. 21 connfd = Acceptdistenfd. cliaddr. &clilen). 22 Pthread_create(&tid NULL &doit (void *) connfd). 23 } 24 } 25 void * 26 doit(void *arg) 27 { 28 void web_child(int) 29 Pthread_detach(ptnread_sel f()); 30 web_child((int) arg). 31 (Tose((int) arg). 32 return (NULL). 33 } Цикл основного потока 9-23 Основной поток блокируется в вызове функции accept, и каждый раз, когда прибывает новое клиентское соединение, функцией pthread_create создается но- вый поток. Функция, выполняемая новым потоком, — это функция doit, а ее ар- гументом является присоединенный сокет. Функция прочих потоков 15-33 Функция doit выполняется как неприсоединенный поток, так что основному потоку пе приходится ждать ее завершения. Doit вызывает функцию web_child (см. листинг 27.5). Когда эта функция возвращает упправление, присоединенный сокет закрывается. В табл. 27.1 мы видим, что эта простая версия с использованием потоков яв- ляется более быстродействующей, чем даже самая быстрая из версий с предвари- тельным порождением процессов — как для Solaris, так и для Digital Unix. Кроме того, эта версия, в которой каждый клиент обслуживается одним потоком, во много
27.1. Сервер TCP с порождением потоков, которые вызывают accept 811 раз быстрее версии, в которой каждый клиент обслуживается специально создан- ным для него дочерним процессом (первая строка табл. 27.1). ПРИМЕЧАНИЕ--------------------------------------------------------- В разделе 23.5 мы упомянули о трех вариантах преобразования функции, которая не является безопасной в многопоточной среде, в функцию, обеспечивающую требуемую безопасность. Функция wcb_child вызывает функцию readhnc, и версия, показанная в листинге 3.11, не является безопасной в многопоточной среде. На примере, приве- денном в листинге 27.20, были испробованы вторая и грет ья альтернативы из разде- ла 23.5. Увеличение быстродействия при переходе от альтернативы 3 к альтернативе 2 составило менее одного процент а, вероятно, потому, что функция readline использова- лась лишь для считывания значения счетчика (5 символов) от клиента. Поэтому в дан- ной главе для простоты мы использовали более медленную версию из листинга 3.10 для сервера с предварительным порождением потоков. 27.11. Сервер TCP с предварительным порождением потоков, каждый из которых вызывает accept Ранее в этой главе мы обнаружили, что версии, в которых заранее создается пул дочерних процессов, работают быстрее, чем те, в которых для каждого клиентс- кого запроса приходится вызывать функцию fork. Для систем, поддерживающих потоки, логично предположить, что имеется та же закономерность: быстрее сразу создать пул потоков при запуске сервера, чем создавать по одному потоку по мере поступления запросов от клиентов. Основная идея такого сервера заключается в том, чтобы создать пул потоков, каждый из которых вызывает затем функцию accept. Вместо того чтобы блокировать потоки в вызове accept, мы используем взаимное исключение, как в разделе 27.8. Это позволяет вызыват ь функцию accept только одному потоку в каждый момент времени. Использовать блокировку файла для защиты accept в таком случае бессмысленно, так как при наличии нескольких потоков внутри данного процесса можно использовать взаимное исключение. В листинге 27.21 показан заголовочный файл pthread07. h, определяющий струк- туру Thread, содержащую определенную информацию о каждом потоке. Листинг 27.21. Заголовочный файл pthread07.h //server/pthread07 h 1 typedef struct { 2 pthread_t thread_tid: /* идентификатор потока */ 3 long thread_count. /* количество обработанных запросов */ 4 } Thread. 5 Thread *tptr: /* массив структур Thread */ 6 int listenfd. nthreads; 7 socklen_t addrlen. 8 pthread_mutex_t mlock: Мы также объявляем несколько глобальных переменных, таких как дескрип- тор прослушиваемого сокета и взаимное исключение, Которые должны совмест- но использоваться всеми потоками. В листинге 27.22 показана функция main.
812 Глава 27. Альтернативное устройство клиента и сервера Листинг 27.22. Функция main для сервера TCP с предварительным порождением потоков //server/serv07 с 1 include "unpthread h" 2 #include "pthread07 h" 3 pthread_mutex_t mlock = PTHREAD_MUTEX_INI7IALIZER. 4 int 5 main(int argc. char **argv) 6 { 7 int 1. 8 void sig_int(int). thread_make(int), 9 if (argc == 3) 10 listenfd = Tcpl isten(NULL. argv[l], &addrlen). 11 else if (argc == 4) 12 listenfd = lcp_listen(argv[l], argv[2], &addrlen); 13 else 14 err_quit("usage serv07 [ <host> ] <port#> <#threads>"). 15 nthreads = atoi(argv[argc - 1]) 16 tptr = Calloc(nthreads. sizeof(Thread)). 17 for (i = 0. i < nthreads. i++) 18 thread_make(i). /* завершается только основной поток */ 19 Signal(SIGINT. sig_int). 20 for (..) 21 paused. /* потоки все выполнили */ 22 } Функции thread make и threadjnain показаны в листинге 27.23. Листинг 27.23. Функции thread_make и threadjnain //server/pthread07 с 1 #include "unpthread.h" 2 #include "pthread07 h" 3 void 4 thread_make(int i) 5 { 6 void *thread__main(void *). 7 Pthread_create(&tptr[i] thread_tid. NULL, &thread_main, (void *) i); 8 return. /* завершается основной поток */ 9 } 10 void * 11 thread_main(void *arg) 12 { 13 int connfd, 14 void web_child(int). 15 socklen_t clilen. 16 struct sockaddr *cliaddr, 17 cliaddr = Malloc(addrlen). 18 printf("thread M start!ng\en", (int) arg); 19 for (..) {
27.12. Сервер с порождением потоков: основной поток вызывает accept 813 20 clilen = addrlen 21 Pthread_mutex_lock(&mlock) 22 connfd = Acceptd istenfd. cliaddr. Scblen), 23 Pthread_mutex_unlock(&mlock). 24 tptr[(int) arg] thread_count++. 25 web_child(connfd). /* обработка запроса */ 26 Close(connfd). 27 } 28 } Создание потоков 7 Создаются потоки, каждый из которых выполняет функцию pthreadjna i п. Един- ственным аргументом этой функции является порядковый номер потока. 21-23 Функция threadjren п вызывает функции pthread_mutex_l ock и pthread_mutex_unl ock соответственно до и после вызова функции accept. Сравнивая строки 6 и 7 в табл. 27.1, можно заметить, что эта последняя версия нашего сервера быстрее, чем версия с созданием нового потока для каждого кли- ентского запроса как для Solaris, так и для Digital Unix. Этого можно было ожи- дать, так как в данной версии мы сразу создаем пул потоков п пе тратим время па создание новых потоков по мере поступления клиентских запросов. На самом деле эта версия сервера — самая быстродействующая для обеих систем. В табл. 27.3 показано распределение значений счетчика thread_count структу- ры Thread, которые мы выводим с помощью обработчика сигнала SIGINT по завер- шении работы сервера. Равномерность этого распределения объясняется тем, что при выборе потока, который будет блокировать взаимное исключение, алгоритм планирования загрузки потоков последовательно перебирает все потоки в цикле. ПРИМЕЧАНИЕ----------------------------------------------------------------------- В Беркли-ядрах (например, Digital Unix) нам не нужна блокировка при вызове функ- ции accept, так что мы можем использовать версию, представленную в листинге 27.23, без взаимных исключений. Но в результате этого время, затрачиваемое центральным процессором, увеличится с 3,5 секунды (строка 7 в табл. 27.1) до 3,9 секунды. Если рас- смотреть два компонента, из которых складывается время центрального процессора — пользовательское и системное время, — то окажется, что первый комионентуменьшается при отсутствии блокировки (поскольку блокирование осуществляется в библиотеке потоков, входящей в пользовательское пространс тво), но системное время возрастает (за счет эффекта «общей побудки», возникающего, когда все потоки, блокированные в вызове функции accept, выходят из состояния ожидания при появлении нового кли- ентского соединения). Для того чтобы каждое соединение передавалось только одно- му потоку, необходима некая разновидность взаимного исключения, и оказывается, что быстрее это делают сами потоки, а не ядро. 27.12. Сервер с предварительным порождением потоков: основной поток вызывает функцию accept Последняя рассматриваемая памп версия сервера устроена следующим образом: главный поток создает пул потоков при запуске сервера, после чего он же вызы- вает функцию accept и передает каждое клиентское соединение какому-либо из
814 Глава 27. Альтернативное устройство клиента и сервера свободных на данный момент потоков. Это аналогично передаче дескриптора в версии, рассмотренной нами в разделе 27.9. При таком устройстве сервера необходимо решить, каким именно образом должна осуществляться передача присоединенного дескриптора одному из пото- ков в пуле. Существует несколько способов решения этой задачи. Можно, как и прежде, использовать передачу дескриптора, но при этом не требуется переда- вать дескриптор от одного потока к другому, так как все они, в том числе и глав- ный поток, принадлежат одному и тому же процессу. Все, что требуется знать потоку, получающему дескриптор, — это номер дескриптора. В листинге 27.24 показан заголовочный файл pthread08 h, определяющий структуру Thread, анало- гичный файлу, показанному в листинге 27.21. Листинг 27.24. Заголовочный файл pthreadO8.h //server/pthread08 h 1 typedef struct { 2 pthread_t thread_tid. /* идентификатор потока */ 3 long thread_count, /* количество обработанных запросов */ 4 } Thread 5 Thread *tptr /* массив структур Thread */ 6 #define MAXNCLI 32 7 int clifd[MAXNCLI] iget. iput, 8 pthread_mutex_t clifdjnutex. 9 pthread_cond_t clifd_cond. Определение массива для записи дескрипторов присоединенных сокетов 6-9 Мы определяем массив cl т fd, в который главный поток записывает дескрипто- ры присоединенных сокетов. Свободные потоки из пула получают по одному де- скриптору из этого массива и обрабатывают соответствующий запрос, т put — это индекс в данном массиве для очередного элемента, записываемого в него глав- ным потоком, a iget — это индекс очередного элемента массива, передаваемого свободному потоку для обработки. Разумеется, эта структура данных, совместно используемая всеми потоками, должна быть защищена, и поэтому мы использу- ем условную переменную и взаимное исключение. В листинге 27.25 показана функция main. Листинг 27.25. Функция main для сервера с предварительным порождением потоков //server/serv08 с 1 #include "unpthread h" 2 include "pthread08 h” 3 static int nthreads. 4 pthread_mutex_t clifdjnutex = PTHREAD_MUTEX_INITIALIZER, 5 pthread_cond_t clifd_cond = PTHREADJONDJNITIALIZER; 6 int 7 mainfint argc char **argv) 8 { 9 int i.listenfd, connfd 10 void sig_int(int) threadjnake(int). 11 socklen_t addrlen. clilen
27,12. Сервер с порождением потоков: основной поток вызывает accept 815 12 struct sockaddr *cliaddr. 13 if (argc == 3) 14 listenfd = Tcpjisten(NULL argv[l] &addrlen). 15 else if (argc “ 4) 16 listenfd = Tcp_listen(argv[l], argv[2], &addrlen). 17 else 18 err_quit("usage serv08 [ <host> ] <port#> <#threads>"): 19 cliaddr = Malloc(addrlen). 20 nthreads = atoi(argv[argc - 1]). 21 tptr = Calloc(nthreads. sizeof(Thread)). 22 iget = iput = 0. 23 /* создание всех потоков */ 24 for (i = 0. i < nthreads: i++) 25 threadjnake(i). /* завершается только основной поток */ 26 Signal(SIGINT. sig_int). 27 for (..) { 28 clilen = addrlen. 29 connfd = Accept(listenfd cliaddr. &clilen) 30 Pthreadjnutex_lock(&clifdjnutex), 31 clifd[iput] = connfd 32 if (++iput — MAXNCLI) 33 iput = 0 34 if (iput == iget) 35 err_quit(’iput = iget = fcd", iput). 36 Pthread_cond_signal(&clifd_cond). 37 Pthread_mutex_unlock(&clifdjnutex). 38 } 39 } Создание пула потоков 23-25 Функция threadjnake создает все потоки. Ожидание прихода клиентского соединения 27-38 Основной поток блокируется в вызове функции accept, ожидая появления но- вого соединения. При появлении этого соединения дескриптор присоединенного сокета записывается в следующий элемент массива clifd после блокирования взаимного исключения. Мы также следим, чтобы индекс iget не совпал со значе- нием индекса iput, что укажет на недостаточно большой размер массива. Услов- ная переменная сигнализирует о прибытии нового запроса, и взаимное исключе- ние разблокируется, позволяя одному из потоков пула обслужить прибывший запрос. Функции threadjnake и thread main показаны в листинге 27.26. Первая из них идентична функции, приведенной в листинге 27.23. Листинг 27.26. Функции threadjnake и threadjnain //server/pthread08 с 1 #include 'unpthread h” 2 #include "pthread08 h" 3 void ..1,13-1 продочжение &
816 Глава 27. Альтернативное устройство клиента и сервера Листинг 27.26 (продолжение) 4 thread_make(int i) 5 { 6 void *thread_main(void *) 7 Pthread_create(&tptr[i] thread_tid NULL &thread_main, (void *) i). 8 return, /* завершается основной поток */ 9 } 10 void * 11 thread_main(void *arg) 12 { 13 int connfd. 14 void web_child(int). 15 printfCthread fcd start!ng\en". (int) arg) 16 for ( .) { 17 Pthread jnutex_lock(&clifdjnutex) 18 while (iget == iput) 19 Pthread_cond_wait(&clifd_cond &clifdjnutex): 20 connfd = clifdfiget] /* присоединенный сокет, который требуется обслужить */ 21 if (++iget == MAXNCLI) 22 iget = 0 23 Pthreadjnutexjinl ock (&cl i fdjnutex). 24 tptr[(int) arg] thread_count++ 25 web_child(connfd), /* обработка запроса */ 26 Close(connfd). Ожидание присоединенного сокета, который требует обслуживания 17-26 Каждый поток из пула пытается блокировать взаимное исключение, блокиру- ющее доступ к массиву clifd. Если после того, как взаимное исключение забло- кировано, оказывается, что индексы i put и i get равны, то вызывается функция pthread_cond_wait, и поток переходит в состояние ожидания, так как ему пока не- чего делать. После прибытия очередного клиентского запроса основной поток вызывает функцию pthread_cond_signal, выводя тем самым из состояния ожида- ния поток, заблокировавший взаимное исключение. Когда этот поток получает соединение, он вызывает функцию web chi 1 d. Значения времени центрального процессора, приведенные в табл. 27.1, пока- зывают, что эта версия сервера медленнее рассмотренной в предыдущем разделе (когда каждый поток из пула сам вызывал функцию accept). Причина заключает- ся в том, что рассматриваемая в данном разделе версия использует как взаимное исключение, так и условную переменную, тогда как в предыдущем случае (см. лис- тинг 27.23) применялось только взаимное исключение. Если мы рассмотрим гистограмму количества клиентов, обслуживаемых каж- дым потоком из пула, то окажется, что распределение клиентских запросов по потокам будет таким же, как показано в последнем столбце табл. 27.3. Это озна- чает, что если основной поток вызывает функцию pthread_cond_si gnа 1, то при вы- боре очередного потока, который будет выведен из состояния ожидания для об- служивания клиентского запроса, осуществляется последовательный перебор всех имеющихся свободных потоков.
27.13. Резюме 817 27.13. Резюме В этой главе мы рассмотрели 9 различных версий сервера и их работу с одним и тем же web-клиентом, чтобы сравнить значения времени центрального процес- сора, затраченного на управление процессом. 0. Последовательный сервер (точка отсчета — управление процессом отсутствует). 1. Параллельный сервер, по одному вызову функции fork для каждого клиента. 2. Предварительное порождение дочерних процессов, каждый из которых вы- зывает функцию accept. 3. Предварительное порождение дочерних процессов с блокировкой файла для защиты функции accept. 4. Предварительное порождение дочерних процессов с блокировкой взаимного исключения дочерними процессами для защиты функции accept. 5. Предварительное порождение дочерних процессов с передачей дескриптора от родительского процесса дочернему. 6. Параллельный сервер, поочередное создание потоков по мере поступления клиентских запросов. 7. Предварительное порождение потоков с блокировкой взаимного исключения потоками для защиты функции accept. 8. Предварительное порождение потоков, основной поток вызывает функцию accept. Резюмируя материал этой главы, можно сделать несколько комментариев. : Если сервер не слишком загружен, хорошо работает традиционная модель параллельного сервера, в которой при поступлении очередного клиентского запроса вызывается функция fork для создания нового дочернего процесса. Этот вариант допускает комбинирование с демоном i netd, принимающим все клиентские запросы. Остальные версии применимы в случае загруженных серверов, таких как web-серверы. i Создание пула дочерних процессов или потоков сокращает временные затра- ты центрального процессора по сравнению с традиционной моделью (один вызов функции fork для каждого запроса) в 10 и более раз. При этом не слиш- ком усложняется код, но становится необходимо (как говорилось при обсуж- дении примеров) отслеживать количество свободных дочерних процессов и корректировать его по мере необходимости, так как количество клиентских запросов, которые требуется обслужить, динамически изменяется. 8 Некоторые реализации допускают блокирование нескольких потоков или до- черних процессов в вызове функции accept, в то время как другие реализации требуют использования блокировки того или иного типа для защиты accept. Можно использовать для этого либо блокировку файла, либо блокировку вза- имного исключения Pthread. ft Как правило, версия, в которой каждый поток или дочерний процесс вызыва- ет функцию accept, проще и быстрее, чем версия, где вызов функции accept осуществляется только основным потоком (или родительским процессом), впоследствии передающим дескриптор присоединенного сокета другому по- току или дочернему процессу.
818 Глава 27. Альтернативное устройство клиента и сервера Блокировка всех дочерних процессов или программных потоков в вызове функции accept предпочтительнее, чем блокировка в вызове функции sei ect, что объясняется возможностью появления коллизий при вызове функции sei ect. Использование потоков, как правило, дает больший выигрыш во времени, чем использование процессов. Но выбор между версиями 1 и 6 (один дочерний процесс на каждый запрос и один поток на каждый запрос) зависит от свойств операционной системы и от того, какие еще программы задействованы в об- служивании клиентских запросов. Например, если сервер, принимающий кли- ентское соединение, вызывает функции fork и ехес, то может оказаться быст- рее породить с помощью функции fork процесс с одним потоком, чем процесс с несколькими потоками. Упражнения 1. Почему на рис. 27.2 родительский процесс оставляет присоединенный сокет открытым, вместо того чтобы закрыть его, когда созданы все дочерние про- цессы? 2. Попробуйте изменить сервер из раздела 27.9 таким образом, чтобы использо- вать дейтаграммный доменный сокет Unix вместо потокового сокета домена Unix. Что при этом изменяется? 3. Запустите клиент и те серверы из рассмотренных в этой главе, которые позво- ляет запустить конфигурация вашей системы, и сравните полученные резуль- таты с приведенными в тексте.
ЧАСТЬ 4 ХТГ. ТРАНСПОРТНЫЙ ИНТЕРФЕЙС X/OPEN
ГЛАВА 28 XTI: TCP-клиенты 28.1. Введение В первой главе книги на рис. 1.6 показано, что API сокетов был введен в 1983 году в реализации 4.2BSD и с самого начала работал с протоколами TCP/IP и домен- ными протоколами Unix. В середине 80-х до завершения работ над Posix. 1 в Unix- сообществе существовало противостояние между «Berkeley Unix» и «AT&T Unix». В кругах специалистов по сетям существовало мнение, что TCP/IP вскоре будет вытеснен протоколами модели OSI. В 1986 году AT&T ввела в операционной системе System V версии 3.0 (SVR3) другой сетевой API, названный TLI ( Transport Layer Interface — интерфейс транс- портного уровня). Хотя между TLI и сокетами имеется большое сходство, TLI был создан в соответствии с Определением транспортной службы модели взаимо- действия открытых систем {OSI Transport Service Def inition). Система SVR3 яви- лась и первым коммерческим выпуском потоковой подсистемы, о которой мы расскажем подробнее в главе 33. К сожалению, система SVR3 не содержала сете- вых протоколов, таких как TCP/IP: она содержала только потоки (streams) и стро- ительные блоки TLI. Это привело к тому, что сетевые протоколы для System V — как правило, TCP/IP и некоторые предварительные реализации протоколов мо- дели OSI — были предоставлены несколькими сторонними фирмами. И только в System V версии 4 (SVR4) в 1990 году протоколы TCP/IP были окончательно включены в базовую операционную систему. В разделе 1.10 мы упоминали группу Х/Open. В 1988 году она выпустила мо- дификацию TLI, названную XTI — Х/Ореп Transport Interface {транспортный интерфейс группы Х/Ореп). Интерфейс XTI, по существу, является расширен- ным вариантом протокола TLI, и в своем развитии он прошел несколько версий. Мы описываем XTI вместо TLI, потому что стандарт Posix.lg стартовал от XTI, а не от TLI. В нескольких последующих главах мы описываем XTI в определении для Unix 98 [74], почти идентичный XTI из Posix.lg. Для описания реализации протокола в XTI используется термин поставщик службы связи {communications provider). Наиболее распространенные поставщи- ки службы связи для интернет-протоколов — это TCP и UDP. Термин точка до- ступа службы связи {communications endpoint) относится к объекту, создаваемому и поддерживаемому поставщиком службы связи и затем используемому прило- жением. Ссылка на эти точки доступа службы связи производится при помощи файловых дескрипторов. Мы часто будем сокращать эти два термина, просто упот- ребляя слова поставщик и точка доступа.
28.2. Функция t open 821 ПРИМЕЧАНИЕ------------------------------------------------—-------- В протоколе TLI эти понятия носят названия «поставщик транспортной службы» (transport provider) и «точка доступа транспортной службы» (transport endpoint). Названия всех функций XTI начинаются с t_. Заголовочный файл, вклю- чаемый в приложение, в котором требуются все определения XTI, называется <xti h>. Некоторые специфичные для Интернета определения становятся дос- тупными после включения файла <xti_inet h>. Мы обсуждаем XTI в следующем порядке: TCP-клиенты. Функции имени и адреса. ТСР-серверы. Клиенты и серверы UDP. Параметры. Потоки. Дополнительные функции. Наше обсуждение XTI будет короче, чем обсуждение сокетов, поскольку для XTI применяются те же приемы сетевого программирования. Изменения сводятся к именам функций, аргументам функций и некоторым практически важным де- талям (например, принятию соединений TCP), поэтому нет необходимости по- вторять каждый из примеров сокетов с использованием XTI. 28.2. Функция t.open Первый шаг в создании конечной точки связи состоит в том, чтобы открыть уст- ройство Unix, идентифицирующее конкретного поставщика связи. Эта функция возвращает дескриптор (короткое целое), который используется другими функ- циями XTI. include <xt । h> include <fcntl h> int t_open(const char *pathname. int oflag struct t_info *info) Возвращает 0 в случае успешного выполнения -1 в случае ошибки Используемое текущее значение аргумента pathname зависит от реализации, но типичными значениями для точек доступа TCP/IP являются /dev/tcp, /dev/ udp и /dev/icmp. Типичные значения для точек доступа обратной связи {loopback endpoints) — это /dev/ticots, /dev/ticotsord и /dev/ticlts. Аргумент of 1 ag задает флаги открытия. Его значение — O RDWR. Для неблоки- руемой точки доступа этот флаг объединен с флагом O_NONBLOCK путем логическо- го сложения. ПРИМЕЧАНИЕ -------------------------------------------------------- Эта функция XTI аналогична функции socket. Обе возвращают файловый дескриптор, который связан с протоколом, заданным пользователем. Структура t_info — это совокупность целых значений, описывающих завися- щие от протокола свойства поставщика службы связи. Значение структуры воз- вращается посредством указателя info, если эго не пустой указатель. Это наша
822 Глава 28. XTI: TCP-клиенты первая встреча с одной из структур XTI, название которой начинается с t_. Всего имеется семь таких структур, о которых мы расскажем подробнее в разделе 28.4. struct t_info { t_scalar_t addr. /* максимальное число байтов в адресе коммуникационного протокола */ t_scalar_t t_scalar_t options. /* максимальное число байтов для транспортных параметров */ tsdu, /* максимальное число байтов в обслуживаемом блоке данных транспортного уровня (TSDU-transport service data unit) */ t_scalar_t t_scalar_t etsdu. /* максимальное число байтов в срочном TSDU (ETSDU) */ connect. /* максимальное число байтов данных, которые можно передать при запросе соединения */ t_scalar_t discon. /* максимальное число байтов данных которые можно передать при разрыве соединения */ t_scalar_t t_scalar_t servtype. /* тип поддерживаемой службы */ flags. /* другая информация (нововведение XTI) */ }. ПРИМЕЧАНИЕ-------------------------------------------------------- Это паша первая встреча с переменной нового типа t_scalar_t datatype, который впер- вые появился в Unix 98. В предшествующих реализациях все эти элементы относи- лись к типу long integer, что создает проблему в 64-разрядной архитектуре, которую мы уже обсуждали в разделе 1.11. Поэтому типы t_scalar_t и t_uscalar_t определены соответственно как int32_t и uint32_t. Прежде чем описывать каждый из элементов структуры t_info, мы приведем в табл. 28.1 и 28.2 некоторые их типичные значения для TCP и UDP с краткими пояснениями. Таблица 28.1. Значения полей структуры tjnfo для TCP AIX 4.2 Dunix 4.OB HP-UX 10.30 Solaris 2.6 UnixWare 2.1.2 addr 16 16 16 16 16 options 512 4096 1024 504 360 tsdu 0 0 0 0 0 etsdu -1 -1 -1 -1 -1 connect -2 -2 -2 -2 -2 discon -2 -2 -2 -2 -2 servtype T_COTS_ORD TCOTSORD T_COTS_ORD T COTS_ORD T_COTS_ORD Таблица 28.2. Значения полей структуры tjnfo для UDP AIX 4.2 Dunix 4.0B HP-UX 10.30 Solaris 2.6 UnixWare 2.1.2 addr 16 16 16 16 16 ' ' options 512 768 256 468 328 tsdu 8192 9216 65 508 65 508 65 508 etsdu -2 -2 -2 -2 -2 connect -2 -2 -2 -2 -2 discon -2 -2 -2 -2 -2 servtype T_CLTS T_CLTS T_CLTS T.CLTS T_CLTS Нас интересуют три значения для каждой из первых шести переменных в стру- ктуре t_info: любое неотрицательное знчение, -1 (также называемое T_INFIN ITE) п -2 (также называемое T_INVALID).
28.2. Функция t open 823 addr. Переменная задает максимальный размер (в байтах) адреса, специфич- ного для данного протокола. Значение -1 указывает, ч го пет ограничения раз- мера, -2 — что у пользователя нет доступа к адресам протокола. Значение 16, показанное в табл. 28.1 и 28.2 для TCP и UDP, — это длина струк- туры sockaddr_in. Для точки доступа протокола IPv6 это значение, вероятно, будет длиной структуры sockaddr_i пб. options. Указывает максимальное количество байтов для транспортных пара- метров. Значение -1 указывает, что нет ограничения размера, а значение -2 — что у пользователя нет доступа к параметрам. Мы поговорим подробнее о па- раметрах XTI в главе 32. Как мы можем видеть из примеров, между различными реализациями имеет- ся мало общего — значения изменяется в диапазоне о г 256 до 4096. & tsdu. TSDU обозначает блок данных транспортного уровня. Эта переменная указывает максимальное количество байтов в записи, размер которой зафик- сирован при передаче от одной точки доступа к другой. Нулевое значение ука- зывает, что поставщик службы связи не поддерживает концепцию передачи данных по транспортному уровню в виде блоков, хотя он поддерживает пото- ковую передачу байтов данных (то есть нет границ записей). Значение -1 ука- зывает, что нет ограничения размера блоков данных, а значение -2 — что не поддерживается передача обыкновенных данных (редкая ситуация). Для протокола TCP эта переменная всегда имеет нулевое значение, потому что этот протокол предоставляет службу потоковой передачи данных без ка- ких-либо границ записей. Преобладающее значение для протокола UDP, как видно из табл. 28.2, — 65 508, что является неверным. Поясним это подробнее. Максимальное значение дейтаграммы для протокола IP равно 65 535 байт (16-разрядная полная длина поля на рис. А.1), таким образом, максимальный размер дейтаграммы протокола UDP равен 65 535 минус 20 (длина IP-заго- ловка) и минус 8 (для UDP-заголовка), что дает 65 507. etsdu. ETSDU обозначает блок срочных данных транспортного уровня, а пере- менная etsdu задает максимальное количество байтов в отправленном блоке данных транспортного уровня. В главе 21 мы называли их внеполосными {out- of-band data), или срочными {expediteddata), данными. Нулевое значение ука- зывает, что поставщик службы связи пе поддерживает концепцию ETSDU, хотя он поддерживает потоковую передачу внеполосных данных (то есть гра- ницы записей во внеполосных данных не сохраняются). Значение -1 указы- вает, что нет ограничения на размер данных. Значение -2 говорит о том, что передача срочных данных не поддерживается. Как мы и предполагали, протокол UDP не поддерживает никакой формы внеполосных данных. Протокол TCP поддерживает эту концепцию, но в нем нет ограничения на размер внеполосных данных, которые может послать при- ложение. В связи с этим вспомните наше обсуждение срочного режима (urgent mode) в разделе 21.2. & connect. Некоторые ориентированные на установление соединения протоко- лы поддерживают передачу данных пользователя вместе с запросом на соеди- нение. Эта переменная указывает максимальный размер таких данных. Зна- чение -1 указывает, что нет ограничения па размер данных, а значение -2 —что поставщик службы связи не поддерживает эту возможность.
824 Глава 28 XTI TCP-клиенты Протокол TCP не поддерживает эту возможность, поэтому для него значение данной переменной всегда -2, а так как протокол UDP не является протоко- лом, ориентированным на установление соединения, то для него эта перемен- ная также имеет значение -2 Протокол транспортною уровня модели OSI ори- ентирован на установление соединения и поддерживает такую возможность ПРИМЕЧАНИЕ---------------------------------------------------------- Заметим что проюкол TCP разрешает посыпать данные вместе с сыменюм SYN как показано на с 14-16 киши (95J Сокеты и XTI однако, не предостав гяюг способа зас- тавить TCP послать данные вмесп е с cei мен) ом SYN Тем пе менее этот элемент структуры t_mfo относится к чему-то другому (например к возможности, предоставляемой протоколом транспортов уровня модели OSI) di scon Некоторые ориентированные на установление соединения протоколы поддерживают передачу данных пользователя вместе с запросом на разрыв соединения Мы увидим эту возможность, когда будем обсуждать функции t_snddis и t_rcvdis далее в этой главе Эта переменная указывает наибольший размер таких данных Значение -1 укатывает, чго нет ограничения па этот размер Значение -2 говорит о том, что поставщик службы связи не поддер- живает такую возможность Эта переменная также указывает размер данных пользователя, которые могут быть посланы при нормальном завершении (oi dei ly i elease) с использованием функции t_sndreldata и t_rcvreldata, о которых рас- сказывается в разделе 34 10 Протокол TCP не поддерживает это свойство, но оно поддерживается прото- колами транспортного уровня модели OS1 servtype Эта переменная указывает гип службы, предоставляемой поставщи- ком службы связи Она может принимать одно из трех значении, показанных в табл 28 3 Таблица 28.3. Типы служб, предоставляемых поставщиками службы связи servtype Описание T COTS Ориентированная на установление соединения служба без нормальною завершения (orderly it lease) T COTS ORD Ориентированная па установление соединения служба с нормальным завершением (orderly release) T CLTS Служба бет поддержки уст аиовления с оединсиия Протокол TCP является ориепт ированным на установление соединения с под- держкой нормального завершения, а протокол UDP относится к службам без поддержки установления соединения fl ags Этот элемент структуры является новым для XTI и задает дополнитель- ные флаги для поставщика службы связи Через этот элемент мог ут быть пе- реданы (возвращены) две константы, представленные в табл 28 4 и определя- емые путем включения заголовочного файла <xtr h> Протокол TCP не поддерживает записи нулевой длины а протокол UDP под держивает (получается 28-байтовая IP-дейтаграмма, содержащая IP-заголо- вок и UDP-заголовок и не содержащая никаких данных) Протокол TCP так- же не поддерживает флаг T_ORDRELDATA
28 3 Функции t error и t strerror 825 Таблица 28.4. Значения элемента flags структуры t info Flag Описание TSENDZERO TORDRELDATA Поставщик поддерживает записи нулевой длины Поставщик поддерживает передачу данных при завершении соединения константы t_sndrcldata и t rcvreldata 28.3. Функции t_error и t_strerror Напомним, что большинство функции сокетов (например, socket, bind, connect и другие) возвращают значение -1, koi да встречают ошибку, и присваивают то или иное значение переменной еггпо, чтобы дать дополнительную информацию об ошибке Функции протокола XTI обычно возвращают значение -1 в случае ошибки и задают некоторое значение переменной t errno Напомним наше об- суждение переменной еггпо в разделе 23 1 и, в частности, то, что для каждого по- тока есть своя переменная еггпо (per thiead-vanable) Каждый поток также имеет свою переменную t_errno Переменная t_errno аналогична переменной еггпо в том, что ей присваивается значение, только если имеет место ошибка, и ее значение не сбрасывается при успешном завершении вызова функции Все коды ошибок протокола XTI определяются во включаемом заголовочном файле <xti h> и начинаются с буквы Т, как, например, TBADADDR (неправильный формат адреса), TBADF (недопустимое значение транспортного дескриптора) и т д Существует одно специальное значение ошибки — TSYSERR Когда переменная t_errno возвращается с этим значением, это указывает приложению, что надо по- смотреть на значение переменной еггпо, чтобы получить значение системной ошибки Две функции, t_error и t_strerror предназначены для того, чтобы задать фор- мат сообщения об ошибках, получаемых о г функции протокола XTI #include <xti h> int t_error (const char *msg) Возвращает 0 const char *t_strerror (int errnum) Возвращает указатель на сообщение Функция t_error посылает сообщение на стандартное устройство вывода со- общений об ошибках Это сообщение состоит из строки, на которую указывает указатель msg (предполагается, что этот указатель непустой), заканчивающейся двоеточием и пробелом За ней идет строка сообщения, соответствующая теку- щему значению переменной t_errno Если значение переменной t_errno равно TSYSERR, то выводится еще строка сообщения, соответствующая текущему значе- нию переменной еггпо В заключение выводится символ перевода строки Функция t_strerror возвращает строку, описывающую значение переменной errnum, которое, как предполагается, принимает одно из возможных для t_errno значений В отличие от функции t_error, функция t_strerror не делает ничего особенного, если это значение — TSYSERR Программа в листинге 28 11 показывает применение этих двух функций выво- да сообщений об ошибках протокола XTI вместе с нашей функцией err xti (По- следнюю функцию мы рассматриваем в разделе Г 4 ) Все исходные коды npoipaMM опубликованные в этой книге вы можете паити по адресу http// www piter com/download
826 Глава 28 XTI: TCP-клиенты Листинг 28.1. Пример использования функций t erro и t_strerro //xtnntro/strerror с 1 include "unpxti h" 2 int 3 main(int argc char **argv) 4 { 5 printfC"&s\en' t_strerror(TPROTO)) 6 errno = ETIMEDOUT 7 printfC'&s\en ' t_strerror(TSYSERR)). 8 t_errno = TSYSERR 9 errno = ETIMEDDUT 10 t_error("t_errorsays“) 11 t_errno = TSYSERR 12 errno = ETIMEDOUT. 13 err_xti("err_xti says") 14 exit(O) 15 } Эта программа выводит следующее: aix % strerror XTI protocol error system error t_error says system error Connection timed out err_xti says system error Connection timed out 28.4. Структуры netbuf и структуры протокола XTI Протокол XTI определяет семь структур, используемых для передачи информа- ции между приложением и функциями протокола XTI. Одна из них, структура t_info, которую мы описали в разделе 28 2, представляет собой набор целых зна- чений, описывающих зависящие от протокола свойства поставщика службы свя- зи. Оставшиеся шесть структур, в свою очередь, содержат каждая от двух до трех структур netbuf. Структура netbuf определяет буфер, используемый для передачи данных от приложения в функции XTI или в обратном направлении. struct netbuf { unsigned int maxlen. /* максимальный размер буфера buf */ unsigned int len /* фактическое количество данных в буфере buf */ void *buf /* данные (до введения Posix 1g имели тип char*) */ } В табл. 28.5 приведены шесть структур XTI, содержащие одну или более струк- туру netbuf и другие элементы структур XTI. Эти шесть структур протокола XTI, содержащие внутри себя структуры netbuf, всегда передаются между приложением и функциями XTI по ссылке. Это значит, что в качестве аргумента функции XTI мы передаем адрес структуры XTI. По- этому функции XTI всегда могут прочесть и изменить любой из трех элементов структуры netbuf (хотя ни одна из функций не меняет элемент maxlen). Использование трех структур netbuf зависит от направления, в котором пере- дается информация: от приложения к функциям XTI или наоборот. Это отраже- но в табл. 28.6, в которой также отмечено, читает функция XTI значение элемен- та или записывает его значение.
28.5. Функция t bind 827 Таблица 28.5. Шесть структур протокола XTI и их элементы Тип данных t_bind tcall Структуры протокола XTI t_unitdata t_discon t_optmgmt t_uderr Struct netbuf addr addr addr addr Struct netbuf opt opt opt opt Struct netbuf udata udata udata t scalar t error t scalar t ) flags unsigned int qlen int reason 1 int sequence sequence Таблица 28.6. Обработка трех элементов структуры netbuf Элемент Передача данных от приложения к XTI Передача данных от XTI к приложению maxlen Игнорируется Только читается Размер буфера, иа который указывает buf Функция XTI поместит в буфер данные не более указанного количества Если значение равно нулю, то ничего не возвращается и значения len и buf игнорируются len Только читается Приложение присваивает этому элементу количество данных, иа которые указывает buf Только записывается Функция XTI присваивает этому элементу фактическое количество данных находящихся в буфере, и оно всегда должно быть меньше или равно maxlen buf Указатель на данные, приготов- ленные приложением, которые будут обрабатываться функцией ХТ1 Укагатель на данные, приготовленные функцией XTI, которые будут обрабатываться приложением Если XTI-функция возвращает больше данных, чем позволяет переменная шах! еп, то вызов функции завершается аварийно и переменная t_errno принимает значение TBUFOVFLW. Так как адрес структуры netbuf всегда передается функции XTI и структура содержит и размер (maxiеп) буфера, и количество данных (len), фактически нахо- дящихся в буфере, то функциям XTI не обязательно иметь аргументы типа «зна- чение-результат» (value-result), как это имеет место для функций сокетов. 28.5. Функция t.bind Эта функция присваивает локальный адрес точке доступа и активизирует ее. В слу- чае протоколов TCP или UDP локальный адрес — это IP-адрес и номер порта, include <xti h> int t_bind (int fd const struct t_bind *request struct t_bind ★return') Возвращает 0 в случае успешного выполнения -1 в случае ошибки Второй и третий аргументы указывают на структуры функции t_bi nd: struct t_bind { struct netbuf addr /* адрес, зависящий от протокола*/
828 Глава 28 XTI TCP-клиенты unsigned int qlen /* максимальное число возможных соединении (если сервер) */ } Точка доступа указывается переменной fd Для аргумента request следует рас- смотреть три случая request = NULL Вызывающий процесс не заботится о том, какой локальный адрес присваивается точке доступа Поставщик службы связи сам выбирает адрес Элементу ql еп присваивается нулевое значение (подробнее мы погово- рим об этом далее) request |= NULL и request->addr len == О Вызывающий процесс не заботится о том, какой локальный адрес присваивается точке доступа, и поставщик служ- бы связи сам выбирает адрес Но в отличие от предыдущего случая, вызываю- щий процесс может задать ненулевое значение для элемента qlen структуры request request '= NULL и request >addr len > 0 Вызывающая программа указывает поставщику службы связи, какой локальный адрес присвоить точке доступа службы связи В обоих случаях (адрес указывается приложением или выбирается поставщи- ком связи) поставщик связи возвращает адрес, который он присвоил точке до- ступа, в структуре return Если аргумент return является пустым указателем, по- ставщик связи не возвращает фактический адрес Значение элемента ql еп имеет смысл только для сервера, ориентированного на установление соединения оно указывает максимальное количество соедине- ний, которое может быть установлено в очередь для этой точки доступа Это зна- чение может быть изменено поставщиком службы связи, и в таком случае элемент qlen структуры return указывает фактическое число соединений, поддерживае- мых поставщиком Мы расскажем подробнее о значениях элемента ql еп и об из- мерении фактического числа соединений, поставленных в очередь, обсуждая табл 30 2 ПРИМ ЕЧАНИЕ -------------------------------------------------------------- Если протокол XTI не может связать (присвоить) запрошенный адрес, то возвращает- ся ошибка TADDRBUSY Если протокот TLI сталкивается с згой проблемой, го он может присвоить другой локальный адрес точке доступа, что приводит к необходимо- сти сравнивать присвоенный адрес с запрошенным Используемый в протоколе XTI метод с указанием поставщику на необходимость вы- бора подходящего адреса является более общим, чем метод связывания с адресом (bind) Например, используя протоколы TCP и UDP в IPv4, мы должны указать поставщику службы связи семеис гво адресов INADDR_ANY и нулевой порт для выбора локально- го адреса Этот подход специфичен для протокола IPv4 и является недостаточно об- щим Значение элемента qlen соответствует аргументу backlog, задаваемому для функции listen Для сервера, ориентированного на установление соединения, функция t_bind выполняет то же деист вие, что функции bind и listen Клиент XTI ориентированный на установление соединения должен вызывать функ- цию t_bmd перед вызовом функции t_connect (которую мы опишем далее) В этом заключается отличие от функции connect, которая вызывает функцию bind самостоя- тельно, если сокет не был связан предварительным вызовом bind
28 6 Функция tconnect 829 Заметим, что элемент addr структуры tjtnnd — это действительно структура netbuf, а не указатель на одну из этих структур Мы увидим, что это общее свой- ство для рассматриваемых структур XTI большинство из них содержат одну или более структур netbuf внутри структуры t_XXX 28.6. Функция t.connect Клиент, ориентированный на установление соединения, инициирует соединение с сервером путем вызова функции t connect Клиент задает адрес протокола сер- вера (например, IP-адрес и порт для ТСР-сервера) nclude <xti h> int t_connect (int fd const struct t_call *sendcall struct t_call ★recvcall} Возвращает 0 в случае успешного выполнения -1 в случае ошибки Второй и третий аргументы функции указывают на структуру t eal 1 struct t_call { struct netbuf addr /* адрес определяемый типом протокола *) struct netbuf opt /* параметры определяемые типом протокола */ struct netbuf udata /* данные пользователя которые должны сопровождать запрос на соединение */ int sequence /* для функции t_listen() и t acceptO */ } Структура t_call, на которую указывает аргумент sendeal 1, задает информа- цию, необходимую поставщику транспортных служб для установления соедине- ния Структура addr задает адрес сервера Структура opt задает параметры, специфичные для данного протокола, по желанию пользователя Структура udata содержит любые данные ноль юва геля, которые должны быть переданы серверу при установлении соединения (Напомним, что, как пока- зано в табл 28 1, протокол TCP не поддерживает передачу данных пользова- теля при запросе на соединение ) Элемент sequence не имеет значения для этой функции, но он используется, когда структура t eal 1 применяется с функцией t_accept При возврате из этой функции структура t call, на которую указывает аргу- мент reeveal 1, содержит информацию о соединении, возвращаемую пользовате- лю поставщиком службы связи Структура addr содержит адрес процесса-собеседника (peei piocess~) Структура opt содержит любые параметры, специфичные для данного прото- кола связанные с соединением Структура udata содержит любые данные пользователя возвращенные постав- щиком транспортной службы процесса-собеседника при установлении соеди- нения Элемент sequence не имеет значения Содержание структуры opt зависит от протокола Пользователь можег задать нулевое значение поля 1 еп этой структуры, указывая тем самым поставщику служ- бы связи, что для всех параметров соединения надо использовать значения
830 Глава 28. XTI: TCP-клиенты заданные по умолчанию. Мы обсудим параметры протокола XTI подробнее в гла- ве 32. Пользователь может задать аргумент recvcall как пустой указатель, если он не заинтересован в возвращении информации о соединении. По умолчанию эта функция не возвращает управление, пока не будет уста- новлено соединение или пока не произойдет ошибка. Мы обсудим, как устано- вить неблокируемое соединение, в разделе 34.3. Мы видели в разделе 4.3, что наиболее распространенные ошибки при уста- новлении соединения — это получение сегмента RST, получение сообщения ICMP Destination unreachable (Получатель недоступен) и отсутствие ответа в течение определенного интервала времени. К сожалению, когда возникает одна из этих ошибок, функция t_connect возвращает значение -1, а переменная t_errno прини- мает значение TLOOK, что требует дополнительных данных для определения точ- ной причины. Мы обсудим эту проблему в разделах 28.9 и 28.10 и рассмотрим пример в листинге 28.2. ПРИМЕЧАНИЕ --------------------------------------------------------- Функция t connect протокола XTI аналогична функции connect протокола TCP. 28.7. Функции t_rcv и t_snd По умолчанию приложения XTI не могут вызывать обычные функции read и wn te (до тех пор, пока модуль ti rdwr не помещен в поток, как мы покажем в разделе 28.12). Вместо этих функций приложения XTI должны вызывать функции t_rcv и t_snd. fiwclude <xti h> int trcv (int fd void *buff. unsigned int nbytes. int *flagsp) . int t_snd (int fd const void *buff. unsigned int nbytes. int flags') . Обе функции возвращают число прочитанных или записанных байтов в случае успешного выполнения, -1 в случае ошибки Первые три аргумента аналогичны первым трем аргументам функций read и write: дескриптор, указатель на буфер и число байтов, которые надо прочитать пли записать. ПРИМЕЧАНИЕ ----------------------------------------------------------- Все функции ввода и вывода API сокетов используют тин данных sizc_t для размера буфера и тип данных ssize t для возвращаемого значения. Функции XTI используют соответственно unsigned int и int. Значение аргумента (флага) fl ags функции tsnd либо равно нулю, либо явля- ется комбинацией констант, приведенных в табл. 28.7. Таблица 28.7. Значения флагов flags и flagsp для функций t_rcv и t_snd Флаг Описание '[' EXPEDITED Послать или принять внеполосные данные Т MORE Еще есть данные для от правки или приема Константа T EXPEDITED используется с функцией t_snd для отправки внеполос- ных данных (см. раздел 34.12). Этот флаг устанавливается при возврате из функ- ции г rrv когпя полvMeiibi внеполосные данные.
28.7. Функции t rcv и t snd 831 Константа T_MORE используется для того, чтобы при множественных вызовах функции t_rcv или t_snd могли читать или писать то, что протокол рассматривает как логическую запись. Это свойство применимо только для протоколов с под- держкой записей. Мы покажем пример использования этого флага с функцией t_rcvudata и ориентированным на записи протоколом UDP в листинге 31.6. Этот флаг также используется с протоколом TCP при чтении внеполосных данных, как мы покажем в разделе 34.12, но с обычными данными протокола TCP он ни- когда не используется. ПРИМЕЧАНИЕ--------------------------------------------------------------- Протокол XTI определяет флаг T_PUSH, сообщающий поставщику службы связи, что надо отправить все накопленные данные, которые еще не отправлены. Этот флаг при- меняется протоколом XTI для SNA (IBM's Systems Network Architecture — архитекту- ра сетевых систем фирмы IBM), но не может использоваться протоколом TCP, так как он не заставляет протокол TCP устанавливать флаг PUSH. Заметим, что аргумент fl ags функции t_snd является целочисленным значе- нием, а соответствующий аргумент функции t_rcv — указателем на целое число. Но значение, на которое указывает fl agsp для функции t_rcv, не является истин- ным аргументом типа «значение-результат», поскольку не проверяется функци- ей, а только устанавливается ею при завершении. Обе эти функции возвращают фактическое число прочитанных или записан- ных байтов. Возвращаемое функцией t_snd значение может быть меньше nbytes, если точка доступа является неблокируемой или если процесс полу- чил сигнал. ПРИМЕЧАНИЕ--------------------------------------------------------------- Эти две функции соответствуют функциям send и recv. Флаг T_EXPEDITED прото- кола XTI соответствует MSG_OOB, хотя средствами протокола XTI мы не можем за- дать этот флаг для функции t_rcv. Напомним, что для сокета TCP получение сегмента FIN заставляет функцию read вернуть нулевое значение, а получение сегмента RST заставляет функцию read вернуть значение -1, присвоив переменной errno значение ECONNRESET. Функция t_rcv ведет себя иначе, когда одно из этих условий имеет место в точке доступа протокола XTI. Если сегмент FIN протокола TCP получен в точке доступа протокола XTI, то функция t_rcv возвращает значение -1, причем переменная t_errno принима- ет значение TLOOK. Затем должна быть вызвана функция t_l ook протокола XTI, которая вернет значение T_ORDREL. Это называется признаком нормального за- вершения {orderly release indication'). & Если сегмент RST протокола TCP получен в точке доступа протокола XTI, то функция t_rcv возвращает значение -1, а переменная t errno принимает зна- чение TLOOK. Затем должна быть вызвана функция t_l ook протокола XTI, которая вернет значение T_DISCONNECT. Это называется разрывом соединения {disconnect) или аварийным завершением {abortive release). Сначала мы обсудим функцию t_look, а затем функции нормального и ава- рийного завершения.
832 Глава 28. XTI: TCP-клиенты 28.8. Функция tjook В точке доступа протокола XTI могут происходить различные события (events). Эти события могут происходить асинхронно (asynchronously). Под этим подразу- мевается, что приложение может быть занято выполнением некоторого задания, когда в точке доступа происходит не связанное с этим заданием событие. Некото- рые события указывают на возникновение аварийной ситуации (например, TJJDERR сообщает об ошибке в уже отправленной дейтаграмме), в то время как другие собы- тия не являются ошибками (T EXDATA сообщает о приходе срочных данных). Например, представим себе, что приложение вызывает функцию t_snd, чтобы отправить данные собеседнику, но непосредственно перед этим на узле собесед- ника что-то происходит, и процесс на узле собседника посылает сегмент RST и завершается. Информация о неожиданном событии (получение сегмента RST, когда приложение вызывает функцию t_snd) передается приложению путем при- сваивания значения -1 коду возврата функции t_snd и присваивания значения TLOOK переменной t_errno. Приложение вызывает функцию t_look, чтобы опреде- лить, какое событие произошло в точке доступа. В рассмотренном примере этим событием будет ^DISCONNECT — прием извещения о разрыве соединения (RST). #include <xti h> int tjook (int fd) . Возвращает константу обозначающую событие (табл 28 8) в случае успешного выполнения. -1 в случае ошибки Целочисленное значение, возвращаемое этой функцией, соответствует одно- му из девяти событий, приведенных в табл. 28.8. Таблица 28.8. События, возникающие в точке доступа протокола XTI Событие Описание TCONNECT Получено подтверждение об установлении соединения T_DATA Получены обычные данные T_DISCONNECT Получено сообщение о разрыве соединения TEXDATA Получены срочные данные TGODATA Сняты ограничения управления потоком для обычных данных TGOEXDATA Сняты ограничения управления потоком для срочных данных TLISTEN Получено сообщение о соединении TORDREL Получен признак нормального завершения TUDERR Ошибка в ранее посланной дейтаграмме Когда в точке доступа протокола XTI происходит событие, оно считается ожи- дающим обработки (outstanding), до тех пор пока не будет обработано (consumed). В табл. 28.9 приведены функции обработки событий XTI, а также показаны два события, обрабатывамые вызовом функции tjook. Таблица 28.9. События протокола XTI и функции, которые их обрабатывают Событие Сбрасывается функцией tjook Функции обработки TCONNECT t connect, t rcvconnect TDATA t rev, t rev., trevudata, trewudata
28.9. Функции t sndrel и t rcvrel 833 Событие Сбрасывается функцией tjook Функции обработки TDISCONNECT t_rcvdis TEXDATA t rcv, t rcw TGODATA Да t snd, tsndv, t sndudata, t_sndvudata TGOEXDATA Да t_snd, t sndv TLISTEN tlisten T_ORDREL trcvrel T_UDERR trcvuderr Для функции t_connect в блокируемой точке доступа (по умолчанию) собы- тие T CONNECT обрабатывается самой функцией и невидимо для приложения. При- менительно к рассмотренному примеру (получение сегмента FIN при вызове функции t_snd) приведенная таблица показывает, что для обработки события мы должны вызвать функцию t_rcvrel. ПРИМЕЧАНИЕ --------------------------------------------------- В начале этого раздела мы показали, как получение сегмента RST генерирует сообще- ние T_DISCONNECT для точки доступа. До появления Unix 98 получение сегмента FIN генерировало событие T_ORDREL для точки доступа. В Unix 98 делать это стало не обязательно. 28.9. Функции t_sndrel и t_rcvrel Протокол XTI поддерживает два способа завершения соединения: нормальное завершение {orderly release) и аварийное завершение {abortive release). Разница за- ключается в том, что аварийное завершение не гарантирует доставку необрабо- танных данных, в то время как нормальное завершение гарантирует это. Все по- ставщики служб связи должны поддерживать аварийное завершение, а поддержка нормального завершения не является обязательной. В связи с этим напомним, что, как следует из табл. 28.1, протокол TCP предоставляет возможность нормаль- ного завершения. Мы можем посылать и принимать сообщение о нормальном завершении с помощью следующих функций: #include <xti.fi> int t_sndrel(int fd) int t_rcvrel(int fd) . Обе функции возвращаю! О в случае успешного выполнения. -1 в случае ошибки Для того чтобы понять семантику нормального завершения, следует вспом- нить, что ориентированные на установление соединения протоколы обычно обес- печивают двустороннее соединение между двумя процессами. Передача данных в одном направлении происходит независимо от другого направления. На рис. 28.1 показано одно из применений этих функций с протоколом TCP, использующее преимущество техники половинного закрытия {half-close), обеспечиваемой этим протоколом. Процесс инициирует нормальное завершение вызовом функции t_sndrel. Тем самым поставщик связи извещается о том, что приложение больше не имеет дан- ных для отправки через эту точку доступа. В точке доступа TCP протокол посы- лает сегмент FIN процессу-собеседнику после отправки ему данных, находившихся
834 Глава 28. XTI: TCP-клиенты Рис. 28.1. Использование протоколом XTI техники половинного закрытия протокола TCP в очереди. Процесс, вызвавший функцию t_sndrel, может продолжать прием дан- ных — он все еще может читать из сокета, но уже не может записывать в него. ПРИМЕЧАНИЕ -------------------------------------------------------- Эта функция выполняет то же действие, что и функция shutdown со вторым аргумен- том SHUT WR на сокете TCP. Процесс-собеседник подтверждает получение сообщения о завершении соеди- нения вызовом функции t_rcvrel. Этот процесс все еще может записывать в со- кет, но уже не может читать из него. ПРИМЕЧАНИЕ -------------------------------------------------------- Для API сокетов нет аналогов функции t rcvrel. Информация о получении сегмента FIN передается процессу в виде признака конца файла (то есть функция read возвра- щает пулевое значение). Это свойство XTI заставляет приложение работать с двусторонним нормальным за- вершением, даже если приложение не заинтересовано в использовании этого свойства, как мы увидим в примере из листинга 28.2, 28.10. Функции t_snddis и trcvdis Следующие две функции обрабатывают аварийное завершение (разрыв соедине- ния): include <xti h> int t_snddis(int fd. const struct t_call *cdll) int t_rcvdis(int fd. struct t_discon ★discon') . Обе функции возвращают 0 в случае успешного выполнения. -1 в случае ошибки Функция t_snddi s используется с двумя целями: для выполнения аварийного завершения существующего соединения что в терминах TCP вызывает отправку сегмента RST; для отказа при получении запроса на соединение.
28.11. Клиент времени и даты для протоколов XTI и TCP 835 Для аварийного завершения существующего соединения аргумент cal 1 может быть пустым указателем — в этом случае информация процессу-собеседнику не посылается. Интерпретация полей структуры t eal 1 для противоположного слу- чая приведена в табл. 28.10. Таблица 28.10. Использование элементов структуры t_call для функции tsnddis Элемент Разрыв существующего соединения Отказ от нового соединения addr Игнорируется И( норируется opt Игнорируется Игнорируется udata Необязательный Необязательный sequence Игнорируется Обязательный Необязательный элемент udata указывает данные пользователя, которые долж- ны пересылаться при разрыве соединения. Но в табл. 28.1 мы видели (речь идет об элементе di scon структуры t i nfo), что это не поддерживается протоколом TCP. ПРИМЕЧАНИЕ--------------------------------------------------------------- Напомним, что для генерации аварийного завершения приложение сокета должно ус- тановить параметр сокета SO_LINGER, присвоить элемешу l_onoff непулевое значе- ние, обнулить элемент l lmger, а затем закрыть сокет (см главу 7). Когда в точке доступа протокола XTI происходит событие T_DISCONNECT (про- токол TCP получает сегмент RST), то приложение должно принять аварийное завершение соединения путем вызова функции t rcvdis. Если аргумент di scon является непустым указателем, то структура t_discon должна содержать код при- чины возникновения аварийного завершения. struct tjjiscon { struct netbuf udata /* данные пользователя */ Tnt reason. /* специфичный для протокола код причины завершения соединения */ int sequence. Элемент udata содержит необязательные данные пользователя, которые сопро- вождают разрыв соединения, элемент reason — зависящий от протокола код при- чины разрыва соединения, а элемент sequence применим только для серверов, при- нимающих соединения. ПРИМЕЧАНИЕ--------------------------------------------------------------- Среди функций API сокетов нет аналогов функции t_rcvdis. Для них получение сег- мента RST передается процессу как ошибка ввода (например, функция read возвраща- ет значение -1) с переменной еппо, установленной в ECONNRESET. Запись в сокет, получивший сегмент RST, генерирует возникновение сигнала SIGPIPE. 28.11. Клиент времени и даты для протоколов XT! и TCP Сейчас мы перепишем нашу программу клиента для протокола TCP, приве- денную в листинге 1.1, используя протокол XTI. Результат приведен в лис- тинге 28.2.
Б36 1лава28. All: 1СР-клиенты Листинг 28.2. Клиент времени и даты, использующий протокол XTI //xti1ntro/daytimecli01 с 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 nclude "unpxti h” int main(int argc, char **argv) ( int tfd. n. flags char recvline[MAXLINE + 1]. struct sockaddr_in servaddr. struct t_call tcall. struct t_discon tdiscon. if (argc l= 2) err_quit("usage daytimecliOl <1Радрес>”); tfd = T_open(XTI_TCP. O_RDWR. NULL). T_bind(tfd NULL. NULL). bzero(&servaddr sizeof(servaddr)). servaddr sin_family = AF_INET. servaddr sin_port = htons(13). /* сервер времени и даты *f Inet_pton(AF_INET. argv[l] &servaddr sin_addr) tcall addr maxlen = sizeof(servaddr) tcall addr len = sizeof(servaddr) tcall addr buf = &servaddr tcall opt len = 0. /* нет параметров при соединении */ tcall udata len = 0. /* нет данных пользователя при соединим */ if (t_connect(tfd Steal 1 NULL) < 0) { if (t_errno = TLOOK) { if ( (n = TJook(tfd)) == T_DISCONNECT) { tdiscon udata maxlen = 0 T_rcvdis(tfd Stdiscon) errno = tdiscon reason. err_sys(“t_connect error"). } else err_quit("unexpected event after t_connect W" n). } else err_xti("t_connect error”). } for (..) { if ( (n = t_rcv(tfd. recvline MAXLINE. Sflags-)) < Q) { , if (t_errno == TLOOK) { if ( (n = TJook(tfd)) == T-ORDREL) { T_rcvrel (tfd). break. } else if (n == ^DISCONNECT) { tdiscon udata maxlen = 0. T_rcvdis(tfd. Stdiscon). errno = tdiscon reason /* вероятно. ECONNRESETs*7 err_sys("serever terminated prematurely"), } else err_quit("unexpected event after t_rcv M" n). } else
28.11. Клиент времени и даты для протоколов XTI и TCP 837 49 err_xti("t_rcv error") 50 } 51 recvline[n] =0 /* завершающий нуль */ 52 fputs(recvline, stdout). 53 } 54 exit(0). 55 } Заголовочный файл unpxti.h 1 Мы определили наш собственный заголовочный файл, включаемый во все наши программы директивой #i ncl ude. Этот файл приведен в разделе Г.З. Создание точки доступа и связывание ее с локальным адресом 12-13 Функция t_open создает точку доступа протокола XTI. Мы разрешаем системе выбрать локальный адрес протокола, вызывая функцию t_bi nd с нулевым значе- нием второго аргумента. Указание адреса и порта сервера 14-22 Мы заполняем структуру адреса сокета Интернета значениями IP-адреса и порта сервера аналогично тому, как это делалось в листинге 1.1. Затем мы заполняем структуру t_cal 1, чтобы передать указатель назначение адреса этого сокета, а также задаем нулевые значения элементов len для структур opt и udata, указывая, что параметры и данные пользователя отсутствуют. ПРИМЕЧАНИЕ -------------------------------------------------- В протоколе XTI не требуется задавать в структуре t_call указатель на структуру sockaddrin для IPv4. Тем не менее почти все реализации Unix воплощают XTI с про- токолами Интернета, используя структуру sockaddr in для передачи адреса протокола между приложением и поставщиком службы связи. В коде программы, не зависящем от протокола, использование этой структуры должно быть скрыто о г приложения. В сле- дующей главе мы покажем, как это сделать. Установление соединения 23-34 Функция t_connect устанавливает соединение, которое в данном случае выпол- няется по схеме трехэтапного рукопожатия TCP. Как упоминалось ранее, если установление соединения заканчивается неудачно с одной из стандартных оши- бок, то функция t connect возвращает значение TLOOK. Далее мы вызываем функцию t look, чтобы определить произошедшее событие, и если это событие T_DISCONNECT, дополнительно вызываем функцию t rcvdis. В этом случае мы также сохраняем код разрыва соединения в переменной errno и вызываем нашу функцию err_sys, чтобы вывести соответствующее сообщение об ошибке. Чтение с сервера и копирование на устройство стандартного вывода 35-53 Мы считываем данные с сервера при помощи функции t_rcv и выводим^МХ^а стандартное устройство вывода до тех пор, пока не встретим конец соёдммпцВД
838 Глава 28. XTI: TCP-клиенты Обработка нормального завершения соединения и разрыва соединения -47 Если функция t_rcv возвращает ошибку и t_errno равна TLOOK, то мы вызываем функцию t_look, чтобы получить значение текущего события для точки доступа. Если это событие — T_ORDREL, мы вызываем функцию t_rcvrel, а если TJHSCONNECT — функцию t_rcvdis. Если мы запустим эту программу на таком же наборе узлов, что и в разде- ле 4.3, то получим следующие результаты. Сначала обратимся к узлу, на котором работает наш сервер времени и даты, и увидим обычный вывод: umxware X daytimecliOl 206.62.226.35 Tue Feb 4 15 00 26 1997 Затем мы укажем несуществующий узел в локальной сети: umxware % daytimecliOl 206.62.226.55 t_connect error Connection timed out Далее мы возьмем маршрутизатор, на котором не работает наш сервер време- ни и даты, и получим RST в ответ на наш SYN: umxware К, daytimecliOl 140.252.1.4 t_connect error Connection refused В заключение мы зададим IP-адрес, который не связан с Интернетом, и в от- вет на наши сегменты SYN получим сообщение ICMP о недоступности узла: umxware % daytimecliOl 192 3 4 5 t_connect error No route to host Возможность взаимодействия сокетов и XTI Заметим, что в первом из рассмотренных выше примеров наш клиент времени и даты, созданный средствами XTI, соединяется с сервером на узле bsdi (206.62. 226.35), написанным с применением сокетов. Тем не менее клиент правильно свя- зывается с сервером. Аналогично мы можем написать сервер времени и даты, ис- пользующий XTI, и он будет корректно связываться с нашим клиентом из лис- тинга 1.1, который использует сокеты. Эта возможность взаимодействия предоставляется набором протоколов Ин- тернета и не связана ни с сокетами, ни с XTI. Клиент, написанный с использова- нием TCP или UDP, взаимодействует с сервером при помощи одного и того же транспортного протокола, если они общаются с помощью одного и того же про- токола уровня приложений (application protocol), независимо от того, какие API используются для написания клиента или сервера. Протокол уровня приложе- ний (например, HTTP, FTP, TELNET и т. д.) и протокол транспортного уровня (TCP или UDP) — вот что определяет возможности взаимодействия, а исполь- зуемый для написания клиента и сервера API не играет никакой роли. 28.12. Функция xtirdwr Как показано в примере предыдущего раздела, мы не можем по умолчанию ис- пользовать функции read и write для дескриптора, который ссылается на точку доступа протокола XTI. Чтобы увидеть, что произойдет, если мы модифицируем листинг 28.2 и используем функции read и write вместо функций t_rcv и t_snd,
28.12 Функция xti rdwr 839 скопируем цикл for из листинга 1.1. Тогда мы получим следующую ошибку по- сле установления соединения: umxware % daytimecli03 206 62 226 35 read read Not a data message Мы получим такие же результаты в AIX, а под управлением HP-UX или Solaris ответ сервера читается, но затем следует ошибка: hpux X daytimecli03 198 69 10 4 Wed Apr 2 18 59 40 1997 read error Bad message Мы объясним разницу между этими сценариями при обсуждении листин- га 33.5. К счастью, есть обходной путь для этой проблемы. Если мы имеем дело с реа- лизацией XTI. использующей потоковый ввод-вывод, а так обычно и бывает, то мы можем поместить в поток потоковый модуль tirdwr и после этого использо- вать функции read и write. На рис. 33.3 показано использование потокового мо- дуля. Но так как эта возможность зависит от реализации, то вместо того чтобы помещать команду ioctl в нашу программу, мы определили свою собственную функцию. Таким образом, для функционирования программы в других реализа- циях достаточно будет заменить только эту библиотечную функцию. #include "unpxti h" int xti_rdwr(int fd) . Возвращает 0 в случае успешного выполнения -1 в случае ошибки В листинге 28.3 показана тривиальная реализацая этой функции, которая про- сто помещает потоковый модуль tirdwr в поток Листинг 28.3. Функция xti_rdwr. помещает потоковый модуль tirdwr в поток 1 #include 'unpxti h" 2 int 3 xti_rdwr(int fd) 4 I 5 return (ioctl(fd I_PUSH "tirdwr")). 6 } При использовании этого модуля нужно учитывать следующие особенности. Этот потоковый модуль может быть использован, только если точка доступа находится в фазе передачи данных (data transfer phase). В нашем примере в ли- стинге 28.2 мы помещаем обращение к функции xti rdwr после того, как воз- вращает управление функция t_connect. С того момента, как этот модуль помещен в поток, ни одна из функций XTI (это функции, начинающиеся с t_) не может быть вызвана. Этот модуль не может быть вызван приложениями, использующими внепо- лосные данные, так как приход внеполосных данных не может быть обрабо- тан функцией read. Если получено сообщение о нормальном завершении соединения, то оно за- ставляет функцию read вернуть нулевое значение (то есть признак конца файла). К сожалению, если получено сообщение о разрыве соединения, то оно тоже заставляет функцию read вернуть нулевое значение. Это делает невозможным отличить стандартное получение сегмента FIN от исключительного получе-
840 Глава 28. XTI: TCP-клиенты ния сегмента RST. Получение сегмента RST также ведет к тому, что любые следующие обращения к функции write потерпят неудачу. ПРИМЕЧАНИЕ--------------------------------------------------- Напомним, что у сокетов получение сегмента RST заставляет функцию read вернуть значение -1 (ошибку ECONNRESET). 28.13. Резюме Клиенты XTI аналогичны клиентам сокетов. Они вызывают функцию t open вме- сто функции socket и функцию t connect вместо connect. В обоих случаях исполь- зуется одна и та же структура адреса сокета Интернета для указания адреса про- токола сервера, хотя в XTI эта структура описывается при помощи структуры netbuf. По умолчанию в XTI используются функции t_rcv и t snd вместо функ- ций read и write, хотя в зависимости от окружения могут использоваться и две последние. Мы видели, что для работы с функциями read и wri te в поток должен быть помещен специальный потоковый модуль. Протокол XTI определяет девять событий, которые могут происходить в точ- ке доступа. Когда одно из них происходит, функция XTI возвращает ошибку TLOOK, и мы должны вызвать функцию t look , чтобы определить, что именно произош- ло, после чего обработать это событие. Обработка этих событий часто увеличива- ет размер кода по сравнению с аналогичным сценарием для сокетов. Упражнения 1. Во втором сценарии для функции t_bind мы говорили, что аргумент request имеет непустое значение, а элемент addr len этой структуры равен нулю, что позволяет вызывающему процессу задать ненулевое значение элемента qlen. Является ли такой сценарий полезным? 2. Когда мы запускали программу из листинга 28.2 для недоступного узла 192.3.4.5, мы сказали, что получили сообщение ICMP о недоступности узла в ответ на наши сегменты SYN. Почему мы говорили именно о сегментах (во множествен- ном числе)? 3. В начале раздела 28.8 мы описали сценарий, в котором приложение вызывает функцию t_snd и получает по этому соединению сегмент RST. Сравните это с тем, как сокеты обрабатывают функцию write, когда по данному соедине- нию получен сегмент RST. 4. Напишите функцию с именем xti_read, которая имеет те же аргументы, что и функция read, но вызывает функцию t_rcv и обрабатывает два сценария из листинга 28.2: если получено сообщение о нормальном завершении связи, то функция возвращает нулевое значение; если же получено сообщение о разры- ве соединения, то функция возвращает значение -1, а переменной еггпо при- сваивается код причины разрыва соединения.
ГЛАВА 29 XTI: функции имен и адресов 29.1. Введение Протокол XTI ничего не говорит о преобразовании имен и адресов. В U nix 98 для решения этих вопросов требуются только функции, рассмотренные в главе 9: gethostbyname, gethostbyaddr, getservbyname и им подобные. Тем не менее, так как многие реализации протокола XTI выполнены в системах, производных от SVR4, большинство из них предоставляют функции имени и адреса, берущие начало от SVR4. Это функции netconfig и netdir. В SVR4 эти функции называются сред- ствами выбора сети и преобразования имен в адреса (network selection and пате- to-address mapping facility). В нашем клиенте из раздела 28.11 мы заполняли структуру адреса сокета Ин- тернета значением IP-адреса и номером порта. Это решение зависит от протоко- ла. Наша же цель в этой главе состоит в том, чтобы избегать необходимости зна- ния конкретного содержимого структуры netbuf, оперируя этой структурой, как «черным ящиком». Мы начнем с имени узла (hostname) и имени службы (sendee пате), вызовем некоторые функции и в результате получим структуру netbuf, готовую к использованию в вызове функции t_connect, например для клиента TCP. Это напоминает применение функции getaddrinfo в разделе 11.2. ПРИМЕЧАНИЕ------------------------------------------ Существует одна проблема В связи с тем, что рассматриваемые нами функции по вхо- дят ни в какие стандарты, для этих функций отсутствуют определения способа их фун- кционирования Например, большинство реализаций функции netdirj’etbyname при- нимают в качестве аргумента либо имя, либо десятичный номер порта для имени службы TCP или UDP, а в других реализациях принимается только имя, а не номер порта. 29.2. Файл /etc/netconfig и функции netconfig Отправной точкой для рассмотрения преобразования имен и адресов в протоко- ле XTI является файл /etc/netconfig. Это текстовый файл, содержащий по одной строке для каждого поддерживаемого протокола. Некоторые типичные значения полей файла для каждого протокола показаны в табл. 29.1. Фактически в каждой строке файла имеется семь полей, но в таблице мы не показали последнее поле, которое задает одну или несколько библиотек поиска по каталогам (directory lookups) для данной системы. Типичные значения этого
842 Глава 29. XTI: функции имен и адресов последнего поля для протоколов Интернета — /use/1 тb/tcpiр so или /usr/lib/ resol v. so. Обычно это динамически загружаемые библиотеки, которые содержат специфические для каждого типа сети фрагменты преобразования имен в адреса. Таблица 29.1. Типичные значения компонентов файла /etc/netconfig Идентифика- Семантика Флаги Семейство Имя протокола Устройство тор сети протоколов tcp tpi_cots_ord V met tcp /dev/tcp udp tpi_clts V met udp /dev/udp icmp tpiraw - met icmp /dev/icmp rawip tpiraw - met - /dev/rawip ticks tpiclts V loopback - /dev/ticlts ticots tpicots V loopback - /dev/ticots ticotsord tpi_cots_ord V loopback - /dev/ticotsord spx tpi_cots_ord V netware spx /dev/nspx2 ipx tpiclts V netware ipx /dev/ipx Идентификаторы сети для четырех протоколов Интернета в таблице такие, как мы ожидали. Следующие три строки таблицы соответствуют записям интер- фейсов закольцовки (loopback entries). Здесь ti обозначает «транспортно-незави- симый» (transport independent). Последние две строки предназначены для про- токолов сетевой операционной системы Nowell Netware, которые мы не обсуждаем в этой книге. Показанные значения сетевой семантики для протоколов Интернета соответ- ствуют типам служб, указанным в табл. 28.1, за исключением tpi raw, которое используется для протокола ICMP и неструктурированного IP. Заметим, что, как следует из табл. 28.1, протокол TCP предоставляет сервис, ориентированный на установление соединения, с поддержкой нормального завершения связи. В настоящее время определен единственный флаг — v. Ои указывает, что поле видимо для библиотечных подпрограмм NETPATH, описываемых ниже. Имя устройства используется как аргумент функции t oper. Библиотека сетевых служб предоставляет многочисленные функции для чте- ния файла retconf 1 g. Функция setretconf i g открывает файл, а функция getnetcor f i g затем читает следующую запись из файла. Функция erdretconfig закрывает файл и освобождает память, выделенную для работы. ПРИМЕЧАНИЕ-------------------------------------------------------------- Термин «библиотека сетевых служб» (network services library) взят из операционной системы System V и обычно относится К библиотеке, которая указывается при вызове компоновщика с помощью параметра -Insl. Эта библиотека, например /usr/hb/libnsl.so, содержит все библиотечные функции проюкола XTI, а также функции, которые мы собираемся описать. void *setnetconfig(void). Возвращает непустой указатель в случае успешного выполнения нулевое значение в случае ошибки struct netconfig *getnetconfig (void *handle} . Возвращает непустой указатель в случае успешного выполнения, нулевое значение в случае конца файла (end-of-file)
29.3 Переменная NETPATH и функция netpath 843 тnt endnetconfigtvoid * handle} . Возвращает 0 в случае успешного выполнения -1 в случае ошибки Указатель, возвращаемый функцией setnetconfig (абстрактный идентифика- тор — handle), используется далее как аргумент для следующих двух функций. Значение каждой записи (строки) файла возвращается в виде структуры netconfig. struct netconfig { char *nc_netid. /* "tep'. "udp" и т д */ unsigned long nc_semantics. /* NC_TPI_CLTS и т д */ unsigned long nc_flag. /* NCJ/TSIBLE и т д */ char *nc_protofmly. /* "met" "loopback' и т д */ char *nc_proto /* ”tcp' "udp" и т д */ char *nc_device /* имя устройства соответствующее идентификатору сети */ unsigned long nejilookups. /* число записей в nc_lookups */ char **nc_lookups. /* список библиотек поиска */ unsigned long nc unused[8] }. J ’ Первые шесть элементов в этой структуре соответствуют шести колонкам табл. 29.1. Если мы напишем примерно следующую программу: void * handle. struct netconfig *nc. handle = setnetconfigO. while ( (nc = getnetconfig(handle)) '= NULL) { /* выводим структуру netconfig */ } endnetconfig(handle) . и предположим, что файл /etc/netconfig такой, как показано в табл. 29.1, то про- грамма выведет девять структур netconfig, по одной на каждую строку таблицы в том же порядке. 29.3. Переменная NETPATH и функция netpath Функция getnetconf 1 g возвращает очередную запись файла, давая нам возмож- ность прочитать весь файл по строкам. Но для интерактивных программ, кото- рыми являются клиенты, нужна возможность ограничивать поиск определенным образом, то есть искать только те протоколы, которые требуются пользователю. Это реализуется за счет того, что пользователю разрешается установить значе- ние переменной окружения NETPATH и далее вместо функций netconf i g, описанных в предыдущем разделе, применяются следующие функции: #i nclude <netconfig h> void *setnetpath(void). Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки struct netconfig *getnetpath(void *handle). Возвращает непустой указатель в случае успешного выполнения. NULL в случае конца файла int endnetpath(void * handle) Возвращает 0 в случае успешного выполнения. -1 в случае ошибки Например, мы можем установить значение переменной окружения, исполь- зуя KornShell, таким образом: export NETPATH=udp tep Если, используя это значение, мы напишем следующую программу:
844 Глава 29. XTI. функции имен и адресов void * handle struct netconfig *nc handle = setnetpathO while ( (nc = getnetpath(handle)) <= NULL) { /* выводим структуру netconfig */ } endnetpath(handle) будут выведены только две строки — одна для протокола UDP и следующая за ней для протокола TCP. Последовательность возвращенных нам структур в этом случае соответствует последовательности протоколов в переменной окружения, а не последовательности в файле netconfig. Если переменная окружения NETPATH не установлена, все видимые записи бу- дут возвращены в том порядке, который определен в файле netconfig. 29.4. Функции netdir Функции netconfig и netpath позволяют найти нужный протокол. Нам также не- обходима возможность поиска имени узла и имени службы на основе протокола, выбранного нами с помощью функции netconfig или netpath. Это позволяет сде- лать функция netdi r_getbyname. #include <netdir h> int netdir_getbyname(const struct netconfig ★nep const struct nd_hostserv *hsp struct nd_addrlist **alpp) Возвращает 0 в случае успешного выполнения ненулевое значение в случае ошибки void netdir_free (void *ptr int type) Первая функция преобразует имя узла и имя службы в адрес. Ее аргумент пер указывает на структуру netconfig, которая была возвращена функцией getnetconfig или getnetpath. Мы также должны заполнить структуру nd_hostserv значениями имени узла и имени службы, а затем передать указатель на эту структуру в каче- стве второго аргумента. struct ndjiostserv { char *h_host. /* имя узла */ char *h_serv. /* имя службы */ }• Третий аргумент указывает на указатель на структуру nd_addrl i st, и в случае успешного завершения функции указатель *а!рр содержит указатель на одну из структур следующего типа: struct nd_addrlist { int n_cnt /* число буферов netbufs */ struct netbuf *n_addrs /* массив netbufs содержащий адреса */ } Заметим, что структура nd_addrl i st указывает на массив из одной или несколь- ких структур netbuf, каждая из которых содержит один из адресов узла. Напом- ним, что узел может быть многоинтерфейсным. Рассмотрим пример, аналогичный представленному парне. 11.1, где имя узла — bsdi (он имеет два IP-адреса), а имя службы — domain (для TCP и UDP порт 53). На рис. 29.1 показана информация, возвращаемая функцией netdi r getbyname, в предположении, что используемая в качестве первого аргумента функции струк- тура netconfig содержит информацию для протокола TCP.
29.4. Функции netdir 845 ПРИМЕЧАНИЕ-------------------------------------------------------- Мы опять предполагаем, что формат, использованный поставщиком услуг связи для представления адреса Интернета, определяется щрукгурой sockaddrin Х01Я эго до- статочно распространенный вариант, он, тем не менее, не является обязательным. Рис. 29.1. Структуры данных, возвращаемые функцией netdir_getbyname Последний аргумент функции netdi r_getbyname в этом примере будет указате- лем на нашу переменную alp. Закончив работу с этими динамически создаваемыми структурами, мы вызыва- ем функцию netdir_fгее с аргументом ptr, указывающим на структуру nd_addrl i st, и аргументом type, равным ND_ADDRLIST. Обратное преобразование, когда задана структура netbuf, содержащая адрес, а нужно получить имя узла и имя службы, осуществляется функцией netdiг_ getbyaddr. #i nclude <netdir h> int netdir_getbyaddr(const struct netconfig *ncp struct ndjiostservlist **hslpp const struct netbuf *addr} Возвращает 0 в случае успешного выполнения ненулевое значение в случае ошибки Первый и третий аргументы — это входные параметры: указатель на структу- ру netconfig и указатель на структуру netbuf. Результатом является указатель на структуру ndjiostsevl i st, и этот указатель записывается в *hsl рр. struct ndjiostservlist { int h_cnt /* число ndjiostservs */ struct ndjiostserv *h_hostservs /* пары имя узла / имя службы */ } Эта структура, в свою очередь, указывает на массив из одной или нескольких структур ndjiostserv. Память для структуры ndJiostservlist, для массива струк-
846 Глава 29. XTI: функции имен и адресов тур nd_hostserv, на который указывает первая структура, и для строк, содержащих имена узла и службы, на которые указывает вторая структура (или структуры), выделяется динамически. Эта память освобождается вызовом функции netdir_ free, параметру type которой присваивается значение ND_HOSTSERVLIST. 29.5. Функции t_alloc и t_free Одно из требований независимости системы функций API от используемого про- токола сводится к тому, чтобы функции умели определять размер адреса прото- кола, не зная точно формата адреса. Для функций сокетов этот размер задается элементом ai_addrlen структуры addrinfo, значение которой возвращается функ- цией getaddri nfo (см. раздел 11.2). Для протокола XTI это значение дает элемент addr структуры t_info, которую возвращает функция t_open. Следующим шагом после получения этого размера является динамическое размещение необходимых структур в памяти. В случае сокетов мы должны забо- титься только о структурах адресов сокетов, и мы просто вызываем функцию malloc, когда это необходимо (см., например, листинг 27.2). А вот для XTJ суще- ствует шесть структур (см. табл. 28 5), каждая из которых содержит одну или не- сколько структур netbuf. Структуры netbuf указывают на буфер, размер которого зависит от размера адреса протокола (например, элемент addr структуры t call в разделе 28.6). Для упрощения динамического размещения в памяти струк- тур XTI и содержащихся в них структур netbuf служат функции t_alloc и t_free. void *t_alloc(int fd int structtype int fields) Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки int t_free(void *ptr int structtype) Возвращает 0 в случае успешного выполнения -1 в случае ошибки Аргумент structtype указывает, для какой из семи структур XTI память долж- на быть динамически выделена или освобождена. Он должен принимать значе- ние одной из констант, приведенных в табл. 29.2. Таблица 29.2. Значения аргумента structtype функций t_alloc и t free для разных структур structtype Тип структуры TBIND struct t bind TCALL struct tcall TDIS struct t discon TINFO struct t info TOPTMGMT struct toptmgmt TUDERR struct t uderr TUNITDATA struct tunitdata Аргумент fields дает нам возможность указать, что память под одну или не- сколько структур netbuf тоже должна быть выделена и инициализирована соот- ветствующим образом. Его значение определяется применением побитового ИЛИ к константам, выбранным из табл. 29.3. Напомним, что, согласно табл. 28.5, объ- екты структурного типа netbuf всегда имеют имена addr, opt или udata.
29.5. Функции t alloc и t free 847 Таблица 29.3. Значения аргумента fields функций tjalloc mr-nctJtnin:a::t-.tnriorirmttrmti.-яrtiiornnoin'nnictmrit-»:-iriirrт--||-г-г1-с:"":Г::::?;?м:»?1Ип:-:Г|иГп:з:;[niti-n.it лидг- :--ci?di.ii.rn..n:t:i".tpr.jo:.-.-n-t]:::::iT:onocT:rrtTOrrtnrrrtinnrti:ci:ni(nifiniiwiwni»iliiinmju jiiiwuvwmh—Wnnrr fields Выделить память и инициализировать TALL T_ADDR торт T.UDATA Все поля заданной структуры Поле addr структур t bind, tcall, tuderr или t_unitdata Поле opt структур toptmgmt, t call, t uderr или t_u nitdata Поле udata структур t call, t discon или t umtdara Причина, по которой введены эти значения аргумента f i el ds, в том, что неко- торые структуры XTI содержат более чем одну структуру netbuf, и для нас может оказаться лишним выделять память под все буферы. Например, структура t_ca 11, которую мы показали в разделе 28.6, содержит три структуры типа netbuf: struct t_call { struct netbuf addr /* адрес специфичный для протокола */ struct netbuf opt /* параметры специфичные для протокола*/ struct netbuf udata. /* пользовательские данные сопровождающие запрос на соединение */ int sequence /* для функций t listenO и t_accept() */ } Возможность задания некоторой комбинации значений T_ADDR, Т_ОРТ и TJJDATA дает нам полное управление процессом выделения памяти. Обычно в наших при- мерах мы будем использовать значение T ALL как простейшее решение. (См. так- же упражнение 29.2.) Если вызвать функцию t_al 1ос, задав аргумент fd соответствующим точке до- ступа протокола TCP, аргумент structtype равным T_CALL, а аргумент f т el ds — рав- ным T_ALL, и учесть значения, приведенные в табл. 28.1 для AIX 4.2, то в результа- те мы получим распределение памяти, показанное на рис. 29.2. t_call{} addr.maxlen 16 netbuf{} - addr.len 0 addr.buf >| Буфер для адреса, специфичного для протокола | addr.maxlen 512 netbuf{} addr.len 0 addr.buf н Буфер для параметров, специфичных для протокола | addr.maxlen 0 netbuf{}- addr.len 0 addr.buf NULL sequence 0 Рис. 29.2. Распределение памяти для структур и буферов функцией t alloc Этот вызов функции tall ос выделяет память для структуры teal 1, содержа- щей три структуры типа netbuf. Один буфер выделен под зависящий от протоко- ла адрес (элемент addr), другой под зависящие от протокола параметры (элемент opt). Два указателя на буферы инициализированы вместе со значениями элемен- тов maxlen, а два значения элементов len равны нулю. Третья структура netbuf не используется для протокола TCP (данные пользователя, сопровождающие зап- рос на соединение), следовательно, две длины в элементе udata нулевые, а указа- тель на буфер имеет значение пустого указателя.
848 Глава 29. XTI: функции имен и адресов Функция t_fгее освобождает память, которая была предварительно выделена функцией t_al 1ос. Аргумент strricttype задает тип структуры, для чего использу- ются константы, показанные в табл. 29.2. Функция t_fгее не только освобождает память, которая была выделена для структуры, заданной параметром structtype, но и проверяет предварительно все структуры netbuf, содержащиеся внутри нее, и освобождает память, используемую под буферы. В нашем примере на рис. 29.2 функция t_fгее сначала освободит память, отведенную для двух буферов, а затем уже память, занятую под структуру t eal 1. 29.6. Функции t_getprotaddr Функция t_getprotaddr возвращает локальный и удаленный адреса протокола, связанные с точкой доступа. nclude <xti h> int t_getprotaddr(mt fd. struct t_bind *localaddr. struct t_bind ★peeraddr) . Возвращает 0 в случае успешного выполнения- -1 в случае ошибки Эта функция использует элементы addr структур netbuf, соответствующие двум структурам t_bi nd. Когда функция вызывается, элементы maxi еп и buf структур netbuf определяют, куда результат должен быть записан. Значение maxi еп, равное нулю, указывает, что значение соответствующего адреса не должно возвращать- ся. При завершении функции элементы len структур netbuf содержат размер ад- ресов, которые были сохранены в элементах buf. Это значение будет равно нулю для локального адреса, если он до сих пор не был связан, и будет равно нулю для адреса собеседника, если точка доступа еще не присоединена. ПРИМЕЧАНИЕ------------------------------------------------------------- Протокол TLI имел недокументированную функцию t getname, которая могла возвра- щать оба адреса протокола — локальный и удаленный. Функция tgetprotaddr является комбинацией двух функций: getsockname и getpeername. Если мы интересуемся только одним из двух адресов, мы все равно должны выделить память для структуры t bind второго адреса и установить для нее нулевое значение элемента maxlen. Более простое решение позволило бы задавать пустой указатель в ка- честве значения любого из двух аргументов функции, если мы не хотим, чтобы значе- ние этого адреса возвращалось функцией. 29.7. Функция xti_ntop Нам необходим простой способ для вывода адреса протокола XTI (более про- стой, чем использование функции netdi rjgetbyaddr). Для этого мы напишем нашу собственную функцию xti_ntop, аналогичную нашей функции sockjitop из разде- ла 3.8. Большинство реализаций протокола XTI предоставляют две функции taddr2uaddr и uaddr2taddr. Аббревиатура taddr соответствует транспортному ад- ресу (transport address), содержащемуся в структуре netbuf, a uaddr — универсаль- ному адресу (universal address), то есть текстовой строке, хранящейся в виде за- канчивающейся нулем строки С. Эти реализации выводят универсальные адреса IPv4 в виде шести десятичных чисел, разделенных пятью десятичными точками,
29.8. Функция tcp connect 849 где первые четыре числа — это адрес IPv4 в точечно-десятичной записи, а после- дние два числа — это 2-байтовый номер порта протоколов TCP или UDP. Тем не менее с применением этих двух функций связана проблема, состоящая в том, что они требуют передачи аргумента для структуры netconfig, дающего информацию о протоколе, адрес которого преобразуется. Но адреса протокола XTI для IPv4 и IPv6 являются самоопределяющимися. Например, когда адреса IPv4 хранятся в структуре netbuf, адрес на самом деле представляет собой насто- ящую структуру адреса сокета, первым элементом которой является имя семей- ства адресов — AF_INET. Длина этой структуры netbuf составляет 16 байт (см., на- пример, строку addr в табл. 28.1 и 28.2). Мы предполагаем, что адреса в IPv6 будут храниться как структуры sockaddrj пб, с именем семейства AF_I NET6 и длиной 24 бай- та. У нас нет гарантии, что все адреса XTI, записанные в структуры netbuf, будут самоопределяющимися, но адреса IPv4 и IPv6 отвечают этому требованию. ПРИМЕЧАНИЕ----------------------------------------------------------- Термин «самоопределяющиеся», может быть, слишком сильный. Для некоторых дру- гих протоколов возможно использование 16-байтового адреса, первые 2 байта которо- го окажутся равными константе AF_NET. Но на практике это не будет составлять про- блемы. Нам следует выбрать, каким образом передать нашей функции адрес протоко- ла. Сначала рассмотрим возможность использовать одну из структур вида t_XXX протокола XTI. Но для клиентов адрес протокола сервера находится в элементе addr структуры t call (см. раздел 28.6), для серверов адрес протокола клиента находится в элементе addr структуры t_bind (см. раздел 30,2) и для любых точек доступа локальный и удаленный адреса, возвращаемые функцией t_getprotaddr, находятся в структуре t_bi nd. Так как здесь нет единообразия (если мы переда- дим указатель на одну из этих структур, то нам придется также передавать флаг, указывающий тип выбранной структуры), то мы не будем пользоваться структу- рами XTI, а вместо этого передадим указатель на структуру типа netbuf. include "unpxti h" char *xti_ntop(const struct netbuf *np) . Возвращает- непустой указатель в случае успешного выполнения. NULL в случае ошибки Аргумент функции — это указатель на структуру типа netbuf, содержащую адрес. Результат сохраняется в статической переменной внутри функции. В слу- чае успешного завершения функция возвращает указатель на строку, содержа- щую адрес в формате представления. Мы будем использовать другую функцию, названную xti_ntop_host, с таким же способом вызова, которая формирует только IP-адрес, игнорируя номер порта. ПРИМЕЧАНИЕ---------------------------------------------------------------------- Эти две функции имеют код, аналогичный показанному в листинге 3.7. Мы не приво- дим исходный код, ио он находится в свободном доступе (см. предисловие). 29.8. Функция tcp_connect Теперь мы можем скомбинировать функцию getnetpath, которая возвращает ин- формацию об одном или нескольких протоколах, с функцией netdir getbyname, тгптппяа шпрт ымгрнй уяттй и службы сияя и ы прпрпрпять нашу (Ъункпию ten cnnnpct
850 Глава 29 XTI функции имен и адресов из раздела 118 так, чтобы использовать протокол XTI вместо сокетов и функции getaddrinfo Мы показываем получившуюся функцию в листинге 29 I1 Листинг 29.1. Функция tcp_connect для протокола XTI //libxti/tcp_connect с 1 #include unpxti h 2 int 3 tcp_connect(const char *host const char *serv) 4 { 5 int tfd i 6 void *handle 7 struct t_call tcall 8 struct tjliscon tdiscon 9 struct netconfig *ncp 10 struct ndjiostserv hs 11 struct nd_addrlist *alp 12 struct netbuf *np 13 handle = SetnetpathO 14 hs h_host = (char *) host 15 hs h_serv = (char *) serv 16 while ( (nep = getnetpath(handle)) '= NULL) { 17 if (strcmp(ncp >nc_proto tep ) '= 0) 18 continue 19 if (netdir_getbyname(ncp &hs Salp) '= 0) 20 continue 21 /* 4 проверки для каждого адреса сервера */ 22 for (1=0 пр = alp >n_addrs i < alp >n_cnt i++ np++) { 23 tfd = T_open(ncp >nc_device O_RDWR NULL) 24 T_bind(tfd NULL NULL) 25 tcall addr len = np >len 26 tcall addr buf = np >buf /* копия указателя */ 27 tcall opt len = 0 /* нет параметров */ 28 tcall udata len = 0 /* нет данных пользователя при соединении */ 29 if (t_connect(tfd Steal 1 NULL) == 0) { 30 endnetpath(handle) /* успешное соединение с сервером */ 31 netdir_free(alp ND_ADDRLIST) 32 return (tfd) 33 } 34 if (t errno == TLOOK SS tjook(tfd) == TDISCONNECT) { 35 t_rcvdis(tfd Stdiscon) 36 errno = tdiscon reason 37 } 38 tclose(tfd) 39 } 40 netdir_free(alp ND_ADDRLIST) 41 } 42 endnetpath(handle) 43 return ( 1) 44 } Все исходные коды программ опубликованные в этой книге вы можете найти по адресу http // www niter com /download
29.8. Функция top connect 851 Инициализация 13 15 Функция setnetpath открывает файл netconfig Структура ndjiostserv инициа- лизируется значениями указателей на имя узла и имя службы Получение следующей строки из файла netconfig 16-18 Функция getnetpath ищет в файле netconfig следующий протокол из перемен- ной окружения NETPATH Если это не протокол TCP, мы игнорируем строку Так как мы ищем только строки для протокола TCP, мы могли бы вызвать функцию таким образом nep = getnetconfigent( tcp ) чтобы определить именно эту строку Последующий вызов функции freenetconfigent(nep) освободит память, выделенную функцией getnetconfigent Но так как мы хотели бы, чтобы этот код работал и с IPv6, мы организуем цикл, который будет про- сматривать каждую структуру netconfig В настоящий момент неизвестно, как будет выглядеть строка в файле netconfi g для протокола TCP в версии IPv6 и как XTI-функции определения имени будут работать в случае IPv6 Поиск имен узла и службы 19 20 Функция netdi r_getbyname ищет имена узла и службы, используя структуру netconfig, возвращенную функцией getnetpath Перебор всех адресов серверов 21 28 В этом цикле проверяется каждый возвращенный адрес сервера посредством вызовов функций t_open, t bind и t_connect до тех пор, пока не будет установлено соединение или не будут перебраны все адреса Структура t_cal1 инициализиру- ется значением из структуры типа netbuf, возвращаемой функцией netdi r_getbyname Соединение установлено 29 33 Если соединение успешно установлено, то производится очистка памяти и воз- вращается значение дескриптора соединения Функция endnetpath освобождает память, выделенную для структуры netconf i g, и закрывает файл netconf i g, а функ- ция netdi r_free освобождает все области памяти, начиная со структуры nd_addrl 1st (см рис 29 1) Обработка ошибок функции t_connect 34 38 Если функция t_connect завершается аварийно, то мы проверяем значение TLOOK кода ошибки и при отказе в соединении вызываем функцию t_rcvdis Мы при- сваиваем переменной errno значение, зависящее от используемого протокола, чтобы дать возможность пользователю диагностировать ситуацию Точка досту- па закрыта Завершение работы со всеми адресами 40-41 После того как все адреса проверены, память, занимаемая структурой nd_addrl i st и массивом структур типа netbuf, на которые структура указывает, освобождает- ся функцией netdiг free Цикл while будет продолжать работать, продвпга-
852 Глава 29. XTI: функции имен и адресов ясь по файлу netconfig и, возможно, находя дополнительные протоколы для проверки. ПРИМЕЧАНИЕ ---------------------------------------------------------- Функция getaddrinfo объединяет вызов функции getnetpath (поиск соогвошвующс'го протокола или семантики) с вызовом функции netdir getbyname. Точка доступа протокола XTI, которой не удалось установить соединение, все же мо- жет быть использована в другом вызове функции t connect, то есть мы можем вынес- ти обращения к функциям t_call и t_bind из цикла for, обращаясь к этим двум функци- ям по одному разу в каждом проходе тела цикла while. Конечно, при этом мы также вынесем вызов функции t_close из цикла for. Но когда вызов функции connect завер- шается неудачно при использовании сокетов, сокет не может более использоваться и должен быть закрыт (см., например, листинг 112) Но в таком подходе к XTI есть одна коварная проблема. Проблема возникает в том случае, когда узел, с которым мы пытаемся соединиться, имеет несколько адресов, а попытка установить соединение с ним завершается неудачно. В этом сценарии значе- ние локального порта никогда не изменяется, и каждое новое обращение к функции t_connect с целью получения следующего адреса задерживается на величину экспо- ненциального смещения относительно времени предшествующего обращения, так как все эти попытки установления соединения делаклся из одной и той же локальной точ- ки доступа. Таким образом, если первое обращение к t call не сработало, то следующее обращение может быть задержано на одну секунду, а если и эта попытка будет неудач- ной, то следующее может быть задержано уже на две секунды, и т д. Чтобы избежать этой трудности, мы будем закрывать точку доступа функцией t close, когда функция t_connect завершается неудачно, а затем создавать новую точку доступа для следую- щего обращения к функции t connect. Пример Сейчас мы воспользуемся нашей функцией t_connect и переделаем программу клиента времени и даты из листинга 11.3, независимого от протокола, применяя технику XTI вместо сокетов. Наша XTl-версия представлена в листинге 29.2. Листинг 29.2. Не зависящий от протокола клиент времени и даты //xtiintro/daytimecli02 с 1 #include "unpxti h" 2 int 3 maintint argc. char **argv) 4 { 5 int tfd. n. flags 6 char recvlinefMAXLINE + 1): 7 struct t_bind *bound *peer. 8 struct t_discon tdiscon. 9 if (argc '= 3) 10 err quitf'usage daytimecli02 <hostname/IPaddress> <service/port#>"). 11 tfd = Tcp_connect(argv[l], argv[2]). 12 bound = T_alloc(tfd T_BIND T ALL),
29.9. Резюме 853 14 T_getprotaddr(tfd bound, peer). 15 printft"connected to £s\en". Xti_ntop(&peer->addr))_ 16 for (..) { 17 if ( (n = t_rcv(tfd. recvline, MAXLINE &flags)) < 0) { 18 if (t_errno == TLOOK) { 19 if ( (n = Tlook(tfd)) == T_0RDREL) { 20 Trcvrel (tfd). 21 break 22 } else if (n = T_DISCONNECT) { 23 T_rcvdis(tfd. &tdiscon). 24 errno = tdiscon reason /* вероятно. ECONNRESET */ 25 err_sys("server terminated prematurely”): 26 } else 27 err_quit("unexpected event after t_rcv fcd". n): 28 } else 29 err_xti(”t_rcv error"). 30 } 31 recvline[n] = 0 /* завершающий нуль */ 32 fputs(recvline stdout). 33 } 34 exit(0) 35 } Установление соединения 11 Вызываем нашу функцию tcp_connect из листинга 29.1 для поиска имени узла, имени службы и установления соединения. Вывод адреса протокола собеседника 12-15 Выделяем память для двух структур t_bi nd и вызываем функцию t_getprotaddr, чтобы получить локальный адрес протокола и адрес протокола собеседника. Выводим адрес собеседника, вызывая нашу функцию xti_ntop. Чтение данных с сервера до получения EOF 16-33 Чтение данных с сервера выполняется идентично коду в разделе 28.11. Мы можем запустить программу следующим образом: umxware % daytimecli02 aix daytime connected to 206 62 226 43 13 Fri Feb 7 13 28 24 1997 29.9. Резюме В реализациях протокола XTI для SVR4 выбор сетевого соединения обычно реа- лизуется с помощью файла /etc/netconfig с последующим поиском функцией netdi r_getbyname имен узла и службы, в результате чего получается массив струк- тур типа netbuf, по одной на адрес и службу. Это аналогично использованию функции getaddrinfo в главе И. Обратное преобразование от адреса протокола к формату представления осуществляется функцией netdi r getbyaddr, которая ана- логична функции getnameinfo. В связи с тем, что в XTI используется так много структур — семь структур типа t_XXX со структурами netbuf, содержащимися в них, — в API протокола ХТ1
854 Глава 29. XTI: функции имен и адресов предоставляются две функции для динамического выделения и освобождения памяти для этих структур: t_al 1ос и t_free. Упражнения 1. Функция getnetconf 1 g возвращает указатель на структуру, которую она запол- няет, аналогично функции gethostbyname. Но мы сказали, что последняя функ- ция не является безопасной в многопоточной среде. Является ли функция getnetconfig безопасной, и если да, то как она достигает этого? 2. Напишите программу, которая вызывает функцию t_a 11 ос дважды для струк- туры t_cal 1 для точки доступа TCP. При первом обращении задайте третьему аргументу значение T_ALL, а при втором — значение T_ADDR | Т_ОРТ | TJJDATA. Что произойдет? 3. Почему функция t_free требует аргумента structtype? 4. Почему в листинге 29.1 мы не инициализировали структуру nd_hostserv сле- дующим образом: struct ndjiostserv hs = { host, serv ),
ГЛАВА 30 XTI: TCP-серверы 30.1. Введение Несомненно, наиболее сложным аспектом XTI является обработка входящих со- единений сервером, ориентированным на установление соединения. При исполь- зовании сокетов мы просто вызываем функцию accept, и вся обработка осуществ- ляется ядром или библиотекой сокетов. В случае TCP прибывающие сегменты SYN просто выстраиваются в очередь не полностью установленных соединений для данного сокета (см. рис. 4.2). Когда завершается трехэтапное рукопожатие, функция accept возвращает управление (см. рис. 2.5). Если в очереди полностью установленных соединений таких соединений несколько, они возвращаются функ- цией accept в порядке FIFO (First In, First Out — первым пришел, первым обслу- жен). В реальных ситуациях (см. табл. 4.5) количество установленных соедине- ний обычно равно нулю, в то время как количество не полностью установленных может быть ненулевым. Предполагаемый результат в модели XTI (которая основана на концепции транспортной службы OSI) состоит в том, чтобы позволить транспортному уров- ню сообщать процессу сервера о прибытии сегмента SYN от клиента (сообщение о соединении — connect indication) и передать серверу адрес протокола клиента (IP-адрес и порт). Процесс сервера затем может либо принять, либо отклонить запрос на соединение. TCP сервера в этой модели не будет посылать свои сегмен- ты SYN/АСК или сегмент RST, пока процесс сервера не сообщит, что нужно де- лать. Эта модель показана на рис. 30.1. Клиент Сервер t_open, t_bind t connect (блокировка) t_connect завершается t_open, t_bind LISTEN (пассивное открытие) t_listen (блокировка) t listen завершается Сервер принимает соединение t_open, t_bind, t_accept Рис. 30.1. Предполагаемая модель XTI принятия запроса на соединение Обратите внимание на вызовы функций на стороне сервера: первый вызов функции t_bi nd (с ненулевым аргументом ql еп) указывает, что точка доступа бу- дет принимать входящие соединения, функция t_l т sten завершается, когда со- единение становится «доступно» (об этом мы поговорим чуть позже), а затем сер-
856 Глава 30. XTI: TCP-серверы вер должен вызывать функции t_open, t_bind и t accept, чтобы принять соединение. С точки зрения TCP точка доступа находится в состоянии LISTEN (см. рис. 2.4). Когда процесс сервера получает извещение о запросе на соединение, он имеет возможность не принимать это соединение, вызвав функцию t_snddi s для откло- нения запроса. Этот вариант проиллюстрирован на рис. 30.2. Здесь сервер ин- формируется о прибытии сегмента SYN (сообщение о соединении), в результате чего сервер решает не принимать это соединение (возможно, основываясь на IP- адресе клиента, на номере порта или на пользовательских данных, присланных вместе с запросом на соединение, если этот параметр поддерживается протоко- лом). Затем приложение вызывает функцию t snddis, в результате чего вместо завершения трехэтапного рукопожатия отсылается сегмент RST. В итоге вызов функции t connect на стороне клиента возвратит ошибку. Клиент Сервер t_open, t_bind t_connect (блокировка) t_connect завершается t_open, t_bind LISTEN (пассивное открытие) t_listen (блокировка) t listen завершается Сервер отклоняет запрос на соединение t snddis Рис. 30.2. Предполагаемая модель XTI отказа в соединении ПРИМЕЧАНИЕ---------------------------------------------------------- Напомним, что при обсуждении сокетов и временной диаграммы в главе 2 (см. рис. 2.4) говорилось, что при использовании сервером сокетов вызов функции connect на сто- роне клиента пе может завершиться неудачно, так как функция accept возвращает уп- равление при завершении трехэтаппого рукопожатия спустя половину времени RTT после завершения функции connect. Если такой сервер не хочет устанавливать соеди- нение сданным клиентом (возможно, основываясь па его IP-адресе или номере пор га), все, что может сделать сервер, — это закрыть соединение либо с помощью обычного вызова функции close (посылая сегмент FIN), либо сначала установив параметр сокета SO LINGER, а затем вызвав функцию close (отсылая сегмент RST). В приведенном выше описании сценария XTI мы выделили слова «предпола- гаемый результат» курсивом, так как на самом деле этот сценарий не выполняет- ся. Хотя он планировался для протоколов OSI, но реально большинство реализа- ций TCP автоматически принимают все входящие запросы па соединения (пока очереди полностью и не полностью установленных соединений не переполнят- ся), а сервер не получает извещения о входящем запросе на соединение, пока не будет завершено трехэтапное рукопожатие. ПРИМЕЧАНИЕ---------------------------------------------------------- Техника, при которой приложение получает извещение о прибытии сегмента SYN, и дальнейшее осуществление трехэтаппого рукопожатия зависит от того, заинтересо- вано ли приложение в установлении данного соединения, иногда называется «отло- женным приемом» (lazy accept). По меньшей мере в двух реализациях TLI (Wollongong Group и Sequent Computer System) использовалась техника отложенного приема.
30.2. Функция tjisten 857 В обоих случаях был изменен фактически ставший стандартным метод возвращения из функции t_listen по завершении трехэтапного рукопожатия. Одной из причин из- менения было то, что отложенный прием вызывает сбой в большинстве реализаций протокола FTP. В Posix.lg также требуется, чтобы успешное завершение функции t listen для точки доступа TCP указывало бы па установление соединения, а не служило бы сообщением о соединении. В [44] говорится, что в 4.4BSD предполагалось включить параметр, который позволял бы каждому сокету в отдельности осуществлять отложенный прием для TCP, по это не было реализовано. 4.4BSD поддерживает отложенный прием для протоколов OS1. 30.2. Функция tjisten Обычным сценарием для XTI-сервера, ориентированного на соединение, являет- ся вызов следующих функций: listenfd = t_open( ). t_bind(listenfd. ). for ( . . ) { t_listentlistenfd. ): connfd = t_open( ). t_bind(connfd, NULL. NULL): taccept(listenfd. connfd. ). \& /* создание прослушиваемой точки доступа */ /* t_bind qlen > 0 */ /* блокируется в ожидании соединения */ /* создаем новый дескриптор fd для присоединенной точки доступа */ /* любой локальный адрес */ /* прием на новом дескрипторе fd */ /* обработка присоединенной точки доступа */ tclose(connfd). } Функция t_l 1 sten обычно блокируется в ожидании соединения от клиента. #i nclude <xti h> int t_listen(int fd. struct t_call ★call}. Возвращает 0 в случае успешного выполнения -1 в случае ошибки Мы уже описывали структуру t call, обсуждая функцию t_connect, йо пока- жем ее здесь снова: struct t_cal1 { struct netbuf addr, struct netbuf opt. struct netbuf udata. int sequence. } /* адрес, специфичный для протокола */ /* параметры, специфичные для протокола*/ /* пользовательские данные, сопровождающие запрос на соединение */ /* для функций tjistent) и t_accept() */ Структура, возвращаемая через указатель call, содержит интересующие нас параметры соединения: addr — это адрес протокола клиента, opt может содержать любые специфичные для протокола параметры, a udata — любые пользовательс- кие данные, которые были посланы вместе с запросом на соединение (эта воз- можность не поддерживается протоколом TCP). Переменная sequence содержит некоторое уникальное значение, идентифицирующее данный запрос на соедине-
858 Глава 30. XTI: TCP-серверы ние. Это значение будет использовано при вызове функции t_accept (или t_snddi s) для идентификации принимаемого (или отклоняемого) соединения. ПРИМЕЧАНИЕ----------------------------------------------------------- Хотя эта функция кажется аналогичной функции accept, она на самом деле отличается от нее, так как функция t_listen только ждет прибытия соединения и не занимается приемом соединения. Для приема соединения следует вызывать функцию t_accept. Переменная sequence является целочисленной, но в некоторых реализациях это поле структуры t_call используется для записи адреса. Не стоит предполагать, что значение этой переменной — небольшое целое число типа дескриптора. 30.3. Функция tcpjisten Теперь мы переходим к созданию нашей собственной функции (листинг 30.11), создающей прослушиваемую точку доступа, через которую можно было бы при- нимать входящие соединения. Вызывающая последовательность идентична той, которая имела место для функции с тем же именем, показанной в листинге 11.4. Листинг 30.1. Функция XTI tcpjisten: создание прослушиваемой точки доступа //libxti/tcpjisten с 1 2 #include "unpxti h” #include <limits h> /* PATH_MAX */ 3 char xti_serv_dev[PATH_MAX + 1]. 4 5 6 7 8 9 10 И 12 13 14 15 int tcp listen(const char *host const char *serv. socklen t *addrlenp) ( int listenfd. void *handle. char *ptr, struct t_bind tbind struct t_mfo tinfo, struct netconfig *ncp. struct ndjiostserv hs. struct nd_addrlist *alp: struct netbuf *np. 16 handle = SetnetconfigO: 17 18 hs hjiost = (host == NULL) ? HOSTSELF ; (char *) host; hs h_serv = (char *) serv. 19 20 while ( (nep = getnetconfig(handle)) !- NULL && strcmp(ncp->nc_proto. “tcp") !- 0) ; 21 22 if (nep == NULL) return (-1). 23 if (netdir_getbyname(ncp. &hs. &alp) |= 0) { 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу www.piter.com/download.
30.3. Функция tcpjisten 859 24 endnetconfig(handle), 25 return (-2). 26 } 27 np = alp->n_addrs. /* используем первый адрес */ 28 listenfd = T_open(ncp->nc_device. O_RDWR, &tinfo). 29 strncpy(xti_serv_dev. ncp->nc_device. sizeof(xti_serv_dev)). 30 tbind addr = *np. /* копируем всю структуру netbuf{} */ 31 /* можем заменить константу LISTENQ переменной окружения */ 32 if ( (ptr = getenvCLISTENQ")) '= NULL) 33 tbind qlen = atoi(ptr). 34 else 35 tbind qlen = LISTENQ, 36 T_bind(listenfd. Stbind NULL). 37 netdir_free(alp ND_ADDRLIST). 38 endnetconfig(handle). 39 if (addrlenp) 40 *addrlenp = tinfo addr. /* размер адреса протокола */ 41 return (listenfd) 42 } Инициализация 16-18 Функция setnetconfig открывает файл /etc/netconfig. Если аргумент host явля- ется пустым указателем, мы передаем функции netdi r_getbyname специальную стро- ку HOST SELF. В результате этого прослушиваемый сокет связывается с универ- сальным адресом (в случае IPv4 это 0.0.0.0). Поиск подходящего протокола 19-22 Мы обрабатываем каждую строку файла /etc/netconfig в поисках протокола TCP. Обратите внимание, что в данном случае мы используем функцию get- netconfig для сервера, в то время как в листинге 29.1 мы вызывали функцию getnetpath для клиента. Это делается для того, чтобы сервер не предположил, что переменная окружения NETPATH имеет какое-либо осмысленное значение, так как сервер может быть запущен через сценарий инициализации или из командной строки. Клиенты, напротив, обычно запускаются в интерактивном режиме из интерпретатора команд от имени пользователя, поэтому им можно позволить предположить, что эта переменная устанавливается пользователем. Поиск имени узла и имени службы ’3-27 Функция netdi r_getbyname ищет имя узла и имя службы, используя указатель на структуру netconfig для искомого протокола. Открытие устройства 18-29 Функция t open открывает соответствующее устройство (например, /dev/tcp), и копия имени этого устройства сохраняется во внешней переменной xti_serv_dev. Это делается потому, что процессу, вызывающему функцию tcp l i sten, необхо- димо снова вызывать функцию t_open для каждого соединения, и ему потребует- ся имя этого устройства для поддержания независимости от протокола. В случае
860 Глава 30. XTI: TCP-серверы сокетной версии функции tcp l i sten нам не нужно было делать ничего подобно- го, так как функция accept (а не процесс) автоматически создавала новый сокет для каждого соединения. ПРИМЕЧАНИЕ --------------------------------------------------------- Описанная технология не является безопасной в многопоточной среде. Этот нежела- тельный побочный эффект является результатом требования о сохранении приложе- нием информации о состоянии (имени устройства) в промежутке между вызовом функ- ции t open для прослушиваемого дескриптора и последующими вызовами функции t_open для каждого присоединенного дескриптора. Одним из способов сделать эту операцию безопасной в многопоточной среде является вызов функции strdup, которая скопирует имя устройства в динамически выделенную область памяти, после чего ука- затель возвратится через другой аргумент функции tcp listen, с тем чтобы вызываю- щий процесс мог вызвать функцию free и освободить выделенную ранее память Переход в состояние TCP LISTEN 1-36 Мы вызываем функцию t_bi nd, связывая адрес, возвращенный функцией netdi г_ getbyname, с точкой доступа. Устанавливая поле qlen структуры t_bind в ненуле- вое значение, мы указываем, что это — прослушиваемая точка доступа, и в случае TCP эта точка доступа входит в состояние LISTEN. (В данном случае мы гово- рим о состоянии LISTEN протокола TCP. В случае XTI это состояние называет- ся T IDLE.) Теперь входящие соединения будут приниматься поставщиком транспортных служб. Мы заменяем значением переменной окружения LISTENQ значение этой константы из нашего заголовочного файла unp.h, заданное по умол- чанию. Аналогичный код можно найти в листинге 4.1, где приводится функция- обертка Li sten для функции сокетов 11 sten. Освобождение памяти, возвращение значений ?-41 Мы вызываем функции netdi r_free и endnetconfig для освобождения выделен- ной области памяти. Мы возвращаем размер адресов протокола (если требуется), а возвращаемым значением функции является дескриптор прослушиваемой точ- ки доступа. Обратите внимание, что мы не вызываем функцию t_l i sten, так как это при- вело бы к блокированию сервера в ожидании входящего соединения. 30.4. Функция t_accept Когда функция t_l i sten сообщает о прибытии соединения, мы можем выбрать, принимать это соединение или отказаться от него. Если мы решаем принять это соединение, следует вызать функцию t_accept. #i ncl tide <xti h> int t_accept(int listenfd int connfd struct t_call *call) Аргумент 11 stenfd указывает точку доступа, на которую прибыло соединение. Иначе говоря, это та точка доступа, которая была аргументом функции t_l 1 sten. Аргумент connfd указывает точку доступа, в которой должно быть установлено со- единение. Обычно сервер создает новую точку доступа connfd для приема соединения.
30.5 Функция xti accept 861 Аргумент cal 1 указывает, какое соединение принимается в данный момент (это актуально в случае, когда принятия ожидают несколько соединений, о чем мы вско- ре расскажем), а его значение — это то, что было возвращено функцией t_l i sten. Обратите внимание на то, что за создание точки доступа на стороне сервера отвечает сам сервер. Обычно это делается с помощью функции t_open в проме- жутке между вызовами функций t_l i sten и t_accept. ПРИМЕЧАНИЕ ------------------------------------------------------------ У нас также есть возможность задавать один и тот же дескриптор для listenfd и для connfd. Иными словами, мы можем принимать новое соединение па прослушиваемой точке доступа. Но в таком случае мы получаем последовательный сервер — никакие другие соединения не могут быть приняты до тех нор, пока не будет закончена работ а с приня гым соединением. В таком сценарии единственное осмысленное значение qlen — это 1. С учетом ограничений, свойственных этому сценарию, и необходимости для сер- веров в большинстве случаев работать одновременно с несколькими клиентскими со- единениями, мы не будем исследовать примеры этого сценария. 30.5. Функция xti_accept Теперь мы напишем простую функцию с именем xti_accept для выполнения ша- гов, необходимых для приема соединения с использованием XTI. В общем слу- чае нужно было бы написать для серверных приложений XTI код, подобный сле- дующему: listenfd - Tcp_listen( ). /* создаем прослушиваемую точку доступа */ for ( . ) { connfd = Xti_accept(listenfd. ... ). /* блокируемся, затем принимаем соединение */ /* обрабатываем connfd */ t_close(connfd) } Это похоже на код, применявшийся в случае использования сокетов, с ТОЙ лишь разницей, что вместо функции accept используется xti_accept. #include "unpxti h" int xti_accept(int listenfd struct netbuf *chaddr. int rdwr). Возвращает неотрицательный дескриптор в случае успешного выполнения -1 в случае ошибки В случае успешного выполнения возвращается новый присоединенный де- скриптор. Адрес клиента возвращается в структуре netbuf, на которую указывает cliaddr, и если аргумент rdwr ненулевой, для присоединенной точки доступа вы- зывается наша функция xti rdwr. В листинге 30.2 мы показываем простую версию пашей функции xti accept. Мы называем ее простой, поскольку, как мы увидим вскоре, она может не срабо- тать в случае, когда одновременно готовы несколько соединений. В разделе 30.8 мы увидим, как можно решить эту проблему. Листинг 30.2. Простая версия функции xti_accept //libxti/xti_accept_simple с 1 #include "unpxti h" 2 mt продолжение &
862 Глава 30. XTI: TCP-серверы Листинг 30.2 (продолжение} 3 xti_accept(int listenfd. struct netbuf *cliaddr. int rdwr) 4 { 5 int connfd. 6 u_int n 7 struct t call *tcallp. 8 tcallp - Ta Hoc (listenfd. T_CALL. T_ALL). 9 T_listen(listenfd. tcallp). /* blocks */ 10 /* дальнейшее предполагает, что вызывающий процесс вызвал функцию tcp_listen() */ И connfd = T_open(xti_serv_dev. O_RDWR NULL). 12 T_bind(connfd, NULL. NULL) 13 T_accept(listenfd. connfd tcallp) 14 if (rdwr) 15 Xtirdwr(connfd). 16 if (cliaddr) { /* возвращает адрес протокола клиента */ 17 n = min(cliaddr->maxlen. tcallp->addr len). 18 memcpy(cliaddr->buf. tcallp->addr buf. n). 19 cliaddr->len = n 20 } 21 T_free(tcallp T_CALL). 22 return (connfd). 23 } Ожидание соединения -9 В памяти выделяется место для структуры t_cal 1, в которой будет храниться информация о соединении с клиентом. Функция t_l i sten блокируется в ожида- нии соединения. Создание новой точки доступа и связывание с локальным адресом 1-12 Функция t_open создает новую точку доступа, используя полное имя, записан- ное во внешней переменной xti_serv_dev функцией tcp_l isten. С этой точкой до- ступа связывается любой локальный адрес. Этот вызов функции t_bi nd является необязательным. Если мы не свяжем с точкой доступа никакого локального адре- са, она останется неприсоединенной на момент вызова функции t accept, и по- ставщик службы связи автоматически свяжет с ней какой-либо подходящий адрес. Принятие соединения 13 Функция t_accept принимает соединение. Опа узнает, какое соединение нужно принимать, на основании значения элемента sequence структуры t_call, которая была заполнена функцией t_l i sten с целью идентификации этого конкретного соединения. При необходимости даем разрешение на использование функций read и write 1-15 Если вызывающий процесс задает ненулевое значение аргумента rdwr, наша функция xti_rdwr помещает модуль ti rdwr в поток, позволяя тем самым исполь- зовать функции read и write вместо t_rcv и t_snd.
30.6. Простой сервер времени и даты 863 Возвращение адреса протокола клиента 16-20 Вызывающий процесс может задать непустой аргумент cliaddr, который ука- зывает на структуру netbuf. Эта структура должна быть инициализирована вы- зывающим процессом так, чтобы указывать на буфер, в который записывается возвращаемый адрес протокола клиента. Мы проверяем, не переполним ли мы буфер вызывающего процесса, а затем присваиваем полю 1 еп размер возвоашае- мого адреса. Освобождение занятой памяти и возвращение присоединенного дескриптора 21-22 Память, занимаемая структурой t_cal 1, освобождается, и возвращается Присо- единенный дескриптор. 30.6. Простой сервер времени и даты Перепишем наш простой сервер времени и даты, приведенный в листинге 11.6, с использованием XTI, вызывая функции tcp_l isten и xti accept. Листинг 30.3. TCP-сервер времени и даты, написанный с использованием XTI //xtiintro/daytimesrvOl с 1 #include "unpxti h" 2 int 3 maindnt argc. char **argv) 4 { 5 int listenfd. connfd. 6 char buff[MAXLINE]. 7 time_t ticks. 8 socklen_t addrlen. 9 struct netbuf cliaddr. 10 if (argc == 2) 11 listenfd = Tcp_listen(NULL. argv[l], (laddrlen). 12 else if (argc == 3) 13 listenfd = Tcp_listen(argv[l], argv[2], (laddrlen). 14 else 15 err_quit(”usage daytimetcpsrvOl [ <host> ] <service or port>"): 16 cliaddr buf = Malloc(addrlen). 17 cliaddr maxlen = addrlen. 18 for (. ) { 19 connfd - xti_accept(listenfd. &cliaddr, 0). 20 printf("connect!on from Xs\en". Xti_ntop(&clladdr)): 21 ticks = time(NULL). 22 snprintf(buff. sizeof(buff). "X 24s\er\en". ctime(8ticks)): 23 T_snd(connfd. buff, strlen(buff). 0): 24 T_close(connfd). Создание точки доступа 10-17 Функция tcp_listen создает прослушиваемую точку доступа. Мы выделяем память для адреса протокола клиента и для этого инициализируем структуру netbuf.
864 Гпава 30. XTI: TCP-серверы Ожидание соединения и его прием L9-20 Наша функция xti_accept ждет прибытия соединения, создает новую точку доступа, возвращает присоединенный дескриптор и возвращает IP-адрес клиента и номер порта. Мы выводим адрес протокола клиента, используя нашу функцию xti_ntop. Генерируем вывод времени и даты ’1-24 Вызов функции time, а затем ctime генерирует получение значения времени и да- ты в текстовом формате, а функция t snd отсылает эти данные обратно клиенту по установленному соединению. Точка доступа закрывается посредством вызова функции t_close. Обратите внимание на то, что мы просто вызываем функцию t_cl ose по завер- шении отправки данных. Так как в TCP имеется возможность нормального за- вершения, то отсылается сегмент FIN и осуществляется обычная последователь- ность закрытия соединения посредством обмена четырьмя пакетами (см. рис. 2.5), по функция t close завершается сразу же. ПРИМЕЧАНИЕ ------------------------------------------------------------------ Здесь имеется полная аналогия с вызовом функции close на сокете TCP. Если бы мы хотели дождаться получения данных собеседником и прихода отправленного им сегмента FIN, мы должны были бы вызвать функцию t_sndrel для отправки своего сегмента FIN, а затем ждать сегмент FIN, который отправит собеседник, с помощью функции t rcvrel. Тогда пришлось бы заменить вызов функции T_close в конце листинга 30.3 следующим кодом: Tsndrel (connfd) while ( (n = t_rcv(connfd buff MAXLINE &flags)) >= 0) if (t_errno == TLOOK) { if ( (n = T_look(connfd)) == T_ORDREL) { I_rcvrel(connfd) } else if (n == T_DISCONNECT) { T_rcvdis(connfd. NULL) } else err_quit('unexpected event after t_rcv 5kT. n): } else err_xti(’t_rcv error') T_close(connfd). Функция t_sndrel отсылает сегмент FIN, и далее мы должны ждать сообщения о нормальном завершении {orderly release indication), после чего можно вызвать функцию t rcvrel. Для этого мы вызываем функцию t rev, игнорируя любые дан- ные, которые могут прибыть. ПРИМЕЧАНИЕ ------------------------------------------------------------------ Этот сценарий аналогичен вызову функции shutdown для сокета, после которого мы ждем, когда функция read возвратит конец файла (см. рис. 7.3). В случае XTI мы также можем вызвать задержку выполнения функций t close или close, если в очереди для отправки клиенту остаются какие-либо данные, вместо того чтобы немедленно завершить соединение. Это осуществляется путем установки пара- метра XTI LINGER, который мы описываем в разделе 32 3. Он аналогичен параметру сокетов SOLINGER.
30.7. Несколько соединений, ожидающих обработки 865 30.7. Несколько соединений, ожидающих обработки Мы уже упоминали о сложностях, возникающих в случае, когда приблизительно в одно и то же время на прослушиваемую точку доступа прибывает несколько соединений. Для того чтобы продемонстрировать эти проблемы, мы вернемся к нашему TCP-серверу, представленному в листинге 27.2. Мы использовали этот сервер для измерения времени, затрачиваемого на управление процессом, для разных типов серверов. Мы можем запустить клиент, написанный для этого сер- вера (см. листинг 27.1), и задать количество дочерних процессов, которые будут порождаться функцией fork, устанавливая тем самым несколько соединений с сервером. В листинге 30.4 показан сервер, который представляет собой повторение сер- вера из листинга 27.2, но использует XTI вместо сокетов. Листинг 30.4. Параллельный сервер TCP, демонстрирующий проблемы с несколькими одновременными соединениями 7/xtiserver/servOl с 1 #include "unpxti h" 2 i nt 3 maindnt argc. char **argv) 4 { 5 int listenfd. connfd 6 pid_t childpid 7 void sig_chld(int) sigint(int). web_child(int>: 8 socklen_t addrlen. 9 struct netbuf cliaddr 10 if (argc “= 2) 11 listenfd = Tcpjisten(NULL. argv[l] &addrlen), 12 else if (argc == 3) 13 listenfd = Tcp_listen(argv[l] argv[2], fiaddrlen); 14 else 15 err_quit("usage servOl [ <host> ] <port#>”) 16 cliaddr buf = Malloc(addrlen) 17 cliaddr maxlen = addrlen 18 Signal (SIGCHLD sig_chld). 19 Signal(SIGINT. sig_int). 20 for (. ) { 21 connfd = Xti_accept(listenfd Scliaddr, 1). 22 printf(“connect!on from fc\en". Xti_ntop(&cliaddr)) 23 if ( (childpid = ForkO) = 0) { /* дочерний процесс */ 24 Closedistenfd). /* закрываем прослушиваемую точку доступа *7 25 web_child(connfd). /* обрабатываем запрос */ 26 exit(O) 27 } 28 Close(connfd) 7* родительский процесс закрывает присоединенную точку доступа */ 29 } } продолжение &
866 Гпава 30. XTI: TCP-серверы Листинг30.4 (продолжение) 31 void 32 sig_int(int signo) 33 { 34 void xti_accept_dump(void); 35 xti_accept_dump(). 36 exit(O), 37 } -22 Мы вызываем наши функции tcp_listen и xt1_accept, которые мы описали ра- нее в этой главе. Обработчик сигнала SIGINT -37 Наш обработчик сигнала вызывает внутреннюю функцию xti_accept_dump, и мы используем ее для получения значений счетчиков, показанных в табл. 30.1 (с. 872). Эта функция выводит значение элемента count каждой структуры cl i (рис. 30.3). Если мы запустим этот сервер для прослушивания на порте TCP 9999: umxware % servOl 9999 и запустим клиент с другого узла приведенной ниже командой: Solaris % client UnixWare 9999 1 600 4000 то все будет работать правильно. (1 — это количество дочерних процессов, по- рождаемых функцией fork, 600 — это количество соединений на один дочерний процесс, а 4000 — это количество байтов на одно соединение.) Наш сервер рабо- тает нормально, поскольку в данном примере порождается всего один дочерний процесс и поэтому соединения от клиента приходят последовательно. Если мы изменим третий аргумент командной строки с 1 на 2, мы почти сразу же получим следующую ошибку от сервера: t_accept error event requires attention Проблема состоит в том, что два соединения прибывают к серверу почти од- новременно — по одному от каждого дочернего процесса. Для каждого из этих соединений выполняется трехэтапное рукопожатие TCP, поскольку на стороне сервера соединение устанавливается с помощью TCP. В то время как TCP сервера устанавливает соединения, наш серверный про- цесс блокируется в вызове функции t_l i sten, которая входит в вызываемую сер- вером функцию xti_accept. Когда по первому соединению завершено трехэтап- ное рукопожатие, завершается функция t_l i sten, а затем вызываются функции t_open и t_accept. Но когда для этого первого соединения вызывается функция t_accept, для второго соединения завершается трехэтапное рукопожатие, и оно также готово к тому, чтобы быть принятым. Правила XTI указывают, что в таком случае вместо завершения установления первого соединения функция t_accept возвращает ошибку со значением переменной t_errno, равным TLOOK (Event requi res attention — Событие требует внимания). Событие, ожидающее обработки, — это T_LISTEN (сообщение о подключении требует обработки), поскольку имеется еще одно соединение, ожидающее обработки (второе соединение). Здесь происходит следующее: функция t_accept всегда возвращает ошибку, если имеется другое соединение, готовое к приему. В таком случае нужно вы- звать функцию t_l 1 sten для получения всех сообщений о соединении, сохранить структуру t_call для каждого соединения, а затем для каждого соединения вы- звать функцию t_accept.
30.8. Функция xti accept (еще раз) 867 ПРИМЕЧАНИЕ ----------------------------------------------------------- Почему в XTI прием соединения осуществляется таким странным образом? Если функ- ция t_listen на самом деле завершается в момент прибытия сегмента SYN от клиента (см. рис. 30.1), то вызов функции t_listen сервером для всех прибывающих сегментов SYN, предшествующий вызову функции t_accept для какого-либо одного из прибыв- ших соединений, дает процессу сервера возможность выбирать порядок приема ожи- дающих соединений. Сервер может задать этот порядок, например, па основании IP- адреса или номера порта клиента, возвращенных функцией t_listen, или на основании пользовательских данных, сопровождающих запрос па соединение (пе поддерживает- ся TCP). Но если функция t_listen не завершается, пока пе закончится трехэтаппое рукопожатие TCP, то описанное свойство лишь добавляет ненужные усложнения па стороне сервера. Все только что сказанное нами относится только к XTI в Unix 95. В Posix.lg и Unix 98 изменено описание функции t_accept: там сказано, что опа можег не сработать и вы- дать ошибку со значением переменной t_errno, установленным в TLOOK. Так или ина- че. мы должны быть готовы к тому, что при вызове функции t_accept возможно появ- ление описанной ошибки. 30.8. Функция xti.accept (еще раз) Перепишем нашу простую версию функции xti_accept, приведенную в листин- ге 30.2, чтобы получить более устойчивую к сбоям версию. Для этого нам нужно решить следующие две проблемы: функция t_accept не срабатывает, когда имеется еще одно (или несколько) соединение, ожидающее обработки (функция t_look возвращает событие T LISTEN); функция t_accept не срабатывает, когда по соединению, ожидающему обра- ботки, прибыл сегмеп г RST (функция t_l ook возвращает событие TJDISCONNECT). Для обработки описанных сценариев нам нужно поддерживать очередь собы- тий, ожидающих обработки. Возможная длина этой очереди определяется зна- чением ql 1 m при вызове функции t_bi nd для прослушиваемой точки доступа. Существует множество структур данных, которые мы можем использовать для отслеживания соединений, ожидающих обработки. Для простоты мы будем использовать простой стек (массив) структур сП. Каждая структура содержит присоединенный дескриптор, диагностический счетчик для определения того, как часто используется данная структура, и указатель на структуру t_cal 1. Предположим, что три клиента устанавливают соединения с нашим сервером примерно в одно и то же время. TCP сервера завершит все три последовательно- сти трехэтапного рукопожатия, и когда функция t_l т sten возвратит первое из установленных соединений, мы используем для него структуру с! т [0] из нашего массива, как показано на рис. 30.3. connfd — это новый дескриптор, созданный путем вызова функции t_open. На нем будет осуществляться прием данного соединения, count — это диагности- ческий счетчик, который мы вскоре исследуем с помощью тестовой программы, teal 1 ар — это указатель на структуру t_cal 1, которая заполняется с помощью функ- ции t_l 1 sten и затем передается функции t_accept. Она содержит адрес протоко- ла клиента (поле addr) и идентификатор соединения (поле sequence). У нас также
868 Глава 30. XTI: TCP-серверы ncli=l Рис. 30.3. Структуры данных после того, как первое соединение с клиентом возвращено функцией tjisten имеется счетчик (ncl т) количества элементов нашего массива структур с 1 т, и его значение пока равно 1. Предоложим, что мы вызываем функцию t_accept для приема первого соеди- нения, по она возвращает ошибку TLOOK, a t_look возвращает T_LISTEN. Тогда мы должны вызвать t_l i sten еще раз для получения нового соединения. Мы просто добавляем еще один элемент в наш массив структур с! т и увеличиваем на 1 зна- чение счетчика ncl т. Эта ситуация проиллюстрирована рис. 30.4. . ncli=2 Рис. 30.4. Структуры данных после того, как второе соединение с клиентом возвращено функцией tjisten Мы всегда вызываем функцию t_accept для «последнего» элемента массива, индекс которого равен ncli -1. В результате мы обрабатываем соединения в по- рядке LIFO (Last In, First Out — последним пришел, первым обслужен), а не в порядке FIFO, как можно было предположить. Порядок обработки можно из- менить, но это потребует некоторых усложнений. ПРИМЕЧАНИЕ ----------------------------------------------------------- В табл 4 5 показано, что даже на умеренно загруженном web-сервере редко случается так, что для приема приложением в одно и то же время гоюво более одного соедине- ния. Следовательно, простота нашей схемы может считаться оправданной В этот момент мы вызываем функцию t_accept для элемента массива cl т [1], но предполагаем, что эта функция также возвращает ошибку TLOOK, a tjook воз- вращает TJ.ISTEN. Мы должны добавить в наш массив другой элемент, cl т [2], как показано на рис. 30.5. На данный момент у нас имеется три соединения, ожидающих приема, и те- перь мы вызываем функцию t_accept для элемента cl т [2]. Но теперь предполо- жим, что первый клиент (EcliOJ) только что разорвал соединение, отослав сег- мент RST. Снова вызов функции t_accept не срабатывает и возвращается ошибка TLOOK, но на этот раз функция t_l ook возвращает ошибку ^DISCONNECT. Теперь нам нужно вызвать функцию t_rcvdi s для закрытия соединения. Вспомним, что одним
30 8. Функция xti accept (еще раз) 869 ncli=3 Рис. 30.5. Структуры данных после того, как третье соединение с клиентом возвращено функцией tjisten из компонентов структуры t_ch scon, которую заполняет данная функция, являет- ся sequence — идентификатор разорванного соединения (см. раздел 28.10). Его следует использовать для поиска в нашем массиве структур cl 1 того элемента, у которого в структуре t_call имеется такой же идентификатор sequence. Затем мы сдвигаем элементы массива «вверх» один за другим, заменяя с 1 т [О J на с 1 т [1], a cl 1 [1] на cl 1 [2]. Кроме того, мы уменьшаем па 1 значение счетчика ncl 1 и полу- чаем структуры данных, представленные на рис. 30.6. ncli=2 Рис. 30.6. Структуры данных после того, как первое соединение было разорвано Мы снова вызываем функцию t_accept для элемента cl 1 [1], но предполагаем, что на этот раз сбоя не произойдет и функция выполнится успешно. Затем мы убираем элемент cl i [1] из массива, уменьшаем значение счетчика ncl i на 1 (он становится равным 1) и передаем третье соединение процессу, вызвавшему xti_ accept. Напомним, что все, о чем здесь шла речь, начиная с вызова функции t_l i sten, происходило в рамках нашего вызова функции xti_accept. Теперь настал момент, когда эта функция в первый раз возвращает вызывающему процессу присоеди- ненный дескриптор. В следующий раз при вызове функции xti_accept значение счетчика ncl т будет равно 1, а функция t_accept будет вызвана для приема соединения, соответ- ствующего элементу cl т [0]. Если предположить, что она будет успешно вы- полнена, вызывающему процессу будет возвращен второй присоединенный дескриптор. Теперь мы покажем исходный код для нашей функции xti_accept, первая часть которого содержится в листинге 30.5.
870 Глава 30. XTI: TCP-серверы Листинг 30.5. Функция xti_accept: первая часть //libxti/xti_accept с 1 include "unpxti h" 2 static int ncli = -1, ndisconn, 3 static struct cl i { 4 int connfd /* присоединенный дескриптор fd или -1. если соединение разорвано */ 5 1 nt count 6 struct t_call *tcallp /* указатель на структуру размещенную в памяти */ 7 } *cli /* используются элементы cli[O]. cli[l] cli[ncli-l] */ 8 int 9 xti_accept(int listenfd struct netbuf *cliaddr int rdwr) 10 { 11 int i event. 12 u_int n. 13 char *ptr 14 struct t_discon tdiscon 15 if (ncli == -1) { /* вначале сквозная инициализация */ 16 if (cl т i- NULL) 17 err_quitCalready initialized"). 18 if ( (ptr - getenv("LISTENQ")) NULL) 19 • n = atoi(ptr). 20 else 21 n - LISTENQ. 22 cli = Callocln sizeoftstruct cli)). 23 for (i -0, i <n i++) 24 cli[i] teal Ip - T_alloc(listenfd T_CALL, T_ALL) 25 ncli - 0. 26 } Объявление статических переменных 7 Счетчик ncli, диагностический счетчик разорванных соединений ndisconn и указатель на наш массив структур cl 1 объявляются как статические переменные. Инициализация при первом вызове -26 При первом вызове мы размещаем в памяти массив структур cl 1, количество элементов в котором равно константе LISTENQ или значению переменной окруже- ния с тем же именем. Это будет то же значение, что использовалось в функции tcp_l isten при вызове функции t_bind. Тогда для каждого элемента массива мы вызываем функцию t_al1ос для размещения структуры t_cal 1 в памяти и записы- ваем возвращенный указатель в нашей структуре cl 1. ПРИМЕЧАНИЕ -------------------------------------------------- В случае использования потоков эта копия массива cli и его счетчик ncli должны быть защищены, чтобы позволить нескольким потокам вызывать функцию xti_accept одно- временно. Вторая часть этой функции показана в листинге 30.6. Листинг 30.6. Функция xti_accept: вторая часть //libxti/xti_accept с 27 for ( .) { 28 if (ncli == 0) { /* нужно ждать соединения */
30,8. Функция xti accept (еще раз) 871 29 TJisten(listenfd cl i[ncli] tcallp), /* блокируемся здесь */ 30 31 32 33 34 35 36 37 38 39 40 /* дальнейшее предполагает что вызывающий процесс вызвал функцию tcp_listen() */ cli[ncli] connfd = T_open(xti_serv_dev, O_RDWR. NULL), T_bind(cli[ncli] connfd NULL NULL). cli[ncli] count++ ПС11++. ) if (t_accept(l istenfd. cli[ncli - 1] connfd cliEncli - 1] tcallp) == 0) { ncli--. /* успех */ if (rdwr) Xti_rdwr(cli[ncli] connfd). 41 if (cliaddr) { /* возвращаем адрес протокола клиента */ 42 n = nnn(cliaddr->maxlen cli[ncli] tcallp->addr len): 43 menicpy(cliaddr->buf. cli[ncli] tcallp->addr buf n). 44 cliaddr->len = n. 45 } 46 return (cli[ncli] connfd). 47 } else if (t_errno == TLOOK) { 48 if ( (event - T_look(listenfd)) -= T_LISTEN) { 49 T_listen(listenfd. cli[ncli] tcallp). /* не блокируется */ 50 cli[ncli] connfd - T_open(xti_serv_dev O_RDWR NULL), 51 T_bind(cli[ncli] connfd, NULL. NULL), 52 cli[ncli] count++. 53 ncli++. 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 } else if (event — T_DISCONNECT) { T_rcvdis(listenfd &tdiscon): for (i - 0. i < ncli. i++) { if (cli[i].tcallp->sequence == tdlscpn.sequence) { T_close(cli[i] connfd) ndisconn++. ncli--. if ( (n - ncli - i) > 0) memmove(&cli[i], &cli[i + 1]. n * sizeof(struct cli)). break. ) ) } else err_quit("unexpected tjook event ^d". event), else err_xti("unexpected t_accept error"). Ожидание соединения 28-35 Если наш массив пуст, мы должны вызвать функцию t_l 1 sten и ждать прибы- тия соединения. В этом состоянии прослушивающий сервер проводит большую часть времени. Когда функция t_11 sten возвращает управление, адрес протокола клиента и идентификатор соединения записываются функцией t_l i sten в струк- туру t_cal 1. Затем мы вызываем функцию t_open для создания новой точки до- ступа, на которой это соединение будет принято, и связываем эту точку доступа с любым локальным адресом.
872 Глава 30. XTI: TCP-серверы Вызов функции t_accept, возвращение в случае успешного выполнения - 46 Мы вызываем функцию t_accept для приема соединения, заданного элементом массива cl i [ncl 1 -1], и если этот прием успешно выполняется, мы возвращаем вызывающему процессу дескриптор присоединенной точки доступа. Мы также вызываем пашу функцию xti_rdwr, если вызывающий процесс должен использо- вать функцию read или write, и при необходимости возвращаем адрес протокола клиента. Обработка дополнительных соединений из очереди • 53 Если функция t_accept возвращает константу TLOOK, a t_1 ook возвращает T_LISTEN, это означает, что имеется еще одно соединение, ожидающее обработки, и следует вызвать функцию t_listen для получения информации о нем. Обратите внима- ние, что этот вызов не будет блокирован, так как в точке доступа имеется собы- тие TJ.ISTEN, ожидающее обработки. Мы также вызываем функции t_open и t_bi nd и сохраняем полученную информацию в элементе массива cl i [ncl i ]. Обработка разрыва соединения, ожидающего приема 66 Если функция t_accept возвращает константу TLOOK, a t_look возвращает ^DIS- CONNECT, это означает, что одно из соединений, которое уже было возвращено функ- цией t_l 1 sten, было впоследствии разорвано клиентом. Для получения информа- ции о разорванном соединении (а точнее, значения его идентификатора sequence) мы вызываем функцию t_rcvdi s и просматриваем наш массив в поисках соответ- ствующего элемента. Найдя этот элемент, мы вычисляем количество соедине- ний, расположенных в массиве после разорванного (п), и вызываем функцию memmove для того, чтобы сдвинуть все последующие элементы. Мы вызываем функцию memmove, а не memepy, так как первая корректно обрабатывает перекрывающиеся об- ласти памяти, которые могут появиться в данном сценарии (см. упражнение 30.3). Таблица 30.1. Сколько раз функция t_accept возвращает константу TJJSTEN Счетчик на стороне сервера Количество дочерних процессов на стороне клиента 1 2 3 4 cli[0].count 600 309 95 102 cli[l].count 291 286 121 cli[2].count 219 235 cli[3].count 142 Всего 600 600 600 600 Мы можем протестировать сервер, приведенный в листинге 30.4, используя эту новую версию функции xti_accept. Она корректно работает при наличии не- скольких соединений. Мы также хотели бы знать, как часто функция t_accept возвращает ошибку в результате прибытия дополнительных соединений. Для этого мы напишем простую функцию, которая выводит значение счетчика count структуры cl 1 и значение счетчика nd 1 sconn (который мы вскоре опишем), а затем вызовем эту функцию из родительского обработчика сигнала SIGINT (см. лис- тинг 30.4). В табл. 30.1 показаны результаты, полученные для сервера, работаю-
30.8. Функция xtiaccept (еще раз) 873 щего под управлением UnixWare 2.1.2, и клиента, работающего под управлением Solaris 2.5.1. Количество дочерних процессов на стороне клиента варьировалось от 1 до 4, при этом общее количество соединений ото всех дочерних процессов всегда было равно 600. Даже если имеется толко 2 клиента, каждый из которых устанавливает по 300 соединений одно за другим, примерно в одно и то же время, функция t_accept примерно в половине случаев возвращает константу TLOOK с T_LISTEN. ПРИМЕЧАНИЕ ------------------------------------------------------------------------ Почему оказывается, что в этом сценарии так часто возникает ситуация, когда одно- временно несколько соединений готовы к приему сервером, если ранее мы говорили (см. табл. 4.5), что даже для загруженных web-серверов это редкий случай? Одна из причин состоит в том, что в рассматриваемом памп сценарии в табл. 30.1 все соедине- ния приходят от клиента по одной и той же локальной сети. Далее, предполагаемая скорост ь прихода соединений, 600 за 12 секунд, соответствует примерно 4 миллионам соединений в день. Наконец, в нашем сценарии для проверки того, как функция xti_ accept обрабатывает несколько соединений, ожидающих приема, мы предполагали, что сервер достаточно медленный (Pentium с частотой центрального процессора 75 МГц). Можно сделать вывод, что такой сценарий не слишком часто встречается в реальной жизни, так что обработка подключений функцией xti_accept с использованием техно- логии LIFO оправданна, по поскольку в нем могут и будут присходить ошибки, сервер должен уметь с ними справляться. Чтобы проверить, как работает этот сервер с клиентом, внезапно разрываю- щим только что установленное соединение, мы несколько модифицируем клиент из листинга 27.1, заменив самый глубокий внутренний цикл следующим кодом: for (j = 0. j < nloops. j++' { fd = Tcp_connect(argvLl]. argv[2]): + if (i == 2 && (j % 3) == 01 { + struct linger ling. + + ling l_onoff = 1 + ling l_linger = 0. + Setsockopt(fd. SDL_SOCKET SOJ.INGER. &ling, sizeof(lmg)). + Close(fd), + + /* и продолжаем далее для этого клиентского соединения */ + fd = Icp_connect(argv[l]. argv[2]), + } Write(fd, request. strien(request)) if ( (n = Readntfd reply, nbytes)) '= nbytes) err_qirt("server returned *d bytes" ni. Close(fd). /* состояние TIME_WAIT на стороне клиента а не сервера */ Строки, перед которыми стоит знак +, мы добавили. В результате этой моди- фикации третий дочерний процесс клиента (для которого 1 равно 2) разрывает каждое третье только что установленное соединение. Для отправки сегмента RST мы устанавливаем параметр сокета SOJ.INGER и закрываем (cl ose) сокет. Затем мы
874 Глава 30. XTI. TCP-серверы создаем другое соединение и продолжаем цикл. Воздействие этой модификации на работу сервера зависит от временных соотношений: некоторые сегменты RST могут прибыть к серверу в промежутках между вызовами функции t_l т sten и функ- ции t_accept (эго подсчитывается нашим счетчиком ndisconn для проверки вы- полнения кода). Другие сегменты могут прибыть еще до начала выполнения функ- ции t_l 1 sten либо после завершения выполнения функции t accept, то есть когда соединение уже установлено. Длина очереди XTI и аргумент backlog функции listen Длина очереди XTJ и аргумент backlog функции 1 т sten похожи, но не одинаковы. Начнем с того, что для аргумента back 1 од функции 1 т sten отсутствует точное опре- деление. В табл. 4.6 показано, что реализация этого аргумента различна в систе- мах, используемых в настоящее время. В Posix. 1g сказано, что длина очереди XTI задает то количество сообщений о соединении, ожидающих обработки, ко горое поставщик поддерживает для дан- ной точки доступа. Сообщение о соединении, ожидающее обрабо I ки, — это такое сообщение, которое было передано приложению поставщиком, но еще не было принято (или отклонено). Поставщик может устанавливать в очередь большее количество сообщений о соединении, чем задано, но при этом он должен следить за тем, чтобы количество доставленных приложению и ожидающих обработки сообщений не превышало величины ql еп в каждый конкретный момент времени. Если бы реализация была такова, что сообщения о соединении передавались бы приложению сразу же по прибытии соединения (то есть по прибытии сегмен- та SYN к серверу), то такой тип формирования очереди мог бы иметь смысл. Но поскольку приложение получает извещение о соединении TCP только тогда, когда это соединение уже полностью установлено, то на самом деле приложению не требуется устанавливать эти соединения в очередь. Как обычно, чтобы разобраться со стандартами, нам нужно посмотреть, како- вы реальные значения ql еп в различных системах. Мы модифицировали листинг Д.9 для работы с XTI вместо сокетов и запустили нашу программу в пяти различ- ных системах, поддерживающих XTI. Результаты показаны в табл. 30.2. Таблица 30.2. Фактическое количество соединений в очереди для различных значений параметра XTI qlen запро- шен** ный qlen AIX 4.2 Dunix 4.0В HP-UX 10.30 Solaris 2.6 Uware2.1.2 возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений 0 0 0 0 0 0 0 0 0 0 1 1 3 1 2 1 1 1 1 1 2 2 6 2 4 2 2 2 2 2 3 3 8 3 6 3 3 3 3 3 4 4 11 4 8 4 4 4 4 4 5 5 13 э 10 5 5 5 5 5
30.8. Функция xti accept (еще раз) 875 запро- AIX4.2 Dunix 4.0В HP-UX 10.30 Solaris 2.6 Uware 2.1.2 шей- ный qlen возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений возвра- щен- ный qlen факти- ческое коли- чество соеди- нений 6 5 13 6 12 6 6 6 6 6 7 5 13 7 14 7 7 7 7 7 8 5 13 8 16 8 8 8 8 8 9 5 13 9 18 9 9 9 9 9 10 5 13 10 20 10 10 10 10 10 И 5 13 11 22 И И И 11 И 12 5 13 12 24 12 12 12 12 12 13 5 13 13 26 13 13 13 13 13 14 5 13 14 28 14 13 14 14 14 Напомним, что, как говорилось в разделе 28.5, третий аргумент функции t_bi nd является указателем на струкуру t_bi nd, которая заполняется по возвращении поставщиком. Поставщик задает значение поля ql еп этой структуры (это называ- ется в XTI согласованным значением — negotiated value). Заметим, что в боль- шинстве систем возвращается то значение ql еп, которое было задано, если не под- держивается меньшее значение (как, например, в AIX). Только в одной системе UnixWare значение не возвращается вовсе. Ни в одной системе не допускаются соединения, если значение qlen равно нулю (в отличие от аргумента backlog функции listen, нулевое значение которого не подразумевает непременного отсутствия соединений, см. табл. 4.6). Две системы вернули ошибку при выполнении функции t_connect (HP-UX и Solaris). Некото- рые реализации устанавливают в очередь большее количество соединений, чем задано значением qlen (AIX и Digital Unix), но остальные три не делают этого. ПРИМЕЧАНИЕ --------------------------------------------------------- Здесь мы подсчитывали количество соединений, установленных в очередь поставщи- ком, а не приложением, так как нас интересует именно та очередь, которая формирует- ся поставщиком Установка сервером единичной длины очереди Одним из способов избежать сложностей, связанных с приемом соединений XTI, является установка значения поля qlen структуры t_bind равным 1. Но с этим решением связана следующая проблема: многие реализации будут устанавливать в очередь только одно соединение, а все остальные приходящие сегменты SYN будут игнорироваться до тех пор, пока стоящее в очереди соединение не будет принято. Мы можем проверить это свойство с помощью нашего сервера из данного раз- дела. Мы снова запустим сервер в системе UnixWare 2.1.2, в которой при единич- ном значении ql еп в очередь устанавливается только одно соединение. Как и в табл. 30.1, мы варьируем количество дочерних клиентских процессов, отправля- ющих запросы на соединение, от 1 до 4, но на этот раз мы измеряем время
876 Глава 30. XTI: TCP-серверы (в секундах), необходимое для установления фиксированного количества соеди- нений (600). Таблица 30.3. Время в секундах, необходимое для установления 600 соединений, в зависимости от количества дочерних процессов Длина очереди Количество дочерних процессов 1 2 3 4 1 10,6 12,2 15,6 13,2 1024 10,6 10,2 10,3 10,4 В случае, когда длина очереди (ql еп) равна 1, время увеличивается пропор- ционально количеству дочерних процессов. Это объясняется гем, что с увеличе- нием количества дочерних процессов все больше клиентских сегментов SYN игнорируется сервером, поскольку в очереди может находиться одновременно только одно соединение, а все сегменты SYN, пришедшие в это время, клиент должен будет передавать повторно. Но если длина очереди (qlen) больше, чем максимально возможное количество одновременно устанавливаемых соединений, то затрачиваемое на обработку соединений время уменьшается, а потом возрас- тает (в зависимости от количества дочерних процессов) Приведенные результаты подтверждают то, что мы говорили ранее: на прак- тике для сервера значение qlen, равное 1, является нереальным. 30.9. Резюме Прием клиентского соединения с использованием технологии XTI гораздо слож- нее, чем с использованием сокетов. Как мы говорили, оправданием этой сложно- сти является возможность осуществления отложенного приема, при котором при- ложение получает извещение в момент прибытия запроса на соединение, а не когда это соединение уже установлено. В реализациях TCP отложенный прием не под- держивается, а в Unix 98 снято требование, заставляющее функцию t_accept воз- вращать событие T_LISTEN, когда другое соединение ждет обработки, но в целях обеспечения обратной совместимости от серверов XTI требуется умение обраба- тывать этот сценарий. Упражнения 1. В разделе 30.2 мы отмечали, что в некоторых реализациях в поле sequence струк- туры t_cal 1 (заполняемой функцией t_l i sten) хранится указатель. Что проис- ходит в 64-разрядных архитектурах? 2. Почему в листинге 30 1 в объявлении xti_serv_dev к РАТН_МАХ добавлена еди- ница? 3. В листинге 30.6 мы вызываем функцию memmove и объясняем это тем, что про- исходит перекрывание копируемых областей (исходной и конечной). Пред- положим, что имеется 4-байтовый массив с элементами от х[0] до х[3] (ин- декс возрастает слева направо) и нам нужно удалить элемент х[1 ], сдвинув при этом следующие два элемента «влево» на 1 байт и оставив 3 элемента.
Упражнения 877 Нарисуйте массив с исходной и конечной областями. Зачем опишите, что про- исходит, если копирование осуществляется из начала области исходной в на- чало конечной (справа налево) Затем опишите, что произойдет, если опера- ция копирования осуществляется из конца исходной области в конец конечной области (слева направо). Гарантирует ли использование функции memcpy ка- кое-либо определенное направление копирования? 4. Модифицируйте листинг Д 9 таким образом, чтобы использовать XTI вместо сокетов. 5. Модифицируйте листинги 30.5 и 30.6 таким образом, чтобы использовать связ- ный список структур cl 1 вместо массива фиксированного размера, который применялся в этой главе для простоты. При этом память под структуры сле- дует выделять динамически.
ГЛАВА 31 XTI: клиенты и серверы UDP 31.1. Введение XTI предоставляет три функции для серверов и клиентов, не ориентированных на установление соединения: t_sndudata для отправки дейтаграммы, t_rcvudata для получения дейтаграммы и t_rcvuderr для получения информации об асинхрон- ных ошибках. При использовании сокетов у нас имеется возможность вызвать функцию connect для приложения UDP, но в XTI этой возможности нет. 31.2. Функции t.rcvudata и t_sndudata Эти две функции используются в протоколах, пе ориентированных на установ- ление соединения (например, UDP), для получения и отправки дейтаграмм, include <xti h> int t_rcvudata(int fd. struct t_umtdata *unitdata. int ★flagsp). int t_sndudata(int fd. struct t_umtdata *umtdata). Обе функции возвращают 0 в случае успешного выполнения. -1 в случае ошибки Для функции t_sndudata структура t_umdata задает адрес получателя, пара- метры и фактические отсылаемые данные. struct t_umtdata { struct netbuf addr. /* адрес специфичный для протокола */ struct netbuf opt. /* параметры специфичные для протокола */ struct netbuf udata. /* пользовательские данные */ }• Для функции t_rcvudata эта структура определяет, где хранится адрес прото- кола отправителя, а также содержит полученные параметры и фактические дан- ные. Обе эти функции возвращают 0 в случае успешного выполнения или -1 в слу- чае ошибки. В этом заключается отличие от большинства функций чтения и за- писи, которые обычно возвращают количество переданных байтов. При выпол- нении функции t_rcvudata размер полученной дейтаграммы возвращается как поле udata len структуры tjjmdata. Функция t_sndudata вовсе не возвращает количе- ство записанных байтов — она просто возвращает 0 в случае успешного выполне- ния (это означает, что вся дейтаграмма была скопирована в буферы ядра). Целочисленное значение, на которое указывает аргумент flagsp, аналогично последнему аргументу функции t_rcv: это не аргумент типа «значение-резуль- тат», поскольку его значение не анализируется функцией, а только задается при
31.3. Функция udp client 879 ее завершении. Если требуется еще раз вызвать функцию t_rcvudata для чтения оставшейся части дейтаграммы (то есть если размер дейтаграммы больше разме- ра буфера приема), то возвращается флаг T_MORE. Подобный пример мы показы- ваем в разделе 31.6. ПРИМЕЧАНИЕ ---------------------------------------------------------- Эти две функции XTI соответствуют функциям sendto и recvfrom. 31.3. Функция udp_client Перед тем как приступать к рассмотрению примера использования XTI для UDP, мы напишем функцию udp_cl lent, которая создает точку доступа XTI для клиен- та UDP, с той же последовательностью вызовов, что была показана в разделе 11.10. Эта функция, приведенная в листинге 31.1 *, выполняет преобразования имени узла и имени службы, описанные в главе 29. Листинг 31.1. Функция udp_client для XTI //libxti/udp_client с 1 #include "unpxti h" 2 int 3 udp_cl lent (const char *host. const char *serv. *votd ’4'vptr/ social etft 4 { 5 int tfd. 6 void *handle. 7 struct netconfig *ncp: 8 struct ndjiostserv hs; 9 struct nd_addrlist *alp. 10 struct netbuf *np, 11 struct t_umtdata *tudptr; 12 handle = SetnetpathO. 13 hs hjiost = (char *) host; 14 hs h_serv = (char *) serv; 15 while ( (nep = getnetpath(handle)) •- NULL) { 16 if (strcmp(ncp->nc_proto, "udp") l= 0) 17 continue 18 if (netdir_getbyname(nep, &hs, &alp) '= 0) 19 continue. 20 tfd = T_open(ncp->nc_device. O_RDWR. NULL); 21 T_bind(tfd. NULL. NULL). 22 tudptr = T_allOC(tfd. TJJNITDATA. T_ADDR). 23 np = alp->n_addrs. /* используем первый адрес */ 24 tudptr->addr len = min(tudptr->addr maxlen np->len), продолжение^ Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter.com/download.
880 Глава 31. XTI: клиенты и серверы UDP Листинг 31.1(продолжение) 25 memcpy(tudptr->addr buf np->buf tudptr->addr len), 26 endnetpatn(handle) 27 netdir_free(alp. ND_ADDRLIST), 28 *vptr - tudptr. /* возвращаем указатель на t_un1tdata{} */ 29 *lenp - tudptr->addr maxlen. /* и размер адресов */ 30 return (tfd) 31 } 32 endnetpath(handle), 33 return (-1) 34 } Поиск имени узла и имени службы 2-19 Вызовы функций getnetpath и netdi r_byname аналогичны вызовам в листинге 29.1. Открытие устройства, связывание его с произвольным локальным адресом 1-21 Функция t_open открывает соответствующее устройство, и функция t_bi nd свя- зывает произвольный локальный адрес с данной точкой доступа. Размещение структуры t_unidata в памяти 22 Функция t_al loc размещает в памяти структуру t umdata, содержащую внутри себя только структуру addr, но не opt или udata. Мы не размещаем в памяти струк- туру opt, поскольку в случае UDP редко используются параметры, относящиеся только к одной дейтаграмме (см. главу 32). Мы не размещаем структуру udata, так как размеры дейтаграмм UDP довольно велики и достигают 65 507 байт, как показано в табл. 28.2, но немногие приложения используют дейтаграммы UDP максимального размера. Поскольку большинство приложений имеют дело с мень- шими по размеру дейтаграммами (максимум до нескольких тысяч байтов), то более разумно разрешить приложению самостоятельно выделять память для бу- фера данных того размера, который требуется в данный момент. Использование первого возвращенного адреса 1-25 Структура addr заполняется данными по первому адресу, возвращенному для данного сервера. На рис. 31.1 показано, какие структуры данных при этом ис- пользуются, в предположении, что две структуры netbuf возвращаются для ука- занного имени узла и имени службы и что в данной реализации используется структура sockaddr_iп для представления адреса IPv4. Значение addr maxi еп должно быть таким же, как значение maxi еп в структуре, возвращенной функцией netdi r_getbyname (16 для IPv4), но мы используем мак- рос mi п, чтобы удостовериться, что мы не переполняем область памяти (прием- ный буфер) при вызове memcpy. Структуры opt и udata содержат четыре нулевых поля длины и два пустых указателя, поскольку функция t_al loc инициализиро- вала соответствующие поля этих структур в соответствии с нашим указанием разместить в памяти и инициализировать только структуру addr с аргументом T_ADDR.
31.3. Функция udp client 881 alp: nd addrliet{} в памяти функцией netdir_getbyname tudptr: memcpy netbuf{}- netbuf{}- netbuf{}* t_unitdata(} addr.maxlen addr.len addr.buf addr.maxlen addr.len addr.buf addr.maxlen addr.len addr.buf О о NULL О О NULL Динамически размещается в памяти функцией t alloc Рис. 31.1. Структуры данных при вызове функции udp_chent Освобождение занятой памяти и возвращение управления 26-33 Функция endnetpath освобождает выделенную под структуру netconfig память, а функция netdi r_fгее освобождает память, которая была выделена функцией netdi r_getbyname (рис. 31.1). Указатель на структуру t_umdata возвращается вызывающему процессу вместе с размером адреса протокола и дескриптором точ- ки доступа. Мы возвращаем размер адреса протокола как addr maxlen вместо addr len, поскольку это значение возвращается вызывающему процессу для ис- пользования при вызове функции ma 11 ос. Если бы адреса имели переменную дли- Кч
882 Глава 31. XTI: клиенты и серверы UDP ну, нам следовало бы возвращать максимальную длину, а не просто размер дан- ного адреса. Пример: клиент времени и даты С помощью нашей функции udp_cl lent мы модифицируем код не зависящего от протокола клиента времени и даты из листинга 11.8 таким образом, чтобы ис- пользовать XTI вместо сокетов. Результат приведен в листинге 31.2. Листинг 31.2. UDP-клиент времени и даты, использующий XTI и функцию udp_client //xtiudp/daytimeudpcli1 с 1 #include “unpxti h“ 2 int 3 maintint argc. char **argv) 4 { 5 int tfd, flags 6 char recvline[MAXLINE + 1]. 7 socklen_t addrlen. 8 struct t_umtdata *sndptr. *rcvptr, 9 if (argc l= 3) 10 err_quit("usage daytimeudpcli <hostname> <service>"). 11 tfd = Udp_client(argv[l], argv[2], (void **) &sndptr &addrlen). 12 rcvptr = T_alloc(tfd. TJJNITDATA. T_ADDR). 13 printfCsending to ^s\en“. Xti_ntop_host(&sndptr->addr)), 14 sndptr->udata maxlen = MAXLINE 15 sndptr->udata len = 1 16 sndptr->udata buf - recvline. 17 recvline[0] - 0. /* однобайтовая дейтаграмма, содержащая нулевой байт */ 18 T_sndudata(tfd, sndptr). 19 rcvptr->udata maxlen = MAXLINE. 20 rcvptr->udata buf - recvline 21 T_rcvudata(tfd, rcvptr &flags). 22 recvline[rcvptr->udata len] - 0. /* завершающий нуль */ 23 printfC'from Xs Xs" Xti_ntop_host(&rcvptr->addr), recvline); 24 exit(O). 25 } Создание точки доступа -13 Мы вызываем нашу функцию udp_cl lent для создания точки доступа XTI и для размещения структуры t_umdata, которую мы будем использовать для отправки дейтаграмм. Мы размещаем в памяти еще одну структуру t_umdata, которая бу- дет использоваться для приема ответов. Для вывода IP-адресов сервера мы вы- зываем функцию xti_ntop_host. Отправка дейтаграммы -18 Мы инициализируем структуру udata, входящую в структуру t_umdata, задавая указатель на буфер recvl i пе и однобайтовую дейтаграмму, содержащую нулевой байт. Функция t_sndudata отправляет дейтаграмму серверу.
31.4. Функция t rcvuderr: асинхронные ошибки 883 ПРИМЕЧАНИЕ ------------------------------------------------------ Вообще говоря, мы должны иметь возможность отправлят ь UDP-дейтаграммы, содер- жащие 0 байт (см. пояснения к табл. 28.4), но многие реализации XTI пе позволяют это делать. Чтение ответа 19-23 Мы инициализируем структуру udata для принимающей структуры t_umdata и вызываем функцию t_rcvudata для чтения ответа сервера. Ответ сервера закан- чивается нулевым байтом (символом конца строки) и выводится в стандартный поток вывода вместе с адресом сервера. В этом примере свойство клиента UDP (отсутствие надежности) останется таким же, как и в сокетной версии этого клиента из листинга 11.8: при отсутствии ответа клиент навсегда заблокируется в вызове функции t_rcvudata. Если мы запустим этот клиент на том же узле, на котором работает сервер, мы получим следующий результат: umxware % daytimeudpclil bsdi daytime sending to 206 62 226 35 from 206 62 226 35 Fri Feb 28 17 23 40 1997 Что если мы пошлем дейтаграмму на тот же узел, но па порт UDP, с которым не связан никакой процесс? Можно предположить, что мы получим сообщение ICMP о недоступности порта. Вспомним, что в случае нашего сокетного клиента, если клиент не вызывает функцию connect, эта ошибка клиенту не возвращается. Для точки доступа UDP при использовании XTI нет аналогов функции connect, но мы видим, что клиенту возвращается ошибка, и функция-обертка T_rcvudata выводит сообщение об этой ошибке: umxware % daytimeudpclil bsdi 9999 sending to 206 62 226 35 t_rcvudata error event requires attention Когда для точки доступа UDP приходит асинхронная ошибка, функция t_rc vudata возвращает ошибку TLOOK, и чтобы определить, какая фактически произошла ошиб- ка, следует вызвать функцию t_rcvudata. Это обсуждается в следующем разделе. 31.4. Функция t_rcvuderr: асинхронные ошибки Для протокола, не ориентированного на установление соединения, ошибки мо- гут возвращаться асинхронно. Это означает, что, возможно, дейтаграмма будет корректно передана стеком протоколов, и только впоследствии при ее передаче по сети будет обнаружена ошибка. Распространенными для дейтаграмм UDP являются сообщения ICMP о недоступности порта (port unreachable) получате- ля или недоступности узла (host unreachable) для какого-либо промежуточного маршрутизатора. Когда подобное сообщение ICMP приходит поставщику спус- тя некоторое время, требуется, чтобы поставщик каким-либо образом уведомил процесс об этой ошибке и чтобы процесс попытался выяснить фактический ха- рактер ошибки Как показано в конце предыдущего раздела, в XTI подобное уве- домление организовано посредством присваивания переменной t_errno значения
884 Глава 31. XTI: клиенты и серверы UDP TLOOK при вызове функции t_rcvudata. Тем самым мы указываем, что в уже отправленной дейтаграмме обнаружена ошибка. Затем мы вызываем функцию t_rcvuderr, чтобы определить, что же произошло на самом деле, и сбросить значе- ние статуса ошибки. include <xti h> int t_rcvuderr(int fd. struct t_uderr *uderr'). Возвращает 0 в случае успешного выполнения. -1 в случае ошибки Если указатель uderr непустой, структура t uderr заполняется в соответствии с информацией об этой ошибке. struct t_uderr { struct netbuf addr, /* адрес, специфичный для протокола */ struct netbuf opt. /* параметры, специфичные для протокола */ t_scalar_t error /* ошибка, специфичная для протокола */ }. Структура addr содержит адрес получателя дейтаграммы, которая вызвала ошибку, структура opt — специфичные для протокола параметры из этой дейта- граммы, a error содержит специфичный для данного протокола код ошибки. Для UDP значение error обычно является одним из значений еггпо из файла <sys /еггпо h>. Если uderr является пустым указателем, то статус ошибки сбрасывается, при- чем никакой информации не возвращается. Пример: сообщение ICMP о недоступности порта Теперь мы изменим наш клиент из листинга 31.2 так, чтобы он мог обрабатывать асинхронные ошибки. Результат показан в листинге 31.3. Листинг 31.3. Клиент UDP, использующий XTI, способный обрабатывать асинхронные ошибки //xtiudp/daytimeudpcli2 с 1 #include "unpxti h" 2 int 3 maintint argc. char **argv) 4 { 5 int tfd. flags. 6 char recvline[MAXLINE + 1]. 7 socklen_t addrlen. 8 struct t_unitdata *sndptr. *rcvptr, 9 struct t_uderr *uderr. 10 if (argc i= 3) 11 err_quit(“usage a out <hostname or IPaddress> <service or port#>"): 12 tfd = Udp_client(argv[l], argv[2J. (void **) Ssndptr, &addrlen), 13 rcvptr = T_alloc(tfd TJJNITDATA T_ADDR), 14 uderr - T_alloc(tfd. TJJDERROR T_ADDR) 15 printfCsending to £s\en". XtijitopJiost(&sndptr->addr)): 16 sndptr->udata maxlen = MAXLINE. 17 <;ndptr->udata len = 1 jntr-x^ta buf = recvline:
31.4. Функция t rcvuderr: асинхронные ошибки 885 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 recvline[0] = 0. /* однобайтовая дейтаграмма содержащая нулевой байт */ T_sndudata(tfd. sndptr) rcvptr->udata maxlen = MAXLINE. rcvptr->udata buf = recvline, if (t_rcvudata(tfd. rcvptr &flags) == 0) { recvline[rcvptr->udata len] = 0. /* завершающий нуль */ printf!"from £s «s' Xti_ntop_host(&rcvptr->addr) recvline): } else { if (t_errno = TLOOK) { T_rcvuderr(tfd. uderr). printfl'error Xld for datagram sent to &s\en uderr->error. Xti_ntop_host(&uderr->addr)). } else err_xtift_rcvudata error"). } exit(O). Обработка асинхронных ошибк 23-33 По сравнению с листингом 11.8 произошли следующие изменения: вызов функ- ции-обертки T_rcvudata заменен вызовом функции t_rcvudata, мы обрабатываем асинхронные ошибки, вызывая функцию t_rcvuderr, и выводим возвращенный код ошибки. Если мы запустим эту программу и отправим дейтаграмму на узел, не поддер- живающий протокол времени и даты, мы получим сообщение ICMP о недоступ- ности порта из функции t rcvuderr: umxware % daytimeudpcli2 gateway.tuc.noao.edu daytime sending to 140 252 104 1 error 146 for datagram sent to 140 252 104 1 umxware % grep 146 /usr/include/sys/errno.h #define ECONNREFUSED 146 /* Отказано в соединении */ Мы видим, что значение error, возвращенное в структуре t_uderr, — это значе- ние еггпо, соответствующее ошибке ICMP (см. табл. А.З). ПРИМЕЧАНИЕ ------------------------------------------------------------------ К сожалению, несмотря па т о, что это свойство весьма удобно (возвращение ошибок ICMP для точек доступа UDP XTI), все же с ним связаны некоторые проблемы. Во-первых, отсутствует требование, заставляющее поставщика извещать приложение, когда воз- никает ошибка. В системе UnixWare 2.1.2, например, сообщения ICMP о недоступнос- ти порта возвращаются приложению, по для сообщений ICMP о недоступности узла это не так. Во-вторых, если мы модифицируем наш клиент таким образом, чтобы три дейтаграммы отправлялись на три различных сервера, а затем прочитаем все ответы и два из них будут содержать сообщения ICMP о недоступности порта, то только пер- вое из этих двух сообщений будет возвращено приложению функцией trcvuderr. Это объясняется тем, что поставщик может принять только одно сообщение об ошибке для каждой точки доступа. Все эти проблемы стали причиной того, что мы разработали независимый способ извещения приложения, работающего с дейтаграммами, об асин- хронных ошибках: это демон icmpd, описанный в разделе 25.7. Обратите внимание па то, что мы получаем лишь код ошибки и адрес получателя дей- таграммы, вызвавшей ошибку. Мы не получаем такую информацию, как, например, адрес отправителя сообщения ICMP об ошибке.
886 Глава 31 XTI клиенты и серверы UDP 31.5. Функция udp.server Мы также можем модифицировать нашу функцию udp_server из листинга 11.10, чтобы использовать XTI Результат приведен в листинге 314 Листинг 31.4. Функция udp_sever, использующая XTI //libxti/udp_server с 1 nclude unpxti h 2 int 3 udp_server(const chan *host const char *serv socklen_t *addrlenp) 4 { 5 int tfd 6 void *handle 7 struct t_bind tbind 8 struct t_info tinfo 9 struct netconfig *ncp 10 struct nd_hostserv hs 11 struct nd_addrlist *alp 12 struct netbuf *np 13 handle = SetnetconfigO 14 hs h_host = (host == NULL) 7 HOST_SELF (char *) host 15 hs h_serv = (char *) serv 16 while ( (nep = getnetconfig(handle)) '= NULL && 17 stremptnep >nc_proto udp ) '= 0) 18 if (nep == NULL) 19 return ( 1) 20 if (netdir_getbyname(ncp &hs &alp) '= 0) 21 return ( 2) 22 np = alp >n_addrs /* используем первый адрес */ 23 tfd = T_open(ncp >nc_device O_RDWR &tinfo) 24 tbind addr = *np /* копируем всю структуру netbuf{} */ 25 tbind qlen = 0 /* не используется для сервера не ориентированного на установление соединения */ 26 Tbind(tfd &tbind NULL) 27 endnetconfig(handle) 28 netdir_free(alp ND_ADDRLIST) 29 if (addrlenp) 30 *addrlenp = tinfo addr /* размер адресов протоколов */ 31 return (tfd) 32 } Поиск протокола, имени узла и имени службы 13 22 Начало этой функции аналогично нашей функции tcp_l i sten (см листинг 30 1): с помощью вызова функции getnetconfig отыскивается протокол, а затем вызы- вается функция netdi r_getbyname для поиска имени узла и имени службы
31 5 Функция udp server 887 Открытие устройства, связывание IP-адреса сервера и порта 23 28 Функция t_open открывает требуемое устройство, а функция t_bi nd связывает IP-адрес сервера (универсальный адрес, если аргумент host является пустым ука- зателем) и порт Память, выделяемая функцией netdir_getbyname, освобождается функцией netdi r_free Получение длины адреса и дескриптора 29 31 Размер адреса протокола возвращается в том случае если последний аргумент является непустым указателем, а возвращаемым значением функции является дескриптор для точки доступа Пример: сервер времени и даты Теперь мы можем переписать наш простой сервер времени и даты из листин- га 11 11 с использованием XTI Полученный результат приведен в листинге 315 Листинг 31.5. UDP-сервер времени и даты с использованием XTI //xtiudp/daytimeudpsrv2 с 1 #include unpxti h 2 #include «time h> 3 int 4 maintint argc char **argv) 5 { 6 int tfd flags 7 char buff[MAXLINE] 8 time_t ticks 9 struct t_umtdata *tud 10 if (argc — 2) 11 tfd = Udp_server(NULL argv[l] NULL) 12 else if (argc == 3) 13 tfd = Udp_server(argv[l] argv[2] NULL) 14 else 15 err_quit( usage daytimeudpsrv [ <host> ] «service or port»") 16 tud = T_alloc(tfd TJJNITDATA T_ADDR) 17 for ( ) { 18 tud >udata maxlen = MAXLINE 19 tud >udata buf = buff 20 if (t_rcvudata(tfd tud &flags) == 0) { 21 printf( datagram from £s\en Xti_ntop(&tud >addr)) 22 ticks = time(NULL) 23 snprintf(buff sizeof(buff) X 24s\er\en ctimet&ticks)); 24 tud >udata len = strlen(buff) 25 T_sndudata(tfd tud) 26 } else if (t_errno == TLOOK) 27 T_rcvuderr(tfd NULL) /* просто устанавливаем в исходное состояние */ 28 else 29 err_xti( trcvudata error ) 30 } 31 }
888 Глава 31 XTI клиенты и серверы UDP Создание точки доступа XTI 10 16 Наша функция udp_server создает точку доступа и связывает с ней локальный IP-адрес и порт Мы вызываем функцию t_al loc с аргументом T_ADDR и размеща- ем в памяти структуру t_umdata, так что фактически в памяти размещается бу- фер только для адреса протокола Считывание запроса, отправка ответа 17 30 Затем программа входит в цикл считывая запрос клиента с помощью функции t_rcvudata и отправляя ответ с помощью функции t_sndudata Если один из наших ответов генерирует асинхронную ошибку, функция t_rcvudata возвратит ошиб- ку, причем переменная t_erгпо примет значение TLOOK и для обработки ошибки мы вызовем функцию t_rcvuderr Обратите внимание, что последним аргумент функции t_rcvuderr является пустым указателем — тем самым осуществляется сброс статуса ошибки без возвращения какой-либо информации (поскольку от нас ничего не требуется в связи с возникновением подобной ошибки) Если бы мы не обрабатывали эти ошибки так, как показано в листинге, а вместо этого при возвращении подобных ошибок функцией t_rcvudata сервер аварийным обрзом заканчивал бы работу, то тогда любой клиент мот бы вызвать сбои на нашем сер- вере, послав па него дейтаграмму, а затем внезапно разорвав соединение Когда на узле клиента был бы получен ответ, клиент отреагировал бы отправкой сооб- щения ICMP о недоступности порга, что привело бы к возвращению ошибки функцией сервера t rcvudata Следовательно, обработка этих асинхронных оши- бок является обязательной для UDP сервера, использующего XTI 31.6. Чтение дейтаграммы по частям Вспомним наше обсуждение в разделе 20 3 относительно усечения дейтаграмм и различных ситуации, возникающих когда дейтаграмма считывается на сокете UDP но ее длина превосходит количество байтов, запрошенных приложением В XTI подобные сценарии обрабатываются иначе Вспомним, что последним аргументом функции t_rcvudata является flagsp Если буфер приложения недостаточно велик для того, чтобы принять в очередь следующую прибывшую дейтаграмму колпчест во возвращенных байтов будет равно udata maxi еп и бит T_M0RE в целочисленном значении на которое указывает flagsp будет включен Этот флаг указывает приложению, что необходимо снова вызвать функцию t rcvudata для считывания оставшейся части дейтаграммы Адрес отправителя и параметры возвращаются функцией только при первом вы- зове функции t_rcvudata и счит ыванип первой части этой дейтаграммы В резуль- тате последующих вызовов функции t rcvudata для считывания оставшейся час- ти этой дейтаграммы элементы addr 1 еп и opt 1 еп по завершении функции станут нулевыми Мы можем продемонстрировать использование этой возможности, модифи- цируя наш клиент из листинга 313, как показано в листинге 316 Листинг 31.6. Использующий XTI UDP-клиент, который считывает возвращенную дейтаграмму по частям //xtiudp/daytimeudpcl14 с 1 #include unpxti h
31 6 Чтение дейтаграммы по частям 889 2 3 #undef MAXLINE #define MAXLINE 2 4 5 6 7 8 9 10 И int mainCint argc char **argv) { mt tfd flags char recvline[MAXLINE + 1] socklen_t addrlen struct t_unitdata *sndptr *rcvptr struct t_uderr *uderr 12 13 if (argc '= 3) err_quit( usage a out <hostname or IPaddress> <service or port#> ) 14 tfd = Udp_client(argv[l] argv[2] (void **) &sndptr &addrlen) 15 16 rcvptr = T_alloc(tfd TJJNITDATA T_ADDR) uderr = T_alloc(tfd TJJDERROR T_ADDR) 17 printfC sending to Xs\en Xti_ntop_host(&sndptr >addr)) 18 19 20 21 22 sndptr >udata maxlen = MAXLINE sndptr >udata len = 1 sndptr >udata buf = recvline recvline[0] = 0 /* однобайтовая дейтаграмма содержащая нулевой байт *f T_sndudata(tfd sndptr) 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 do { rcvptr >udata maxlen = MAXLINE rcvptr >udata buf = recvline flags - 0 if (t_rcvudata(tfd rcvptr &flags) =- 0) { recvline[rcvptr->udata len] = 0 /* завершающий нуль */ if (rcvptr->addr len > 0) printfC from Xs Xti_ntop_hostC&rcvptr->addc)) printfC Xs\en recvline) } else { if (t_errno “ TLOOK) { T_rcvuderr(tfd uderr) printfC error XId from Xs\en uderr->error xti_ntop_host(&ud$rr->iddr)) } else err_xti( t_rcvudata error ) flags - 0 } } while (flags & T MORE) ex1t(0) } Переопределение константы MAXLINE 2-3 Мы переопределяем константу MAXLINE, заданную в заголовочном фалле unp.h, так, чтобы она была равна 2 байтам — это размер нашего приемного буфера Создание точки доступа, отправка дейтаграммы серверу 12-22 Эта часть кода не изменилась относительно листинга 313
890 Глава 31. XTI: клиенты и серверы UDP Считывание ответа по 2 байта 1-41 Мы вызываем функцию t_rcvudata в цикле, пока включен бит T_MORE в перемен- ной flags. Мы выводим IP-адрес сервера только для первой части дейтаграммы, пока функция возвращает непулевое значение поля addr 1 еп. Теперь мы запустим этот клиент и отправим запрос серверу времени и даты: umxware % daytimeudpcli4 bsdi daytime sending to 206 62 226 35 from 206 62 226 35 Su n Ma r 2 1 1 53 5 0 19 97 Если мы уберем символ конца строки из формата, заданного в функции pri ntf для recvline (строка 31 в листинге 31 6, которую мы использовали только для того, чтобы показать, сколько данных возвращается функцией t_rcvudata), мы полу- чим более знакомый для пас формаг вывода результата: umxware % daytimeudpcli4 bsdi daytime sending to 206 62 226 35 from 206 62 226 35 Sun Mar 2 12 04 48 1997 31.7. Резюме Две функции XTI, t_rcvudata и t_sndudata, аналогичны функциям recvfrom и sendto. Одна из новых возможностей, предоставляемых XTI и отсутствующая при ис- пользовании сокетов, — это чтение дейтаграмм по частям. При этом использует- ся флаг T_MORE, указывающий, что дейтаграмма прочитана не полностью и необ- ходимо снова вызывать функцию t_rcvudata для считывания оставшейся части. В XTI возвращение асинхронных ошибок организовано с помощью функций t_rcvudata и t_sndudata, возвращающих ошибку TLOOK. В этом случае вызывается функция t_rcvuderr для получения более подробной информации (в зависимос- ти от используемого протокола) об этой ошибке. Этот вариант имеет определен- ные преимущества перед обработкой асинхронных ошибок для сокетов (асинх- ронные ошибки возвращаются, только если сокет является присоединенным), но даже в случае XTI асинхронные ошибки могут быть потеряны, и наше приложе- ние по-прежнему зависит от стека протоколов, определяя, какие именно ошибки ICMP надо возвратить. Более удачным решением является использование демо- на, подобного icmpd (раздел 25.7), и возвращение ошибок по отдельному каналу.
ГЛАВА 32 Параметры XTI 32.1. Введение Еще одним сложным для восприятия аспектом XTI всегда была обработка пара- метров. В спецификациях и руководствах многие страницы отводятся описанию сложностей, связанных с обработкой и согласованием параметров, при этом не приводится никаких примеров, а в конце стоит фраза наподобие следующей: «по- дробности зависят от реализации». Термин согласование (negotiation) часто используется при обсуждении пара- метров XTI. Параметр не «устанавливается», он является предметом «согласова- ния», то есть поставщик может не установить тот параметр, который мы запроси- ли. Когда параметр XTI согласовывается, возвращается фактическое значение параметра, используемое поставщиком, так что мы можем узнать это значение. В табл. 32.1 перечислены все стандартные параметры XTI, как собственные (generic), которые начинаются с ХТ1_, так и параметры для IPv4. ПРИМЕЧАНИЕ----------------------------------------------------- В Unix 98 перед всеми именами, начинающимися с INET_, ТР_ и UDP_, добавляется Т_, но в Posix. 1g этого не делается. Например, в Posix. 1g есть параметр UDP, называе- мый UDP_CHESKSUM В Unix 98 такие имена Posix.lg признаются корректными, но мы в данном тексте будем использовать более новую версию имен. В XTI параметры классифицируются либо как сквозные (end-to-end), либо как локальные (local). Сквозные параметры обычно инициируют передачу информа- ции одного и того же типа по сети собеседнику. Примером может служить поле типа службы в IPv4. Этот параметр может быть установлен в одной точке досту- па (либо для UDP, либо для TCP), передан в заголовке IPv4 и получен в другой точке доступа. Параметры заголовка IPv4 и контрольная сумма UDP, указанные в табл. 32.1, также могут служить примером сквозных параметров. Примером ло- кального параметра является T_IP_REUSEADDR, так как этот параметр влияет на воз- можность вызывающего процесса связывать номер порта, который уже исполь- зуется, с его точкой доступа, но никак не влияет на данные, отсылаемые на другую точку доступа. Некоторые параметры XTI классифицируются как обязательные (absolute requirement), что также отражено в табл. 32.1. При установлении значения како- го-либо параметра с таким свойством возвращается сообщение об ошибке, если запрошенное значение не может быть присвоено данному параметру. Если же у параметра это свойство отсутствует и мы пытаемся присвоить ему некое значе-
892 Глава 32 Параметры XTI ние, лежащее вне области поддерживаемых для него значений, то поставщик из- менит запрошенное значение на некоторое другое из разрешенных пределов Примером такого параметра может служить размер буфера приема XTI_RCVBUF, так как в большинстве систем задан как верхний, так и нижнии предел для этого размера Если мы запросим некоторый размер, меньший нижнего предела или больший верхнего, этот размер будет изменен и доведен до ближайшего прием- лемого значения Таблица 32.1. Параметр! □I XTI Уровень Название Тип данных Сквозной Обязательный Описание ХТ1_ GENERIC XTI_DEBUG t_uscalar_t[] Разрешает отслежи вание при отладке XTILINGER t_linger{) Задержка при за крытии если оста лись данные для отправки XTIRCVBUF tuscalart Получение размера буфера ХТ1_ RCVLOWAT t_uscalar_t Получение мини- мального объема буфера XTI_SNDBUF t_uscalar_t Отправка размера буфера ХТ1_ SNDLOWAT t_uscalar_t Отправка мини малыюг о объема буфера T_INETJ> T_INET_ BROADCAST u_int Разрешается от- правка широкове- щательных сообщении T_INET_ DONTROUTE uint Обход поиска в таб лице маршрути- зации 1INET OPTIONS u_char[] • Параметры заг о- ловкаIP T_1NET_ REUSEADDR u_int Позволяет исполь- зовать локальный адрес повторно TJNFT_TOS u_char • Тип службы и по- рядок старшинства TINETTTL u_char Время жи ши Т INET T ГСР t_kpalive() Периодические ТС? KFEPALIVE проверки действует ли соединение TJTCP_ MAXSEG t_uscalar_t TCP MSS (только для чтения) T TCP t uscalar l Отключает алго- NODELAY ритм Нагла Г INET T UDP t uscalar t • Разрешает исполь- UDP CHESKSUM зование контроль- ных сумм
32 1 Введение 893 Чтобы задать или получить значение параметра XTI, можно использовать сле- дующие способы 1 Вызвав функцию t_optmgmt, можно задать любые желаемые параметры (сквоз- ные или локальные) Мы также можем вызвать згу функцию для получения текущего значения параметра или его значения по умолчанию 2 Для точки доступа UDP мы можем задавать требуемые параметры (сквозные или локальные) при каждом вызове функции t_sndudata, используя поле opt структуры t_umtdata 3 Для точки доступа UDP любые сквозные параметры прибывающие с дейта- граммой, возвращаю гея функцией t_rcvudata через поле opt структуры t_umtdata 4 Для клиента TCP мы можем задать требуемые параметры (сквозные и локаль- ные) при вызове функции t_connect через поле opt структуры t_cal 1 5 Для сервера TCP любые сквозные параметры, прибывающие с соединением, возвращаются функцией t_lт sten через поле opt ciруктуры t eal 1 ПРИМЕЧАНИЕ-------------------------------------------------------------- Функция t_optmgmt является комбинацией функции getsockopt и setsockopt В API сокетов, тем не менее, отсутствует возможноегьзадавагь параметры при oiправке или получении дейтаграмм UDP и при инициализации или приеме соединении TCP Фун- кции sendmsg и recvmsg позволяют задавать и получать вспомогательные данные и эта возможность используется в протоколе IPv6 В табл 32 2 приводится краткая информация об отправке и получении пара- метров функциями XTI Таблица 32.2. Функции XTI, с помощью которых можно задавать или получать значения параметров Точка доступа Функция Возвращает только Возвращает Задает сквозные сквозные параметры сквозные и локальные и локальные параметры параметры Любая TCP UDP toptmgmt • • taccept • tconnect • • t_listen • t_rcvconnect • trevudata • t_icvvudata • t_rcvuderr • tsndudata • t_sndvudata • В табл 32 2 указано, что мы можем задавать параметры с помощью функции t_accept В случае TCP эго невозможно, поскольку при завершении функции t_l т sten соединение уже установлено Следовательно, любые сквозные парамет- ры, которыми мы хотим возденет вовагь на процесс трехэтаппого рукопожатия, должны быть заданы для прослушиваемой точки доступа
894 Глава 32. Параметры XTI 32.2. Структура t_opthdr Для того чтобы задать или получить значения параметров XTI, всегда использу- ется структура netbuf, называемая opt, которая является элементом структур t_cal 1, t_optmgrrt, t_uderr и t_umdata (см. табл. 28.5). Буфер opt содержит одну или не- сколько структур t opthdr, после каждой из которых следует значение параметра, struct t_opthdr { t_uscalar_t len. /* полная длина параметра sizeoftstruct t_opthdr) + length of value */ t_uscalar_t level. /* протокол, на который воздействует параметр */ t_uscalar_t name /* название параметра */ t_uscalar_t status /*статус */ /* далее следует значение параметра и. возможно, заполнение */ } ПРИМЕЧАНИЕ ----------------------------------------------------------- Одно из различий между TLI и XTI заключается в том, что в ТЫ ничего пе говорится о формате буфера параметров, кроме того, чю он зависит от реализации. Многие реа- лизации ТЫ использовали структуру, называемую opthdr, в которой имеется только три элемента: level, name и len. На рис. 32.1 мы показываем две из этих трех структур XTI, на которые указы- вает структура netbuf, являющаяся частью структуры t_umdata. netbuf{} - netbuf{) - t_unitdata{) addr.maxlen addr.len addr.buf opt.maxlen opt.len 40 opt.buf ► len 17 udata.maxlen level INET IP IP TTL t_opthdr{} netbuf{} - udata.len name udata.buf status len level name status Г 137 17 INET IP ip_tos -tcpthdrf) 0x00 Рис. 32.1. Пример двух параметров, на которые указывает структура netbuf На рис. 32.1 для IP мы задаем величину TTL, равную 137, а полю IP тип служ- бы присваиваем 0x10 (приоритет программ и малая задержка). Значение каждого параметра — 1 байт типа u_char (см. табл. 32.1). После каждого значения идут 3 бай- та заполнения. Здесь мы также предполагаем, что тип данных t_uscal ar_t занима- ет 4 байта, следовательно, общий размер буфера параметров составляет 40 байт. На рис. 32.2 мы показываем другой пример задания параметров, на этот раз с помощью вызова функции t_optmgmt, которую мы описываем в разделе 32.4. Ар- гументами этой функции являются указатели на две структуры t_optmgmt — одна содержит входные данные, а другая — результат.
32.3. Параметры XTI 895 Входные данные t_optmgmt{ } opt.maxlen opt.len - opt.buf flags len level name status len level name status Результат Рис. 32.2. Запрос значения по умолчанию двух параметров из t_optmgmt В этом примере мы запрашиваем значения параметров IP TTL и TOS, задава- емые по умолчанию (поле fl ags имеет в данном случае значение T_DEFAULT), так что для каждого параметра мы задаем только структуру t_opthdr, не указывая никаких значений. Результатом является копия входных данных, при этом зна- чения, заданные по умолчанию, возвращаются после каждой из структур t_opthdr. В структуре t_optmgmt, содержащей результат, поле status также заполняется (его значением является T_SUCCESS). ПРИМЕЧАНИЕ---------------------------------------------------- В Unix 98 (но не в Posix.lg) определены три макроса, которые могут быть использованы при обработке структуры t_opthdr и данных, следующих за пей: T_OPT_FIRSTHDR, T_OPT_NEXTHDR и T_OPT_DATA. Они аналогичны трем макросам: CMSG FIRST- HDR, CMSG_NXTHDR и CMSG_DATA, предназначенным для обработки вспомога- тельных данных для сокетов, как показано в разделе 13.6. 32.3. Параметры XTI Для большинства параметров XTI существует прямая аналогия с параметрами сокетов, описанными в главе 7. Поэтому здесь мы приводим лишь краткое описа- ние параметров XTI. Также следует отметить, что для определения констант, ис- пользуемых во всех параметрах IP, TCP и UDP, необходимо включить заголо- вочный файл <xti_inet .h>. ПРИМЕЧАНИЕ---------------------------------------------- Заметим, что в XTI не определены никакие возможности многоадресной передачи. Параметр XTI_DEBUG Этот параметр аналогичен параметру сокетов SO_DEBUG и поддерживается обычно только для TCP. Для того чтобы отключить этот параметр, достаточно не указы-
896 Глава 32. Параметры XTI вать никакого значения в его заголовке. Иными словами, поле len структуры t_opthdr должно быть установлено равным размеру этой структуры (как показа- но, например, на рис. 32.2,16 байтам). Параметр XTIJJNGER Этот параметр аналогичен параметру сокетов SO_LINGER и поддерживается TCP. Он определяет, что происходит, когда точка доступа закрывается. Структура t_l inger выглядит следующим образом: struct t_linger { t_scalar_t l_onoff. /* T_NO. T_YES */ t_scalar_t 11inger. /* TJJNSPEC (используем значение по умолчанию). T_INFINITE или время задержки в секундах */ } В табл. 32.1 мы указали, что этот параметр является обязательным, но на са- мом деле обязательно только значение l_onoff, а значение 1_1 inger не является таковым. Это означает, что верхний и нижний пределы времени задержки уста- навливаются для каждой реализации. В отличие от параметра сокетов SO LINGER, параметр XTI_LINGER не использует- ся для отправки сегмента RST. Этот сегмент отправляет функция t_snddi s. Параметры XTI_RCVBUF и XTIRCVLOWAT Эти два параметры аналогичны параметрам сокетов SO RCVBUF и SO_RCVLOWAT. Пер- вый из них задает размер приемного буфера точки доступа, а второй — минималь- ный объем приемного буфера, используемый функциями pol 1 и select. В табл. 32.1 параметр XTI_RCVBUF не рассматривается как сквозной, но при под- держке каналов TCP с повышенной вместимостью (RFC 1323 [45]) этот параметр на самом деле может считаться сквозным, так как он воздействует на параметр масштабирования окна TCP, который согласовывается в процессе трехэтапного рукопожатия. Параметры XTI_SNDBUF и XTI.SNDLOWAT Эти два параметра аналогичны параметрам сокетов SO SNDBUF и SO SNDLOWAT. Пер- вый из них задает размер буфера отправки точки доступа, а второй — минималь ный объем буфера отправки, используемый функциями pol 1 и sel ect. Параметр T_IP_BROADCAST Этот параметр аналогичен параметру сокетов SO_BROADCAST. Он может принимать значение T_YES либо T_NO. Параметр T_IP_DONTROUTE Этот параметр аналогичен параметру сокетов SO_DONTROUTE. Значением этого па- раметра может быть либо T YES, либо T_NO. Параметр T_IP_OPTIONS Этот параметр аналогичен параметру сокетов IP_OPTIONS. Значение этого пара- метра используется при формировании параметров заголовка IPv4, примеры ко-
32.3. Параметры XTI 897 торых приведены в главе 24. Для отключения этого параметра достаточно задать его без указания значения (то есть задать только структуру t_opthdr). Вызывая функцию t_optmgmt с запросом T_CURRENT, мы получаем текущие зна- чения параметров IP, которые будут использоваться в исходящих дейтаграммах. Параметр T_IP_REUSEADDR Этот параметр аналогичен параметру сокетов SO_REUSEADDR. Значение этого пара- метра может быть либо T_YES, либо T_NO. Параметр T_IP_TOS Этот параметр аналогичен параметру сокетов IP_TOS. Значение параметра является комбинацией значений поля приоритета IPv4 (возможные значения приведены в табл. 32.3) и поля типа сервиса (возможные значения приведены в табл. 32.4). Таблица 32.3. Значения поля приоритета IPv4, используемые с параметром TIPTOS Константа Значение TROUTINE 0 TPRIORITY 1 TIMMEDIATE 2 TFLASH 3 TOVERRIDEELASH 4 TCRITICECP 5 TINETCONTROL 6 TNETCONTROL 7 Таблица 32.4. Значения поля типа сервиса IPv4, используемые с параметром TIPTOS Константа Описание TNOTOS Нормальный TLDELAY Минимальная задержка THITHRPT Максимальная производительность T_HIREL Максимальная надежность TLOCOST Минимальная стоимость Макрос SET_TOS (определенный в подключаемом файле <xti .h>) объединяет свой первый аргумент (значение поля приоритета из табл. 32.3) со вторым аргу- ментом (значением поля типа сервиса из табл. 32.4), и этот результат использует- ся параметром XTI. Вызывая функцию t_optmgmt с запросом T CURRENT, мы получаем текущее значе- ние параметров IP, которые будут использоваться в исходящих дейтаграммах. Параметр T_IP_TTL Этот параметр аналогичен параметру сокета IP_TTL. Значением этого параметра является значение поля TTL протокола IPv4. Этот параметр устанавливается для того, чтобы задать значение, используемое в исходящих дейтаграммах. Тем не
898 Глава 32. Параметры XTI менее не существует способа получить значение поля TTL из полученной дей- таграммы. Параметр T_TCP_KEEPALIVE Этот параметр аналогичен параметру сокетов SOJCEEPALIVE — он контролирует отправку пакетов для осуществления проверки наличия связи с собеседником через соединение TCP. В этом параметре XTI используется следующая структура: struct t_kpalive { t_scalar_t kp_onoff /* T_NO (отключаем). T_YES (включаем) или T_YES|T_GARBAGE (включаем и отправляем "мусорный" байт) */ t_scalar_t kp_timeout, /* время ожидания в минутах: TJJNSPEC - значение по умолчанию */ }• Этот параметр аналогичен параметру XTI_LINGER в том, что значение kp onoff является обязательным, но значение kp_timeout не является таковым. ПРИМЕЧАНИЕ ------------------------------------------------------------- Отправка «мусорного» байта не должна быть обязательным требованием, и на самом деле константа T_GARBAGE была удалена из Unix 98. Использование «мусорного» байта обсуждается на с. 335 книги [94]. Параметр T_TCP_MAXSEG Этот параметр аналогичен параметру сокетов TCP_MAXSEG. Он предназначен толь- ко для чтения и возвращает максимальный размер сегмента (MSS) для точки доступа TCP. Поскольку этот параметр доступен только для чтения, его значение не может являться обязательным. Параметр T_TCP_NODELAY Этот параметр аналогичен параметру сокетов TCP_NOOELAY. Значением этого пара- метра может быть либо T_YES (отключение алгоритма Нагла), либо T_NO (значение по умолчанию, алгоритм Нагла включен). Об алгоритме Нагла более подробно говорится в разделе 7.9. Параметр T_UDP_CHECKSUM Этот параметр XTI является одним из сквозных параметров, следовательно, функ- ция t_rcvudata всегда возвращает его значение, если запрошены полученные па- раметры (то есть если значение opt maxlen ненулевое). Значением этого парамет- ра может быть либо T YES, либо T_NO. ПРИМЕЧАНИЕ--------------------------------------------- Этот параметр никогда не должен быть включен. Вообще предоставление приложе- нию возможности отключать для какой-либо точки доступа отправку контрольных сумм UDP является ошибкой. Можно привести немало таких примеров, когда при отключе- нии контрольных сумм UDP происходило нарушение целостности данных. Более того, не существует причины, по которой вообще следовало бы отключать контрольные сум- мы UDP. Единственным разумным применением этого параметра является проверка, включены ли контрольные суммы UDP у собеседника.
32.5. Проверка наличия параметра и получение значения по умолчанию 899 32.4. Функция t_optmgmt Функция t_optmgmt позволяет осуществить следующие действия с параметрами XTI: проверить, поддерживается ли определенный параметр (или параметры); ' получить значение по умолчанию одного или нескольких параметров; - получить текущее значение одного или нескольких параметров; согласовать значения одного или нескольких параметров. include <xti h> int t_optmgmt(int fd. const struct t_optmgmt ★request. struct t_optmgmt ★result'). Возвращает 0 в случае успешного выполнения. -1 в случае ошибки Запрос задается в виде структуры t_optmgmt, а результат возвращается также в виде подобной структуры. Если мы не заинтересованы в получении ответа, мы присваиваем полю maxlen структуры, на которую указывает аргумент result, ну- левое значение. Пример этих двух структур показан на рис. 32.2. struct t_optmgmt { struct netbuf opt. /* одна или более структур t_opthdr */ t_scalar_t flags. /* действие при вводе, результат при выводе */ } Поле flags структуры request указывает, какое именно действие требуется предпринять: Т_СНЕСК — проверить, поддерживаются ли указанные параметры; T_DEFAULT — получить значения параметров по умолчанию; • T_CURRENT — получить текущие значения параметров; T NEGOTIATE — согласовать значения параметров. В последующих разделах мы исследуем каждое из этих четырех действий. У нас есть возможность задать сразу несколько параметров в одном и том же вызове функции t optmgmt, как показано на рис. 32.2. Но в таком случае для всех параметров должно быть задано одно и то же значение поля level. Для рис. 32.2 это не вызывает затруднений, поскольку поле level для обоих параметров имеет значение T_INET_IP. При использовании функции t optmgmt для согласования сра- зу нескольких значений параметров возникают определенные затруднения: воз- вращенное поле flags содержит только один (наихудший) результат, хотя для каждого параметра возвращается его статус (status). Чтобы избежать указанных проблем, проще всего будет оперировать только одним параметром при каждом вызове функции t_optmgmt. ПРИМЕЧАНИЕ-------------------------------------------------------------- Эта функция XTI соответствует функциям сокетов getsockopt и setsockopt. 32.5. Проверка наличия параметра и получение значения по умолчанию Нашим первым примером работы с параметрами XTI будет проверка того, какие из параметров, перечисленных в табл. 32.1, поддерживаются в нашей системе.
900 Глава 32. Параметры XTI Для каждого поддерживаемого параметра мы получим и выведем его значение по умолчанию. Программа представлена в листинге 32.1*. Листинг 32.1. Проверка наличия параметров XTI //xtiopt/checkopts с 1 #тnclude "unpxti h" 2 3 4 5 6 7 struct xti_opts { char *opt_str. t_us ca1 a r_t opt_leve1. t_uscalar_t opt_name. char *(*opt_val_str)(struct t_opthdr *). } xti_opts[] = { 8 "XTI DEBUG". XTI GENERIC. XTI DEBUG. xti_str_uscalard. 9 "XTI LINGER". XTI GENERIC. XTIJINGER, xti_str_linger. 10 "XTI_RCVBUF". XTI_GENERIC. XTI_RCVBUF. xti_str_uscalard. 11 "XTI_RCVLOWAT". XTI_GENERIC. XTI RCVLOWAT. xtijtrjjscalard. 12 "XTI_SNDBUF". XTI_GENERIC. XTIJNDBUF. xti_str_uscalard. 13 "XTI SNDLOWAT". XTI GENERIC. XTI_SNDLOWAT. xtistruscalard. 14 "T_IP_BROADCAST". T_INET_IP, T_IP_BROADCAST. xti_str_uiyn. 15 "T_IP_DONTROUTE". TJNETJP. T_IP DONTROUTE. xti_str_uiyn. 16 "T IP OPTIONS". TJNETJP. T IP OPTIONS. xti_str_uchard. 17 "T_IP_REUSEADDR". T INET IP. T IP REUSEADDR. xti_str_uiyn. 18 "T IP TOS". T INET IP. T IP TOS. xti_str_ucharx. 19 "T_IP_TTL". TJNETJP, TJPJTL. xti_str_uchard. 20 "T TCP KEEPALIVE" . TJNETJCP. T_TCP_KEEPALIVE ,xti_str_kpalive. 21 "T_TCP_MAXSEG". T INET TCP. T_TCP_MAXSEG, xti_str_uscalard. 22 "T TCP NODELAY". T INET TCP. TTCP NODELAY. xti_str_usyn. 23 "T UDP CHECKSUM". T INET UDP. T UDP CHECKSUM xti str usyn. 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 NULL. 0. 0. }• int maindnt argc. char **argv) { int fd. struct t_opthdr *topt. struct t_optmgmt *req. *ret: struct xti_opts *ptr, if (argc != 2) err_quit("usage checkopts <device>"}; fd = T_open(argv[l]. O_RDWR. NULL). T_bind(fd. NULL. NULL). req = T_alloc(fd. T_OPTMGMT. T_ALL): ret = T_alloc(fd, T_OPTMGMT. T_ALL): for (ptr = xti_opts. ptr->opt_str '= NULL, ptr++) { topt = (struct t_opthdr *) req->opt.buf. topt->level = ptr->opt_level. topt->name = ptr->opt_name; topt->len = sizeof(struct t_opthdr). req->opt len = topt->len. NULL Все исходные коды программ, опубликованные в этой кнйге, вы MoitferC найти по адресу http;// www.piter.com/download.
32.5. Проверка наличия параметра и получение значения по умолчанию 901 45 req->flags = Т_СНЕСК. 46 printf("Xs ". ptr->opt_str). 47 if (t_optmgmt(fd. req, ret) < 0) { 48 err_xti_ret("t_optmgmt error") 49 } else { 50 topt = (struct t_opthdr *) ret->opt buf. 51 printfC'Xs". xti_str_flags(topt->status)). 52 if (topt->status == T_SUCCESS || topt->status == T_READONLY) { 53 req->flags = T_DEFAULT, 54 if (t_optmgmt(fd. req. ret) < 0) { 55 err_xti_ret("t_optmgmt error for T_DEFAULT"). 56 } else { 57 topt = (struct t_opthdr *) ret->opt buf. 58 printfC, default = Xs". (*ptr->opt_val_str) (topt)): 59 } 60 } 61 printf СЛеп"). 62 } 63 } 64 exit(O). 65 } 2-25 Мы определяем и инициализируем структуру, определяющую все параметры XTI, приведенные в табл. 32.1. Последнее поле каждого элемента массива — это указатель на функцию, выводящую значение параметра. Для каждого типа пара- метра нам нужна своя функция. Здесь мы не приводим исходный код для всех этих функций. Открытие устройства 35-38 Используя имя устройства в качестве аргумента командной строки, мы откры- ваем это устройство. Это позволят нам дважды выполнить программу — один раз для устройства /dev/tep, а другой — для устройства /dev/udp, так как мы предпо- лагаем, что разные поставщики поддерживают различные параметры. Мы связы- ваем с точкой доступа произвольный локальный адрес, так как для большинства вызовов функции t_optmgmt требуется, чтобы точка доступа была связана. Также мы размещаем в памяти две структуры t_optmgmt — одну для нашего запроса, а дру- гую — для ответа, возвращаемого функцией. Вызов функции t_optmgmt для проверки наличия параметров (Т_СНЕСК) 39-48 Для каждого параметра из массива xti_opts мы вызываем функцию t_optmgmt, устанавливая в структуре req флаг Т СНЕСК. Мы заполняем структуру req, форми- руя в буфере opt одну структуру t_opthdr (см. раздел 32.2). Структура req содер- жит только структуру t_opthdr, без каких-либо данных (аналогично тому, что изоб- ражено в левой части рис. 32.2). Вызов функции t_optmgmt для получения значений параметров по умолчанию (T_DEFAULT) 49-62 Если первый вызов функции t_optmgmt прошел успешно, мы выводим значение поля status для каждого параметра. Если это значение равно T SUCCESS или
902 Глава 32. Параметры XTI T_READ0NLY, то мы снова вызываем функцию t optmgmt, на этот раз с флагом T_DEFAULT. Если и на этот раз функция выполняется успешно, мы вызываем функцию, на которую указывает поле opt_val_str структуры xti_opts для вывода значения па- раметра, заданного по умолчанию. Когда мы вызываем функцию t optmgmt во вто- рой раз, мы изменяем только значение поля fl ags структуры req. Поскольку ука- затель на эту структуру в прототипе функции имеет квалификатор const, мы можем быть уверены, что структура осталась той же, которая фигурировала в первом вызове функции. Теперь мы дважды запустим зту программу под AIX 4.2: один раз для TCP, а другой — для UDP. Обратите внимание, что в AIX используются имена уст- ройств /dev/xti/tcp и/dev/xti/udp: aix % checkopts /dev/xti/tcp XTI_DEBUG T_SUCCESS. default = 0 XTIJ.INGER T_SUCCESS. default = T_NO. 0 sec XTI-RCVBUF T_SUCCESS. default = 16384 XTI_RCVLOWAT TSUCCESS default = 1 XTI_SNDBUF T_SUCCESS. default = 16384 XTI_SNDLOWAT T_SUCCESS. default = 4096 T_IP_BROADCAST T_SUCCESS. default = T_NO T_IP_DONTROUTE T_SUCCESS, default = T_NO T_IP_OPTIONS T-SUCCESS, default - 0 (length of value) T_IP_REUSEADDR TSUCCESS. default = T_NO T_IP_TOS TSUCCESS, default = 0x00 T_IP_TTL T_SUCCESS. default = 0 T_TCP_KEEPALIVE T_SUCCESS. default = T_NO. TJJNSPEC T_TCP_MAXSEG T_READONLY. default = 512 T_TCP_NODELAY T_SUCCESS. default = T_NO T_UDP_CHECKSUM t_optmgmt error incorrect option format aix % checkopts /dev/xti/udp XTI_DEBUG T_SUCCESS. default = 0 XTI_LINGER TSUCCESS. default = T_NO. 0 sec XTI_RCVBUF T_SUCCESS. default = 41600 XTI_RCVLOWAT ^SUCCESS, default = 1 XTI_SNDBUF T_SUCCESS. default = 9216 XTI_SNDLOWAT T_SUCCESS. default = 4096 T_IP_BROADCAST T_SUCCESS default = T_NO T_IP_DONTROUTE T_SUCCESS. default = T_NO T-IP-OPTIONS T_SUCCESS. default = 0 (length of value) T-IP-REUSEADDR T_SUCCESS. default = T_NO T-IP-TOS T-SUCCESS, default = 0x00 T-IP-TTL T_SUCCESS. default = 0 T_TCP_KEEPALIVE t_optmgmt error incorrect option format T_TCP_MAXSEG t_optmgmt error incorrect option format T_TCP_NODELAY t_optmgmt error incorrect option format T_UDP_CHECKSUM T_NOTSUPPORT Полученные значения всех параметров (кроме T_IP_TTL) таковы, как мы и ожи- дали. Для параметра T_UDP_CHECKSUM, который не поддерживается TCP, и трех па- раметров TCP, не поддерживаемых UDP, функция t_optmgmt возвращает ошибку TBADOPT. Поскольку параметр TJJDP_CHECKSUM не поддерживается поставщиком UDP, функция возвращает ошибку T_NOTSUPPORT. Строка length of value (размер значе- ния), соответствующая параметру T_IP_OPTIONS, указывает, что значение поля len в ответе было равно нулю, то есть не было получено никакого значения для вывода.
32.6. Получение и установка значений параметров XTI 903 32.6. Получение и установка значений параметров XTI В этом разделе мы приводим примеры получения и установки значений парамет- ров XTI. Мы определяем две собственные функции, xti_getopt и xti setopt, по- следовательность вызовов в которых такая же, как и в функциях getsockopt и setsockopt. #include "unpxti h" int xti_getopt(int fd. int level int name void *optval socklen_t *optlen) int xti_setopt(int fd. int level, int nave, const void ★optval. socklen_t optlen). Обе функции возвращают 0 в случае успешного выполнения. -1 в случае ошибки Эти функции могут упростить наши XTI-программы, так как каждая из них заменяет 20-30 строк кода на языке С. Функция xti_getopt Для получения текущего значения параметра XTI мы вызываем функцию t_optmgmt, установив значение поля fl ags структуры req равным T_CURRENT. В лис- тинге 32.2 показана функция xti_getopt. Листинг 32.2. Функция xti_getopt: получение текущего значения параметра XTI //libxti/xti_getopt с 1 #include "unpxti h" 2 int 3 xti getoptdnt fd int level, mt name, void *optval. socklen t *optlenp) 4 { 5 int rc. len 6 struct t_optmgmt *req *ret 7 struct t_opthdr *topt. 8 req = T_alloc(fd, TJjPTMGMT T ALL). 9 ret = T_alloc(fd. TJjPTMGMT T_ALL) 10 if (req->opt maxlen == 0) 11 err_quit("xti_getopt opt maxlen == 0"): 12 topt = (struct t_opthdr *) req->opt buf. 13 topt->level = level. 14 topt->name = name. 15 topt->len = sizeoftstruct t_opthdr), /* просто t_opthdr{} */ 16 req->opt len = topt->len. 17 req->flags = T_CURRENT. 18 if (t_optmgmt(fd. req. ret) < 0) { 19 T_free(req T_0PTMGMT) 20 T_free(ret. T_0PTMGMT), 21 return (-1), 22 } 23 rc = ret->flags. 24 if (rc — T_SUCCESS || rc = T_READONLY) { 25 /* копируем значение и длину */ 26 topt = (struct t_opthdr *) ret->opt buf продолжение #
904 Глава 32. Параметры XTI Листинг 32.2 (продолжение) 27 len = topt->len - sizeof(struct topthdr); 28 len = minden. *optlenp). 29 memcpytoptval. topt + 1. len). 30 *optlenp = len. 31 } 32 If ree(req, T_0PTMGMT), 33 T_free(ret. T_0PTMGMT). 34 if (rc = T_SUCCESS || rc == T_READONLY) 35 return (0). 36 return (-1). /* T_N0TSUPP0RT */ 37 } Размещение структур для запроса и ответа в памяти 8-11 Мы вызываем функцию t_al 1 ос для выделения в памяти места под структуры, в которые будут записаны запрос и ответ (req и ret). Также мы проверяем, что размер буфера параметров ненулевой. ПРИМЕЧАНИЕ ---------------------------------------------------- В более старых реализациях TLI часто использовалось нулевое значение для размера параметров TLI. При этом подразумевалось, что приложение должно выделить соб- ственный буфер для записи этих значений. Заполнение структуры t_opthdr 12-16 Мы заполняем структуру t opthdr, записывая в нее значения полей 1 evel и name для данного параметра. В структуру req (запрос) мы не записываем никакого зна- чения, так как для получения текущего значения параметра это не требуется. Вызов функции t_optmgmt и получение значения параметра 17-31 Мы вызываем функцию t_optmgmt и сохраняем значение поля flags структуры ret в переменной гс. Если возвращенным значением было T SUCCESS или T_READONLY, мы копируем обратно значение параметра и размер этого значения. (Выражение topt+1 указывает на возвращенное значение параметра, расположенное сразу же после структуры t opthdr.) Последний аргумент нашей функции имеет тип «зна- чение-результат», и мы должны следить за тем, чтобы не переполнить буфер вы- зывающего процесса (на тот случай, если он слишком мал). Освобождение занятой памяти и возвращение управления 32-36 Мы освобождаем память, выделенную функцией t_alloc, и возвращаем нуле- вое значение в случае успешного выполнения нашей функции и -1, если про- изошла ошибка. Функция xti setopt Для того чтобы задать значение параметра XTI, мы вызываем функцию t_optmgmt со значением поля fl ags структуры req (запрос), установленным в T_NEGOTIATE. В листинге 32.3 показана наша собственная функция xti setopt, за некоторыми исключениями аналогичная функции xti_getopt из листинга 32.2.
32.6. Получение и установка значений параметров XTI 905 Листинг 32.3. Функция xti setopt: установление значения параметра XTI //libxti/xti_setopt с 1 include "unpxti h" 2 int 3 xti_setopt(int fd, int level, mt name, void *optval. socklen_t optlen) 4 { 5 int rc, 6 struct t_optmgmt *req. *ret. 7 struct t_opthdr *topt. 8 req = T_alloc(fd. TOPTMGMT. T ALL); 9 ret = T_alloc(fd. T_OPTMGMT, T_ALL), 10 if (req->opt maxlen == 0) 11 err_quit("xti_setopt req opt maxlen == 0”): 12 topt = (struct t_opthdr *) req->opt buf 13 topt->1evel = level. 14 topt->name = name, 15 topt->len = sizeoftstruct t_opthdr) + optlen: 16 if (topt->len > req->opt maxlen) 17 err_quit(”optlen too big") 18 req->opt len = topt->len; 19 memcpyttopt + 1. optval. optlen); /* копируем значение параметра */ 20 req->flags = T_NEGOTIATE. 21 if (t_optmgmt(fd. req. ret) < 0) { 22 T_free(req. T_0PTMGMT), 23 T_free(ret. T_OPTMGMT); 24 return (-1), 25 } 26 rc = ret->flags: 27 T_free(req T_0PTMGMT). 28 T_free(ret T_0PTMGMT); 29 if (rc == T_SUCCESS || rc — T_PARTSUCCESS) 30 return (0). 31 return (-1). /* T_FAILURE. TJOTSUPPORT. T_READONLY */ 32 } Копирование значения, заданного вызывающим процессом 12-19 Мы копируем значение параметра, заданное вызывающим процессом, в пост- роенный нами буфер, размещая его непосредственно после структуры t_opthdr. Вызов функции t_optmgmt 20-26 Теперь значение поля flags структуры req для функции t_optmgmt равно T_NEGOTIATE. Освобождение занятой памяти и завершение функции 27-31 Если значение параметра не является обязательным, то возвращаемое значе- ние будет равно T PARTSUCCESS, что можно считать признаком успешного выпол- нения функции. В XTI мы можем задавать и получать значения параметров с помощью един- ственного вызова функции t_optmgmt. Для тех параметров, значения которых не являются обязательными (например, размер буферов отправки и приема), это
906 Глава 32. Параметры XTI свойство удобно. При использовании наших собственных функций, представлен- ных в этом разделе, для получения того же результата придется сначала вызвать функцию xti_setopt, а затем xti_getopt. Мы могли бы определить функцию, вы- полняющую обе эти задачи, но дополнительный вызов функции xti_getopt вряд ли может явиться узким местом для какого-либо приложения. Пример Теперь мы покажем пример использования приведенных выше функций. Про- грамма, показанная в листинге 32.4, получает текущее значение максимального размера сегмента TCP (параметр MSS), устанавливает размер буфера отправки равным 65 536, а затем получает и выводит значение размера буфера отправки. Если мы откомпилируем и запустим эту программу, то получим следующий ре- зультат: aix % getsetopt TCP mss = 512 send buffer size = 65536 Листинг 32.4. Пример использования наших функций xtijgetppt и xti_setopt //xtiopt/getsetopt с 1 #include "unpxti h" 2 int 3 maintint argc char **argv) 4 { 5 int fd. 6 socklen_t optlen, 7 t_uscalar_t mss. sendbuff. 8 fd = T_open(XTI_TCP. O_RDWR NULL). 9 T_bind(fd. NULL. NULL). 10 optlen = sizeof(mss). 11 Xti_getopt(fd. T_INET_TCP T_TCP_MAXSEG. toss Soptlen). 12 printfC"TCP mss = £d\en", mss). 13 sendbuff = 65536, 14 Xti_setopt(fd. XTI_GENERIC. XTI_SNDBUF &sendbuff. sizeof(sendbuff)). 15 optlen = sizeof(sendbuff), 16 xti_getopt(fd, XTI_GENERIC. XTI_SNDBUF. &sendbuff. Soptlen) 17 printfCsend buffer size = M\en". sendbuff). 18 exit(O). 19 } 32.7. Резюме Параметры XTI подлежат согласованию. При этом поставщик может вернуть не такое значение параметра, которое было указано в запросе. Хотя в XTI имеется много возможностей обработки параметров, самым простым способом для полу- чения и установки значений этих параметров является определение двух базо- вых функций наподобие getsockopt и setsockopt и вызов этих функций из прило- жений.
ГЛАВА 33 Потоки 33.1. Введение Прежде чем рассматривать некоторые из дополнительных свойств XTI, таких как управляемый сигналом ввод-вывод и внеполосные данные, необходимо разобрать- ся с некоторыми подробностями реализации XTI. XTI и сетевые протоколы обыч- но реализуются с использованием потоковых систем, как, например, терминаль- ная система ввода-вывода в большинстве ядер SVR4. В этой главе мы приводим обзор потоковых систем и функций, используемых приложением для доступа к потоку. Нашей целью является понять, как реализо- ваны сетевые протоколы в рамках потоковых систем. Также мы создаем простой клиент TCP с использованием TPI — интерфейса, который обеспечивает доступ к транспортному уровню и обычно применяется как XTI, так и сокетами в систе- мах, основанных на потоках. Дополнительную информацию о потоках, в том чис- ле о написании программ для ядер, использующих потоки, можно найти в [85]. ПРИМЕЧАНИЕ —---------------------------------------------- Технология потоков была введена Денисом Ритчи (Dennis Ritchie) [88] и получила широкое распространение с появлением системы SVR3 в 1986 году. Потоки никогда не были стандартизованы Posix. Основные функции потоков, к которым относятся getmsg, getpmsg, putmsg, putpmsg, fattch и все потоковые команды ioctl, являются обя- зательными в Unix 98. В реализациях XTI часто используются потоки. Любая система, производная от System V, должна поддерживать потоки, а различные системы 4x.BSD потоки не поддерживают. Потоковая система часто обозначается как STREAMS, но поскольку это название не является акронимом, то в данной книге используется слово «потоки». Не следует смешивать «потоковую систему ввода-вывода» (streams I/O system), кото- рую мы описываем в данной главе, и «стандартные потоки ввода-вывода» (standard I/O streams). Второй термин используется применительно к стандартной библиотеке ввода-вывода (например, таким функциям, как fopen, fgets, printf и т. п ). 33.2. Обзор Потоки обеспечивают двустороннее соединение между процессом и драйвером, как показано на рис. 33.1. Хотя нижний блок мы называем драйвером, его не сле- дует ассоциировать с каким-либо аппаратным устройством, поскольку это может быть и драйвер псевдоустройства (например, программный драйвер).
908 Глава 33. Потоки Рис. 33.1. Поток между процессом и драйвером Головной модуль потока {stream head) состоит из программ ядра, которые за- пускаются при обращении приложения к дескриптору потока (например, при вызове функций read, putmsg, wctl и т. п.). Процесс может динамически добавлять и удалять промежуточные модули об- работки (processing modules) между головным модулем и драйвером. Такой мо- дуль осуществляет некий тип фильтрации сообщений, проходящих в одну или другую сторону по потоку. Этот процесс показан на рис. 32.2. Рис. 33.2. Поток с модулем обработки В поток может быть помещено любое количество модулей. Под словом «по- местить» (push) в данном случае понимается, что каждый новый модуль вставля- ется сразу после (на рисунке — ниже) головного модуля. Определенный тип псевдодрайвера называется мультиплексором (multiplexor). Он принимает данные из различных источников Основанная на потоках реали- зация набора протоколов TCP/IP, используемая, например, в SVR4, может иметь вид, показанный на рис. 33.3.
33.2. Обзор 909 Рис. 33.3. Упрощенный вид реализации набора протоколов TCP/IP, основанной на потоках При создании сокета библиотекой сокетов в поток помещается модуль sockmod. Именно комбинация библиотеки сокетов и потокового модуля обеспечивает API сокетов для процесса. При создании точки доступа XTI библиотекой XTI в поток помещается мо- дуль timod. Именно комбинация библиотеки XTI и потокового модуля обес- печивает API XTI для процесса. В разделе 28.12 мы упомянули, что обычно для использования функции read или wri te в точке доступа XTI требуется поместить в поток потоковый модуль tirdwr. Это осуществляется процессом, использующим TCP, который на рис. 33.3 изображен четвертым слева. Вероятно, этот процесс тем самым отка- зался от использования XTI, поэтому мы убрали надпись «библиотека ХТ1» из соответствующего блока. Три служебных интерфейса определяют формат сетевых сообщений, прохо- дящих вверх и вниз по потоку. TPI (Transport Provider Inter/асе — интерфейс поставщика транспортных служб) [103] определяет интерфейс, предостав- ляемый поставщиком услуг транспортного уровня (например, TCP или UDP).
910 Глава 33. Потоки NPI (Network Provider Interface— интерфейс поставщика сетевого уровня) [102] определяет интерфейс, предоставляемый поставщиком услуг сетевого уровня (например, IP). DLPI (Data Link Provider Interface) — это интерфейс постав- щика канального уровня [101]. Еще один источник информации по TPI и DLPI, в котором имеются также исходные коды на языке С, — это [85]. ПРИМЕЧАНИЕ -------------------------------------------------------- В Usenet часто можно встретить утверждение типа «в потоковой среде сокеты реали- зованы через TLI (XTI)». Это неверно. Как показано на рис. 33.3, и сокеты, и XTI реа- лизуются поверх TPI. Указанное утверждение часто сопровождается таким: «и поэто- му TLI (XTI) быстрее, чем сокеты». Это также неверно. Уровни TCP, UDP и IP остаются теми же независимо от того, что используется — сокеты или XTI. Меняется только поль- зовательская библиотека и модуль, помещаемый в поток, — timod или sokmod. Но ав- тор не имеет в своем распоряжении никаких численных данных для сравнения этих библиотек и модулей, так как узкое место для всех приложений — это передача дан- ных, а структура кода, вероятно, одинакова для XTI и сокетов, если только какая-либо техника оптимизации не была применена в одном случае и отсутствовала в другом. Каждый компонент потока — головной модуль, все модули обработки и драй- вер — содержит по меньшей мере одну пару очередей: очередь на запись и оче- редь на чтение. Это показано на рис. 33.4. Рис. 33.4. Каждый компонент потока содержит по меньшей мере одну пару очередей getpmsg Типы сообщений Потоковые сообщения могут быть классифицированы как имеющие высокий при- оритет (high priority), входящие в полосу приоритета (priority band) и обычные (normal). Существует 256 полос приоритета со значениями между 0 и 255, при- чем обычные сообщения соответствуют полосе 0. Приоритет потокового сообще- ния используется как при постановке сообщения в очередь, так и для управления
33.2. Обзор 911 потоком (flow control). По соглашению, на сообщения с высоким приоритетом управление потоком не влияет. На рис. 33.5 показан порядок следования сообщений в одной конкретной оче- реди. Срочные данные Начало очереди Конец очереди Полосы приоритета Высокий приоритет Рис. 33.5. Порядок следования потоковых сообщений в очереди в зависимости от их приоритета Обычные Хотя потоковые системы поддерживают 256 различных полос приоритета, в сетевых протоколах обычно используется полоса 1 для срочных (внеполосных) данных и полоса 0 для обычных данных. ПРИМЕЧАНИЕ ------------------------------------------------------------ Внеполосные данные TCP в TPI не рассматриваются как истинные срочные данные. В самом деле, в TCP полоса 0 используется как для обычных, так и для внеполосных данных (что будет продемонстрировано в листинге В.1). Полоса 0 используется для отправки срочных данных в тех протоколах, в которых срочные данные (а не просто срочный указатель, как в TCP) отправляются перед обычными данными. В данном контексте следует внимательно отнестись к термину «обычный» (normal). В системах SVR, предшествующих SVR4, не было полос приоритета, а сообщения де- лились на обычные и приоритетные (priority messages). В SVR4 были введены полосы приоритета, что потребовало также введения функций getpmsg и putpmsg, которые мы вскоре опишем. Приоритетные сообщения были переименованы в сообщения с высо- ким приоритетом, и встал вопрос, как называть сообщения, относящиеся к полосам приоритета от 1 до 255. Наиболее распространенной является терминология [85], со- гласно которой все сообщения, которые не являются сообщениями с высоким приори- тетом, называются обычными сообщениями и разделяются на подкатегории согласно своим полосам приоритета. Термин «обычное сообщение» в любом случае должен со- ответствовать сообщению из полосы приоритета 0. Хотя пока мы говорили только о сообщениях с высоким приоритетом и об обычных сообщениях, существует около 12 типов обычных сообщений и около 18 типов сообщений с высоким приоритетом. С точки зрения приложений и функ- ций getmsg и putmsg, которые мы опишем в следующем разделе, нам интересны только три различных типа сообщений: M_DATA, M_PROTO и M_PCPROTO (PC означает «priority control», то есть приоритетное управление, и подразумевает сообщения с высоким приоритетом). В табл. 33.1 показано, как эти три типа сообщений гене- рируются функциями write и putmsg.
912 Глава 33. Потоки Таблица 33.1. Типы потоковых сообщений, генерируемые функциями write и putmsg Функция Управляющая Данные? Флаги Генерируемый тип сообщения информация? write putmsg putmsg putmsg Да M_DATA Нет Да 0 МДЭАТА Да Все равно 0 MPROTO Да Все равно MSG_HIPRI M PCPROTO Что подразумевается под управляющей информацией, данными и флагами, станет ясно из нашего описания функций getmsg и putmsg. 33.3. Функции getmsg и putmsg Данные, передаваемые в обоих направлениях по потоку, состоят из сообщений, а каждое сообщение содержит данные, управляющую информацию или и то и дру- гое. Если мы используем функцию read или write, то мы можем передавать толь- ко данные. Для того чтобы процесс мог записывать и считывать как данные, так и управляющую информацию, необходимо добавить две новые функции. #i ncl tide <stropts h> int getmsg(int fd. struct strbuf *ctlptr. struct strbuf *dataptr int ★flagsp') int putmsgdnt fd. const struct strbuf *ctlptr const struct strbuf *dataptr int flags'). Обе функции возвращают неотрицательное значение в случае успешного выполнения (см пояснения в тексте). -1 в случае ошибки Обе составляющие сообщения — и сами данные, и управляющая информа- ция — описываются структурой strbuf: struct strbuf { int maxlen. /* максимальный размер буфера buf */ int len; /* фактическое количество данных в buf */ char *buf. /* данные */ }• ПРИМЕЧАНИЕ--------------------------------------------------------------------- Обратите внимание на аналогию между структурами strbuf и netbuf. Имена элементов обеих структур одинаковы. Однако обе длины в структуре netbuf относятся к типу данных unsigned int (целое без знака), тогда как обе длины в структуре srtbuf — к типу int (целое со знаком). Причина в том, что некоторые потоковые функции используют значение -1 элементов len и maxlen для указания на определенные специальные ситуации. С помощью функции putmsg мы можем отправлять или данные, или управля- ющую информацию, или и то и другое вместе. Для указания на отсутствие управ- ляющей информации мы можем или задать ctlptr как пустой указатель, или ус- тановить значение ctlptr -> len равным -1. Этот же способ используется для указания на отсутствие данных. В случае отсутствия управляющей информации функцией putmsg генерирует- ся сообщение типа М DATA (табл. 33.1), в противном случае генерируется сообще-
33.4. Функции getpmsg и putpmsg 913 ние типа M_PROTO либо M_PCPROTO в зависимости от значения аргумента fl ags. Этот аргумент функции putmsg имеет нулевое значение для обычных сообщений, а для сообщений с высоким приоритетом его значение равно RS HIPRI. Последний аргумент функции getmsg имеет тип «значение-результат». Если при вызове функции целочисленное значение, на которое указывает аргумент fl agsp, — это 0, то возвращается первое сообщение из потока (которое может быть как обычным, так и имеющим высокий приоритет). Если при вызове функции целочисленное значение соответствует RS HIPRI, то функция будет ждать появле- ния в головном модуле потока сообщения с высоким приоритетом. В обоих слу- чаях в зависимости от типа возвращенного сообщения значение, на которое ука- зывает аргумент fl agsp, будет либо 0, либо RS_HIPRI. Предположим, что мы передаем функции getmsg непустые указатели ctlptr и dataptr. Тогда указанием на отсутствие управляющей информации (возвраща- ется сообщение типа M DATA) является значение ctlprt->len, установленное в -1. Аналогично, если отсутствуют данные, указанием на это является значение -1 элемента dataptr->len. Если функция putmsg выполнилась успешно, то она возвращает нулевое зна- чение, а в случае ошибки возвращается значение -1. Но функция getmsg возвра- щает нулевое значение только в том случае, если вызывающему процессу было доставлено все сообщение целиком. Если буфер, предназначенный для приема управляющей информации, слишком мал, то возвращается значение MORECTL (о ко- тором заранее известно, что оно является неотрицательным). Аналогично, если буфер для приема данных оказывается слишком мал, возвращается значение MOREDATA. Если же оба эти буфера оказываются слишком малы, то возвращается дизъюнкция (логическое ИЛИ) этих двух флагов. 33.4. Функции getpmsg и putpmsg Когда с выпуском SVR4 к потоковым системам была добавлена поддержка раз- личных полос приоритета, появились новые варианты функций getmsg и putmsg. include <stropts h> int getpmsglint fd. struct strbuf *ctlptr, struct strbuf *dataptr. int *bandp. int *flagsp). int putpmsg!int fd. const struct strbuf *ctlptr. const struct strbuf *dataptr, int band, int flags). Обе функции возвращают неотрицательное значение в случае успешного выполнения. -1 в случае ошибки Аргумент band функции putpmsg должен иметь значение в пределах от 0 до 255 включительно. Если аргумент fl ags имеет значение MSG_BAND, то генерируется со- общение в соответствующей полосе приоритета. Присваивание аргументу fl ags значения MSG_BAND и задание полосы 0 эквивалентно вызову функции putmsg. Если значение аргумента flags равно MSG_HIPRI, то аргумент band должен быть равен нулю, и тогда генерируется сообщение с высоким приоритетом. (Обратите вни- мание на то, что этот флаг имеет название, отличающееся от названия RS HIPRI, используемого в случае функции putmsg.) Два целочисленных значения, на которые указывают аргументы bandp и fl agsp функции getpmsg, являются аргументами типа «значение-результат». Целочислен-
914 Глава 33 Потоки ное значение, на которое указывает аргумент fl agsp функции getpmsg, может со- ответствовать MSG_HIPRI (для чтения сообщений с высоким приоритетом), MSG_BAND (для чтения сообщений из полосы приоритета, по меньшей мере равной целочис- ленному значению, на которое указывает аргумент bandp) или MSG ANY (для чтения любых сообщений) По завершении функции целочисленное значение, на кото- рое указывает аргумент bandp, указывает на полосу приоритета прочитанного со- общения, а целое число, на которое указывает аргумент fl agsp, соответствует MSG_HIPRI (если было прочитано сообщение с высоким приоритетом) или MSG_BAND (если было прочитано иное сообщение) 33.5. Функция ioctl Говоря о потоках, мы снова возвращаемся к функции ioctl, которая уже была описана в главе 16 #i nclude <stropts h> int ioctl(int fd int request /* void *arg */ ) Возвращает 0 в случае успешного выполнения 1 в случае ошибки Единственным изменением относительно прототипа функции, приведенного в разделе 16 2, является включение заголовочных файлов, необходимых для ра- боты с потоками Существует примерно 30 запросов (request) так или иначе влияющих на го- ловной модуль потока Каждый из запросов начинается с 1_, и обычно докумен- тация на них приводится в руководстве streann о Мы показали запрос I PUSH в ли- стинге 28 3, когда помещали модуль ti rdwr в поток При обсуждении управляемого сигналом ввода-вывода с использованием XTI в главе 34 мы обсудим запрос I_SETSIG 33.6. TPI: интерфейс поставщика транспортных служб На рис 33 3 мы показали, что TPI — это интерфейс, предоставляющий доступ к транспортному уровню для расположенных выше уровней Этот интерфейс ис- пользуется в потоковой среде как сокетами, так и XTI Из рис 33 3 видно, что комбинация библиотеки сокетов и sokmod, а также комбинация библиотеки XTI и imod обмениваются сообщениями TPI с TCP и UDP TPI является интерфейсом, основанным на сообщениях (message-based) Он определяет сообщения, которыми обменивается приложение (например, XTI или библиотека сокетов), и транспортный уровень Точнее, TPI задает формат этих сообщений и то, какое действие производит каждое из сообщений Во многих случаях приложение посылает запрос поставщику (например, «Связать данный локальный адрес»), а поставщик посылает обратно ответ («Выполнено» или «Ошибка») Некоторые события, происходящие асинхронно на стороне постав- щика (например, прибытие запроса на соединение с сервером), инициируют от- правку сигнала или сообщения вверх по потоку Мы можем обойти как XTI, так и сокеты, и использовать непосредственно TPI В этом разделе мы заново перепишем код нашего простого клиента времени и даты
33 6 TPI интерфейс поставщика транспортных служб 915 с использованием TPI вместо сокетов (сокетная версия представлена в листин- ге 1 1) или XTI (см листинг 28 2) Если провести аналогию с языками програм- мирования, то использование XTI или сокетов можно сравнить с программиро- ванием на языках высокого уровня, такими как С или Pascal, а непосредственно TPI — с программированием на ассемблере Мы не являемся сторонниками не- посредственного использования TPI в реальной жизни Но понимание того как работает TPI, и написание примера с использованием этого протокола позволит нам глубже понять, как работают библиотеки сокетов и XTI в потоковой среде В листинге 33 I1 показан наш заголовочный файл tpi_daytime h Листинг 33.1. Наш заголовочный файл tpi_daytime h //streams/tpi_daytime h 1 include unpxti h 2 #include <sys/stream h> 3 #include <sys/tihdr h> 4 void tpi_bind(int const void * size_t) 5 void tpi_connect(int const void * size_t) 6 ssize_t tpi_read(int void * size_t) 7 void tpi_close(int) Нам нужно включить еще один дополнительный заголовочный файл помимо <sys/tihdr h>, содержащего определения структур для всех сообщений TPI Листинг 33.2. Функция main для нашего клиента времени и даты с использованием TPI //streams/tpi_daytime с 1 #include tpi_daytime h 2 int 3 maindnt argc char **argv) 4 { 5 int fd n 6 char recvline[MAXLINE + 1], 7 struct sockaddrjn myaddr servaddr 8 if (argc ' = 2) 9 err_quit( usage tpi_daytime <IPaddress>"). 10 fd = Open(XTI_TCP O_RDWR 0) 11 /* связываем произвольным локальный адрес */ 12 bzero(&myaddr sizeof(myaddr)) 13 myaddr sin_family = AF_INET 14 myaddr sin_addr s_addr = htonl (INADDR_ANY) 15 myaddr sin_port = htons(O) 16 tpi_bind(fd &myaddr sizeoftstruct sockaddr_in)) 17 /* заполняем адрес сервера */ 18 bzerot&servaddr sizeof(servaddr)) 19 servaddr sin_family = AF_INET 20 servaddr sin_port = htons(13) /* сервер времени и даты */ продолжение 1 Все исходные коды программ опубликованные в этой книге вы можете найти по адресу http // www piter com/dov. nload
916 Глава 33. Потоки Листинг 33.2 (продолжение) 21 Inet_pton(AF_INET. argv[l], &servaddr sin_addr). 22 tpi_connect(fd. &servaddr. sizeoftstruct sockaddr_in)). 23 for (. ) { 24 if ( (n = tpi_read(fd, recvline. MAXLINE)) <= 0) { 25 if (n == 0) 26 break, 27 else 28 err_sys("tpi_read error") 29 } 30 recvline[n] = 0. /* завершающий нуль */ 31 fputs(recvline. stdout). 32 } 33 tpi_close(fd), 34 exit(O). 35 } Открытие транспортного устройства, связывание локального адреса 10-16 Мы открываем устройство, соответствующее поставщику транспортных служб (обычно /dev/tcp). Заполняем структуру адреса сокета Интернета значениями INADDR_ANY и 0 (для порта), указывая тем самым TCP связать произвольный ло- кальный адрес с нашей точкой доступа. Вызываем свою собственную функцию tpi_bind (которая будет приведена чуть ниже) для выполнения этого связы- вания. Заполнение структуры адреса сервера, установление соединения 17-22 Мы заполняем другую структуру адреса сокета Интернета, внося в нее IP-ад- рес сервера (из командной строки) и порт (13). Мы вызываем нашу функцию tpi_connect для установления соединения. Считывание данных с сервера, копирование в стандартный поток вывода 23-33 Как и в случае других клиентов времени и даты, мы просто копируем данные, пришедшие по соединению, в стандартный поток вывода, останавливаясь при получении признака конца файла, присланного сервером (например, сегмент FIN). Мы сделали этот цикл похожим на тот, который использовался в коде сокетного клиента (см. листинг 1.1), а не клиента XTI (см. листинг 28.2), поскольку наша функция tpi_read при нормальном завершении соединения на стороне сервера будет возвращать нулевое значение. Затем мы вызываем нашу функцию tpi cl ose для того, чтобы закрыть эту точку доступа. Наша функция tpi_bi nd показана в листинге 33.3. Листинг 33.3, Функция tpi_bind: связывание локального адреса с точкой доступа //streams/tpi_bind с 1 include ~ "tpi_daytime h" 2 void
33.6. TPI: интерфейс поставщика транспортных служб 917 3 4 5 б 7 8 9 10 11 12 13 14 15 tpi binddnt fd const void *addr size t addrlen) { struct { struct T_bind_req msgjidr. char addr[128] } bind_req struct { struct T_bind_ack msg_hdr. char addr[128], } bind_ack. struct strbuf ctlbuf struct T_error_ack *error_ack int flags. 16 17 18 19 20 bind_req msgjidr PRIMjype = T_BIND_REQ, bind_req msgjidr ADDRJength = addrlen, bind_req msgjidr ADDR_offset = sizeof(struct T_bind_req) bind_req msgjidr CONlND_number = 0 memcpy(bind_req addr addr addrlen). /* sockaddr_in{} */ 21 22 23 ctlbuf len = sizeofCstruct T_bind_req) + addrlen ctlbuf buf = (char *) &bind_req Putmsg(fd. &ctlbuf. NULL. 0) 24 25 26 27 28 29 30 ctlbuf maxlen = sizeof(bind_ack), ctlbuf len = 0 ctlbuf buf = (char *) &bind ack flags = RSJIIPRI. Getmsg(fd, &ctlbuf NULL &flags). if (ctlbuf len < (int) sizeof(long)) err_quit("bad length from getmsg"). 31 32 33 switch (bind ack msg hdr PRIM type) { case T_BIND_ACK return. 34 35 36 37 38 39 case T_ERRDR__ACK if (ctlbuf len < (int) sizeoftstruct T_error_ack)) err_quit("bad length for TJRRORJO") error_ack = (struct T_error_ack *) &bind_ack msgjidr. err_quit("T_ERROR_ACK from bind (M W)" error_ack->TLI_error error_ack->UNIX_error) 40 41 42 43 default err quit("unexpected message type 2d bind ack msg hdr.PRIM type); } } Заполнение стркутуры T_bind_req 16-20 Заголовочный файл <sys/ti hdr п> определяет структуру T_bind_req: struct T_bind_req { long PRIM_type /* T_BIND_REQ */ long ADDRJength /* длина адреса */ long ADDR_offset, /* смещение адреса */ unsigned long CONINDjiumber /* сообщения о соединении *f /* далее следует адрес протокола для связывания */
918 Глава 33. Потоки Все запросы TPI определяются как структуры, начинающиеся с поля типа 1 ong. Мы определяем свою собственную структуру bind_req, начинающуюся со струк- туры T_bi nd req, после которой располагается буфер, содержащий локальный ад- рес для связывания. TPI ничего не говорит о содержимом буфера — оно опреде- ляется поставщиком. Поставщик TCP предполагает, что этот буфер содержит структуру sockaddrjn. Мы заполняем структуру T_bi nd_req, устанавливая элемент ADDR_1 ength равным размеру адреса (16 байт для структуры адреса сокета Интернета), а элемент ADDR_ offset — равным байтовому сдвигу адреса (он следует непосредственно за струк- турой T_bi nd_req). У нас нет гарантии, что это местоположение соответствующим образом выровнено для записи структуры sockaddr i п, поэтому мы вызываем функ- цию memcpy, чтобы скопировать структуру вызывающего процесса в нашу струк- туру bi nd_req. Мы присваиваем элементу CONINDjiumber нулевое значение, потому что находимся на стороне клиента, а не на стороне сервера. Вызов функции putmsg 21-23 TPI требует, чтобы только что созданная нами структура была передана по- ставщику как одно сообщение M PROTO. Следовательно, мы вызываем функцию putmsg, задавая структуру bi nd_req в качестве управляющей информации, без ка- ких-либо данных и с флагом 0. Вызов функции getmsg для чтения сообщений с высоким приоритетом 24-30 Ответом на наш запрос T_BIND_REQ будет либо сообщение T_BIND_ACK, либо сооб- щение T_ERROR_ACK. Сообщения, содержащие подтверждение, отправляются как сообщения с высоким приоритетом (M PCPROTO), так что мы считываем их при по- мощи функции getmsg с флагом RS_HIPRI. Поскольку ответ является сообщением с высоким приоритетом, он получает преимущество перед всеми обычными со- общениями в потоке. Эти два сообщения выглядят следующим образом: struct T_bind_ack { long PRIM_type, /* T_BIND_ACK */ long ADDR_1ength. /* длина адреса */ long ADDR_offset /* смещение адреса */ unsigned long CONIND_numben. /* индекс подключения для помеценм в очередь */ /* затем следует связанный адрес */ }• }• struct T_error_ack { long PRIM_type /* T_ERROR_ACK */ long ERROR_prim /* примитивная ошибка long TLI_error, /* код ошибки TLI */ long UNIX_error. /* код ошибки UNIX */ В начале каждого сообщения указан его тип, так что мы можем начать считы- вать ответ, предполагая, что это сообщение T_BIND_ACK, а затем, прочитав его тип, обрабатывать его тем или иным способом. Мы не ждем никаких данных от по- ставщика, поэтому третий аргумент функции getmsg мы задаем как пустой указа-
33.6. TPI: интерфейс поставщика транспортных служб 919 ПРИМЕЧАНИЕ ---------------------------------------------------------- Когда мы проверяем, соответствует ли количество возвращенной управляющей ин- формации по меньшей мере размеру длинного целого, нужно проявить осторожность, преобразуя значение sizeof в целое число. Оператор sizeof возвращает целое число без знака, но существует вероятность того, что значение возвращенного поля len будет -1. Поскольку при выполнении операции сравнения слева располагается значение со зна- ком, а справа — без знака, компилятор преобразует значение со знаком в значение без знака. Если рассматривать -1 как целое без знака в архитектуре с дополнением до 2, это число получается очень большим, то есть -1 оказывается больше 4 (если предпо- ложить, что длинное целое число занимает 4 байта). Обработка ответа 31-33 Если ответ — это сообщение T_BIND_ACK, то связывание прошло успешно, и мы возвращаемся. Фактический адрес, связанный с точкой доступа, возвращается в элементе addr нашей структуры bi nd_ack, которую мы игнорируем. 34-39 Если ответ — это сообщение T ERROR ACK, мы проверяем, было ли сообщение получено целиком, и выводим три значения, содержащиеся в возвращенной струк- туре. В этой простой программе при возникновении ошибки мы просто прекра- щаем выполнение и ничего не возвращаем вызывающему процессу. Чтобы увидеть ошибки, которые могут возникнуть в результате запроса на связывание, мы слегка изменим нашу функцию main и попробуем связать какой- либо порт, отличный от 0. Например, если мы попробуем связать порт 1 (что тре- бует прав привилегированного пользователя, так как это порт с номером меньше 1024), мы получим следующий результат: aix % tpi_daytime 206.62.226.33 T_ERROR_ACK from bind (3 0) В этой системе значение константы EACCESS равно 3. Если мы поменяем номер порта, задав значение, большее 1023, но используемое в настоящий момент дру- гой точкой доступа TCP, мы получим: aix % tpidaytime 206.62.226.33 T_ERROR_ACK from bind (23. 0) В данной системе значение константы EADDRBUSY равно 23. ПРИМЕЧАНИЕ--------------------------------------------------------------— Эта ошибка была введена в TPI для поддержки XTI. Более старые версии TPI с под- держкой TLI в такой ситуации связали бы другой, неиспользуемый в настоящий мо- мент порт, если запрошенный порт был бы занят. Это означало бы, что сервер, связы- вающий заранее известный порт, должен был бы сравнивать возвращенный адрес (из сообщения TJbind—ack, которое возвращается функцией t_bind, если ее третий аргу- мент является непустым указателем) с запрошенным адресом, и если они не равны, прекращать работу. Следующая функция показана в листинге 33.4. Это функция tpi connect, ус- танавливающая соединение с сервером. Листинг 33.4. Функция tpi_connect: установление соединения с сервером //streams/tpi_connect с
920 [лава 33 Потоки Листинг 33.4 (продолжение) 3 tpi_connect(int fd const void *addr size t addrlen) 4 { 5 struct { 6 struct f_conn_req msgjidr 7 char addr[128] 8 } conn_req 9 struct { 10 struct T_conn_con msgjidr 11 char addr[128] 12 } conn_con 13 struct strbuf ctlbuf 14 union T_primifives rcvbuf 15 struct T_error_ack *error_ack 16 struct T_discon_ind *discon_ind 17 int flags 18 conn_req msgjidr PRIM_type = T_CONN_REQ 19 conn_req msgjidr DEST_length = addrlen 20 conn_req msgjidr DEST_offset = sizeof(struct T_conn_req). 21 conn_req msgjidr DPTJength = 0 22 conn_req msgjidr 0PT_offset = 0 23 memcpy(conn_req addr addr addrlen) /* sockaddr_in{} */ 24 ctlbuf len = sizeof(struct T_conn_req) + addrlen 25 ctlbuf buf = (char *) &conn_req 26 Putmsg(fd &ctlbuf NULL ОТ 27 ctlbuf maxlen = sizeof(union T_primitives) 28 ctlbuf len = 0 29 ctlbuf buf = (char *) &rcvbuf 30 flags = RSJHIPRI 31 Getmsg(fd &ctlbuf NULL &flags) 32 if (ctlbuf len < (int) sizeof(long)) 33 err_quit( tpi_connect bad length from getmsg ) 34 switch (rcvbuf type) { 35 case T_0K_ACK 36 break 37 case T_ERROR_ACK 38 if (ctlbuf len < (int) sizeof(struct T error ack)) 39 err_quit( tpi_connect bad length for T_ERROR_ACK ) 40 error_ack = (struct T_error_ack *) fircvbuf 41 err_quit( tpi_connect T_ERROR_ACK from conn (fcd fcd) 42 error_ack >TLI_error error ack >UNIX_error) 43 default 44 err_quit( tpi_connect unexpected message type Xd rcvbuf type). 45 } 46 ctlbuf maxlen = sizeof(conn_con) 47 ctlbuf len = 0 48 ctlbuf buf = (char *) &conn_con 49 flags = 0 50 Getmsg(fd &ctlbuf NULL & fl ags) 51 if (ctlbuf len < (int) sizeof(long)) 52 err_quit( tpi_connect2 bad length from getmsg )
33 6 TPI интерфейс поставщика транспортных служб 921 53 switch (conn_con msg_hdr PRIM_type) { 54 case T_CONN_CON 55 break 56 case T_DISCON_IND 57 if (ctlbuf len < (int) sizeoftstruct T_discon_ind)) 58 err_quit( tpi_connect2 bad length for T_DISCON_IND ) 59 discon_ind = (struct T_discon_ind *) &conn_con msg_hdr 60 err_quit( tpi_connect2 T_DISCON_IND from conn (Й) 61 discon_ind >DISCON_reason) 62 default 63 err_quit( tpi_connect2 unexpected message type M 64 сопП—COn msgjidr PRIM_type) 65 } 66 } Заполнение структуры запроса и отправка поставщику 18 26 В TPI определена структура T_conn_req, содержащая адрес протокола и пара- метры для соединения struct T_conn_req { long PRIM_type /* T_CONN_REQ */ long DESTJength /* длина адреса получателя */ long DEST_offset /* смещение адреса получателя */ long OPT_length /* длина параметров */ long OPT_offset /* смещение параметров */ /* затем следуют адреса протокола и параметры соединения */ } Как и в случае функции tpi_bi nd, мы определяем свою собственную структуру с именем conn req, которая включает в себя структуру T_conn_req, а также содер- жит место для адреса протокола Мы заполняем структуру conn_req, обнуляя поля ОРТ_1 ength и OPT_offset Мы вызываем функцию putmsg только с управляющей информацией и флагом 0 для отправки сообщения типа M_PROTO вниз по потоку Чтение ответа 17 45 Мы вызываем функцию getmsg, ожидая получить в ответ либо сообщение Т_ОК_АСК, если было начато установление соединения, либо сообщение T_ERROR_ACK (которые мы уже показывали выше) В случае ошибки мы завершаем выполне- ние программы Поскольку мы не знаем, сообщение какого типа мы получим, мы определяем объединение с именем T pnmitives для приема всех возможных за- просов и ответов и размещаем это объединение в памяти как входной буфер для управляющей информации при вызове функции getmsg struct T_ok_ack { long PRIM_t.ype long CORRECT prim } /* T_OK_ACK */ /* корректный примитив */ Ожидание завершения установления соединения 6 65 Сообщение Т_ОК_АСК, полученное нами на предыдущем этапе, указывает лишь на то, что соединение успешно начало устанавливаться Теперь нам нужно дож- даться сообщения T_CONN_CON, указывающего на то, что другой конец соединения подтверждает получение запроса на соединение
922 Глава 33. Потоки struct T_conn_con { long PRIM_type. /* T_CONN_CON */ long RES_1ength. /* длина адреса собеседника */ long RES_offset. /* смещение адреса собеседника */ long OPT_1ength /* длина параметра */ long OPTof fset. /* смещение параметра */ /* далее следуют адрес протокола и параметры собеседника */ }• Мы снова вызываем функцию getmsg, но ожидаемое нами сообщение посыла- ется как сообщение типа M_PROTO, а не как сообщение M PCPROTO, поэтому мы обну- ляем флаги. Если мы получаем сообщение T_CONN_CON, значит, соединение уста- новлено, и мы возвращаемся, но если соединение не было установлено (по причине того, что процесс собеседника не запущен, истекло время ожидания или еще по какой-либо причине), то вместо этого вверх по потоку отправляется сообщение T_DISCON_IND: struct T_disconind { long PRIM_type. /* T_DISCON_IND */ long DISCON_reason. /* причина разрыва соединения */ long SEQ_number. /* порядковый номер */ } Мы можем посмотреть, какие ошибки могут быть возвращены поставщиком. Сначала мы задаем IP-адрес узла, па котором не запущен сервер времени и даты: solaris26 % tpi_daytime 140.252.1.4 tpi_connect2 T_DISCON_IND from conn (146) Код 146 соответствует ошибке ECONNREFUSED. Затем мы задаем IP-адрес, кото- рый не связан с Интернетом: solaris26 % tpi_daytime 192.3.4.5 tpi_connect2 T_DISCDN_IND from conn (145) На этот раз возвращается ошибка ETIMEDOUT. Но если мы снова запустим нашу программу, задавая тот же самый IP-адрес, мы получим другую ошибку: solaris26 % tpidaytime 192.3.4.5 tpi_connect2 T_DISCON_IND from conn (148) На этот раз мы получаем ошибку EHOSTUNREACH. Различие в том, что в первый раз не было возвращено сообщение ICMP о недоступности узла, а во второй раз мы получили это сообщение. Следующая функция, которую мы рассмотрим, — это tpi_read, показанная в листинге 33.5. Она считывает данные из потока. Листинг 33.5. Функция tpi_read: считывание данных из потока //streams/tpi_read с 1 include "tpi_daytime h" 2 ssize_t 3 tpi_read(int fd. void *buf size_t len) 4 { 5 struct strbuf ctlbuf. 6 struct strbuf datbuf. 7 union T_primitives rcvbuf; 8 int flags. 9 ctlbuf maxlen = sizeof(umon Tjjrinii lives); 10 ctlbuf buf = (char *) brcvbuf:
33.6. TPI: интерфейс поставщика транспортных служб 923 11 datbuf maxlen = len: 12 datbuf buf = buf. 13 datbuf.len = 0. 14 flags = 0. 15 Getmsglfd. bctlbuf. &datbuf. &flags): 16 if (ctlbuf len >= (int) sizeof(long)) { 17 if (ncvbuf type == T_DATA_IND) 18 retunn (datbuf len). 19 else if (rcvbuf type == T_ORDREL_IND) 20 return (0). 21 else 22 err_quit("tpi_read unexpected type W'. rcvbuf.type); 23 } else if (ctlbuf len == -1) 24 return (datbuf len): 25 else 26 err_quit("tpi_read bad length from getmsg"); 27 } Считывание управляющей информации и данных, обработка ответа 9-26 На этот раз мы вызываем функцию getmsg для считывания как данных, так и управляющей информации. Структура strbuf, предназначенная для данных, ука- зывает на буфер вызывающего процесса. В потоке события могут развиваться по четырем различным сценариям. Данные могут прибыть в виде сообщения M_DATA, и указанием на это является возвращенное значение длины управляющей информации, равное -1. Данные скопированы в буфер вызывающего процесса функцией getmsg, и функция просто возвращает длину этих данных. Данные могут прибыть как сообщение T_0ATA_IN0, в этом случае управляющая информация будет содержаться в структуре T_data_i nd: struct T_data_ind { long PRIM_type. /* T_DATA_IND */ long M0RE_flag. /* еще данные */ }• Если возвращено такое сообщение, мы игнорируем поле MORE_fIag (оно вооб- ще не задается для таких протоколов, как TCP) и просто возвращаем длину данных, скопированных в буфер вызывающего процесса функцией getmsg. Сообщение T_ORDEL_IND возвращается, если все данные получены и следующим элементом является сегмент FIN: struct T_ordrel_ind { long PRIM type. /* T_ORDREL_IND */ }• Это нормальное завершение, о котором рассказывается в разделе 28.9. Мы просто возвращаем нулевое значение, указывая вызывающему процессу, что по соединению получен признак конца файла. Сообщение T_DISCON_IND возвращается, если произошел разрыв соединения. Этот сценарий обсуждался в разделе 28.10, где было сказано, что это происхо-
924 Глава 33. Потоки дит в случае TCP, если по существующему соединению получен сегмент RST. В этом простом примере мы не обрабатываем данный сценарий, но в листин- ге 28.3 он был обработан. Теперь мы можем объяснить два различных сценария, которые мы видели в разделе 28.12, когда была вызвана функция read, но модуль ti rdwr не был поме- щен в поток. В первом примере, когда была сгенерирована ошибка read error Not a data message, поставщик послал сообщение T_DATA_IND вверх по потоку как сооб- щение типа M PROTO (так как в нем содержались и данные, и управляющая инфор- мация). Но функция read обрабатывает только сообщения М_0АТА, что и вызвало ошибку. Во втором примере возникла ошибка read error Bad message, но это произош- ло уже после того, как ответ сервера был получен и выведен на экран. В этой реализации поставщик посылал данные вверх по потоку как сообщение типа M_DATA, так что оно было корректно обработано функцией read. Но следующим сообщением, посланным вверх по потоку, было T_ORDREL_IND, которое функция read не обрабатывает. Наша последняя функция — это tpi_close, показанная в листинге 33.6. Листинг 33.6. Функция tpi_close: отправка запроса о завершении собеседнику //streams/tpi_close с 1 #тnclude "tpi_daytiric h” 2 void 3 tpi_close(int fd) 4 { 5 struct T_ordrel_req ordrel_req, 6 struct strbuf ctlbuf. 7 ordrel_req PRIM_type = T_ORDREL_REQ 8 ctlbuf len = sizeoftstruct T_ordrel_req). 9 ctlbuf buf = (char *) &ordrel_req 10 Putmsgtfd. &ctlbuf. NULL. 0). 11 Close(fd) 12 } Отправка запроса о завершении собеседнику 7-10 Мы формируем структуру T_ordrel_req: struct l_ordrel_req { long PRIM_type. /* T_ORDREL_REQ */ } и посылаем ее как сообщение M PROTO с помощью функции putmsg. Это соответ- ствует функции XTI t sndrel. Этот пример позволил нам почувствовать специфику TPI. Приложение по- сылает сообщения вниз по потоку (запросы), а поставщик посылает сообщения вверх по потоку (ответы). Некоторые обмены сообщений организованы согласно простому сценарию «запрос-ответ» (связывание локального адреса), в то время как остальные могут занять некоторое время (установление соединения), позво- ляя нам заняться чем-то другим в процессе ожидания ответа. Для знакомства с TPI мы выбрали этот пример (написание клиента TCP) из-за его относительной про-
Упражнение 925 стоты. Если бы мы решили написать с использованием TPI TCP-сервер, обраба- тывающий одновременно несколько соединений (как показано в разделе 30.7), это было бы гораздо сложнее. Из вышесказанного должно быть понятно, что между TPI и функциями XTI довольно тесная связь. С другой стороны, связь между сокетами и TPI уже не столь очевидна. Тем не менее и библиотека сокетов, и XTI упрощают наши при- ложения, так как с их помощью решается множество вопросов, связанных с TPI. ПРИМЕЧАНИЕ---------------------------------------------—----------- Можно сравнить количество системных вызовов, необходимых для осуществления определенных сетевых операций, показанных в этой главе, в случае применения TPI и в случае, когда используется ядро, реализующее сокеты. Связывание с локальным адресом в случае TPI требует двух системных вызовов, но в случае сокетного ядра тре- буется только один вызов 1105, с. 454]. Для установления соединения на блокируемом дескрипторе в случае использования TPI требуется три системных вызова, а в случае сокетного ядра — только один [105, с. 466]. 33.7. Резюме XTI часто реализуется с использованием потоков. Для обеспечения доступа к потоковой подсистеме вводятся четыре новые функции: getmsg, putmsg, getpmsg и putpmsg. 7'акже в потоковой подсистеме широко используется уже описанная ранее функция i octi. TPI представляет собой потоковый интерфейс системы SVR4, предоставляющий доступ из верхних уровней на транспортный уровень. Он используется как сокетами, так и XTI, как показано на рис. 33.3. В этой главе в качестве примера использования основанного на сообщениях интерфейса мы разработали версию клиента времени и даты, в котором непосредственно приме- няется интерфейс TPI. Упражнение В листинге 33.6 мы вызываем функцию putmsg, чтобы отправить вниз по пото- ку запрос на нормальное завершение соединения, а затем немедленно вызы- ваем функцию cl ose для закрытия потока. Что произойдет, если наш запрос будет потерян потоковой подсистемой, а мы закроем поток?
ГЛАВА 34 XTI: дополнительные функции 34.1. Введение В предыдущих главах мы рассмотрели функции XTI для клиентов TCP; поиска имен узлов и служб; серверов TCP; клиентов и серверов UDP; параметров; типичных реализаций потоков. В этой главе рассматриваются остальные функции XTI. 34.2. Неблокируемый ввод-вывод Точка доступа может быть переведена в неблокируемый режим. Для этого требу- ется задать флаг O_NONBLOCK путем вызова функции t_open, когда эта точка доступа создается, или позднее с помощью функции fcntl (как показано в разделе 7.10). Действие некоторых функций XTI изменяется, когда точка доступа становит- ся неблокируемой. Функция t_connect немедленно возвращает значение -1, и переменная t_errno принимает значение TNODATA. В случае TCP этот вызов инициирует трехэтап- ное рукопожатие, и нам следует вызвать функцию t rcvconnect (см. раздел 34.3), чтобы дождаться завершения установления соединения. Функция t rcvconnect возвращает значение -1. При этом значение t_errno ста- новится равным TNODATA, если соединение устанавливается, но процесс уста- новления еще не завершен. Функция tjisten немедленно возвращает значение -1. При этом значение t_errno становится равным T_N0DATA, если на данный момент нет соединений, готовых к тому, чтобы приложение их приняло (с помощью функции t_accept). Если данные для приема отсутствуют, то четыре функции — t_rcv, t rcvudata, t_tcvv и t_rcvvudata — возвращают -1. При этом переменная t_errno принимает значение TNODATA. Если некоторое количество данных доступно, то эти данные возвращаются приложению, хотя их может быть меньше, чем было запрошено приложением. (Последние две из упомянутых выше функций встречаются в этой книге впервые и будут описаны в разделе 34.8.)
34.3. Функция trcvconnect 927 :' Если поставщик не в состоянии принимать какие-либо данные, то четыре функ- ции — t_snd, t_sndudata, t_sndv и t_sndvudata — возвращают значение -1. При этом переменная t_errno принимает значение TFLOW. Если часть данных может быть принята, то возвращаемое значение может быть меньше количества, зап- рошенного функциями t_snd и t_sndv. Две функции для работы с дейтаграм- мами записывают сразу целую дейтаграмму, в противном случае они возвра- щают ошибку. (Последние две из упомянутых выше функций встречаются в этой книге впервые и будут описаны в разделе 34.9.) 34.3. Функция t_rcvconnect В предыдущем разделе мы упоминали об инициировании соединения в неблокр- руемом режиме, когда для того, чтобы дождаться завершения установлений to- единения, вызывается функция t_rcvconnect. #include <xti h> int t_getstate(int fd). Обычно эта функция выполняется в несколько этапов следующим образом: 1. С помощью функции t_open создается точка доступа и устанавливается в не- блокируемый режим. 2. Функция t_connect инициирует установление соединения. Поскольку точка доступа находится в неблокируемом режиме, эта функция немедленно воз- вращает значение -1, a t_errno принимает значение TN0DATA. 3. В некоторый последующий момент времени процесс вызывает функцию t_rcv - connect, чтобы определить, завершилось ли установление соединения. Если точка доступа вышла из неблокируемого режима (после вызова на этапе 2 функции t_connect процесс отключил флаг отсутствия блокировки), то функ- ция t rcvconnect блокируется, пока соединение не будет установлено. Если же точка доступа все еще находится в неблокируемом режиме, то этот вызов функ- ции t rcvconnect либо немедленно возвращается со значением 0, если соеди- нение установлено, либо возвращает значение -1 со значением t_errno, рав- ным TN0DATA, если соединение еще не установлено. Обратите внимание, что в случае блокируемой функции t connect (режим ра- боты по умолчанию) поставщик возвращает информацию в структуре t call, на которую указывает третий аргумент функции t_connect. Но в случае неблокируе- мой функции t_connect эта информация возвращается в структуре t eal 1, на ко- торую указывает второй аргумент функции t_rcvconnect. Если приложение не переводит точку доступа из неблокируемого режима в блокируемый в промежутке между вызовами функций t connect и t_rcvconnect (второй и третий этапы, упомянутые выше), то вызов функции t_rcvconnect для определения того, когда завершится неблокируемое установление соединения, — просто потеря времени, так как приложение должно вызывать функцию t_rcvcon - nect в теле некоторого цикла, ожидая, когда соединение установится (или будет возвращена ошибка). Этот процесс называется опросом (polling). Для ожидания завершения неблокируемого установления соединения более удачным решени- ем было бы вызывать либо функцию sei ect, либо функцию pol 1 (см. главу 6) или использовать управляемый сигналом ввод-вывод (см. раздел 34.11).
928 Глава 34. XTI: дополнительные функции 34.4. Функция t_getinfo Вернемся к структуре t_i nfo, которую возвращает функция t open (см. раздел 28.2). Следующая функция возвращает вызывающему процессу ту же информацию. #тnclude <xti h> int t_getinfo(int fd struct t_info *info) Возвращает 0 в случае успешного выполнения 1 в случае ошибки Эта функция вызывается, например, функцией t alloc для получения такой информации об уже открытой точке доступа, как требуемые значения размера буфера 34.5. Функция t_getstate С каждой транспортной точкой доступа связано целое число, характеризующее ее текущее состояние (сип ent state) Следующая функция возвращает вызываю- щему процессу это целое число #i nclude <xti h> int t_getstate(int fd) Возвращает текущее состояние в случае успешного выполнения -1 в случае ошибки Текущее состояние характеризуется одной из констант, приведенных в табл. 34.1. Последние три колонки указывают состояния, допустимые для различных типов служб (см. табл. 28.3). Таблица 34.1. Возможные состояния точки доступа XTI Состояние Описание T.COTS T_COTS_ORD T_CLTS TDATAXFER Передача данных • • Т IDLE Связанное, но не активное • • • TINCON Входящее соединение ждет обработки на неактивной точке доступа • • TINREL Входящий запрос на нормальное завершение • TOUTCON ' Исходящее соединение ждет обработки на активной точке доступа • • TOUTREL Исходящий запрос на нормальное завершение • TUNBND Несвязанное • • • TUNINIT Не инициализированное начальное • • • и конечное состояние Для того чтобы показать, как именно меняется состояние конечной точки в результате вызовов различных функций XTI и других событий, происходящих в этой точке, можно построить диаграмму переходов состояний. Эта диаграмма также позволит показать, какие функции XTI допустимы для тех или иных со- стояний Например, в состоянии TJJNINT допустим вызов только одной функции — t open, в результате чего точка доступа переходит в состояние T UNBANO. В состоя- нии T_UNBND возможны четыре события:
34.6. Функция t sync 929 1. В случае успешного выполнения функции t close состояние точки доступа меняется на TJJNINT. 2. Вызов функции t_optmgmt допустим, но он не меняет состояния точки доступа. (Одним из аспектов, которые не могут быть отражены в диаграмме переходов состояний точки доступа, является изменение способа обработки параметров при переходе из одного состояния в другое. Например, параметр TJJDP CHESKSUM обрабатывается в состоянии TJJNBND не так, как в других состояниях.) 3. Успешное выполнение функции t_bi nd изменяет состояние точки доступа на TJDLE. 4. Допускается передача соединения на точку доступа (с помощью функции t_accept), в результате чего состояние точки доступа меняется на T_DATAXFER. Если переходить к дальнейшим состояниям точки доступа, диаграмма пере- ходов становится слишком громоздкой, поэтому мы не будем продолжать здесь ее рассмотрение. 34.6. Функция t_sync Исторически интерфейс TLI был реализован как библиотека функций в системе SVR3. Рассмотрим npoi рамму, использующую TLI, которая вызывает функцию ехес, как показано на рис. 34.1. Программа А — это, возможно, прослушивающий сервер, ожидающий прибытия соединения, которое он должен принять, после чего он вызовет функцию ехес для выполнения программы В (обработка клиентского запроса). Напомним, что идентификатор процесса не меняется при выполнении ехес, но в память вызывающего процесса записывается новая программа, которая начинает выполняться как функция main. С этим сценарием в системе SVR3 связана следующая проблема: информация о состоянии сохраняется и в библиотеке TLI в пределах данного процесса, и в ко- де поставщика в ядре После выполнения функции ехес вся информация о состо- янии в библиотеке стирается, и библиотека возвращается в исходное состояние, готовая к выполнению новой программы Целью функции t_sync является синх- ронизация состояния библиотеки (справа на рис. 34.1) при выполнении новой программы поставщиком в ядре. В системе SVR4, однако, отпадает необходимость в вызове функции t_sync в сценарии, изображенном на рис. 34.1. Библиотечные функции TLI могут опре- делить необходимость синхронизации самостоятельно. Например, если в библио- теке имеется переменная, объявленная следующим образом: static int synced /* инициализируется нулем при запуске программы */ то каждая функция может начинаться с последовательности, аналогичной при- веденной ниже: t_connect(fd. ) { if (synced == 0) tsync(fd) /* также задает synced = 1 */ \& }
930 Глава 34. XTI: дополнительные функции Рис. 34.1. Реализация вызова функции ехес в TLI В то время как описанным выше способом обрабатывается ситуация с вызо- вом функции ехес, имеется еще один сценарий, который, хотя и нечасто встреча- ется, требует вызова функции t_sync: это происходит, когда несколько процессов совместно используют точку доступа XTI. В этом сценарии предполагается, что процессы взаимодействуют между собой и каждый из них вызывает функцию t_sync, когда это становится необходимо (что зависит, безусловно, от специфики приложения). Один из примеров ситуации, когда необходима функция t_sync, — это взаимодействие между родительским и дочерним процессами, когда роди- тельский процесс вызывает функцию t_l 1 sten, а дочерний затем вызывает функ- цию t_accept. Родительскому процессу потребуется вызвать функцию t_sync для обновления библиотечной копии значения, указывающего на состояние точки доступа, которое может измениться после вызова дочерним процессом функции t_accept. include <xti h> int t_sync(int fd): Возвращает текущее состояние в случае успешного выполнения, -1 в случае ошибки При успешном выполнении эта функция возвращает одно из значений, при- веденных в табл. 34.1. ПРИМЕЧАНИЕ---------------------------------------------------------------- В API сокетов не существует функции, которая выполняла бы аналогичное действие.
34.8. Функции t rcw и t rcwudata 931 34.7. Функция t_unbind Действие функции t_unbi nd противоположно действию функции t_bi nd. include <xti h> int tjjnbind(int fd). Возвращает 0 в случае успешного выполнения. -1 в случае ошибки Эта функция отключает точку доступа, определяемую дескриптором fd. Для этой точки доступа после выполнения данной функции не будут приниматься никакие данные. Тем не менее можно вызвать функцию t_bi nd и связать с данной точкой доступа другой локальный адрес. ПРИМЕЧАНИЕ---------------------------------------------------------- В случае сокетов подобное действие может быть выполнено только для присоединен- ных сокетов UDP путем вызова функции bind, если задать некорректный адрес. 34.8. Функции t_rcw и t_rcwudata Эти две функции расширяют возможности, предоставляемые функциями t_rcv и _rcvudata, для работы с вектором буферов (vector of buffers), а не просто с от- дельным буфером. Они обеспечивают возможности распределяющего чтения (scatter read). ПРИМЕЧАНИЕ------------------------------------------------------------- Эти две функции вместе с двумя функциями, описанными в следующем разделе, были введены в Posix.lg. Концепция работы с вектором буферов берет свое начало от функций readv и writev, а также функций recvmsg и sendmsg. include <xti h> int t_rcvv(int fd struct t_iovec *iov. unsigned int iovcnt. int * flags). int t_rcvvudata(int fd. struct t_umtdata *umtdata, struct t_iovec *iov. unsigned int iovcnt. int * flags'). Обе функции возвращают количество считанных или записанных байтов в случае успешного выполнения. -1 в случае ошибки Аргумент iov обеих функций является указателем на массив структур t_iovec: struct t_iovec { void *iov_base /* начальный адрес буфера */ size_t iov len. /* размер буфера в байтах */ ) Количество элементов в массиве задается аргументом iovcnt. Максимальное количество элементов в массиве задается константой T_I0V^MAX, определенной в подключаемом заголовочном файле <xti h>. Значение этой константы не менее 16. Сравнивая эти новые функции с их приведенными ранее аналогами, мы полу- чим следующие выводы: Указатель на буфер и его длина являются соответственно вторым и третьим аргументами функции t_rcv.
932 Глава 34 XTI: дополнительные функции Указатели на буферы и их длины содержатся в массиве структур t_iovec для функции t_rcvv, и ее второй аргумент указывает на этот массив структур, а третий задает количество элементов массива. Для функции t_rcvudata указатель на буфер и его размер задаются полем udata структуры t umtdata. Эта функция возвращает нулевое значение в случае ус- пешного выполнения, а фактическая длина полученной дейтаграммы содер- жится в поле udata len структуры t_umtdata. Для функции t_rcvvudata указатели на буфер и их размеры содержатся в поле udata структуры t_umtdata. Третьим аргументом функции является указатель на этот массив структур, а четвертым — количество элементов в этом массиве. Указатель па структуру t_umtdata — это второй аргумент функции. Поля addr и opt этой структуры по-прежнему используются (для адреса протокола от- правителя и любых полученных параметров), но поле udata игнорируется. Эта функция возвращает количество байтов в дейтаграмме, а не нулевое значе- ние. 34.9. Функции t_sndv и t sndvudata Эти две функции расширяют возможности, предоставляемые функциями t_snd и t_sndudata, для работы с вектором буферов вместо одного буфера. Это аналоги двух функций, описанных в предыдущем разделе, которые предоставляют воз- можность объединяющей записи, или записи со слиянием {gather write). #тnclude <xti h> int t_sndv(int fd struct t_iovec *iov unsigned int icvcnt int flags) Возвращает количество считанных или записанных байтов в случае успешного выполнения -1 в случае ошибки int t_sndvudata(int fd struct t_umtdata *umtdata struct t_iovec *iov unsigned mt lovcnt) Возвращает 0 в случае успешного выполнения -1 в случае ошибки Аргумент 1 ov в обеих функциях — это указатель на массив структур t_iovec, которые были описаны в предыдущем разделе. Количество элементов в массиве задается аргументом i event. Буферы вывода задаются вторым и третьим аргументами функции t_sndv, ко- торые несколько отличаются от соответствующих аргументов функции t_snd. Для функций, работающих с дейтаграммами, буфер вывода задается элементом udata структуры t umdata в случае функции t_sndudata и вектором iov в случае функ- ции t_sndvudata. Поле udata структуры t_umdata функция t_sndvudata игнорирует. 34.10. Функции t_rcvreldata и t_sndreldata Если мы посылаем сообщение о нормальном завершении с помощью функции t_sndrel (см. раздел 28.9), мы не можем вместе с этим уведомлением посылать какие-либо данные (единственным аргументом функции является дескриптор). Но если это сообщение посылается функцией t_snddis (см. раздел 28 10), мы мо- жем заодно отправить и данные (в поле udata структуры t_call) Для функции t_rcvrel, в отличие от t_rcvdis, имеется такое же ограничение. Для тою чтобы
34.11. Управляемый сигналом ввод-вывод 933 преодолеть его, в XTI введены две новые функции, способные посылать и полу- чать данные вместе с уведомлением о нормальном завершении. include <xti h> int t_sndreldata(int fd. const struct t_discon *discon). int t_rcvreldata(int fd struct t_discon *disconj Обе функции возвращают 0 в случае успешного выполнения -1 в случае ошибки Различие между этими двумя функциями и функциями t_sndrel и t_rcvrel заключается в появлении второго аргумента (указателя на структуру t_ch scon). Эти функции могут быть полезны только в том случае, когда поставщик под- держивает отправку данных вместе с сообщением о нормальном завершении, на что указывает флаг T_ORDRELDATA в поле fl ag структуры t_i nfo (см. табл. 28.4). Если эта функциональность поддерживается, то количество данных, отправляемых вместе с уведомлением о нормальном завершении, ограничено значением поля discon структуры t_info. ПРИМЕЧАНИЕ----------------------------------------------------------------- Эта функциональная возможность пе является обязательной и не поддерживается про- токолом TCP. 34.11. Управляемый сигналом ввод-вывод Управляемый сигналом ввод-вывод обеспечивается потоковой системой, а не XTI. Имя сигнала в данном случае — SIGPOLL, и для доставки сигнала процессу недо- статочно просто задать функцию-обработчик сигнала. Процесс также должен со- общить ядру о своей готовности принять сигнал с помощью запроса I SETSIG по- токовой функции ioctl, указав при этом, какие условия могут сгенерировать данный сигнал. Здесь можно заметить аналогию с действиями, необходимыми для получения сигналов SIGIO и SIGURG, о которых мы говорили, обсуждая API сокетов. Третий аргумент функции ioctl — это целое число, определяющее условия, при которых должен быть сгенерирован сигнал SIGPOLL. Если это нулевое значе- ние, процесс не будет больше получать сигнал SIGPOLL для потока. Значение этого аргумента также может быть сформировано из следующих констант путем при- менения логического сложения: SJ3ANDURG. Если этот флаг задан вместе с флагом S_RDBAND, вместо сигнала SIGPOLL будет сгенерирован сигнал SIGURG, когда можно считывать сообщение из более высокой, чем нулевая, полосы приоритета. S_ERROR. Ошибка в потоке. SJHANGUP. До головного модуля потока дошло сообщение о зависании. S__HI PRI. Может быть прочитано сообщение с высоким приоритетом. S_JNPUT. Эквивалентно S_RDNORM | S_RDBAND и означает, что может быть прочита- но сообщение из любой полосы приоритета (включая нулевую). SJ3UTPUT. На очередь записываемых сообщений, расположенную непосредствен- но под головным модулем потока, не распространяется управление передачей (для обычных сообщений из полосы приоритета 0).
934 Глава 34. XTI: дополнительные функции S_MSG. Сигнальное потоковое сообщение расположено в начале очереди счи- тываемых сообщений в потоке. S_RDNORM. Можно считывать обычное сообщение (из полосы приоритета 0). S_RDBAND. Может быть прочитано сообщение из полосы приоритета выше ну- левой. S_WRNORM. Эквивалент S_OUTPUT. S_WRBAND. На очередь записываемых сообщений, расположенную непосредствен- но под головным модулем потока, не распространяется управление передачей (для сообщений из полосы приоритета выше нулевой). ПРИМЕЧАНИЕ-------------------------------------------------------- Флаг SBANDURG используется в API сокегов, когда он реализуется с использова- нием потоков. Для записи сообщений пе существует аналога флагу S HIPRI, используемому при чте- нии сообщений. Это объясняется тем, что функции putmsg и putpmsg пе блокируются при отправке сообщений с высоким приоритетом: па эти сообщения пе распространя- ется управление передачей. Потоковый сигнал SIGPOLL используется как для управляемого сигналом ввода-вы- вода, так и для уведомлении о прибытии внеполосных данных. Это соответствует двум сигналам — SIGIO и SIGURG, которые упоминались при описании API сокетов. Действием по умолчанию для сигнала SIGPOLL является завершение процесса, поэтому, используя этот сигнал, мы должны установить обработчик сигнала, а за- тем вызвать функцию ioctl для включения сигнала. ПРИМЕЧАНИЕ ------------------------------------------------------- Имеется некоторое противоречие между действиями по умолчанию для сигнала SIGPOLL (завершение процесса, как только что было сказано) и для сигнала SIGIO. В Posix.lg указано, что по умолчанию сигнал SIGIO игнорируется. Поскольку в систе- мах SVR4 указано, что эти два сигнала одинаковы, эти системы должны будут изме- нить действие по умолчанию для сигнала SIGPOLL (он должен также игнорировать- ся), чтобы обеспечить совместимость с Posix.lg. Хотя сигнал SIGPOLL генерируется в различных ситуациях, обычные приложе- ния, не взаимодействующие с внеполосными данными, заинтересованы только в условиях, характеризуемых константами S RDNORM и S_WRNORM. 34.12. Внеполосные данные Внеполосные данные называются в XTI срочными данными. Поддержку этой фун- кциональности обеспечивает поставщик транспортных услуг и потоковая систе- ма. В главе 33 мы упоминали, что внеполосные данные часто реализуются как обычные данные из полосы приоритета 1. Обычные данные, не являющиеся вне- полосными, относятся к полосе приоритета 0. ПРИМЕЧАНИЕ-------------------------------------------------------- В главе 33 мы также говорили, что поскольку внеполосные данные TCP не являются истинными срочными данными (в том смысле, который вкладывается в это понятие в случае XTI), факт ически они реализуются как данные из полосы приоритета 0, а не 1.
34.12, Внеполосные данные 935 Все, что мы говорили в главе 21 о поддержке внеполосных данных в TCP и о со- поставлении срочного режима TCP с внеполосными данными, применимо к XTI так же, как и к интерфейсу сокетов. Для отправки внеполосных данных в случае XTI используется функция t_snd, аргументу fl ags которой присваивается значение T EXPEDITED. Значение этого флага также возвращается вызывающему процессу функцией t rcv. При создании приложения, оперирующего внеполосными данными, мы не можем работать с функциями read и write (вспомните нашу функцию xti rdwr из раздела 28.12). Вместо них следует использовать функции t snd и t rcv. Поскольку внеполосные данные TCP соответствуют обычным сообщениям из полосы приоритета 0, для получения сигнала SIGPOLL при появлении внепо- лосных данных требуется, чтобы при вызове функции ioctl с запросом I_SETSIG ее третьим аргументом была константа S RDNORM (см. раздел 34.11). При этом сиг- нал будет генерироваться и в случае появления обычных данных, поэтому мы должны вызвать функцию t_look из нашего обработчика сигнала и проверить, какое событие имело место — T DATA или T_EXDATA (см. табл. 28.8). Как только получен сегмент TCP со срочным указателем, в XTI устанавливается событие T_EXDATA, и оно остается установленным, пока все данные вплоть до срочного ука- зателя не будут получены. ПРИМЕЧАНИЕ-------------------------------------------------------- Сигнал SIGPOLL соответствует сигналу SIGURG в случае сокеюв. При использовании функции pol 1 для ожидания прибытия внеполосных дан- ных (см. раздел 6.10) поле events структуры pol 1 fd должно быть равно POLLRDNORM, поскольку эти данные рассматриваются как обычные данные из полосы приори- тета 0. В листинге 34.2 и в листинге В.2 мы проверяем, что внеполосные данные TCP представляются для функции pol 1 как обычные данные. Также отметим, что подобная трактовка внеполосных данных в XTI отличается от их трактовки в случае интерфейса сокетов, где внеполосные данные рассматриваются как при- надлежащие к полосе приоритета. ПРИМЕЧАНИЕ ------------------------------------------------------- Использование функции poll соответствует вызову функции select и ожиданию воз- никновения исключительной ситуации, по функция poll пе сообщает, данные какого типа прибыли — для этого мы должны вызвать функции t look и t_rcv. Напомним, что по умолчанию API сокетов удаляет внеполосный байт из потока обыч- ных данных, помещая его в специальный одпобайтовый буфер, из которого приложе- ние считывает данные с помощью функции recv с флагом MSG OOB. В XTI нет ана- лога этим действиям: внеполосные данные TCP всегда принимаются вместе с обычными данными, что в случае сокетов реализуется с помощью параметра SO OOBINLINE. Теперь мы рассмотрим несколько примеров, демонстрирующих, как функция pol 1 и управляемый сигналом ввод-вывод работают с внеполосными данными XTI. Пример использования сигнала SIGPOLL В листинге 34.1 показана программа, использующая сигнал SIGPDLL для уведом- ления о наличии данных в точке доступа XTI.
936 Глава 34 XTI дополнительные функции Листинг 34.1 ’.Получение обычных и внеполосных данных с использованием сигнала SIGPOLL в точке доступа XTI //xtioob/tcprecvOl с 1 include unpxti h 2 #define NREAD 100 3 int listenfd connfd 4 void sig_poll(int) 5 int 6 main(int argc char **argv) 1 { 8 int n flags 9 char buff[NREAD + 1] /* +1 для нуля на конце */ 10 if (argc == 2) 11 listenfd = Tcpjisten (NULL argv[l] NULL) 12 else if (argc == 3) 13 listenfd = Tcpjisten(argv[l] argv[2] NULL) 14 else 15 err_quit( usage tcprecvOl [ <host> ] <port#> ) 16 connfd = Xti_accept(listenfd NULL NULL) 17 Signal(SIGPOLL sigjaoll) 18 Ioctl(connfd I SETSIG S_RDNORH) 19 for ( ) { 20 flags = 0 21 if ( (n = t_rcv(connfd buff NREAD &flags)) < 0) { 22 if (terrno == TLOOK) { 23 if ( (n = Tjook(connfd)) == T_ORDREL) { 24 printf( received T_ORDREL\en ) 25 exit(0) 26 } else 27 err_quit( unexpected event after t_rcv Xd". TO. 28 } 29 err_xti( t_rcv error ) 30 } 31 buff[n] = 0 /* завершающим нуль */ 32 printf( read Xd bytes Xs flags = Xs\en 33 n buff Xti_flags_str(flags)) 34 } 35 } 36 void 37 sig_poll(int signo) 38 { 39 printf( SIGPOLL received event - Xs\en". Xt1_tlook_str(connfd)) 40 } Создание прослушиваемой точки доступа и ожидание соединения 10 16 Мы вызываем нашу функцию tcp_l i sten для создания прослушиваемой точки доступа, а затем нашу функцию xti_accept для приема соединения 1 Все исходные коды программ, опубликованные в этой книге вы можете найти по адресу http // www piter com/download
34 12 Внеполосные данные 937 Установка обработчика сигнала 17 18 Мы вызываем функцию signal для установки обработчика сигнала SIGPOLL, а за- тем вызываем функцию ioctl, чтобы генерировать сигнал в случае прибытия вне- полосных данных на точку доступа Считывание данных (цикл) 19 34 Мы вызываем функцию t_rcv для получения данных В случае, если собесед- ник закрывает соединение, мы осуществляем нормальное закрытие Мы выво- дим количество байтов полученных данных и сами эти данные, а также значение аргумента fl ags, возвращенное функцией t_rcv Наша функция xti_f 1 ags_str воз- вращает указатель на сообщение, описывающее флаги, которые были переданы в качестве аргументов Обработчик сигнала 36 40 Наш обработчик сигнала просто выводит сообщение, указывающее на текущее событие в данной точке доступа Наша функция xti_tlook_str вызывает функ- цию t_l ook и возвращает указатель на сообщение, описывающее текущее собы- тие для данной точки доступа Мы запустили эту программу, а также запустили в качестве клиента програм- му из листинга 211 Ниже приводятся результаты работы нашего сервера umxware X tcprecvOl 9999 read 3 bytes 123 flags = 0 SIGPOLL received event = T_EXDATA read 1 bytes 4 flags = T_EXPEDITED SIGPOLL received event = T_DATA read 2 bytes 56 flags = 0 SIGPOLL received event = T_EXDATA read 1 bytes 7 flags = T_EXPEDITED SIGPOLL received event = T_DATA read 2 bytes 89 flags = 0 SIGPOLL received event - T_ORDREL received T_ORDREL Первые 3 байта получены как обычные данные, но сигнал SIGPOLL не аенери- рован Это вопрос согласования во времени Вспомним, что, как показано на рис 2 5, клиентская функция connect (пли t_connect, если используется XTI) за- вершается раньше, чем серверная функция accept (или t_accept), на половину периода RTT — это обусловлено тем, как осуществляется трехэтапное рукопожа- тие Это дает клиенту преимущество при отправке его первого сегмента данных, и как мы видим в данном примере, первые 3 байта прибывают раньше, чем уста- навливается обработчик сигнала Затем мы получаем сигнал SIGPOLL, и происходит событие T EXDATA Функция t rcv возвращает флаг T_EXPEDITED Из последующих строк вывода мы видим, что каждый раз, когда прибывает сегмент TCP, генерируется сигнал SIGPOLL и чтобы узнать, какое событие произошло, следует вызвать функцию t_l ook Когда мы заканчиваем считывание данных и получаем уведомление о том, что клиент закрыл свой конец соединения, генерируется соответствующий сигнал э; и происходит событие T_ORDREL, как мы и ожидали
938 Глава 34 XTI дополнительные функции Пример использования функции poll В нашем следующем примере, приведенном в листинге 34 2, используется функ- ция pol 1 Листинг 34.2. Получение обычных и внеполосных данных с использованием функции poll в точке доступа XTI //xtioob/tcprecv03 с 1 include unpxti h 2 #define NREAD 100 3 int listenfd connfd 4 int 5 main(int argc char **argv) 6 { 7 int n flags 8 char buff[NREAD + 1] /* +1 для нуля на конце */ 9 struct pollfd pollfd[l] 10 if (argc == 2) 11 listenfd = Tcpjisten (NULL argv[l] NULL) 12 else if (argc == 3) 13 listenfd = Tcp_listen(argv[l] argv[2] NULL) 14 else 15 err_quit( usage tcprecv03 [ <host> ] <port#> ) 16 connfd = Xti_accept(listenfd NULL NULL) 17 pollfd[0] fd = connfd 18 pollfd[0] events = POLLIN 19 for ( ) { 20 Poll(pollfd 1 INFTIM) 21 printf( revents = Xx\en pollfd[0] revents) 22 if (pollfd[0] revents & POLLIN) { 23 flags = 0 24 if ( (n = t_rcv(connfd buff NREAD &flags)) < 0) { 25 if (t_errno = TLOOK) { 26 if ( (n = T_1ook(connfd)) = T-ORDREL) { 27 pnntf( received T_ORDREL\en ) 28 exit(0) 29 } else 30 err_quit( unexpected event after t_rcv W. n) 31 } 32 err_xti( t_rcv error ) 33 } 34 buff[n] =0 /* завершающим нуль */ 35 printf( read W bytes Us flags = Xs\en 36 n buff Xti_flags_str(flags)) 37 } 38 } 39 } Ожидание запроса на соединения от клиента 10 16 Создание прослушиваемой точки доступа и прием запроса на соединение с кли- ентом остались такими же, как в листинге 34 1
34 13 Поставщики транспортных служб закольцовки 939 Подготовка к вызову функции poll 17 18 Мы размещаем в памяти массив pol 1 fd, состоящий из одного элемента, и ини- циализируем его таким образом, чтобы получать уведомление в случае прибы- тия в точку доступа обычных или приоритетных данных Вызов функции poll 19 38 Мы вызываем функцию pol 1 с аргументом timeout (время ожидания), равным INFTIM (то есть время ожидания не ограничено) Когда функция завершается, мы выводим значение поля revents структуры pol 1 fd, чтобы узнать, данные какого типа прибыли в точку доступа Если событие — POLL IN, то мы вызываем функцию t_rcv для считывания данных и выводим данные и возвращенные значения флагов Мы запускаем эту программу, а в качестве клиента возьмем программу, при- веденную в листинге 211 (ту же программу, что использовалась как клиент в пре- дыдущем примере) umxware % tcprecv03 7777 revents = 1 read 3 bytes 123 flags = 0 revents = 1 read 1 bytes 4 flags = T_EXPEDITED revents = 1 read 2 bytes 56 flags = 0 revents = 1 read 1 bytes 7 flags = T_EXPEDITED revents = 1 read 2 bytes 89 flags = 0 revents = 1 received T_ORDREL Каждый раз при завершении функции pol 1 происходит событие, обозначен- ное как 1, что в данной системе соответствует событию POLL IN По значениям фла- гов, возвращенных функцией t rcv, мы узнаем, данные какого типа прибыли в точку доступа 34.13. Поставщики транспортных служб закольцовки Во многих реализациях XTI предусмотрены поставщики транспортных служб закольцовки (loopback transport provider) Имена трех типов поставщиков XTI, указанные в файле netconfig, обычно таковы ticlts, ticots и ticotsord (см табл 28 3) (ti в начале имени означает «transport independent», то есть «транс- портно-независимый» ) Эти три имени также являются именами файлов в ката- логе /dev для функции t_open В системах, основанных на потоках, доменные сокеты Unix часто реализуют- ся при помощи двух из этих трех поставщиков для сокетов SOCK_DGRAM использу- ется ti cl ts, а для сокетов SOCK_STREAM — либо ticots, либо ti cotsord в зависимости от того, какой из них первым расположен в файле netconf i g В XTI при непосредственном использовании этих поставщиков следует учи- тывать одну особенность — используемые адреса называются гибкими адресами (Jlex addresses), которые представляют собой произвольные строки, занимающие
940 Глава 34 XTI дополнительные функции один или более байтов Эти адреса не заканчиваются символами конца строки (нулями) их длина задается элементом 1 еп структуры netbuf, содержащей адрес Но поле addr структуры t_i nfo может быть возвращено со значением -1 (T_INFINITE), в результате чего функция t_al 1 ос не сможет разместить в памяти буфер для это- го адреса ПРИМЕЧАНИЕ--------------------------------------------------------- Одно из различии между XTI и TLI заключается в том, как функция t alloc интерпре- тирует константу T_INFINITE В TLI эта функция по умолчанию размешает в памяти буфер размером 1024 байта, в то время как в XTI функция t_alloc в таком случае не размещает буфер в памяти 34.14. Резюме Неблокируемый режим ввода-вывода для точки доступа XTI устанавливается при вызове функции t_open с флагом 0J10NBL0CK или (для уже существующей точки доступа) с помощью функции fcntl Происходящие при этом изменения в точке доступа аналогичны тем, что имеют место для сокетов, исключение составляет неблокируемая функция t_connect Для ожидания завершения установления со- единения мы пользуемся функцией t_rcvconnect В XTI определены четыре новые функции ввода-вывода, предназначенные для работы с векторами буферов t_rcvv, t_rcvvudata, t_sndv и t_sndvudata Для точки доступа XTI допускается управляемый сигналом ввод-вывод Что- бы войти в этот режим, необходимо вызвать функцию ioctl и задать все условия, при которых должен генерироваться сигнал, с помощью флагов S xxx Внеполос- ные данные отсылаются функцией t_snd, если установлен флаг T_EXPEDJTED Для получения внеполосных данных не требуется никаких специальных усилий функ- ция t_rcv возвращает флаг T_EXPED ITED Кроме того, сигнал может быть сгенериро- ван, когда прибывают внеполосные данные
ПРИЛОЖЕНИЯ
ПРИЛОЖЕНИЕ А Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 А. 1. Введение В этом приложении приведен обзор протоколов IPv4, IPv6, ICMPv4 и ICMPv6. Данный материал позволяет глубже понять рассмотренные в главе 2 протоколы TCP и UDP. Некоторые возможности IP и ICMP рассматриваются также более подробно и в других главах, например параметры IP (см. главу 24) наряду с про- граммами Ping и Traceroute (см. главу 25). А.2. Заголовок IPv4 Уровень IP обеспечивает не ориентированную на установление соединения (con- nectionless) и ненадежную службу доставки дейтаграмм (RFC 791 [81]). Уровень IP делает все возможное для доставки IP-дейтаграммы определенному адресату, но не гарантирует, что дейтаграмма будет доставлена. Если требуется надежная доставка дейтаграммы, она должна быть обеспечена на более высоком уровне. В случае приложений TCP надежность обеспечивается уровнем TCP. В случае приложений UDP надежность должно обеспечивать само приложение, посколь- ку уровень UDP также не предоставляет гарантии падежной доставки дейтаграмм, что было показано на примере в разделе 20.5. Одной из наиболее важных функций уровня IP является маршрутизация {routing). Каждая IP-дейтаграмма содержит адрес отправителя и адрес получате- ля. На рис. А.1 показан формат заголовка IPv4. Значение 4-разрядного поля версия (version) равно 4. Это версия протокола IP, используемая с начала 80-х. В поле длина заголовка (header length) указывается полная длина IP-заголов- ка, включающая любые параметры, описанные 32-разрядными словами. Мак- симальное значение этого 4-разрядного поля равно 15, и это значение задает максимальную длину IP-заголовка 60 байт. Таким образом, если заголовок занимает фиксированные 20 байт, то 40 байт остается на различные параметры. 8-разрядное поле тип службы (сервиса) (lype-of-sen ice, TOS) включает 3-раз- рядное поле приоритета (которое игнорируется), 4 бита, определяющие тип сервиса, и неиспользуемый бит, который должен быть нулевым. Данное поле можно установить с помощью параметра сокета IP T0S (см. табл. 7.4).
А.2. Заголовок IPv4 943 О 3 4 78 15 16 31 Версия Длина Тип (4) заголовка сервиса Общая длина (в байтах) Идентификация Смещение фрагмента Время жизни (TTL) Протокол Контрольная сумма заголовка 20 байт 32-битовый 1Ру4-адрес отправителя 32-битовый 1Ру4-адрес получателя Параметры (если есть) Рис. А.1. Формат заголовка IPv4 Поле общая длина (total length) имеет размер 16 бит и задает полную длину IP- дейтаграммы в байтах, включая заголовок IPv4. Количество данных в дейта- грамме равно значению этого поля минус длина заголовка, умноженная на 4. Данное поле необходимо, поскольку некоторые каналы передачи данных за- полняют кадр до некоторой минимальной длины (например, Ethernet) и воз- можна ситуация, когда размер действительной IP-дейтаграммы окажется мень- ше требуемого минимума. 16-разрядное поле идентификации (identification) является уникальным для каждой IP-дейтаграммы и используется при фрагментации и последующей сборке в единое целое (см. раздел 2.9). Бит DF (флаг запрета фрагментации), бит MF (указывающий, что есть еще фраг- менты для обработки) и 13-разрядное поле смещения фрагмента (fragment offset) также используются при фрагментации и последующей сборке в единое целое. 8-разрядное поле времени жизни (time-to-live, TTL) устанавливается отправи- телем и уменьшается на единицу каждым последующим маршрутизатором, через который проходит дейтаграмма. Дейтаграмма отбрасывается маршрутиза- тором, который уменьшает данное поле до нуля. При этом время жизни любой дейтаграммы ограничивается 255 пересылками. Обычно по умолчанию данное поле имеет значение 64, но можно сделать соответствующий запрос и изменить его с помощью параметров сокета IP_TTL и IP_MULTICAST_TTL (см. раздел 7.6). 8-разрядное поле протокола (protocol) определяет тип данных, содержащихся в IP-дейтаграмме. Характерные значения этого поля — 1 (ICMPv4), 2 (IGMPv4), 6 (TCP) и 17 (UDP). Эти значения определены в RFC 1700 [87]. 16-разрядная контрольная сумма заголовка (header checksum) вычисляется для IP-заголовка (включая параметры). В качестве алгоритма вычисления исполь-
944 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 зуется стандартный алгоритм контрольных сумм для Интернета — простое суммирование 16-разрядных обратных кодов, как показано в листинге 25.10. Два поля — IPv4-adpec отправителя (source IPv4 address) и IPv4-adpec получа- теля (destination IPv4 address) — занимают по 32 бита. Поле параметров (options) описывается в разделе 24.2, а пример IPv4-napa- метра маршрута от отправителя приведен в разделе 24.3. А.З. Заголовок IPv6 На рис. А.2 показан формат заголовка IPv6 (RFC 1883[25]). »• Значение 4-разрядного поля номера версии (version) равно 6. Данное поле за- нимает первые 4 бита первого байта заголовка (так же как и в версии IPv4, см. рис. А.1), поэтому если получающий стек IP поддерживает обе версии, он имеет возможность определить, какая из версий используется. Когда в начале 90-х развивался протокол IPv6 и еще не был принят номер версии 6, протокол назывался IPng (IP next generation — IP нового поколения). До сих пор можно встретить ссылки на IPng. ' Поле приоритета занимает 4 бита и устанавливается отправителем. ПРИМЕЧАНИЕ ------------------------------------------------------------- Полезность этого поля до сих пор является предметом исследований. Документ RFC 1883 [25] определяет для этого поля два набора значений: значения от 0 до 7 обознача- ют трафик, который может откладываться при перегруженной линии (например, ТСР- даппые), а значения от 8 до 15 — трафик, который откладывать нельзя (например, па- кеты реального времени, требующие передачи с постоянной скоростью). С середины 1997 года предполагалось, что эти четыре бита не имеют значения для получателя, в то время как отправитель может установить бит младшего разряда, указав тем самым, что трафик является «интерактивным» (то есть задержка важнее пропускной способнос- ти). Также зти 4 бита могут быть переписаны маршрутизатором для частных целей.
А.З. Заголовок IPv6 945 Поле метки потока (flow label) занимает 24 бита и может заполняться прило- жением для данного сокета случайным образом. (Использование данного поля все еще остается экспериментальным.) Поток представляет собой последова- тельность пакетов от конкретного отправителя определенному получателю, для которых отправитель потребовал специальную обработку промежуточны- ми маршрутизаторами. Если для данного потока отправитель назначил метку, она уже не изменяется. Метка потока, равная нулю (по умолчанию), обозна- чает пакеты, не принадлежащие потоку. Комбинация полей приоритета и метки потока называется информацией о по- токе (flow information). Оба поля содержатся в элементе sw6_flowinfo струк- туры адреса сокета sockaddr_i пб (см. листинг 3.3). Поле длины данных (payload length) занимает 16 бит и содержит длину дан- ных в байтах, которые следуют за 40 байтами IPv6-заголовка. Нулевое значе- ние этого поля указывает, что длина требует больше 16 бит и содержится в па- раметре размера увеличенного поля данных (jumbo payload length option) (см. рис. 24.5). Данные с увеличенной таким образом длиной называются джумбограммой (jumbogram). Следующее поле содержит 8 бит и называется полем следующего заголовка (next header). Оно аналогично полю протокола (protocol) IPv4. Действительно, ког- да верхний уровень в основном не меняется, используются те же значения, — например, 6 для TCP и 17 для UDP. Но при переходе от IPv4 к IPv6 возникло так много изменений, что для последнего было принято новое значение 58. Поле ограничения пересылок, или предельного количества транзитных узлов, (hop limit) аналогично полю TTL IPv4. Значение этого поля уменьшается на единицу каждым маршрутизатором, через который проходит дейтаграмма, и дейтаграмма отбрасывается тем маршрутизатором, который уменьшает дан- ное поле до нуля. Значение этого поля можно установить и получить с по- мощью параметров сокета IPV6_UNICAST_H0PS и IPV6_MULTICAST_H0PS (см. раздел 7.8 и 19.5). Параметр сокета IPV6_H0PLIMIT также позволяет установить это поле и узнать его значение для полученной дейтаграммы. Два следующих поля — IPv6-adpec отправителя (source IPv6 address) и IPv6- адрес получателя (destination IPv6 address) — занимают по 128 бит. ПРИМЕЧАНИЕ---------------------------------------------------------------- Дейтаграмма IPv6 может иметь несколько дополнительных заголовков, следующих за 40-байтовым заголовком IPv6 Поэтому поле называется «следующий заголовок», а пе «протокол». В дейтаграмме IPv4 присутствует только один заголовок протокола после заголовка IPv4. В ранних спецификациях IPv4 маршрутизаторы должны были уменьшать значение поля TTL на единицу или па значение, равное количеству секунд, в течение которых маршрутизатор обрабатывал пакет (это значение могло быть больше единицы) Отсю- да и название «время жизни». Но на самом деле значение этого поля всегда уменьша- лось на единицу. В версии IPv6 значение поля ограничения пересылок уменьшается всегда на единицу, отсюда и смена названия. Наиболее значительным изменением, произошедшим при переходе от IPv4 к IPv6, несомненно, является увеличение поля адресов в IPv6. Другое изменение
946 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 относится к упрощению заголовка, поскольку чем проще заголовок, тем быстрее он будет обработан маршрутизатором. Кроме того, можно отметить еще несколь- ко различий между заголовками: В IPv6 нет поля длины заголовка, поскольку в заголовке отсутствуют парамет- ры. Существует возможность использовать после фиксированного 40-байтового заголовка дополнительные заголовки, но каждый из них имеет свое поле длины. Два адреса IPv6 выровнены по 64-разрядной границе, если заголовок также является 64-разрядным. Такой подход может увеличить скорость обработки на 64-разрядных архитектурах. В заголовке IPv6 нет поля фрагментации, поскольку для этой цели существу- ет специальный заголовок фрагментации. Такое решение было принято, по- скольку фрагментация является исключением, а исключения не должны за- медлять нормальную обработку. Заголовок IPv6 не включает в себя свою контрольную сумму. Такое измене- ние было сделано, поскольку все верхние уровни — TCP, UDP и ICMPv6 — имеют свои контрольные суммы, включающие в себя заголовок верхнего уров- ня, данные верхнего уровня и такие поля из 1Р\’6-заголовка, как IPv6-адрес отправителя, IPv6-адрес получателя, длина данных и следующий заголовок. Исключив контрольную сумму из заголовка, мы приходим к тому, что марш- рутизатор, перенаправляющий пакет, не должен будет пересчитывать конт- рольную сумму заголовка после того, как изменит поле ограничения пересы- лок. Ключевым моментом здесь также является скорость маршрутизации. Если это ваше первое знакомство с IPv6, также следует отметить главные от- личия IPv6 от IPv4: В IPv4 отсутствует многоадресная передача (см. главу 18). Групповая адреса- ция (см. главу 19), не являющаяся обязательной для IPv4, требуется для IPv6. В IPv6 маршрутизаторы не фрагментируют перенаправляемые пакеты Фраг- ментация при использовании IPv6 осуществляется только узлом отправителя. IPv6 требует поддержки параметра аутентификации (подтверждения прав доступа) и параметра обеспечения безопасности. IPv6 требует поддержки обнаружения транспортной MTU (см раздел 2 9). Формально такая поддержка не является обязательной и может не присут- ствовать в минимальных реализациях, таких как начальный загрузчик. Но если узел не реализует данной возможности, он не может посылать дейтаграммы, превосходящие по размеру минимальную канальную MTU версии IPv6 (576 байт, см. раздел 2.9). А.4. Адресация IPv4 32-разрядный адрес IPv4 может иметь один из пяти форматов, показанных на рис А.З. Исторически так сложилось, что организации присваивался один их трех идентификаторов сети: класса А, класса В или класса С, и далее организация мог- ла делать что угодно с другой частью адреса — идентификатором узла. Но подход к адресации изменился в середине 90-х с появлением бесклассовых {classless) ад- ресов, о которых будет вскоре рассказано.
А.4. Адресация IPv4 94i7 7 бит 24 бита КлассА 0 Идентификатор сети Идентификатор узла 14 бит 16 бит КлассВ 1 0 Идентификатор сети Идентификатор узла КлассС 21 бит 8 бит 1 1 0 Идентификатор сети Идентификатор узла 28 бит 1 1 1 0 Адрес широковещательной группы Класс D 27 бит Класс Е 1 1 1 1 0 Зарезервировано для будущего использования Рис. А.З.Формат адресов IPv4 Адреса IPv4 обычно записываются как четыре десятичных числа, разделен- ных точкой, причем каждое из десятичных чисел представляет один из четырех байтов 32-разрядного адреса. Такая запись называется точечно-десятичной. Как показано в табл. А. 1, первое из четырех десятичных чисел обозначает класс адреса. Таблица А. 1. Диапазоны значений пяти различных классов адресов IPv4 Класс Диапазон А В С D Е От 0 0 0 0 до 127 255 255 255 От 128 0 0 0 до 191 255 255 255 От 192 0 0 0 до 223 255 255 255 От 224 0 0 0 до 239 255 255 255 От 240 0 0 0 до 247 255 255 255 Бесклассовые адреса и CIDR В настоящее время адреса IPv4 рассматриваются как бесклассовые. Это означа- ет, что можно игнорировать различия между классами А, В и С, а также неявные границы между идентификатором сети и идентификатором узла, показанные на рис. А 3 для классов адресов А, В и С. Когда организации присваивается IPv4- адрес сети, это фактически означает, что ей присваивается 32-разрядный адрес сети и соответствующая 32-разрядная маска сеги. Биты маски, равные 1, указы-
948 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 вают адрес сети, а нулевые биты — адрес узла. Поскольку биты со значением 1 всегда занимают места в маске непрерывно, начиная с крайнего левого бита, а ну- левые биты — начиная с крайнего правого бита, то маску адреса можно опреде- лить как префиксную длину (prefix length), указывающую на количество запол- ненных единицами битов, начиная с крайнего левого бита. Например, адреса класса А имеют маску 255.0.0.0 и префиксную длину 8, адреса класса В — маску 255.255.0.0 и префиксную длину 16, а адреса класса С — маску 255.255.255.0 и префиксную длину 24. Но преимущество бесклассовых адресов состоит в том, что для классов А, В и С снимается ограничение адреса фиксированными префиксными длинами 8, 16 и 24. Вместо этого можно присваивать адреса с различной префиксной дли- ной. Например, используя бесклассовые адреса, интернет-провайдер (Internet Service Provider, ISP) может взять адрес класса С и присвоить его четырем раз- личным заказчикам, каждому с маской 255.255.255.192, префиксная длина кото- рой 26. Таким образом, каждый из четырех заказчиков может распоряжаться 6 би- тами (вместо 8) в отношении выбора границ подсети (если таковые имеются), азатем присваивать идентификаторы подсети и идентификаторы узлов. (О де- лении на подсети мы поговорим далее.) Все IPv4-адреса, присваиваемые в настоящее время в сети Интернет, являют- ся бесклассовыми. Тот же принцип используется и в случае IPv6. Обычно сете- вые адреса IPv4 представлены в точечно-десятичной записи, после которой ука- зывается префиксная длина, отделенная косой чертой. Примеры вы можете найти на рис. 1.7. Использование бесклассовых адресов требует бесклассовой маршрутизации. Эта технология обычно называется CIDR (RFC 1519 [29]). Цель использования CIDR заключается в уменьшении размера основных таблиц маршрутизации Ин- тернета, а также в уменьшении скорости истощения 1Ру4-адресов. Более подроб- но CIDR описывается в разделе 10.8 книги [94]. Адреса подсетей Обычно IPv4-адреса разделяются на подсети (RFC 950 [72]). Такой подход до- бавляет еще один уровень иерархии адресов: идентификатор сети (присваивается предприятию); идентификатор подсети (выбирается предприятием); идентификатор узла (выбирается предприятием). Граница между идентификатором сети и идентификатором подсети фиксиро- вана префиксной длиной присвоенного адреса сети. Эта префиксная длина при- сваивается организациям их интернет-провайдером. Граница же между иденти- фикатором подсети и идентификатором узла выбирается предприятием. Все узлы данной подсети имеют одинаковую маску подсети, которая и определяет грани- цу между идентификатором подсети и идентификатором узла. Биты, заполнен- ные единицами в маске подсети, соответствуют идентификатору подсети, а биты, заполненные нулями, — идентификатору узла. В качестве примера рассмотрим подсеть, показанную на рис. 1.7. Адрес сети, присвоенный интернет-провайдером, выглядит как 206.62.226.0/24 и является сетью класса С. Далее, оставшиеся 8 бит разделены на 3-битовый идентификатор
А.4. Адресация IPv4 949 подсети и 5-битовый идентификатор узла. Рисунок А.4 иллюстрирует такое де- ление. Маска подсети, соответствующая таким адресам, имеет вид Oxff ff f feO или 225.225.225.224. Адрес: Маска подсети: Граница определена Граница префиксной длиной определена присвоенного адреса сети1 24 бита ) маской подсел г 3 бита 1 f 5 бит Адрес сети=206.62.226.0/24 Идентификатор подсети Идентификатор узла 1111 1111 1111 1111 1111 1111 1111 00000 L J Единичные биты соответствуют адресу сети и идентификатору подсети 4 *1 Нулевые биты соответствуют идентификатору узла Рис. А.4. 24-битовый адрес сети, содержащий 3-битовый идентификатор подсети и 5-битовый идентификатор узла В верхней подсети, приведенной на рис. 1.7, три бита подсети установлены в 001, и такая сеть обозначается как 206.62.226.32/27. Запись «/27» обозначает, что маска подсети охватывает 27 битов слева. Такая префиксная запись исполь- зуется как для полного адреса сети (206.62.226.0/24), так и для адреса подсети (206.62.226.32/27). Узлы данной подсети будут иметь адреса, расположенные в интервале от 206.62.226.33 до 206.62.226.62, а адрес с единичными битами иден- тификатора узла (206.62.226.63) является широковещательным адресом данной подсети (см. раздел 18.2). Для другой подсети на рис. 1.7 три бита подсети уста- новлены в 010, и такая сеть может быть обозначена как 206.62.226.64/27. ПРИМЕЧАНИЕ------------------------------------------------------------------ В середине 80-х, когда IP-адреса подсетей только начинали использоваться, допуска- лось (но не рекомендовалось) задание маски подсети, не являющейся непрерывной (RFC 950 [72]). Но при применении бесклассовых адресов такие маски подсети недо- пустимы. Версия IPv6 также требует, чтобы все маски адресов были непрерывными и начинались с крайнего левого бита. В RFC 950 рекомендовано не использовать адреса, в которых идентификатор подсети целиком заполнен нулевыми либо единичными битами. Некоторое современные про- граммы, однако, используют эти две формы идентификаторов подсети. В качестве другого примера подсетей рассмотрим на рис. 1.7 нижнюю подсеть. Сети noao.edu присвоен адрес 140.252.0.0/16, что в точности соответствует сети класса В. Затем NOAO делит оставшиеся 16 бит на 8-битовый идентификатор подсети и 8-битовый идентификатор узла, что характерно для организаций с ад- ресами класса В. Такое деление показано на рис. А.5. Маска этой подсети имеет вид Oxf ff f f fOO или 255.255.255.0. Приведенная под- сеть имеет идентификатор подсети, состоящий только из единиц, и такая подсеть обозначается как 140.252.1.0/24.
950 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 Граница определена префиксной длиной присвоенного адреса сеп 16 бит ) Границе определенг маской подсеп г 8 бит ) г 8 бит Адрес: Адрес сети=140.252.0.0/16 Идентификатор подсети Идентификатор узла Маска подсети: 1111 1111 1111 1111 1111 1111 1111 0000000000 < kl * Единичные биты соответствуют адресу сети и идентификатору подсети Нулевые биты соответствуют идентификатору узла Рис. А.5. 16-битовый адрес сети с 8-битовым идентификатором подсети и 8-битовым идентификатором узла Адрес закольцовки По соглашению адрес 127.0.0.1 присвоен интерфейсу закольцовки на себя (loopback interface). Все, что посылается на этот IP-адрес, получается самим узлом. Обыч- но этот адрес используется при тестировании клиента и сервера на одном узле. Этот адрес известен под именем INADDR_LOOPBACK. ПРИМЕЧАНИЕ ----------------------------------------------------- Любой адрес из подсети 127/8 можно присвоить интерфейсу закольцовки, но обычно используется именно 127.0.0.1. Неопределенный адрес Адрес, состоящий из 32 нулевых битов, является в IPv4 неопределенным (unspeci- fied) адресом. В пакете IPv4 он может появиться только как адрес получателя в тех пакетах, которые посланы узлом, находящимся в состоянии загрузки, когда узел еще не знает своего IP-адреса. В API сокетов этот адрес называется универ- сальным адресом (wildcard address) и обычно обозначается INADDR ANY. Многоинтерфейсность и псевдонимы адресов Традиционно многоинтерфейсный узел определяется как узел с несколькими интерфейсами, например узел, имеющий два интерфейса Ethernet или интерфей- сы Ethernet и РРР. Каждый из интерфейсов должен иметь свой уникальный IPv4- адрес. При подсчете интерфейсов (для определения, является ли узел многоин- терфейсным) интерфейс закольцовки не учитывается. Маршрутизатор по определению является многоинтерфейсным, поскольку он пересылает пакеты, поступившие на один интерфейс, через другой интерфейс. Но обратное неверно, то есть многоинтерфейсный узел не является маршрутиза- тором, если он не передает пакеты. Действительно, многоинтерфейсный узел еще не может рассматриваться как маршрутизатор. Он будет функционировать как маршрутизатор, только если он сконфигурирован для такой работы (обычно ад- министратор должен включить соответствующие параметры конфигурации).
А.5. Адресация IPv6 951 Термин «многоинтерфейсность» является более общим и охватывает два раз- личных сценария (раздел 3.3.4 RFC 1122 [9]). 1. Узел с несколькими интерфейсами является многоинтерфейсным, при этом каждый интерфейс должен иметь свой IP-адрес. Это традиционное определение. 2. Современные узлы имеют возможность присваивать одному физическому интерфейсу несколько IP-адресов. Каждый IP-адрес, созданный в дополне- ние к первичному, или основному (primary), называется альтернативным именем, псевдонимом (alias) или логическим интерфейсом. Часто альтернатив- ные IP-адреса используют ту же маску подсети, что и основной адрес, но име- ют другие идентификаторы узла. Но допустима также ситуация, когда псев- донимы имеют адрес сети пли подсети, совершенно отличный от первичного адреса. В разделе 16.6 приведен пример альтернативных адресов. Таким образом, многоинтерфейсные узлы — это узлы, имеющие несколько интерфейсов, независимо от того, являются ли эти интерфейсы физическими или логическими. ПРИМЕЧАНИЕ--------------------------------------------------------- Многоинтерфейсность также используется в другом контексте. Сеть, имеющая несколь- ко соединений с сетью Интернет, также называется многоинтерфейсной. Например, некоторые сайты имеют два соединения с Интернетом вместо одного, что обеспечива- ет дублирование на случай неполадок. А.5. Адресация IPv6 Адреса IPv6 содержат 128 бит и обычно записываются как восемь 16-разрядных шестнадцатеричных чисел. По сути, здесь не существует классов адресов, как было в IPv4. Вместо этого старшие биты 128-разрядного адреса обозначают тип адреса [36]. В табл. А.2 приведены различные значения старших битов и соответствую- щий им тип адреса. Таблица А.2. Значение старших битов адреса IPv6 Назначение Форматный префикс Зарезервирован Не присвоен Зарезервирован для NSAP Зарезервирован для IPX Не присвоен Не присвоен Не присвоен Объединяемые глобальные индивидуальные адреса Не присвоен Не присвоен Не присвоен Не присвоен Не присвоен Не присвоен 0000 0000 00000001 0000 001 0000010 0000 011 0000 1 0001 001 010 011 100 101 110 1110 продолжение £?
952 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 Таблица А.2 (продолжение) Назначение Форматный префикс Не присвоен 11110 Не присвоен 111110 Не присвоен 1111110 Не присвоен 1111 11100 Индивидуальный в пределах физической подсети адрес 1111 1110 10 Индивидуальный в пределах сайта адрес 1111 1110 и Адрес многоадресной передачи 11111111 Эти старшие биты называются форматным префиксом. Например, если 3 стар- ших бита — 001, адрес называется объединяемым глобальным индивидуальным ад- ресом (aggregatable global unicast address). Если 8 старших битов —11111111 (Oxff), это групповой адрес. Если старшие 8 битов — 00000000, то адрес является зарезер- вированным, и некоторые примеры таких адресов будут рассмотрены далее. Объединяемые глобальные индивидуальные адреса Вероятно, самым употребительным из адресов IPv6 будет объединяемый глобаль- ный индивидуальный адрес, который согласно табл. А.2 начинается с 3-разряд- ного префикса 001. Такие адреса заменят собой 1Ру4-адреса классов А, В и С. ПРИМЕЧАНИЕ------------------------------------------------------ Исходная спецификация IPvG-адресов RFC 1884 [35] предусматривала индивидуаль- ные адреса направленной передачи, зависящие от провайдера (provider-based unicast address), имеющие префикс 010 (RFC 2073 [86]) На заседании IETF (Internet Engi- neering Task Force) в марте 1997 года было принято решение продолжать использова- ние других форм индивидуальных адресов. Формат объединяемых индивидуальных адресов определяется в [38] и содер- жит следующие поля, слева направо: форматный префикс (001); TLA ID (top-level aggregation identifier) идентификатор объединения верхне- го уровня;. NLA ID (next-level aggregation identifier) идентификатор объединения следу- ющего уровня; SLA ID (site-level aggregation identifier) идентификатор объединения уровня предприятия или идентификатор подсети; идентификатор интерфейса. На рис. А.6 приведен пример объединяемого глобального индивидуального адреса. Идентификатор интерфейса должен быть построен в формате IEEE EUI-64 (Extended User Interface — расширенный интерфейс пользователя) [42]. Это мно- жество 48-разрядных адресов IEEE 802 MAC (Media Access Control — уровень управления доступом к среде передачи), которые присвоены большинству карт сетевых интерфейсов локальной сети. Этот идентификатор должен автоматически
А.5. Адресация IPv6 953 3 13 бита 16 бит 64 бита 001 TLA ID 1 1 1 NLA ID 1 1 1 1 SLA ID l 1 1 1 1 1 1 1 Идентификатор интерфейса i i i i i i i и ► -< ► L< U Открытая топология Топология Идентификатор сайта интерфейса Рис. А.6. Объединяемый глобальный индивидуальный адрес IPv6 присваиваться интерфейсу и по возможности основываться на МАС-адресе кар- ты. Более подробное описание построения идентификаторов интерфейса, осно- ванных на EUI-64, описывается в приложении А [36]. Тестовые адреса бЬопе бЬопе — это виртуальная сеть, используемая для тестирования протоколов IPv6 (см. раздел Б.З). На момент написания этой книги объединяемые глобальные индивидуальные адреса еще не назначались, хотя уже планировалось использо- вание специального формата этих адресов в сети бЬопе [37]. Вместо этого для всех узлов в сети бЬопе используется формат адреса, документированный в RFC 1897 [39] и приведенный на рис. А.7. 5f AS Первые 24 бита 1Ру4-адреса сети Идентификатор подсети Идентификатор интерфейса 1 байт 2 13 12 6 Рис. А.7. Тестовые адреса IPv6 для сети бЬопе Эти адреса рассматриваются как временные, и узлы, использующие такие ад- реса, необходимо будет перенумеровать, когда будут назначены объединяемые глобальные индивидуальные адреса. Старший байт имеет значение 0x5f. 16-битовое поле А5 (autonomous system number) — это автономный системный номер, присвоенный организации или ее интернет-провайдеру. Они используются в версии IPv4 для идентификации до- менов маршрутизаторов. Следующее поле содержит 24 старших бита текущего IPv4-a/ipeca узла. Идентификатор подсети — это то, что задает организация, а идентификатор интерфейса обычно является 48-битовым адресом IEEE 802 МАС. В разделе 9.2 был показан 1Руб-адрес 5flb dfOO сеЗе е200 0020 0800 2078 еЗеЗ для узла solans (см. рис. 1.7). Поле AS имеет значение 7135 (Oxlbdf), а 206.62.226 соответствует 0хсеЗее2. Идентификатор подсети 0x0020, а младшие 48 бит занима- ет МАС-адрес Ethernet карты узла. Адреса IPv4, преобразованные к виду IPv6 Адреса IPv4, преобразованные к виду IPv6 (IPv4-mappcd IPv6 addresses), позво- ляют приложениям, запущенным на узлах, поддерживающих как IPv4, так и IPv6, связываться с узлами, поддерживающими только IPv4, в процессе перехода сети Интернет на версию протокола IPv6. Такие адреса автоматически создаются на серверах DNS (см. табл. 9.1), когда приложением IPv6 запрашивается IPvb-ад- рес узла, который имеет только адреса IPv4.
954 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 Рисунок 10.3 показывает, что использование данного типа адресов с сокетом IPv6 приводит к отправке IPv4-дейтаграммы узлу. Такие адреса не хранятся ни в каких файлах данных DNS — при необходимости они создаются сервером. На рис. А 8 приведен формат таких адресов. Младшие 32 бита содержат адрес IPv4. 0000 0000 FFFF Адрес IPv4 80 бит 16 32 Рис. А.8. Адреса IPv4, преобразованные к виду IPv6 При записи IPv'6-адреса последовательная строка из нулей может быть сокра- щена до двух двоеточий. Вложенный IPv4-адрес представлен в точечно-десятич- ной записи. Например, преобразованный к виду IPv6 1Ру4-адрес 0 0 0 0 0 FFFF 206 62 226 33 можно сократить до FFFF 206 62 226 33. Адреса IPv4, совместимые с IPv6 Для перехода от версии IPv4 к IPv6 используются также адреса IPv4, совмести- мые с IPv6 (IPv4-compatible IPv6 addresses). Администратор узла, поддерживаю- щего как IPv4, так и IPv6, и не имеющего соседнего 1Ру6-маршрутизатора, дол- жен создать DNS-запись типа АААА, содержащую адрес IPv4, совместимый с IPv6. Любой другой 1Р\'6-узел, посылающий 1Ру6-дейтаграмму на адрес IPv4, совмес- тимый с IPv6, должен упаковать {encapsulate) IPv6-дейтаграмму в заголовок IPv4 — такой способ называется автоматическим туннелированием {automatic tunnel). Более подробно вопросы туннелирования будут рассмотрены в разделе Б 3, а на рис. Б 2 будет приведен пример 1Р\’6-дейтаграмм такого типа, упакован- ных в заголовок IPv4. Однако в сети бЬопе каждый туннель должен быть сконфи- гурирован {configured) (то есть записан администратором в файл запуска), тогда как при использовании адреса IPv4, совместимого с IPv6, вручную необходимо конфигурировать только адрес (то есть поместить в файл данных DNS запись типа АААА), после чего будет осуществляться автоматическое туннелирование. На рис. А 9 показан формат адреса IPv4, совместимого с IPv6. В качестве примера такого адреса можно привести 206 62 226 33. 0000 0000 0000 Адрес IPv4 80 бит 16 32 Рис. А.9. Адрес IPv4, совместимый с IPv6 Адрес закольцовки Адрес IPv6 1, состоящий из 127 нулевых битов и единственного единичного бита, является адресом закольцовки IPv6. В API сокетов он называется i n6addr_ loopback или IN6ADDR_L00PBACK_INIT. Неопределенный адрес Адрес IPv6, состоящий из 128 нулевых битов, записываемый как 0 0 или просто , является неопределенным адресом IPv6 (unspecified address). В пакете IPv6
A 6 )CMPv4 и )CMPv6: протокол управляющих сообщений в сети Интернет 955 он может появиться только как адрес получателя в пакетах, посланных узлом, который находится в состоянии загрузки и еще не знает своего 1Р\'6-адреса В API сокетов этот адрес называется универсальным адресом, и его использо- вание, например, в функции bind для связывания прослушиваемого сокета TCP означает, что сокет будет принимать клиентские соединения, предназначенные любому из адресов узла. Этот адрес имеет имя in6addr_any или IN6ADDR_ANY_INIT. Адрес локальной связи Адрес локальной связи (link-local, локальный в пределах физической подсети) используется для соединения в пределах одной физической подсети, когда изве- стно, что дейтаграмма не будет перенаправляться. Примерами использования таких адресов являются автоматическая конфигурация адреса во время загрузки и поиска собеседника (neighbor discovery) (подобно ARP для IPv4). На рис. А. 10 приведен формат такого адреса 1111111010 0000 0000 Идентификатор интерфейса 10 бит 54 64 Рис. А. 10.1Ру6-адрес локальной связи Такие адреса всегда начинаются с fe80. Маршрутизатор IPv6 не должен пере- направлять дейтаграммы, у которых в поле отправителя или получателя указан адрес локальной связи, по другому соединению. В разделе 9.2 приведен адрес ло- кальной связи, связанный с именем атх-611. Адрес, локальный на уровне сайта Адрес, локальный в пределах сайта, используется для адресации внутри пред- приятия, когда не требуется глобальный префикс. На рис. А.И показан формат таких адресов. 1111111011 0000 . . . 0000 Идентификатор подсети Идентификатор интерфейса 10 бит 38 16 64 Рис. А.11. 1Рч6-адрес, локальный в пределах сайта Такие адреса всегда начинаются с fecO. Маршрутизатор IPv6 не должен пере- направлять дейтаграммы, для которых в поле отправителя или получателя ука- зан такой адрес, за пределы предприятия. А.6. ICMPv4 и ICMPv6: протокол управляющих сообщений в сети Интернет Протокол ICMP (Internet Control Message Protocol) является необходимой и не- отъемлемой частью любой реализации IPv4 или IPv6. Протокол ICMP обычно используется для обмена сообщениями об ошибках между узлами, как маршру- тизирующими, так и обычными, но иногда этот протокол используется и прило-
956 Приложение А Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 жениями Например, приложения Ping и Traceroute (см. главу 25) используют протокол ICMP. Первые 32 бита сообщений совпадают для ICMPv4 и ICMPv6 и приведены на рис А.12 ICMPv4 документируется в RFC 792 [82], a ICMPvg —в RFC 1885 [21]. 15 16 31 Тип Код Контрольная сумма Остальное зависит от типа, кода и от протокола (ICMPv4 или ICMPv6) Рис. А.12. Формат сообщений ICMPv4 и ICMPv6 8-битовое поле тип (type) указывает тип сообщения ICMPv4 или ICMPv6, а некоторые типы имеют дополнительную 8-разрядную информацию, указанную в поле кода (code). Поле контрольной суммы (checksum) является стандартной контрольной суммой, используемой в сети Интернет Отличия между ICMPv4 и ICMPv6 заключаются в том, какие именно поля используются при подсчете контрольной суммы. С точки зрения сетевого программирования необходимо понимать, какие со- общения ICMP могут быть возвращены приложению, что именно вызывает ошиб- ку и каким образом эта ошибка возвращается приложению В табл А 3 приведе- ны все сообщения ICMPv4 и показано, как они обрабатываются операционной системой 4 4BSD. В последнем столбце приведены значения переменной еггпо — то есть те ошибки, которые возвращаются приложениям В табл. А.4 приведен список сообщений ICMPv6. Таблица А.3. Обработка различных типов ICMP-сообщений в 4.4BSD Тип Код Описание Обработчик или еггпо 0 0 Echo-reply (Эхо-ответ) Пользовательский процесс (Pmg) 3 Destination unreachable (Получатель недоступен) 0 Network unreachable (Сеть недоступна) EHOSTUNREACH 1 Host unreachable (Узел недоступен) EHOSTUNREACH 2 Protocol unreachable (Протокол недоступен) ECONNREFUSED 3 Port unreachable (Порт недоступен) ECONNREFUSED 4 Fragmentation needed but DF bit set (Необходима фрагментация, но установлен бит DF) EMSGSIZE 5 Source route failed (Сбой маршрута отправителя) EHOSTUNREACH 6 Destination network unknown (Неизвестна сеть получателя) EHOSTUNREACH 7 Destination host unknown (Неизвестен узел получателя) EHOSTUNREACH 8 Source host isolated (Узел отравителя изолирован) Устаревший тип сообщений EHOSTUNREACH 9 Destination network administratively prohibited (Ceib получателя запрещена администратором) EHOSTUNREACH
A 6 ICMPv4 и ICMPv6 протокол управляющих сообщений в сети Интернет 957 Тип Код Описание Обработчик или еггпо 10 Destination host administratively prohibited (Узел получателя запрещен администратором) EHOSTUNREACH 11 Network unreachable for TOS (Сеть недоступна для TOS) EHOSTUNREACH 12 Host unreachable for TOS (Узел недоступен для TOS) EHOSTUNREACH 13 Communication administratively prohibited (Связь запрещена администратором) (Игнорируется) 14 Host precedence violation (Нарушение порядка старшинства узлов) (Игнорируется) 15 Piccedence cutoff m effect (Действует старшинство узлов) (Игнорируется) 4 0 Source quench (Отключение отправителя) Обрабатывается ядром в случае TCP, игнориру- ется в случаеИ DP 5 Redirect (Перенаправление) 0 Redirect for network (Перенаправление для сети) Ядро обновляет таблицу маршрутизации 1 Redirect for host (Перенаправление для узла) Ядро обновляет таблицу маршрутизации 2 Redirect for type-of-service and network Ядро обновляет таблицу (Перенаправление для тина сервиса и сети) маршрутизации 3 Redirect for type of service and host Ядро обновляет таблицу (Перенаправление для типа сервиса и узла) маршрутизации 8 0 Echo request (Эхо-запрос) Ядро генерирует от вет 9 0 Router advertisement (Извещение маршрут нзатора) Пользовательский процесс 10 0 Router solicitation (Запрос маршрутизатору) Пользовательский процесс 11 Tune exceeded (Превышено время передачи) 0 TTL equals 0 during transit (Время жизни равно 0 во время передачи) Пользова гельскии процесс 1 TTL equals 0 during reassembly (Время жизни равно 0 во время сборки) Пользовательский процесс 12 Parameter problem (Проблема с параметром) 0 IP header bad (Неправильный IP-заготовок) Типичная ошибка ENOPROTOOPT 1 Required option missed (Пропущен необходимый параметр) ENOPROTOOPT 13 0 Timestamp request (Запрос отметки времени) Ядро юперирует ответ 14 0 Timestamp reply (Ответ об отметке времени) Пользовательски!! процесс 15 0 Information request (Информационный запрос) Устаревший тип сообщении (игнорируется) 16 0 Information reply (Информационный ответ) Устаревший тип сообщении Полыовательскнн процесс 17 0 Address mask request (Запрос маски адреса) Ядро генерирует ответ 18 0 Address mask reply (Ответ маски адреса) Пользовательский процесс Таблица А.4. Сообщения ICMPv6 Тип Код Описание Обработчик или еггпо 1 Destination unreachable (Получатель недоступен) 0 No route to destination (Нет маршрута до получателя) EHOSTUNREACH продолжение &
958 Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 Таблица А.4 (продолжение) Тип Код Описание Обработчик или errno 1 Administratively prohibited, firewall filter (Запрещено администратором, фильтр брандмауэра) EHOSTUNREACH 2 Not a neighbor, incorrect strict source route (He сосед, некорректный маршрут отправителя) EHOSTUNREACH 3 Address unreachable (Адрес недоступен) EHOSTDOWN 4 Port unreachable (Порт недоступен) ECONNREFUSED 2 0 Packet too big (Слишком большой пакет) Ядро выполняет обнару- жение транспортной MTU 3 Time exceeded (Превышено время передачи) 0 Hop limit exceeded in transit (При передаче превышено значение предельного количества транзитных узлов) Пользовательский процесс 1 Fragment reassembly time exceeded (Истекло время сборки из фра1 ментов) Пользовательский процесс 4 Parameter problem (Проблема с параметром) 0 Erroneous header filed (Ошибочное поле заголовка) ENOPROTOOPT 1 Unrecognized next header (Следующий заголовок пераспознаваем) ENOPROTOOPT 2 Unrecognized option (Неизвестный параметр) ENOPROTOOPT 128 0 Echo request (Эхо-запрос (Ping)) Ядро1енерируег ответ 129 0 Echo reply (Эхо-ответ (Ping)) Пользовательский процесс (Ping) 130 0 Group membership query (Запрос о членстве в ipynne) Пользовательский процесс 131 0 Group membership report (Отче г о членстве в i руппе) Пользовательский процесс 132 0 Group membership reduction (Сокращение членства в группе) Пользовательский процесс 133 0 Router solicitation (Запрос маршрутизатору) Пользовательский процесс 134 0 Router advertisement (Извещение маршрутизатора) Пользовательский процесс 135 0 Neighbor solicitation (Запрос соседу) Пользовательский процесс 136 0 Neighbor advertisement (Извещение соседа) Пользовательский процесс 137 0 Redirect (Перенаправление) Ядро обновляет таблицу маршрутизации Запись «пользовательский процесс» в этой таблице означает, что ядро не об- рабатывает сообщение и ждет обработки данного сообщения от пользовательс- кого процесса с символьным сокетом. Также следует отметить, что различные реализации могут обрабатывать одни и те же сообщения по-разному. Например, в Unix сообщения типа Router solicitation (Запрос маршрутизатору) и Router advertisement (Извещение маршрутизатора) обычно обрабатываются как пользо- вательские процессы, но некоторые реализации могут обрабатывать эти сообще- ния в ядре. Версия ICMPv6 сбрасывает старший бит поля тип для сообщения об ошиб- ке (типы 1-4) и устанавливает этот бит для информационного сообщения (типы 128-137).
ПРИЛОЖЕНИЕ Б Виртуальные сети Б.1. Введение Поддержка новых возможностей протокола TCP, например каналов с повышен- ной пропускной способностью (RFC 1323), требуется только на узле, использу- ющем TCP, тогда как маршрутизаторы в модернизации не нуждаются. Эти изме- нения, описанные в RFC 1323, постепенно проявляются в реализациях TCP на узлах. Когда устанавливается новое TCP-соединение, каждая сторона может опре- делить, поддерживает ли другая сторона новую возможность, и если для обоих узлов это так, ею можно воспользоваться. Иная ситуация с изменениями IP-уровня, такими как многоадресная переда- ча, появившаяся в конце 80-х, или новая версия протокола IPv6, возникшая в се- редине 90-х, поскольку они требуют изменений на всех узлах и на всех маршрути- заторах. Но люди хотят начать использовать новые возможности, не дожидаясь, когда все системы будут модернизированы. Для этого существующий протокол IPv4 был дополнен так называемыми виртуальными сетями (virtual network), ис- пользующими туннели (tunnels). Б.2. МВопе Наш первый пример виртуальной сети, построенной с использованием тунне- лей, — это сеть МВопе, которая начала использоваться примерно с 1992 года [27]. Если два или более узлов в локальной сети поддерживают многоадресную пере- дачу, то на всех этих узлах могут быть запущены приложения многоадресной пе- редачи, которые могут общаться друг с другом. Для соединения одной локальной сети с другой локальной сетью, также содержащей узлы с возможностью много- адресной передачи, между двумя узлами из каждой локальной сети конфигури- руется туннель, как показано на рис. Б.1. На этом рисунке отмечены следующие шаги: 1. Приложение на узле отправителя МН 1 посылает групповую дейтаграмму ад- ресам класса D. 2. На рисунке эта дейтаграмма показана как UDP-дейтаграмма, поскольку боль- шинство приложений многоадресной передачи используют протокол UDP. Более подробно о многоадресной передаче и о том, как посылать и получать многоадресные дейтаграммы, рассказано в главе 19. 3. Дейтаграмма принимается всеми узлами в локальной сети, поддерживающи- ми многоадресную передачу, в том числе и MR2. Отметим, что MR2 также
960 Приложение Б. Виртуальные сети MR5 Заголовок IPv4, добавленный начальной точкой туннеля и удаленный конечной точкой туннеля |Ру4-адрес получателя = индивидуальный адрес конечной точки туннеля, поле протокола IPv4 = 4 (IPv4 в IPv4) Рис. Б.1. Упаковка IPv4 в IPv4, применяемая в МВопе работает как многоадресный маршрутизатор, на котором запущена програм- ма mrouted, осуществляющая маршрутизацию многоадресной передачи. 4. MR2 добавляет перед дейтаграммой другой IPv4-заголовок, в котором в поле адреса получателя записан индивидуальный адрес конечного узла туннеля (tunnel endpoint) MR5. Этот индивидуальный адрес конфигурируется адми- нистратором узла MR2 и считывается программой mrouted при ее запуске. Ана- логичным образом индивидуальный адрес узла MR2 сконфигурирован на узле MR5 — на другом конце туннеля. В поле протокола нового IPv4-заголовка уста- новлено значение 4, соответствующее упаковке IPv4 в IPv4. Дейтаграмма посы- лается следующему маршрутизатору, UR3, который явно указан как маршрути- затор направленной передачи, то есть не поддерживает многоадресную передачу, и поэтому приходится использовать туннель. Выделенная на рисунке серым цве- том часть 1Р\'4-дейтаграммы не изменяется по сравнению с шагом 1, только зна- чение поля TTL в выделенном цветом IPv4-заголовке уменьшается на 1. 5. UR3 узнает адрес получателя из самого внешнего IPv4-заголовка и перенаправ- ляет дейтаграмму следующему маршрутизатору направленной передачи — UR4.
Б.З. бЬопе 961 6. UR4 доставляет дейтаграмму по назначению — узлу MR5, который является конечным узлом туннеля. 7. MR5 получает дейтаграмму, и поскольку в поле протокола указана упаковка IPv4 в IPv4, удаляет внешний IPv4-заголовок и передает оставшуюся часть дейтаграммы (копию той, которая была групповой дейтаграммой в локальной сети, изображенной на рисунке вверху) в качестве многоадресной дейтаграм- мы в своей локальной сети. 8. Все узлы сети, изображенной на рисунке внизу, поддерживающие многоад- ресную передачу, получают многоадресную дейтаграмму. В результате многоадресная дейтаграмма, отправленная в локальной сети, изображенной вверху, передается как многоадресная дейтаграмма в локальной сети, изображенной внизу. Это происходит несмотря на то, что два маршрутиза- тора, присоединенные к этим двум локальным сетям, а также все маршрутизато- ры между ними не поддерживают многоадресную передачу. В данном примере показана функция маршрутизации многоадресной переда- чи, осуществляемая программой mrouted, запущенной на одном из узлов в каждой из локальных сетей. Таким образом запускается сеть МВопе. Около 1996 года большинство основных поставщиков маршрутизаторов начали включать функ- цию групповой маршрутизации в свои маршрутизаторы. Если бы два маршрути- затора направленной передачи UR3 и UR4 на рис. Б.1 имели возможность марш- рутизации многоадресной передачи, то нам не пришлось бы запускать mrouted, а маршрутизаторы UR3 и UR4 работали бы как маршрутизаторы многоадресной передачи. Но если между UR3 и UR4 существуют другие маршрутизаторы, не поддерживающие многоадресную передачу, туннель все же необходим. Но ко- нечными пунктами туннеля в этом случае могут стать MR3 (новое имя для UR3, поддерживающего многоадресную передачу) и MR4 (новое имя для UR4, под- держивающего многоадресную передачу), а не MR2 и MR5. ПРИМЕЧАНИЕ---------------------------------------------------------- В сценарии, приведенном на рис. Б.1, каждый многоадресный пакет появляется дваж- ды в локальной сети, расположенной вверху рисунка, и дважды в локальной сети, рас- положенной внизу. Один раз это многоадресный пакет, а второй раз — направленный пакет внутри туннеля, так как пакет идет между узлом, на котором запущена програм- ма mrouted, и следующим маршрутизатором направленной передачи (то есть между MR2 и UR3, а затем между UR4 и MR5). Лишняя копия — это цена туннелирования. Преимущество замены маршрутизаторов направленной передачи UR3 и UR4 на рис. Б.1 на маршрутизаторы многоадресной передачи (те, что мы назвали MR3 и MR4) заклю- чается в том, что мы избежали появления этой дополнительной копии многоадресного пакета в каждой из сетей. Даже если MR3 и MR4 должны установить туннель между собой, поскольку некоторые промежуточные маршрутизаторы между ними (которые на рисунке не показаны) не поддерживают многоадресную передачу, такой вариант предпочтительнее, так как в этом случае не происходит дублирования пакетов в каж- дой из локальных сетей. Б.З. бЬопе Виртуальная сеть бЬопе была создана в 1996 году по тем же причинам, что и МВопе: пользователи в группах узлов, поддерживающих версию протокола IPv6,
962 Приложение Б Виртуальные сети хотели соединить их вместе с помощью виртуальной сети, не дожидаясь поддер- жки IPv6 всеми промежуточными маршрутизаторами На рис Б 2 приведен при- мер двух локальных сетей, поддерживающих IPv6, соединенных с помощью тун- неля только через маршрутизаторы IPv4 На рисунке отмечены следующие шаги 1 Узел Н1 локальной сети, показанной на рисунке вверху, посылает IP-дейта- грамму, содержащую TCP-сегмент, узлу Н4 из локальной сети, показанной внизу Будем называть эти два узла 1Р\’6-узлами, хотя, вероятно, оба они под- держивают и протокол IPv4 В таблице маршрутизации IPv6 на узле Н1 запи- сано, что следующим маршрутизатором является узел Н2, и IPv6-дейтаграм- ма отсылается этому маршрутизатору Заголовок IPv4 добавленный начальной точкой туннеля и удаленный конечной точкой туннеля 1Ру4-адрес получателя = индивидуальным адрес конечной точки туннеля поле протокола IPv4 = 4 (IPv6 в IPv4) Рис. Б.2. Упаковка IPv6 в IPv4, используемая в сети бЬопе 2 На узле HR2 имеется сконфигурированный туннель до узла HR3 Этот тун- нель позволяет посылать IPv6-дейтаграммы между двумя конечными узлами туннеля через сеть IPv4 путем упаковки 1Ру6-дейтаграмм в IPv4-дейтаграм- мы (упаковка IPv6 в IPv4) В поле протокола указано значение 4 Отметим, что оба узла IPv4/IPv6 на концах туннеля — HR2 и HR3 — работают как мар-
Б 3 6bone 963 шрутизаторы IPv6, поскольку они перенаправляют IРуб-дейтаграммы, полу- чаемые на один интерфейс, через другой интерфейс Сконфигурированный туннель считается интерфейсом, хотя он является виртуальным, а не физи- ческим интерфейсом 3 Конечный узел туннеля (HR3) получает упакованную дейтаграмму, отбрасы- вает IPv4-заголовок и посылает IPv6-дейтаграмму в свою локальную сеть 4 Дейтаграмма приходит по назначению на узел Н4 Описанные виртуальные сети нужны лишь на время переходного периода, так как когда промежуточные маршрутизаторы получат требуемую функциональ- ность (многоадресная передача в случае МВопе и маршрутизация IPv6 в случае бЬопе), необходимость в виртуальных сетях исчезнет Мы описали обе эти вир- туальные сети, поскольку некоторые примеры в тексте книги используют поня- тия МВопе и бЬопе
ПРИЛОЖЕНИЕ В Техника отладки Это приложение содержит некоторые рекомендации и описание методов отлад- ки сетевых приложений. Ни один из приведенных методов не является панацеей от всех возможных проблем, однако существует множество инструментальных средств, с которыми следует ознакомиться, чтобы в дальнейшем использовать подходящие для конкретной среды. В.1. Трассировка системного вызова Многие версии Unix предоставляют возможность трассировки (отслеживания) системных вызовов. Зачастую это может оказаться полезным методом отладки. Работая на этом уровне, необходимо различать системный вызов и функцию. Системный вызов является точкой входа в ядро, и именно это можно отследить с помощью инструментальных средств, описанных в данном разделе. Стандарт Posix и большинство других стандартов используют термин функция, вкладывая в это понятие тот же смысл, что и пользователи, хотя на самом деле это может быть системный вызов. Например, в Беркли-ядрах socket — это системный вы- зов, хотя программист приложений может считать, что зто обычная функция языка С. В системе SVR4, как будет показано далее, это функция из библиотеки сокетов, которая содержит вызовы putmsg и getmsg, в действительности являющи- еся системными вызовами. В этом разделе мы рассмотрим системные вызовы, задействованные в работе клиента времени и даты. В листинге 1.1 была показана версия с использованием сокетов, а XTI-версия приведена в листинге 28.2. Библиотека потоковых сокетов SVR4 Начнем с потоковых реализаций сокетов, таких, как поддерживаются в системе SVR4. Программа truss SVR4 предназначена для запуска программы и трасси- ровки выполняемых системных вызовов. Запустим эту программу следующим образом: umxware % truss -о truss.out -v getmsg, putmsg, ioctl \ daytimetcpcli 140 252 1 54 (Длинную команду пришлось записать в две строки.) Параметр -о направляет вывод в файл (данный метод отладки всегда связан с большим количеством вы- водимых данных), а параметр -v включает подробную отладку для трех указан- ных системных вызовов. Он выдает дополнительную информацию об аргумен- тах для этих системных вызовов.
В. 1. Трассировка системного вызова 965 Начало вывода (примерно 40 строк) относится к связыванию программы с динамическими библиотеками, использующими ввод-вывод с распределением памяти. Это осуществляется с помощью системных вызовов open и гпгпар. (Послед- ний системный вызов описан в разделе 12.9 [93] и не имеет отношения к рассмот- рению сетевых API.) Эту часть вывода мы рассматривать не будем. Далее следует открытие файла /etc/netconfi g и считывание (read) всего файла (806 байт): open("/etc/netconfig'. O_RDONLY. 0666) = 3 read(3. "t с p\t t р i _ с о t s' . 8192) = 806 read(3 0x0804A6B8 8192) - 0 close(3) = 0 Некоторые не относящиеся к рассматриваемому вопросу системные вызовы 1 octi и 1 seek опущены. Вместо пропущенных вызовов вставлено многоточие. Зна- чение 3 справа от знака равенства — это возвращаемый дескриптор для систем- ного вызова open. Системный вызов read запрашивает 8192 байта, а возвращает значение 806 (размер файла). Следующий вызов read возвращает нулевое значе- ние (конец файла). Программа truss также показывает первые 12 байт, возвра- щаемые функцией read (начинающиеся с tcp), являющиеся началом первой стро- ки файла. Затем файл закрывается. Можно предположить, что считывание файла netconfig заканчивается, когда при первом вызове функции soket запускается библиотека сокетов. Следующий системный вызов представляет собой открытие (вызов open) уст- ройства /dev/tcp, и возвращаемый дескриптор опять равен 3. open("/dev/tcp". O_RDWR. 027776333624) -3 ioctl(3. I_FIND. "sockmod") = 0 ioctl(3. I_PUSH. "sockmod”) =0 ioctl(3. I STR 0x080467E4) =0 cmd=TI_BIND timeout=-l len=32 dp=0x0804ABA8 Вызов ioctl после вызова open проверяет, включен ли в поток модуль sockmod. Возвращаемое нулевое значение говорит о том, что этого модуля в потоке нет, и вызов 1 octi с аргументом I PUSH помещает его в поток. Далее следует несколько вызовов ioctl для потоков и обработка множества сигналов (которая здесь опус- кается). Вызов ioctl с аргументом I_STR посылает внутреннему потоку сообще- ние ioctl, при этом используется команда TI_BIND. Длина 32, вероятно, означает запрос Т_В I ND_REQ, состоящий из четырех 4-байтовых полей в структуре T_bi nd_req (она описана при обсуждении листинга 33.3), за которой следует 16-байтовая структура sockaddr in. Вероятно, это связывание некоторого локального адреса, выполняемое библиотекой сокетов при вызове connect на неприсоединением со- кете. ПРИМЕЧАНИЕ---------------------------------------------------------------------- Мы ожидали увидеть здесь putmsg, а затем getmsg, как в функции tpi bind. Далее рассмотрим первый вызов putmsg, осуществляемый только для получе- ния управляющей информации, со значением флага 0.
966 Приложение В Техника отладки putmsg(3 0x08046958. 0x00000000 0) = 0 ctl maxlen=428 len=36 buf=0x0804ABA8 getmsg(3. 0x08046924. 0x08046918 0x08046934) = 0 ctl maxlen=428 len-8 buf=0x0804ABA8 dat maxlen=128 len=-l buf=0x08046898 getmsg(3 0x08046964 0x08046958. 0x08046970 = 0 ctl maxlen=428 len=56 buf=0x0804ABA8 dat maxlen-128 len=-l buf=0x08046808 Длина 36, вероятно, относится к запросу T_CONN_REQ (см. листинг 33.4): пять 4-байтовых значений, за которыми следует 16-байтовая структура sockaddr_i п. Сле- дующий вызов getmsg возвращает 8 байт управляющей информации без каких- либо данных, что, вероятно, является сообщением Т_ОК_АСК (см. листинг 33.4). Следующий вызов getmsg возвращает 56 байт управляющей информации без каких-либо данных, что, вероятно, является сообщением T_CONN_CON (см. лис- тинг 33.4): пять 4-байтовых полей, 16-байтовая структура sockaddr in и 20 байт параметров. Можно только предположить, что последние 20 байт являются па- раметрами, исходя из размера структуры t opthdr (четыре 4-байтовых поля, см. раздел 32.2), после чего следует 1-байтовый параметр IP_TOS (единственный сквоз- ной параметр из табл. 32.1, возвращения которого мы можем ожидать в данной точке) и далее 3-байтовое заполнение (см также рис. 32.1). Следующим системным вызовом является getmsg, возвращающий 8 байт уп- равляющей информации и 26 байт данных. Вероятно, это соответствует сообще- нию T_DATA_IND. getmsg(3. 0х08046АЕС 0х08046АЕ0 0х08046В14) = 0 ctl maxlen=428 len=8 buf=0x0804ABA8 dat maxlen=4096 len=26 buf=0x08046BB0 writeQ. 'F r 1 A p r 4 0" 26) = 26 getmsg(3 OxO8O46AEC 0x08046AE0. 0x08046B14) = 0 ctl maxlen-428 len=-l buf=0x0804ABA8 dat maxlen=4096 len=0 buf=0x08046880 _exit(0) Далее наш клиент вызывает wri te, чтобы записать в стандартный поток выво- да (дескриптор 1) 26 байт данных. Следующий вызов getmsg возвращает управ- ляющую информацию и 0 байт данных — вероятно, указание от поставщика о том, что встретился конец файла. ПРИМЕЧАНИЕ----------------------------------------------------------------------- В этом месте ожидается получить сообщение T ORDREL IND. Потоковая XTI-библиотека SVR4 В следующем примере рассмотрим потоковые XTI-библиотеки операционной системы Solaris 2.6. Предполагается, что системные вызовы будут аналогичны вызовам, описанным в разделе 33.6 После отображения библиотеки в память с помощью системного вызова mmap обнаруживаем вызов open для поставщика транспортных служб TCP, после чего следует проверка модуля timod и включение данного модуля в поток. open("/dev/tcp' O_RDWR) = 3 ioctl (3 I_FIND 'timed') =0 ioctl(3 I_PUSH 'timod") = 0
В. 1. Трассировка системного вызова 967 Затем мы обнаруживаем несколько вызовов ioctl вместе с обработкой сигна- лов (которую мы опускаем). Один из этих вызовов ioctl появляется в ответ на наш вызов t_bi nd, и оказывается, что библиотека XTI осуществляет связывание путем данного вызова ioctl в модуле timod. (Аналогично тому, что мы видели в предыдущем примере сокетов и потоков в UnixWare.) Первый вызов putmsg посылает 36 байт управляющей информации и не посы- лает никаких данных Вероятно, это запрос T_CONN_REQ из нашего вызова t_connect. putmsg(3 0xEFFFE7F4 0x00000000 0) - 0 ctl maxlen=912 len=36 buf=0x0002BAB8 '\0\0\0\0\0\0\010" getmsg(3 0xEFFFE710 0xEFFFE700. 0xEFFFE71C) - 0 ctl maxlen-912 len=8 buf=0x0002ClE8 '0\0\0\013\0\0\0\0” dat maxlen=0 len—1 buf=0x00000000 flags 0x0001 getmsg(3 0xEFFFE7F4. 0xEFFFE770 0xEFFFE77C) = 0 ctl maxlen-912 len=36 buf=0x0002BAB8 "\0\0\0\f\0\0\010" dat maxlen-0 len=-l buf=0x00000000 flags 0x0000 Также видно, что Solaris выводит первые 8 байт буфера в шестнадцатеричной системе с управляющими последовательностями языка С. При вызове putmsg име- ем 7 нулевых байтов, за которыми следует байт 0x10 (16). Но на самом деле это два 4-байтовых поля: первые 4 байта — это запрос T_CONN_REQ (нулевое значение), далее следует длина адреса получателя (16). Первый вызов getmsg в предыдущем выводе возвращает сообщение Т_ОК_АСК, а следующий — сообщение T_CONN_CON. Можно определить тип первого возвраща- емого сообщения, поскольку Т_ОК_АСК соответствует значение 19 (0x13), а следую- щие 4 байта указывают на уже известный примитив (T_CONN_REQ, значение которого, как уже говорилось, равно 0). Можно также определить тип второго возвращае- мого сообщения, поскольку T_CONN_CON имеет значение 12 (которое выводится на экран как управляющий символ языка С для перехода на новую страницу — \f), за которым следует длина адреса собеседника (16, отображается как 0x10). Solaris выводит флаговую переменную, на которую указывает последний ар- гумент в системном вызове getmsg, и мы видим, что первый вызов возвращает зна- чение 1 (это MSG_HIPRI, как и ожидалось, поскольку сообщение Т_ОК_АСК — это со- общение типа М_РСPROTO), а второй вызов возвращает нулевое значение (что также совпадает с ожидаемым результатом, так как это обычное сообщение). Также сле- дует отметить, что сообщение Solans T_CONN_CON не возвращает никаких парамет- ров (длина 36), в то время как в примере с UnixWare оно возвращало еще 20 байт параметров. Следующий интересующий нас системный вызов — это другой вызов getmsg, который, вероятно, осуществляется в ответ на вызов t rcv. Этот вызов возвраща- ет 26 байт данных без управляющей информации, вероятно, сообщение M DATA с ответом сервера. getmsg(3 0xEFFFE7EC. OxEFFFEZDC 0xEFFFE81C) ctl maxlen-912 len=-l buf=0x0002BAB8 dat maxlen=4096 len=26 buf=0xEFFFE8D0 flags 0x0000 wnte(l ' F r 1 Apr 4 1" 26) getmsg!3 0xEFFFE7EC 0xEFFFE7DC 0xEFFFE81C) ctl maxlen=912 len=4 buf=0x0002BAB8 dat maxlen=4096 len=-l buf=0xEFFFE8D0 - 0 ' F r i Apr = 26 = 0 "\0\0\017'
968 Приложение В. Техника отладки flags 0x0000 _exit(0) Наш клиент вызывает write, и следующий вызов getmsg возвращает сообще- ние T_ORDERL_IND (4 байта управляющей информации без данных). Сообщению T_ORDERL_INO соответствует значение 23, которое выводится на экран как 0x17. Сокеты ядра BSD В следующем примере мы рассмотрим операционную систему BSD/OS — Берк- ли-ядро, в котором все функции сокетов являются системными вызовами. Про- грамма трассировки системных вызовов имеет название ktrace. Она выводит ин- формацию о трассировке в файл (по умолчанию имя этого файла ktrace out), который можно вывести на экран с помощью kdump. Клиент сокета запускается следующим образом: bsdi X ktrace daytlmetcpcll 206.62.226.43 Fri Apr 4 17 24 30 1997 Затем запускаем kdump, чтобы направить трассировочную информацию в стан- дартный поток вывода. 13187 daytlmetcpcll CALL 13187 daytlmetcpcll RET socket(0x2 0x1.0) socket 3 13187 daytlmetcpcll CALL connect(0x3 0xefbfc9a0 0x10) 13187 daytlmetcpcll RET connect 0 13187 daytlmetcpcll CALL read(0x3,0xefbfc9b0 0x1000) 13187 daytlmetcpcll GIO fd 3 read 26 bytes "Fri Apr 4 17 24 30 1997\r\n" 13187 daytlmetcpcll RET read 26/0xla 13187 daytlmetcpcll CALL write(0x1,0x9000.Oxla) 13187 daytlmetcpcll GIO fd 1 wrote 26 bytes "Fri Apr 4 17 24 30 1997\r\n" 13187 daytlmetcpcll RET write 26/0xla 13187 daytlmetcpcll CALL read(0x3.Oxefbfc9b0.0x1000) 13187 daytlmetcpcll GIO fd 3 read 0 bytes 13187 daytlmetcpcll RET read 0 13187 daytlmetcpcll CALL exit(0) Число 13 187 является идентификатором процесса. CALL идентифицирует сис- темный вызов, RET обозначает возвращение управления, GI0 подразумевает об- щий процесс ввода-вывода. Мы видим системные вызовы socket и connect, за ко- торыми следуют вызовы read, возвращающие 26 байт. Наш клиент записывает эти байты в стандартный поток вывода, и при следующем вызове read возвращает нулевое значение (конец файла). Сокеты ядра Solaris 2.6 Операционная система Solaris 2.x основывается на SVR4, и во всех версиях ра- нее 2.6 сокеты реализуются так, как показано па рис. 33.3. Однако во всех верси- ях SVR4 с подобными реализациями сокетов существует одна проблема: они редко
В.З. Программа sock 969 обеспечивают полную совместимость с сокетами Беркли-ядер. Для обеспечения дополнительной совместимости в Solaris 2.6 способ реализации изменен за счет использования файловой системы sockfs. Такой подход обеспечивает поддержку сокетов ядра, что можно проверить с помощью truss на нашем клиенте (исполь- зующем сокеты). Solaris26 X truss -v connect daytimetcpcli 198.69.10.4 Sat Apr 5 11 32 07 1997 После обычного подключения библиотеки осуществляется первый системный вызов so_socket — системный вызов, инициированный нашим вызовом socket. so_socket(2. 2. 0. 1) =3 connect(3. 0xEFFFE8C8. 16) - 0 name = 198 69 10 4/13 read(3. " S a t Ар г 5 1“ 4096) = 26 Sat Apr 5 11 32 07 1997 writeQ. " S a t A p r 5 1" 26) = 26 read(3, 0xEFFFE8D8. 4096) = 0 _exit(0) Первые три аргумента системного вызова so_socket являются нашими аргу- ментами socket. Далее мы видим, что connect является системным вызовом, a truss при вызове с флагом -v connect выводит на экран содержимое структуры адреса сокета, на которую указывает второй аргумент (IP-адрес и номер порта). Мы не показыва- ем системные вызовы, относящиеся к стандартным потокам ввода и вывода. ПРИМЕЧАНИЕ--------------------------------------------------------- Одним из побочных эффектов новой реализации является добавление в ядро 18 сис- темных вызовов. В.2. Стандартные службы Интернета Рекомендуем ознакомиться со стандартными службами Интернета, приведенны- ми в табл. 2.1. Для тестирования наших клиентов мы много раз использовали служ- бу, позволяющую определить дату и время. Служба, игнорирующая присылае- мые данные, является удобным портом, на который можно отправлять данные. Эхо-служба аналогична эхо-серверу, неоднократно упоминаемому в этой книге. П РИ МЕЧ АН И Е---------------------------------------- В настоящее время многие сайты перекрывают доступ к этим службам с помощью бран- дмауэров, так как некоторые атаки типа «отказ в обслуживании» (DoS), имевшие мес- то в 1996году, были направлены именно па эти службы (см. упражнение 12.3). Тем не менее можно успешно использовать эти службы внутри локальной сети. В.З. Программа sock Программа sock, написанная автором книги, впервые появилась в книге [94], где широко использовалась для генерации специальных условий, большинство ко- торых затем проверялось с помощью программы tcpdump. Удобство этой програм-
970 Приложение В. Техника сладки мы заключается в том, что она генерирует такое множество различных сценари- ев, что нет необходимости писать специальные тестовые программы. В этой книге исходный код программы не приведен (более 2000 строк на языке С), но он находится в свободном доступе (см. предисловие). Программа работает в одном из четырех режимов, и в каждом из них можно использовать либо протокол TCP, либо протокол UDP. 1. Клиент стандартного ввода и стандартного вывода (рис. В.1). sock Рис. В.1. Клиент sock: стандартный ввод и стандартный вывод В клиентском режиме все, что считывается из стандартного потока ввода, пе- редается в сеть, а все, что получается из сети, записывается в стандартный по- ток вывода. Должны быть указаны IP-адрес сервера и номер порта, и в случае TCP выполняется активное открытие. 2. Сервер стандартного ввода и стандартного вывода. Этот режим аналогичен предыдущему, за исключением того, что программа связываег заранее извест- ный порт со своим сокетом и в случае TCP осуществляется пассивное откры- тие. 3. Клиент-отправитель (рис. В.2). sock (клиент-отправитель) Рис. В.2. Программа sock в качестве клиента-отправителя Программа осуществляет фиксированное количество передач пакетов неко- торого определенного размера в сеть. 4. Сервер-получатель (рис. В.З). sock (сервер-получатель) Рис. В.З. Программа sock в качестве сервера-получателя Программа осуществляет фиксированное количество считываний из сети. Эти четыре рабочих режима соответствуют следующим четырем командам:
В.З Программа sock 971 sock [параметры] узел служба sock [параметры] -s [узел] служба sock [параметры] -1 узел служба sock [параметры] -is [узел] служба где узел— это имя или IP-адрес узла, а служба — это имя или помер порта. В двух серверных режимах выполняется связывание с универсальным адресом, если не задан необязательный параметр узел. Можно также определить около 40 параметров командной строки, запускаю- щих дополнительные возможности программы. Здесь мы не будем подробно останавливаться на этих параметрах, отметим только, что можно использовать почти все параметры сокетов, упомянутые в главе 7. Запуск программы без аргу- ментов выводит на экран краткое описание всех параметров: - b п связывает л в качестве клиентского локального номера порта - с конвертирует символ новой строки в CR/LF и наоборот - f а Ь с d р удаленный IP-адрес = a b с d удаленный номер порта = р - g а Ь с d свободная маршрутизация - h половинное закрытие TCP при получении EOF из стандартного потока ввода - 1 отправка данных на сокет прием данных с сокета (w/-s) - j а Ь с d присоединение к группе многоадресной передачи - к осуществляет write или writev порциями - 1 а b с d р клиентский локальный IP-адрес = а b с d локальный номер порта = р - п п размер буфера для записи клиентом 'рассылки"(по умолчанию 1024) - о НЕ присоединять UDP-клиент - р п время ожидания (в мс) перед каждым считыванием или записью (рассылка/прием) - q п размер очереди на прослушиваемом сокете для сервера TCP (по умолчанию 5) - г п количество байтов за одну операцию считывания (read) для сервера 'приема'' (по умолчанию 1024) - s работает как сервер а не как клиент - и использовать UDP вместо TCP - v подробный вывод - w п количество байтов для каждой записи (write) клиента 'рассылки'' (по умолчанию 1024) - х п время (в ms) для SD_RCVTIMEO (получение тайм-аута) - у п время (в ms) для SO_SNDTIMED (отправка тайм-аута) - А параметр SO_REUSEADDR - В параметр SO_BROADCAST - D параметр SO_DEBUG - Е параметр IP_RECVDSTADDR - F порождение дочерних процессов (fork) после установления соединения (параллельный ТСР-сервер) - G а b с d жесткая маршрутизация - Н п параметр IP_TOS (16=min del 8=max thru 4=max rel 2=min cost) - I сигнал SIGID - J n параметр IP_TTL - К параметр SOKEEPALIVE - L n параметр SO_LINGER n = linger time - N параметр TCP_NODELAY - 0 n время (в мс) для ожидания после вызова listen но перед первым приемом (accept) - Р п время (в мс) перед первым считыванием или записью (рассылка/прием) - Q п время (в мс) ожидания после получения FIN, но перед закрытием - R п параметр SO_RCVBUF - S п параметр SO_SNDBUF - Т параметр SO_REUSEPORT - U п войти в срочный режим прежде чем записать число п (только для отправителя) - V использовать writevO вместо writeO включает -к - W игнорировать ошибки записи для клиента приема - X п параметр TCP_MAXSEG (устанавливает MSS)
972 Приложение В. Техника отладки - Y параметр SO_DONTROUTE - Z MSG_PEEK - 2 параметр IP_DNESBCAST (255 255 255 255) для широковещательной передачи В.4. Небольшие тестовые программы Другим полезным методом отладки, которым автор пользовался при написании книги, является создание небольших тестовых программ, позволяющих увидеть, как работает одно конкретное свойство в тщательно выстроенной тестовой ситу- ации. При написании небольших тестовых программ полезно иметь набор биб- лиотечных функций-оберток и некоторых простых функций вывода сообщений об ошибках, наподобие тех, что использовались на протяжении всей книги. Такой подход уменьшает размер создаваемого кода и в то же время обеспечивает требу- емую проверку ошибок. Пример: определение полосы приоритета внеполосных данных XTI Чтобы проиллюстрировать этот метод в комплексе с трассировкой системных вызовов, ответим на вопрос: как XTI посылает внеполосные данные в TCP? Уста- новим TCP-соединение с сервером, вызовем t snd и отправим 1 байт с установ- ленным флагом T_EXPEDITED. В листинге В.Р приведена наша простая тестовая программа. Листинг В.1. Простая тестовая программа, показывающая, как XTI отсылает внеполосные данные TCP 7/debug/testOl с 1 include ''unpxti h" 2 int 3 main(int argc char **argv) 4 { 5 int tfd. 6 if (argc '= 3) 7 err quitC usage testOl <hostname/IPaddress> <service/port#>”): 8 tfd = Tcp_connect(argv[l], argv[2]), 9 t_snd(tfd, 1 ^EXPEDITED). 10 exit(0). П } Затем запускаем данную программу под управлением Solaris 2.6, используя программу truss для трассировки системных вызовов. solaris26 % truss -v putmsg.putpmsg testOl 198.69.10.4 discard Последние строки вывода отвечают на наш вопрос: putpmsg(3 OxEFFFF7D4 0xEFFFF7C0. 0. 0x0004) = 0 ctl maxlen=8 len=8 buf=0xEFFFF7CC '\0\0\004\0\0\0\0" dat maxlen=l len=l buf=0x00015318 ’\0" Все исходные коды программ, опубликованные в этой книге, вы можеЛ найти по адресу http:// www piter com/download.
В.4. Небольшие тестовые программы 973 Третий аргумент putmsg равен 0, это номер диапазона. Значение 4 в первых четырех байтах контрольного буфера соответствует T_EXDATA_REQ (запрос на сроч- ные данные), а значит, библиотека XTI отправляет это сообщение поставщику, как обычное сообщение из полосы 0. Пример: определение события для получения внеполосных (срочных) данных XTI Далее в отношении внеполосных данных TCP и XTI нас интересует следующий вопрос: какое из возможных событий ввода, приведенных в табл. 6.2, следует зап- рашивать при ожидании внеполосных данных: POLL IN, POLLRDNORM, POLLRDBAND или POLLPRI? На этот раз начнем с нашего простого XTI-сервера, приведенного в листинге 30.3, и составим программу, приведенную в листинге В.2. Листинг В.2. Простая тестовая программа для проверки опроса в ожидании внеполосных данных TCP //debug/test03 с 1 include "unpxti h” 2 int 3 main(int argc. char **argv) 4 { 5 mt listenfd connfd n. fla^S: 6 char buffEMAXLINE], 7 struct pollfd fds[l], 8 if (argc = 2) 9 listenfd = Tcpjisten (NULL argvEl] NULL). 10 else if (argc == 3) 11 listenfd = TcpJistentargvEU. argv[2] NULL). 12 else 13 err_quit("usage daytimetcpsrvOl E <host> ] «service or port>“). 14 connfd = Xti_accept(listenfd NULL. 0). 15 fds[O] fd = connfd 16 fdsCO] events = POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI. 17 for (. ) { 18 n = polKfds 1. INFTIM). 19 printf("poll returned Xd revents = OxfcxXen". n. fdsEO] revents). 20 n = T_rcv(connfd. buff, sizeof(buff) &flags), 21 printf("received £d bytes, flags = Td\en". n. flags) 22 } 23 } Здесь мы задаем все четыре возможных события и выводим на экран возвра- щаемое событие. Далее мы вызываем t_rcv и выводим количество байтов, возвра- щаемых вместе с флагами. Чтобы послать этой программе и обычные, и внеполосные данные, на другом узле запустим в качестве клиента программу sock: solans % sock -v -i -w 1 -n 3 -U 2 -p 4000 192.9.5.9 8888 connected on 206 62 226 33 34560 to 192 9 5 9 888B TCP_MAXSEG = 1460
974 Приложение В. Техника отладки wrote 1 bytes wrote 1 byte of urgent data wrote 1 bytes wrote 1 bytes Флаг -v включает режим подробного вывода, -i заставляет программу запи- сывать (отправлять) данные в сеть (режим клиента рассылки), -w 1 приводит к за- писи порциями по 1 байту, -п 3 производит три записи, -0 2 приводит к записи 1 байта внеполосных данных непосредственно перед второй записью, а -р 4000 вызывает паузу в 4000 миллисекунд (4 секунды) после каждого вызова write. Запустив нашу тестовую программу, а затем программу sock, мы увидим следую- щий вывод тестовой программы: solaris26 % test03 8888 poll returned 1. revents = 0x41 received 1 bytes flags = 0 poll returned 1. revents = 0x41 received 1 bytes, flags = 2 poll returned 1. revents = 0x41 received 1 bytes flags = 0 poll returned 1. revents = 0x41 received 1 bytes flags = 0 poll returned 1. revents = 0x41 t_rcv error An event requires attention Каждый раз возвращается событие POLLIN или POLLRDNORM, что говорит о том, что функция pol 1 специально не обрабатывает прибывающие внеполосные дан- ные. (Ищем значения двух битов в возвращаемом событии 0x41 в заголовочном файле <sys/pol 1 h>. При написании подобных тестовых программ гораздо проще и быстрее выводить эти значения в численном представлении, а затем искать их в соответствующих заголовочных файлах.) Но trcv возвращает флаг 2 (T_EXPEDITED), когда возвращает внеполосные данные. В.5. Программа tcpdump Бесценным средством отладки в сетевом программировании является такая про- грамма, как tcpdump. Она считывает пакеты из сети и выводит на экран большое количество информации об этих пакетах. Эта программа также позволяет нам задать некоторые критерии отбора пакетов, в результате чего будут выводиться только пакеты, удовлетворяющие этим критериям. Например, % tcpdump '(udp and port daytime) or icmp' выводит только UDP-дейтаграммы с номером порта отправителя или получате- ля, равным 13 (сервер времени и даты), или ICMP-пакеты. Следующая команда: % tcpdump 'tep and port 80 and tcp[13:l] & 2 != O' выводит только TCP-сегменты с номером порта отправителя или получателя, равным 80 (сервер HTTP), у которых установлен флаг SYN. Флаг SYN имеет зна- чение 2 в 13-м байте от начала TCP-заголовка. Следующая команда: % tcpdump 'tep and tcp[0:2] > 7000 and tcp[0:2] <= 7005' выводит только те TCP-сегменты, у которых номер порта отправителя лежит в интервале от 7001 до 7005. Номер порта отправителя занимает 2 байта в самом начале TCP-заголовка (нулевое смещение). В приложении А книга [94] более подробно описано действие данной программы.
В.7. Программа Isof 975 ПРИМЕЧАНИЕ ----------------------------------------------------------------- Эта программа доступна по адресу ftp.ee.lbl.gov и работает под множеством реализа- ций Unix. Опа написана Ван Якобсоном (Van Jacobson), Крэгом Лсрссом (Craig Leres) и Стивеном МакКаном (Steven McCanne). Некоторые поставщики предлагают свои программы, обладающие темп же возможно- стями. Например, в Solans 2.x есть программа snoop. Но программа tcpdump функцио- нирует под множеством версий Unix, а возможность использования одного и того же средства в неоднородном окружении является большим преимуществом. В.6. Программа netstat В тексте книги много раз использовалась программа netstat. Эта программа слу- жит для следующих целей. Она выводит статус точек доступа сети. Это было показано в разделе 5.6, ког- да мы прослеживали статус нашей точки доступа при запуске клиента и сер- вера. Она показывает, к какой группе принадлежит каждый из интерфейсов узла. Обычно для этой цели используется флаг -та, а в Solaris 2.x используется флаг -д. С параметром -s эта программа сообщает статистику по каждому протоколу. Подобный пример был приведен в разделе 8.13, когда мы говорили о недоста- точном управлении потоками в UDP. При использовании параметра -г программа выводит таблицу маршрутиза- ции, а с параметром -т — информацию об интерфейсе. Эта возможность была использована в разделе 1.9, когда с помощью программы netstat мы выясняли топологию сети. Программа netstat обладает и другими возможностями, а многие поставщики добавляют свои собственные. Обратитесь к руководству по вашей системе. В.7. Программа Isof Название 1 sot происходит от «list open file» (перечислить открытые файлы). Как и tcpdump, эта программа является общедоступной и представляет собой удобное средство для отладки, которое было перенесено на множество версий Unix. Одним из общих способов применения программы 1 sot при работе в сети яв- ляется выявление процесса, имеющего открытый сокет, по указанному IP-адресу или порту. Программа netstat позволяет выяснить, какой IP-адрес или порт ис- пользуется, а также узнать состояние TCP-соединения, но она не позволяет иден- тифицировать процесс. Например, чтобы определить, какой процесс запустил сер- вер времени и даты, выполним следующую команду: Solaris % Isof -i TCPidaytime COMMAND PID USER FD TYPE DEVICE SIZE/OFF INODE NAME inetd 222 root 15u met 0xf5a801f8 OtO TCP * daytime В выводе приводятся следующие данные: команда (данный сервис обеспечи- вается сервером i netd), идентификатор процесса, владелец процесса, дескриптор
976 Приложение В. Техника отладки (15 и и означает, что он открыт на чтение и на запись), тип сокета, адрес протокола блока управления, размер смещения файла (не имеет значения для сокета), тип протокола и имя. Еще один из традиционных случаев применения данной программы имеет место, когда мы запускаем сервер, который связывает свой заранее известный порт и получает ошибку, указывающую, что адрес уже используется. Тогда мы запускаем программу 1 sof, чтобы выяснить, каким процессом используется дан- ный порт. Поскольку программа 1 sof сообщает об открытых файлах, она не может сооб- щать о точках доступа, не ассоциированных с открытым файлом, то есть точках доступа TCP в состоянии TIME_WAIT. ПРИМЕЧАНИЕ ------------------------------------------------------------------ Программа находится по адресу ftp://vic.cc.purdue.edu/pub/tools/unix/lsof. Опа напи- сана Виком Абелем (Vic Abell). Некоторые поставщики предлагают свои программы с похожими возможностями. На- пример, в BSD/OS предлагается программа fstat. Однако программа Isof работает под множеством версий Unix, а использование одного инструмента в неоднородном окру- жении вместо подбора различных средств для каждой среды является большим пре- имуществом.
ПРИЛОЖЕНИЕ Г Различные исходные коды Г.1. Заголовочный файл unp.h Почти каждая программа в этой книге начинается с подключения заголовочного файла unp. h, показанного в листинге Г. 1 *. Этот файл подключает все стандартные системные заголовочные файлы, необходимые для работы большинства программ, а также некоторые общие системные заголовочные файлы. В нем также опреде- лены такие константы, как MAXLINE, прототипы функций ANSI С для тех функ- ций, которые мы определяем в тексте (например, readl ine), и все используемые нами функции-обертки. Сами прототипы в приведенном ниже листинге мы не показываем. Листинг Г.1. Заголовочный файл unp.h //11b/unp.h 1 /* Наш собственный заголовочный файл */ 2 #ifndef __unp_h 3 #define _unp_h 4 #include "../config.h" /* параметры конфигурации для данной ОС */ 5 /* "../config.h” генерируется сценарием configure */ 6 /* изменив список директив #include. 7 нужно также изменить файл acsite.rM */ 8 #include <sys/types.h> /* основные системные типы данных */ 9 #include <sys/socket.h> /* основные определения сокетов */ 10 #i ncl ude <sys/time.h> /* структура timeval{} для функции selectO */ 11 #include <time.h> /* структура timespec{} для функции pselectO */ 12 #i nclude <netinet/in.h> /* структура sockaddr_in{} и другие сетевые определения */ 13 #i nclude <arpa/inet.h> /* inet(3) функции */ 14 #include <errno.h> 15 #i nclude <fcntl.h> /* для неблокируемых сокетов */ 16 #include <netdb.h> 17 #include <signal.h> 18 #include <stdio.h> 19 #include <stdlib.h> 20 #i ncl ude <string.h> 21 #i nclude <sys/stat.h> /* для констант S_xxx */ 22 #include <sys/uio.h> /* для структуры iovec{} и readv/writev */ продолжение & Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www.piter.com/download.
978 Приложение Г Различные исходные коды Листинг Г. 1 (продолжение) 23 #1 ncl ude <umstd h> 24 #include <sys/wait h> 25 #include <sys/un h> /* для доменных сокетов Unix */ 26 #ifdef HAVE_SYS_SELECT_H 27 #include <sys/select h> 28 #endif 29 #ifdef HAVE_PDLL_H /* для удобства */ 30 #include <poll h> 31 #endif 32 #ifdef HAVE_STRINGS_H /* для удобства */ 33 #i ncl ude <stnngs h> 34 #endif /* для удобства */ 35 /* Три заголовочных файла обычно нужны для вызова ioctl 36 * для сокета/файла <sys/ioctl h> <sys/filio h> <sys/sodcto h> 37 */ 38 #ifdef HAVE_SYS_IOCTL_H 39 include <sys/ioctl h> 40 #endif 41 #lfdef HAVE_SYS_FILIO_H 42 include <sys/filio h> 43 #endif 44 #ifdef HAVE_SYS_SOCKIO_H 45 #include <sys/sockio h> 46 #endif 47 #ifdef HAVE_PTHREAD_H 48 include <pthread h> 49 #endif 50 /* OSF/1 фактически запрещает recv() и send!) 6 <sys/socket h> */ 51 #i fdef __osf_ 52 #undef 53 #undef 54 #define 55 #define 56 #endif recv send recvia sendla bed) bed) recvfrom(a.b.c.d.O.O) sendto(a.b.c.d.O.O) 57 #ifndef INADDR_ NONE /* $$ Ic INADDR_NONE$$ */ 58 #define INADDR_NONE Oxffffffff /* должно было быть в <netinet/in h> */ 59 #endif 60 #ifndef SHU1__RD /* три новые константы Posix 1g */ 61 #define SHUT_RD 0 /* отключение чтения */ 62 #define SHUT_WR 1 /* отключение записи */ 63 #define SHUT_RDWR 2 /* отключение чтения и записи */ 64 #endif 65 #lfndef INET_ADDRSTRLEN /* $$ Ic INET_ADDRSTRLEN$$ */ 66 #define INET_ADDRSTRLEN 16 /* ddd ddd ddd ddd\e0 67 1234567890123456 */ 68 #endif
Г1 Заголовочный файл unp h 979 69 /* Нужно даже если нет поддержки IPv6 чтобы мы всегда могли разместить в памяти 70 буфер требуемого размера без директив #т fdef в коде */ 71 #ifndef INET6_ADDRSTRLEN /* $$ Ic INET6_ADDRSTRLEN$$ */ 72 #define INET6_ADDRSTRLEN 46 /* максимальная длина строки адреса IPv6 73 хххх хххх хххх хххх хххх хххх хххх хххх или 74 хххх хххх хххх хххх хххх хххх ddd ddd ddd ddd\e0 75 1234567890123456789012345678901234567890123456 */ 76 #endif 77 /* Определяем bzeroO как макрос если эта функция отсутствует в стандартной библиотеке С */ 78 #ifndef HAVE_BZERO 79 #define bzerolptr n) memsettptr 0 n) 80 #endif 81 /* В более старых распознавателях отсутствует gethostbyname2() */ 82 #ifndef HAVE_GETH0STBYNAME2 83 #define gethostbyname2(host family) gethostbyname!(host)) 84 #endif 85 /* Структура возвращаемая функцией recvfrom_flags() */ 86 struct in_pktinfo { 87 struct in_addr ipi_addr /* IPv4 адрес получателя */ 88 int ipi_ifindex /* полученный индекс интерфейса *7 89 } 90 /* Нам нужны более новые макросы CMSG_LEN() и CMSG_SPACE() но в настоящее время их 91 поддерживают далеко не все реализации Им требуется 92 макрос ALIGN!) но зто зависит от реализации */ 93 #ifndef CMSG_LEN 94 #define CMSG_LEN(size) (sizeof(struct cmsghdr) + (size)) 95 #endif 96 #ifndef CMSG_SPACE 97 ^define CMSG_SPACE(size) (sizeoftstruct cmsghdr) + (size)) 98 #endif 99 /* Posix 1g требует макрос SUN_LEN() но он определен 100 не во всех реализациях Этот макрос 4 4BSD работает 101 независимо от того имеется ли поле длины */ 102 #ifndef SUN_LEN 103 ^define SUN_LEN(su) \е 104 (sizeof(*(su)) - sizeoft(su) >sun_path) + strlen((su) >sun_path)) 105 #endif 106 /* В Posix 1g домен Unix называется локальным IPC 107 Но пока не во всех системах определены AF_LOCAL и PF_LOCAL */ 108 #ifndef AF_LOCAL 109 #define AF_LDCAL AF_UNIX 110 tfendif 111 tfifndef PF_LOCAL 112 ^define PF_LOCAL PF_UNIX 113 #endif 114 /* Posix 1g требует определения константы INFTIM в <pol1 h> но во многих системах 115 она по прежнему определяется в <sys/stropts h> 116 Чтобы не подключать все функции работы с потоками определяем ее здесь 117 Это стандартное значение но нет гарантии что оно равно 1 */ продолжение &
980 Приложение Г. Различные исходные коды Листинг Г. 1 (продолжение) 118 #ifndef INFTIM 119 #define INFTIM (-1) /* бесконечный тайм-аут */ 120 #Tfdef HAVE_POLL_H 121 #define INFTIMJJNPH /* надо указать в unpxti h, что эта константа определена здесь */ 122 #endif 123 #endif 124 /* Это значение можно было бы извлечь из SOMAXCONN в <sys/socket h>. 125 но многие ядра по-прежнему определяют его как 5 хотя на самом деле поддерживается гораздо больше */ 126 ^define LISTENQ 1024 /* Второй аргумент функции listen!) */ 127 /* Различные константы */ 128 #define MAXLINE 4096 /* максимальная длина текстовой строки */ 129 #define MAXSOCKADDR 128 /* максимальный размер структуры адреса сокета */ 130 #define BUFFSIZE 8192 /* размер буфера для чтения и записи */ 131 /* Определение номера порта который может быть использован для взаимодействия клиент-сервер */ 132 #define SERV_PORT 9877 /* клиенты и серверы ТОР и U0P */ 133 #define SERV_PORT_STR '9877' /* клиенты и серверы TCP и U0P */ 134 #define UNIXSTR_PATH "/tmp/umx str” /* потоковые клиенты и серверы домена Umx */ 135 #define UNIXDG_PATH "/tmp/umx dg" /* клиенты и серверы протокола дейтаграмм домена Umx */ 136 /* Дальнейшие определения сокращают преобразования типов аргументов-указателей */ 137 #define SA struct sockaddr 138 #define FILE MODE (SIRUSR | SJWUSR | SJRGRP | S_IROTM) 139 /* заданные по умолчанию разрешения на доступ для новых файлов */ 140 #define DIR_MODE (FILE_M0DE | SJXUSR | SJXGRP | S IXOTH) 141 /* заданные по умолчанию разрешения на доступ к файлам для новых каталогов */ 142 typedef void Sigfunc (int) /* для обработчиков сигналов */ 143 #define ттп(а b) ((a) < (b) 7 (a) (b)) 144 #define max(a b) ((a) > (b) 7 (a) (b)) 145 #ifndef HAVE_ADDRINFO_STRUCT 146 include ' /lib/addrinfo h" 147 #endif 148 #ifndef HAVE_IF_NAMEINDEX_STRUCT 149 struct if_nameindex { 150 unsigned int if_index /*1 2 .. */ 151 char *if_name /* имя. заканчивающееся нулей: ”1e0". ...Щ 152 } 153 #endif 154 #ifndef HAVE_TIMESPEC_STRUCT 155 struct timespec { 156 time_t tv_sec /* секунды */ 157 long tv_nsec /* и наносекунды ★) 158 } 159 #endif
Г.2. Заголовочный файл config.h 981 Г.2. Заголовочный файл config.h Для обеспечения переносимости всего исходного кода, используемого в тексте книги, применялась утилита GNU autoconf. Ее можно загрузить по адресу ftp:// prep.ai.mit.edu/pub/gnu/. Эта программа генерирует сценарий интерпретатора с на- званием configure, который надо запустить после загрузки программного обеспе- чения в свою систему. Этот сценарий определяет, какие свойства обеспечивает ваша система Unix: имеется ли в структуре адреса сокета поле длины, поддержи- вается ли многоадресная передача, поддерживаются ли структуры адреса сокета канального уровня, и т. д. В результате получается файл с названием config h. Этот файл — первый заголовочный файл, включенный в unp h (см предыдущий раз- дел). В листинге Г.2 показан заголовочный файл config h для BSD/OS 3.0. Строки, начинающиеся с #def i пе, относятся к тем свойствам, которые обеспе- чены данной системой. Закомментированные строки и строки, начинающиеся с #undef, относятся к свойствам, данной системой не поддерживаемым Листинг Г.2. Заголовочный файл config.h для BSD/OS i386-pc-bsdi3 0/config h 1 /* config h Автоматически генерируется сценарием configure */ 2 /* Определяем константы если имеется соответствующий заголовочный файл */ 3 #define CPU_VENDOR_OS "i386-pc-bsdi3 О' 4 /* #undef HAVE_NETCONFIG_H */ /* <netconfig h> */ 5 /* #undef HAVE_NETDIR_H */ 6 #define HAVE_PTHREAD_H 1 7 #define HAVE_STRINGS_H 1 8 /* #undef HAVE_XTI_INET_H */ 9 #define HAVE_SYS_FILIO_H 1 10 #define HAVE_SYS_IDCTL_H 1 11 ^define HAVE_SYS_SELECT_H 1 12 #define HAVE_SYS_SOCKIO_H 1 13 #define HAVE_SYS_SYSCTL_H 1 14 #define HAVE_SYS_TIME_H 1 /* <netdir h> */ /* <pthread h> */ /* «strings h> */ /* <xti_inet h> */ /* <sys/filio h> */ /* <sys/ioctl h> */ /* <sys/select h> */ /* <sys/sockio h> */ /* <sys/sysctl h> */ /* <sys/time h> */ 15 /* Определена если можно подключить <time h> и <sys/time h> */ 16 ^define TIME_WITH_SYS_TIME 1 17 /* Определены если имеются соответствующие функции */ 18 #define HAVE_BZERO 1 19 ^define HAVE_GETH0STBYNAME2 1 20 7* #undef HAVE_PSELECT */ 21 #define HAVE_VSNPRINTF 1 22 /* Определены, если прототипы функций есть 23 /* #undef HAVE_GETADDRINFO_PROTO */ 24 /* #undef HAVE_GETNAMEINFO_PROTO */ 25 ^define HAVE_GETHDSTNAME_PROTO 1 26 #define HAVE_GETROSAGE_PROTO 1 27 #define HAVE_HSTRERROR_PROTD 1 28 /* #undef HAVE_IF_NAMETOINDEX_PROTO *7 29 #define HAVE_INET_ATON_PROTO 1 30 #define HAVE_INET_PTON_PRDTO 1 31 /* tfundef HAVE_ISFDTYPE_PROTO */ 32 /* #undef HAVE_PSELECT_PROTO */ 33 #define HAVE_SNPRINTF_PROTO 1 34 /* #undef HAVE_SOCKATMARK_PROTO */ в заголовочном файле */ /* <netdb h> */ /* <netdb h> */ /* <umstd h> */ /* <sys/resource h> */ /* <netdb h> */ /* <net/if h> */ /* <arpa/inet h> */ /* <arpa/inet h> */ /* <sys/stat h> */ /* <sys/select h> */ /* <stdio h> */ /* <sys/socket h> */ продолжение if'
982 Приложение Г. Различные исходные коды Листинг Г.2 (продолжение) 35 /* Определены, если определены соответствующие структуры */ 36 /* #undef HAVE_ADDRINFO_STRUCT */ /* <netdb h> */ 37 /* #undef HAVE_IF_NAMEINDEX_STRUCT */ /* <net/if h> */ 38 #define HAVE_SOCKADDR_DL_STRUCT 1 /* <net/if_dl h> */ 39 #define HAVE_TIMESPEC_STRUCT 1 /* <time h> */ 40 /* Определены если имеется указанное свойство */ 41 #define HAVE_SOCKADDR_SA_LEN 1 /* структура sockaddr{} содержит поле sa_len */ 42 ^define HAVE_MSGHDR_MSG_CONTROL 1 /* структура msghdr{} содержит поле msg_control */ 43 /* Имена устройств XTI для TCP и UDP */ 44 /* #undef HAVE_DEV_TCP */ /* большинство здесь */ 45 /* #undef HAVE_DEV_XTI_TCP */ /* для AIX */ 46 /* #undef HAVE_DEV_STREAMS_XTISO_TCP */ /* для OSF 32*/ 47 /* При необходимости определяем типы данных */ 48 /* #undef int8_t */ /* <sys/types h> */ 49 /* #undef intl6 t */ /* <sys/types h> */ 50 /* #undef int32 t */ jp. /* <sys/types h> */ 51 #define uint8_t unsigned char /* <sys/types h> */ 52 ^define uintl6 t unsigned short /* <sys/types h> */ 53 ^define uint32 t unsigned int /* <sys/types h> */ 54 /* #undef size_t */ /* <sys/types h> */ 55 /* #undef ssizet */ /* <sys/types h> */ 56 /* socklen_ _t должен иметь тип uint32_t. но configure определяет его 57 как unsigned int. так как это значение используется в начале компиляции. 5В иногда до того, как в данной реализации определяется тип uint32_t */ 59 #define socklen_t unsigned int /* <sys/socket h> */ 60 #define sa_family_t SA_FAMILY_T /* <sys/socket h> */ 61 ^define SA_FAMILY_T umt8_t 62 #define t_scalar_t int32_t /* <xti h> */ 63 #define t_uscalar_t uint32_t /* <xti h> */ 64 /* Определены если система поддерживает указанное свойство */ 65 #define IPV4 1 /* 66 #define IPv4 1 /* 67 /* #undef IPV6 */ /* 68 /* tfundef IPv6 */ /* 69 #define UNIXDOMAIN 1 /* 70 #define UNIXdomain 1 /* 71 #define MCAST 1 /* IPv4 V в верхнем регистре */ IPv4, v в нижнем регистре, на всякий случай */ IPv6. V в верхнем регистре */ IPv6 v в нижнем регистре на всякий случай */ доменные сокеты Unix */ доменные сокеты Unix */ поддержка многоадресной передачи */ Г.З. Заголовочный файл unpxti.h Все программы XTI подключают заголовочный файл unpxti h, показанный в ли- стинге Г.З. Как и в случае заголовочного файла upn h в разделе Г.1, мы пропуска- ем все прототипы функций. Мы также пропускаем все определения Т_ххх между T_INET_TCP и T_TP_BROADCAST, так как они практически идентичны. Листинг Г.З. Заголовочный файл unpxti.h для программ //ХТ111bxti/unpxti h 1 #ifndef _unp_xti_h 2 #define _unp_xti_h 3 #include "unp h"
Г.4. Стандартные функции обработки ошибок 983 4 include <xti h> 5 #ifdef HAVE_XTI_INET_H 6 #include <xti_inet h> 7 #endif 8 #ifdef HAVEJJETCCNFIGJ 9 #include <netconfig h> 10 #endif 11 #ifdef HAVE_NETDIR_H 12 #include <netdir h> 13 #endif 14 #ifdef INFTIMJJNPH 15 #undef INFTIM /* отсутствовала в <poll.h>. не определена для «stropts.ti!> */ 16 #endif 17 include <stropts h> 18 /* Обеспечение переносимости с новыми именами начинающимися с Т_ 19 в выпуске XNS 5 отсутствует в Posix 1g *7 20 #ifndef T_INET_TCP 21 #define T_INET_TCP INETTCP 22 #endif 56 #ifndef T_IP_BROADCAST 57 #define T_IP_BROADCAST IP_BROADCAST 58 #endif 59 /* Определение соответствующих устройств для t open!) */ 60 #ifdef HAVE_DEV_TCP 61 #define XTI_TCP "/dev/tcp” 62 #define XTIJJDP ’’/dev/udp" 63 #endif 64 #ifdef HAVE_DEV_XTI_TCP 65 #define XTI_TCP "/dev/xti/tcp" 66 #define XTIJJDP "/dev/xti/udp” 67 #endif 68 #ifdef HAVE_DEV_STREAMS_XTISO_TCP 69 #define XTIJFCP ”/dev/streanis/xtiso/tcp+" /* + для XPG4 */ 70 #define XTIJJDP '/dev/streams/xtiso/udp+" /* + для XPG4 */ 71 #endif 72 /* устройство для t_open() в t_accept(). задается с помощью tcpjisten!) */ 73 extern char xti_serv_dev[] Г.4. Стандартные функции обработки ошибок В этой книге мы определяем набор своих собственных функций для обработки ошибок. Причина, по который мы создаем эти функции, заключается в том, что они позволяют нам обрабатывать ошибки с помощью одной строки кода, как, на- пример, показано ниже: if [условие ошибки') егг_зу5(формат printf с любым количеством аргументов). . Р2
984 Приложение Г Различные исходные коды вместо if (условие ошибки) { char buff[200] snprintftbuff sizeof(buff). формат printf с любым количеством аргументов), perror(buff) exit(l) } Наши функции обработки ошибок используют следующую возможность ANSI С’ список аргументов может иметь переменную длину Более подробную информа- цию об этом вы найдете в разделе 7 3 книги [57] В табл Г 1 показано, в чем заключаются различия между функциями обра- ботки ошибок Если глобальная целочисленная переменная daemori proc отлична от нуля, то сообщение об ошибке передается функции syslog с указанным уров- нем, в противном случае оно отправляется в с.андар”яый поток вывода сообще- ний об ошибках Таблица Г. 1. Стандартные функции обрабо~ки ошибок Функция strerror (errno?) Завершение? Уровень syslog errduinp Да abort(), LOGERR err_msg Нет return LOGINFO errquit Нет exit(l). LOGERR errret Да тс turn. LOGINFO err sys Да exit(l) LOGERR errxti Да ex,t(l) LOGERR crrxtiret Да retu-n LOGINFO В листинге Г 4 показаны первые пять функций z.c "абл Г 1 Листинг Г.4. Стандартные функции обработки ошибок //1ib/error с 1 #include unp h 2 #include <stdarg h> /* заголовочный файл ANS Г */ 3 #incl ude <syslog h> /* для sys^g ) */ 4 int daemon_proc /* устанавливается j ненулевое зн чо-ие с помощью daemon_init() */ 5 static void err_doit(int int cons iiir * va_' st) 6 /* Нефатальная ошибка связанная с системным вызовем 7 Выводим сообщение и возвращаем уграаление */ 8 void 9 err_ret(const char *fmt ) Ю { 11 va_list ap 12 va_start(ap fmt) 13 err_doit(l LOG_INFO fmt ap), 14 va_end(ap) 15 return 16 }
Г 4 Стандартные функции обработки ошибок 985 17 /* Фатальная ошибка связана» ~ систеивдч зьповом 18 Выводим сообщение j завершаем рабо у */ 19 void 20 err_sys(const char *frt 21 { 22 va_l i st ap 23 va_start(ap fmt) 24 err_doit(l LOG_ERR fnt, ap) 25 va_end(ap) 26 exit(l) 27 } 28 /* Фатальная ошибка связанная с системным вызовом 29 Выводим сообщение сохраняем дамп памяти процесса и заканчиваем работу *•! 30 void 31 err_dump(const char *fint 32 { 33 va_list ap 34 va_start(ap fmt) 35 err_doit(l LOG_ERR fm4- ap) 36 va_end(ap) 37 abort!) /* сохраняем дамп памяти и заканчиваем работу */ 38 exit(l) 39 } 40 /* Нефатальная ошибка не относяиаяся к систеиному вызову 41 Выводим сообщение и возвращаем управление */ 42 void 43 err_msg(const char *fmt ) 44 { 45 va_list ap 46 va_start(ap fmt) 47 err_doit(0 LOG_INEO fnt ap), 48 va_end(ap) 49 return 50 } 51 /* Фатальная ошибка не относяшаяся к системному вызову 52 Выводим сообщение и заканчиваем работу *7 53 void 54 err_quit(const char *fmt ) 55 { 56 va_l1 st ap 57 va_start(ap fmt) 58 err_doit(0 LOGERR fmt аэ1 59 va_end(ap) 60 exit(l) 61 } 62 /* Выводим сообщение и во"вращаем утравлени 63 Вызывающий процесс задаст етпоЛад и eve */ продолжали
986 Приложение Г Различные исходные коды Листинг Г.4 (продолжение) 64 static void 65 err_doit(int errnoflag int level const char *fmt va_list ap) 66 { 67 int errno_save n 68 char buf[MAXLINE + 1] 69 errno_save = errno /* значение может понадобиться вызвавшему процессу *7 70 #lfdef HAVE_VSNPRINTF 71 vsnprintf(buf MAXLINE fmt ар) /* защищенный вариант */ 72 #else 73 vsprintf(buf fmt ар) /* незащищенный вариант */ 74 #endif 75 n = strlen(buf) 76 if (errnoflag) 77 snprintftbuf + n MAXLINE - n. " Xs' strerror(errno_save)) 78 strcat(buf \en ) 79 if (daemon_proc) { 80 syslogdevel buf) 81 } else { 82 fflush(stdout) /* если stdout и stderr совпадают */ 83 fputstbuf stderr) 84 fflush(stderr) 85 } 86 return 87 }
ПРИЛОЖЕНИЕ Д Решения некоторых упражнений Глава 1 3 В операционной системе AIX получаем. aix % daytlmetcpcll 206 62 226 33 socket error Addr family not supported by protocol Для получения дополнительной информации об этой ошибке сначала исполь- зуем программу grep, чтобы найти строку Addr в заголовочном файле <sys /еггпо h> aix % grep Addr Zusr/include/sys/errno h #define EAFNOSUPPORT 66 /* Семейство адресов не поддерживается семейством протоколов */ #define EADDRINUSE 67 /* Адрес уже используется */ Первая строка содержит значение переменной еггпо и возвращается функци- ей socket Далее смотрим в руководство пользователя. aix % man socket В большинстве руководств пользователя в конце под заголовком «Errors» при- водится дополнительная, хотя и лаконичная информация об ошибках. 4 Заменяем первое описание на следующее. int sockfd n counter = О Добавляем оператор counter++ в качестве первого оператора цикла while. Наконец, прежде Чем дрервать про- грамму, выполняем printfl counter = M\en counter) На экран всегда выводится значение 5 . Объявим переменную i типа int и заменим вызорфункции write на следую- щий for (1 = 0 1 < strlen(buff) 1++) Writelconnfd &buff[i] 1) Результат зависит от расположения клиентского узла и узла сервера Если кли- ент и сервер находятся на одном узле, счетчик обычно равен 1 Это значит, что даже если сервер выполнит функцию write 26 раз, данные будут возвращены за одну операцию считывания (read) Но если клиент запущен в Solans 2 5.1,
988 Приложение Д. Решения некоторых упражнений а сервер в BSD/OS 3.0, счетчик обычно равен 2. Просмотрев пакеты Ethernet, мы увидим, что первый символ отправляется в первом пакете сам по себе, а следующий пакет содержит остальные 25 символов. (Обсуждение алгорит- ма Нагла в разделе 7.9 объясняет причину такого поведения.) Если клиент за- пущен в BSD/OS 3.0, а сервер в Solaris 2.5.1, то счетчик будет равен 26. При просмотре пакетов мы обнаружим, что каждый символ передается в отдель- ном пакете. Цель этого примера — продемонстрировать, что разные реализации TCP по- разному поступают с данными, поэтому наше приложение должно быть гото- во считывать данные как поток байтов, пока не будет достигнут конец потока. Глава 2 1. Все RFC бесплатно доступны по электронной почте, через FTP или Web. Стартовая страница для поиска находится по адресу http://www.ietf.org. Од- ним из мест расположения RFC является каталог ftp://ftp.isi.edu/in-notes. Для начала следует получить файл с текущим каталогом RFC, обычно это файл rfc-index.txt. Далее нужно найти в этом каталоге RFC 1340 под названием «Assigned Numbers» («Присвоенные номера»). Обратите внимание, что этот докумет RFC уже устарел вследствие выхода RFC 1700. Хотя в настоящий , момент RFC 1700 находится в стадии написания, возможно, к моменту про- чтения вами этой книги он также может устареть. Пропустите список уста- ревших RFC и найдите текущий документ (с наибольшим номером) среди всех RFC под заголовком «Assigned Numbers». В разделе «Version Numbers» данного RFC описываются различные номера версий протокола IP. Версия 0 зарезервирована, версии 1-3 не присвоены, а версия 5 — это потоковый протокол Интернета (Internet Stream Protocol). 2. Если с помощью какого-либо редактора осуществить поиск термина «stream» (поток) в указателе RFC (см. решение предыдущего упражнения), мы выяс- ним, что RFC 1819 определяет версию 2 потокового протокола Интернета. Какую бы информацию, которая может содержаться в RFC, мы ни искали, для поиска следует использовать указатель (каталог) RFC. 3. В версии IPv4 при таком значении MSS генерируется 576-байтовая дейта- грамма (20 байт для заголовка IPv4 и 20 байт для заголовка TCP). Это мини- мальный размер буфера для сборки фрагментов в IPv4. 4. В данном примере сервер (а не клиент) осуществляет активное закрытие. 5. Узел в сети Token Ring не может посылать пакет, содержащий больше чем 1460 байт данных, поскольку полученное им значение MSS равно 1460. Узел в сети Ethernet может посылать пакет размером до 4096 байт данных, но не превышающий величину MTU исходящего интерфейса (Ethernet) во избежа- ние фрагментации. Протокол TCP не может превысить величину MSS, объяв- ленную другой стороной, но он всегда может посылать пакеты меньшего раз- мера. 6. В разделе «Protocol Numbers» (номера протоколов) RFC «Assigned Numbers» («Присвоенные номера») указано значение 89 для протокола OSPF.
Глава 5 989 Глава 3 1. В языке С функция не может изменить значение аргумента, передаваемого по значению. Чтобы вызван! аг функция изменила значение, передаваемое вы- зывающим процессом, требуется, чтобы вызывающий процесс передал указа- тель на значение, подлежащее изменению. 2. Указатель должен увеличиваться на количество считанных или записанных байтов, но в языке С нет возможности увеличивать указатели типа void (по- скольку компилятору не известю, на какой тип данных указывает указатель). Глава 4 1. Посмотрите на определение1 констант, начинающихся с INADDR_, кроме INADDR_ANY (состоит из нулевых битов) и ’NADDRJOE (состоит из единичных битов). Например, адрес многоадресной передачи класса D INADDR_MAX_LOCAL_GROUP оп- ределяется как OxeOOOOOff с комментарием «224.0.0.255», что явно указывает на порядок байтов узла. 2. Приведем новые строки, добавленные после вызова connet: len = sizeoftclladdr). Getsocknametsockfd. (SA*) Scliadd-'. &len) printfC’local addr £s\en" Sock_ntop((SA *) Selladdr len)) Это требует описания переменной len как socklen_t, a cliaddr как структуры stuct sockaddr_in. Обратите внимание, что аргумент типа «значение-резуль- тат» для функции getsockname (len) должен быть до вызова функции инициа- лизирован размером переменной, на которую указывает второй аргумент. Наи- более частая ошибка программирования при использовании аргументов типа «значение-результат» заключается в том, что про эту инициализацию забыва- ют. 3. Когда дочерний процесс вызывает функцию cl ose, счетчик ссылок уменьша- ется с 2 до 1, так что клиенту не посылается сегмент FIN. Позже, когда роди- тельский процесс вызывает функцию close, счетчик ссылок уменьшается до нуля, и тогда сегмент FIN посылается. 4. Функция accept возвращает значение EINVAL, так как первый аргумент не яв- ляется прослушиваемым сокетом. 5. Вызов функции 11 sten без вызова функции bi nd присваивает прослушиваемо- му сокету динамически назначаемый порт. Глава 5 1. Длительность состояния TIMEWAIT должна находиться в интервале меж- ду 1 и 4 минутами, что дает величину MSL от 30 секунд до 2 минут. 2. Наши клиент-серверные программы не работают с двоичными файлами. Допустим, что первые 3 байта в файле являются двоичной единицей (1), двоич- ным нулем (0) и символом новой строки. При вызове функции fgets в листин-
990 Приложение Д. Решения некоторых упражнений ге 5.4 либо считывается MAXLINE-1 символов, либо считываются символы до символа новой строки или до конца файла. В данном примере функция счита- ет три символа, а затем прервет строку нулевым байтом. Но вызов функции strlen в листинге 5.4 возвращает значение 1, так как она остановится на пер- вом нулевом байте. Один байт посылается серверу, но сервер блокируется в своем вызове функции readl i пе, ожидая символа новой строки. Клиент бло- кируется, ожидая ответа от сервера. Такое состояние называется зависанием, или взаимной блокировкой', оба процесса блокированы и при этом каждый ждет от другого некоторого действия, которое никогда не произойдет. Проблема заключается в том, что функция fgets обозначает нулевым байтом конец воз- вращаемых ею данных, поэтому данные, которые она считывает, не должны содержать нулевой байт. 3. Программа Telnet преобразует входные строки в NVT ASCII (см. раздел 26.4 книги [94]), что означает прерывание каждой строки 2-символьной последо- вательностью CR (carriage return — возврат каретки) и LF (linefeed — новая строка). Наш клиент добавляет только разделитель строк (newline), который в действительности является символом новой строки (linefeed, LF). Тем не менее можно использовать клиент Тelnet для связи с нашим сервером, посколь- ку наш сервер отражает каждый символ, включая CR, предшествующий каж- дому разделителю строк. 4. Нет, последние два сегмента из последовательности завершения соединения не посылаются. Когда клиент посылает серверу данные после уничтожения дочернего процесса сервера (ввод строки another line, см. раздел 5.12), сервер TCP отвечает сегментом RST. Сегмент RST прекращает соединение, а также предотвращает переход в состояние TIME_WAIT на стороне сервера (конец соединения, осуществивший активное закрытие). Ничего не меняется, потому что процесс, запущенный на узле сервера, создает прослушиваемый сокет и ждет прибытия запросов на соединение. На третьем шаге мы посылаем сегмент данных, предназначенный для установленного со- единения TCP (состояние ESTABLISHED). Наш сервер с прослушиваемым сокетом не увидит этот сегмент данных, и TCP сервера по-прежнему будет посылать клиенту сегмент RST. 6. В листинге Д.11 приведена программа. Запуск этой программы в AIX генери- рует следующий вывод: aix % tsigpipe 206.62.226.34 SIGPIPE received write error Broken pipe Начальный вызов функции sleep и переход в режим ожидания на 2 секунды нужны, чтобы сервер времени и даты отправил ответ и закрыл свой конец со- единения. Первая функция write отправляет сегмент данных серверу, кото- рый отвечает сегментом RST (поскольку сервер времени и даты полностью закрыл свой сокет). Обратите внимание, что наш TCP позволяет писать в со- кет, получивший сегмент FIN. Второй вызов функции sleep позволяет полу- 1 Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http// www.piter.com/download.
Глава 5 991 чить от сервера сегмент RST, а во втором вызове функции write генерируется сигнал SIGPIPE. Поскольку наш обработчик сигналов возвращает управление, функция write возвращает ошибку EPIPE. Листинг Д.1. Генерация SIGPIPE //tcpcliserv/tsigpiре с 1 #include "unp h" 2 void 3 sig_pipe(int signo) 4 { 5 printf("SIGPIPE received\en"): 6 return: 7 } 8 mt 9 mainlint argc, char **argv) 10 { 11 int sockfd. 12 struct sockaddr_in servaddr: 13 if (argc '= 2) 14 err_quit("usage tcpcli <IPaddress>"), 15 sockfd = Socket(AF_INET. SOCK_STREAM. 0): 16 bzero(8servaddr sizeof(servaddr)): 17 servaddr sin_family = AFJNET, 18 servaddr sin port = htons(13), /* сервер времени и даты */ 19 Inet_pton(AF_INET. argv[l]. &servaddr sin_addr). 20 Signal(SIGPIPE, sig_pipe), 21 Connect(sockfd. (SA *) &servaddr, sizeof(servaddr)). 22 sleep(2). 23 VJrite(sockfd "hello". 5); 24 sleep(2). 25 VJrite(sockfd. "world". 5). 26 exit(O), 27 } 7. В предположении, что узел сервера поддерживает модель системы с гибкой привязкой (см. раздел 8.8), все будет работать, то есть узел сервера примет IP- дейтаграмму (которая в данном случае содержит TCP-сегмент), прибывшую на самый левый канал, даже если IP-адрес получателя является адресом само- го правого канала. Это можно проверить, если запустить наш сервер на узле bsdi (см. рис. 1.7), а затем запустить клиент на узле sol ari s, но на стороне кли- ента задать другой IP-адрес сервера (206.62.226.66). После установления со- единения, запустив на стороне сервера программу netstat, мы увидим, что ло- кальный IP-адрес является IP-адресом получателя из клиентского сегмента SYN, а не IP-адресом канала, на который прибыл сегмент SYN (как отмеча- лось в разделе 4.4).
992 Приложение Д. Решения некоторых упражнений 8. Наш клиент был запущен в системе Intel с прямым порядком байтов, где 32-раз- рядное целое со значением 1 хранится так, как показано на рис. Д.1. 32-разрядное целое г* ►! 00 00 00 |01 Адрес: А+3 А+2 А+1 А Рис. Д. 1. Представление 32-разрядного целого числа 1 в формате прямого порядка байтов 4 байта посылаются на сокет в следующем порядке: А, А+1, А+2 и А+3, и там хранятся в формате обратного порядка байтов, как показано на рис. Д.2. [ 01 | 00 [ 00 | 00 | А А+1 А+2 А+3 Рис. Д.2. Представление 32-разрядного целого числа с рис. Д.1 в формате обратного порядка байтов Значение 0x01000000 интерпретируется как 16 777 216. Аналогично, целое чис- ло 2, отправленное клиентом, интерпретируется сервером как 0x02000000, или 33 554 432. Сумма этих двух целых чисел равна 50 331 648, или 0x03000000. Когда это значение, записанное в обратном порядке байтов, отправляется клиенту, оно интерпретируется клиентом как целое число 3. Но 32-разрядное целое число -22 представляется в системе с прямым поряд- ком байтов так, как показано на рис. Д.З (мы предполагаем, что используется поразрядное дополнение до двух для отрицательных чисел). | ff ~f | ff | еа | А+3 А+2 A+1 A Рис. Д.З. Представление 32-разрядного целого числа -22 в формате прямого порядка байтов В системе с обратным порядком байтов это значение интерпретируется как Oxeaffffff, или -352 521 537. Аналогично, представление числа -77 в прямом порядке байтов выглядит как 0xffffffb3, но в системах с обратным порядком оно представляется как 0xb3ffffff, или -1 275 068 417. Сложение, выполняе- мое сервером, приводит к результату 0x9efffffe, или -1 627 389 954. Получен- ное значение в обратном порядке байтов посылается через сокет клиенту, где в прямом порядке байтов оно интерпретируется как 0xfeffff9e, или -16 777 314 — это то значение, которое выводится в нашем примере. 9. Метод правильный (преобразование двоичных значений в сетевой порядок байтов), но нельзя использовать функции hton] и ntohl. Хотя символ 1 в назва- ниях данных функций обозначает «long», этн функции работают с 32-разряд- ными целыми (раздел 3.4). В 64-разрядных системах long занимает 64 бита, и эти две функции работают некорректно. Для решения этой проблемы еле-
Глава 6 993 дует определить две новые функции hton64 и ntoh64, но они не будут работать в системах, представляюпщх значения типа 1 ong 32 битами. 10. В первом сценарии сервер будет навсегда блокирован при вызове функции readn в листинге 5.14, поскольку клиент посылает два 32-разрядпых значения, а сервер ждет два 64-разрядных значения. В случае, если клиент и сервер по- меняются узлами, клиент будет посылать два 64-разрядных значения, а сер- вер считает только первые 64 бита, интерпретируя их как два 32-разрядных значения. Второе 64-разрядное значение останется в приемном буфере сокета сервера. Сервер отправит обратно 32-разрядное значение, и клиент навсегда заблокируется в вызове функции readn в листинге 5.13, поскольку будет ждать для считывания 64-разрядное значение. 11. Функция IP-маршрутизации просматривает IP-адрес получателя (1Р-адрес сервера) и пытается по таблице маршрутизации определить исходящий ин- терфейс и следующий маршрутизатор (см. главу 9 [94]). В качестве адреса от- правителя используется первичный IP-адрес исходящего интерфейса, если сокет еще не связан с локальным IP-адресом. Глава 6 1. Массив целых чисел содержится внутри структуры, а язык С позволяет ис- пользовать со структурами оператор присваивания. 2. Если функция select сообщает, что сокет готов к записи, причем буфер от- правки сокета вмещает 8192 байта, а мы вызываем для этого блокируемого со- кета функцию write с буфером размером 8193 байта, то функция write может заблокироваться, ожидая места для последнего байта. Операции считывания на блокируемом сокете будут возвращать сообщение о неполном считывании, если доступны какие-либо данные, но операции записи на блокируемом соке- те заблокированы до принятия всех данных ядром. Поэтому, чтобы избежать блокирования при использовании функции sel ect для проверки на возмож- ность записи, следует переводить сокет в неблокируемый режим. 3. Если оба дескриптора готовы для чтения, выполняется только первый тест — тест сокета. Но это не прерывает работу клиента, а только лишь уменьшает ее эффективность. Поэтому если при завершении функции select оба дескрип- тора готовы для чтения, первое условие i f оказывается истинным, в результа- те чего сначала вызывается функция readl те для считывания из сокета, а за- тем функция fputs для записи в ст андарт пый поток вывода. Следующее условие if пропускается (поскольку мы добавили else), но функция select вызывает- ся снова, сразу находит стандартное устройство ввода, готовое к чтению, и за- вершается. Суть в том, что условие готовности стандартного потока ввода для чтения сбрасывается считыванием из сокета, а пе возвратом функции select. 4. Воспользуйтесь функцией getrl i пл t для получения значений константы RL 1М1Т_ NOFILE, а затем вызовите функцию setrlimit для установки текущего гибкого предела (rl im cur) равным жесткому пределу (rl imjnax). Например, в Solaris 2.5 гибкий предел равен 64, но любой процесс может увеличить эго значение до используемого по умолчанию значения жесткого предела (1024).
994 Приложение Д. Решения некоторых упражнений Функции getrl'imit и setrlinnt пе входят в стандарт Posix.l, но требуются в Unix 98. 5. Серверное приложение непрерывно посылает данные клиенту, клиент TCP подтверждает их прием и отбрасывает. 6. Функция shutdown с аргументами SHUT_WR и SHUT_RDWR всегда посылает сегмент FIN, в то время как функция close посылает сегмент FIN, только если в мо- мент вызова функции close счетчик ссылок дескриптора равен 1. 7. Функция readl i пе возвращает ошибку, и наша функция-обертка Readl i пе за- вершает работу сервера. Но серверы должны справляться с такими ситуация- ми. Обратите внимание на то, как мы обрабатываем эти условия в листин- ге 6.6, хотя даже этот код не является удовлетворительным. Рассмотрим, что произойдет, если соединение между клиентом и сервером прервется и время ожидания одного из ответов сервера будет превышено. Возвращаемой ошиб- кой может быть ошибка ETIMEDOUT. Обычно сервер не должен прекращать свою работу из-за подобных ошибок. Он должен записать ее в файл журнала, закрыть сокет и продолжать обслужи- вание других клиентов. Следует понимать, что обработка таких ошибок путем прекращения работы сервера недопустима для серверов, у которых один про- цесс выполняет обработку всех клиентов. Но если сервер был дочерним про- цессом, обрабатывающим только один клиент, то прекращение работы одного дочернего процесса не отразится ни на родительском процессе (который, по нашему предположению, обрабатывает все новые соединения и порождает новые дочерние процессы), ни на одном из других дочерних процессов, обра- батывающих другие клиенты. Глава 7 2. Решение упражнения приведено в листинге Д.2. Вывод строки данных, воз- вращаемых сервером, был удален, поскольку это значение нам не нужно. Листинг Д.2. Вывод размера приемного буфера сокета и MSS до и после установ- ления соединения //sockopt/rcvbuf с 1 #include "unp h" 2 #include <netinet/tcp h> /* для TCP_MAXSEG */ 3 int 4 mainfint argc. char **argv) 5 { 6 int sockfd. rcvbuf. mss. 1 sock 1 enJ len. 8 struct sockaddr_in servaddr. 9 if (argc 1= 2) 10 err_quit("usage rcvbuf <IPaddress>”). 11 sockfd = Socket(AFJNET. SOCK_STREAM, 0): 12 len = sizeof(rcvbuf); 13 Getsockopt(sockfd. SOL_SOCKET. SO_RCVBUF. &rcvbuf. &len).
Глава 7 995 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 3. len = sizeof(mss). Getsockopt(sockfd. IPPR0T0_TCP. TCP_MAXSEG. &mss, &len). printff"defaults. S0_RCVBUF = &j. MSS = £d\en". rcvbuf, mss): bzerof&servaddr. sizeof(servaddr)). servaddr sin_family = AF_INET. servaddr sin_port = htons(13). /* сервер времени и даты */ Inet_pton(AF_INET. argv[l]. &servaddr sin_addr). Connect(sockfd. (SA *) Sservaddr. sizeof(servaddr)) len = sizeof(rcvbuf). Getsockopttsockfd. S0L_S0CKET. S0_RCVBUF. &rcvbuf. &len). len = sizeof(mss). Getsockopt(sockfd. IPPROTO_TCP. TCP_MAXSEG &mss. &len), printfC'after connect S0_RCVBUF = Й MSS = й\еп". rcvbuf. mss): exit(0). } He существует какого-то одного «правильного» вывода для данной програм- мы. Результаты зависят от системы. Некоторые системы (в особенности Sola- ris 2.5.1 и более ранние версии) всегда возвращают нулевой размер буфера сокета, не давая нам возможности увидеть, что происходит с этим значением в процессе соединения. До вызова функции connect выводится значение MSS по умолчанию (часто 536 или 512), а значение, выводимое после вызова функции connect, зависит от возможных параметров MSS, полученных от собеседника. Например, в ло- кальной сети Ethernet после выполнения функции connect MSS может иметь значение 1460. Однако после соединения (connect) с сервером в удаленной сети значение MSS может быть равно значению по умолчанию, если только ваша система не поддерживает обнаружение транспортной MTU. Если это возмож- но, запустите во время работы вашей программы программу tcpdump или по- добную ей (см. раздел В.5), чтобы увидеть фактическое значение параметра MSS в сегменте SYN, полученном от собеседника. Многие реализации после установления соединения округляют размер при- емного буфера сокета в большую сторону, чтобы он был кратным MSS. Чтобы узнать размер приемного буфера сокета после установления соединения, мож- но исследовать пакеты с помощью программы типа tcpdump и посмотреть, ка- ков размер объявленного окна TCP. Разместите в памяти структуру ] i nger по имени 11 ng и проинициализируйте ее следующим образом: str_cli(stdin, sockfd). ling l_onoff = 1. ling l_linger = 0. Setsockoptfsockfd, SOLSOCKET. S0_LINGER. &ling. sizeof(ling)): exit(0). Это заставит TCP на стороне клиента прекратить работу путем отправки сег- мента RST вместо нормального обмена четырьмя сегментами. Дочерний про- цесс сервера вызывает функцию readl 1 пе, возвращает ошибку ECONN RESET и вы- водит следующее сообщение:
996 Приложение Д Решения некоторых упражнений readline error Connection reset by peer Клиентский сокет не должен проходить через состояние ожидания TIME WAIT, даже если клиент выполняет активное закрытие. 4. Первый клиент вызывает функции setsockopt, bind и connect. Но если второй клиент вызовет функцию bi nd между вызовами функций bi nd и connect перво- го клиента, возвращается ошибка EADDRINUSE. Но как только первый клиент установит соединение с собеседником, вызов функции bi nd второго клиента будет работать, поскольку сокет первого клиента уже присоединен. В случае возвращения ошибки EADDRINUSE второму клиенту следует вызывать bind не- сколько раз, а не останавливаться при появлении первой ошибки — это един- ственный способ справиться с данной ситуацией. (Подобная ситуация гонок описана в стандарте Posix.lg ) 5. Запускаем программу на узле без поддержки многоадресной передачи (Unix- Ware 2.1 2). umxware % sock -s 9999 & запускаем первый сервер с универсальным адресом [1] 29697 umxware % sock -s 206 62 226 37 9999 пробуем второй сервер но без -А can't bind local address Address already in use umxware % sock -s -A 206 62 226 37 9999 & пробуем опять с -А работает [2] 29699 umxware % sock -s -A 127 0.0.1 9999 & третий сервер с -А работает [3] 29700 umxware Ж netstat -na | grep 9999 tcp 0 0 127 0 0 1 9999 * * LISTEN tcp 0 0 206 62 226 37 9999 * * LISTEN tcp 0 0 * 9999 * * LISTEN 6 Сначала пробуем на узле без поддержки многоадресной передачи (UnixWare 2.1.2). umxware % sock -s -u -A 206.62.226 37 8888 & сначала запускаем первый [4] 29707 umxware % sock -s -u -A 206 62 226 37 8888 can’t bind local address Address already in use не мошем запустить второй В обоих случаях мы задаем параметр SO REUSEADDR, но это не действует Теперь попробуем проделать то же на узле с поддержкой многоадресной пере- дачи, но без поддержки параметра SO_REUSEADDR (Solans 2.6). solaris26 % sock -s -u 8888 & запускаем первый [1] 1135 solaris26 % sock -s -u 8888 can't bind local address Address already in use solaris26 % sock -s -u -A 8888 & снова пробуем запустить второй с -А: работает solaris26 % netstat -na | grep 8888 дублированное связывание * 8В88 Idle * 8888 Idle В этой системе задавать параметр SO REUSEADDR было необходимо только для второго связывания. Наконец, запускаем сценарий в BSD/OS 3 0, где поддерживается как много- адресная передача, так и параметр SO_REUSEPORT Сначала пробуем использо- вать SO_REUSEADDR для обоих серверов, но это не работает
Глава 7 997 bsdi % sock -u -s -A 7777 & [1] 17610 bsdi % sock -u -s -A 7777 can’t bind local address Address already in use Тогда пробуем использовать параметр SO_REUSEPORT только для второго серве- ра. Это также не работает, так как полностью дублированное связывание тре- бует включения данного параметра для всех сокетов, совместно использую- щих соединение. bsdi % sock -u -s 8888 & [1] 17612 bsdi % sock -u -s -T 8888 can t bind local address Address already in use Наконец, задаем параметр SO_REUSEPORT для обоих серверов, и этот вариант ра- ботает. bsdi % sock -u [1] 17614 bsdi % sock -u [2] 17615 bsdi % netstat udp 0 udp 0 -s -T 9999 & -s T 9999 & -na | grep 9999 0 * 9999 0 * 9999 7 Этот параметр (-d) не делает ничего, поскольку программа Ping использует ICMP-сокет, а параметр сокета SO_DEBUG влияет только на TCP-сокеты. Опи- сание параметра сокета SO_DEBUG всегда было довольно расплывчатым, напо- добие «этот параметр допускает отладку на соответствующем уровне прото- кола», и единственный уровень протокола, где реализуется данный параметр, — это TCP. 8 Временная диаграмма приведена на рис Д.4. Рис. Д.4. Взаимодействие алгоритма Нагла с задержанными сегментами АСК 9 Установка параметра сокета TCP_NODELAY приводит к немедленной отправке данных из второй функции wri te, даже если имеется еще один небольшой пакет,
998 Приложение Д. Решения некоторых упражнений ожидающий отправки. Это показано на рис. Д.5. Полное время в данном при- мере превышает 150 миллисекунд. Рис. Д.5. Предотвращение алгоритма Нагла путем установки параметра TCPNODELAY 10. Как показано на рис. Д.6, преимущество данного решения состоит в уменьше- нии числа пакетов. Рис. Д.6. Использование функции writev вместо параметра сокета TCP_NODELAY 11. В разделе 4.2.3.2 говорится, что «задержка ДОЛЖНА быть меньше 0,5 секун- ды, а в потоке полноразмерных сегментов СЛЕДУЕТ использовать сегмент АСК по крайней мере для каждого второго сегмента». Беркли-реализации за- держивают сегмент АСК более чем на 200 миллисекунд [105, с. 821]. 12. Родительский процесс сервера в листинге 5.1 большую часть времени блоки- рован в вызове функции accept, а дочерний процесс в листинге 5.2 большую часть времени блокирован в вызове функции read, который содержится в функ- ции readl 1 пе. Проверка работоспособности с помощью параметра SO_KEEPALI VE не влияет на прослушиваемый сокет, поэтому в случае, если клиентский узел выйдет из строя, родительский процесс не пострадает. Функция read дочерне- го процесса возвратит ошибку ETIMEDOUT примерно через 2 часа после послед- него обмена данными через соединение. 13. Клиент, приведенный в листинге 5.4, большую часть времени блокирован вы- зовом функции fgets, который, в свою очередь, блокирован операцией чтения из стандартной библиотеки ввода-вывода на стандартном устройстве ввода. Когда примерно через 2 часа после последнего обмена данными через соеди-
Глава 8 999 нение истечет время таймера проверки работоспособности и проверочные со- общения не выявят работоспособности сервера, ошибка сокета, ожидающая обработки, примет значение ETIMEDOUT. Но клиент блокирован вызовом функ- ции fgets, поэтому он не увидит этой ошибки, пока не осуществит чтение или запись на сокете. Это одна из причин, по которой в главе 6 листинг 5.4 был изменен таким образом, чтобы использовать функцию sei ect. 14. Этот клиент большую часть времени блокирован вызовом функции sei ect, которая сообщит, что сокет гогов для чтения, как только ожидающая обработ- ки ошибка будет установлена в ETIMEDOUT (как показано в предыдущем реше- нии). 15. Только двумя сегментами, а не четырьмя. Вероятность того, что таймеры двух систем будут строго синхронизированы, очень мала, следовательно, на одном конце соединения таймер проверки работоспособности сработает немного раньше, чем на другом. Первый из сработавших таймеров посылает провероч- ное сообщение, заставляя другой конец послать в ответ сегмент АСК. Но по- лучение проверочного сообщения приводит к тому, что таймеру проверки работоспособности с более медленными часами будет присвоено новое значе- ние — он сдвинется на 2 часа вперед. 16. Изначально в API сокетов не было функции listen Вместо этого четвертый аргумент функции socket содержал параметр сокета, а параметр SD_ACCEPTCON использовался для задания прослушиваемого сокета. Когда добавилась функ- ция 11 sten, флаг остался, по теперь его может устанавливать только ядро [105, с. 456]. Глава 8 1. Да Функция read возвращает 4096 байт данных, а функция recvfrom возвра- щает 2048 байт (первую из двух дейтаграмм) Функция recvfrom на сокете дей- таграмм никогда не возвращает больше одной дейтаграммы, независимо от того, сколько приложение запрашивает. 2. Если протокол использует структуры адреса сокета переменной длины, clilen может быть слишком длинным В главе 14 будет показано, что это не вызыва- ет проблем со структурами адреса доменного сокета Unix, но корректным ре- шением будет использовать для функции sendto фактическую длину, возвра- щаемую функцией recvfrom. 4. Запуск программы pi ng с такими параметрами позволяет просмотреть ICMP- сообщения, получаемые узлом, на котором она запущена. Мы используем уменьшенное количество отправляемых пакетов вместо обычного значения 1 пакет в секунду, только чтобы уменьшить объем выводимой на экран ин- формации. Если запустить наш UDP-клиент на узле solans, указав IP-адрес сервера 206.62.226.42, а затем запустить программу pi ng, получим следующий вывод: solans % ping -v -I 60 127.0.0.1 PING 127 0 0 1 56 data bytes 64 bytes from local host (127 0 0 1) icmp_seq=0 time=2 ms ICMP Port Unreachable from gateway alpha kohala com (206 62 226 42)
10ОО Приложение Д. Решения некоторых упражнений for udp from solans kohala com (206 62 226 33) to alpha kohala com (206 62 226 42) port 9877 5. Прослушиваемый сокет может иметь приемный буфер определенного разме- ра, но прослушиваемым TCP-сокетом данные никогда не принимаются. Боль- шинство реализаций не выделяют заранее память под буферы отправки и при- ема. Размеры буферов сокета, определяемые параметрами SO_SNDBUF и SD_RCVBUF, являются предельными значениями для соответствующего сокета. 6. Запустим программу sock с параметром -и (использовать UDP) и параметром -1 (определяет локальный адрес и порт) на многоинтерфейсном узле bsdi. bsdi % sock -u -1 206.62.226.66 4444 206.62.226.42 8888 hello recv error Connection refused Локальный IP-адрес является адресом нижней сети Ethernet на рис. 1.7, но чтобы достичь получателя, дейтаграмма должна выйти через верхнюю сеть Ethernet. Возвращенная ошибка Connection refused (В соединении отказано) возникает вследствие того, что программа sock вызывает функцию connect, а узел сервера возвращает ICMP-ошибку недоступности порта. Наблюдая за сетью с помощью программы tcpdump, мы увидим, что IP-адрес отправителя, связанный с клиентом, не является адресом исходящего интерфейса. 14 39 46 211130 206 62 226 66 4444 > 206 62 226 42 8888 udp 6 14 39 46 211656 206 62 226 42 > 206 62 226 66 icmp 206 62 226 42 udp port 8888 unreachable 7. Использование функции pri ntf на стороне клиента приведет к возникнове- нию задержки между отправками дейтаграмм, что позволит серверу получать большее количество дейтаграмм. Использование функции pri ntf на стороне сервера приведет к тому, что сервер будет терять большее количество дейта- грамм. 8. Наибольший размер 1Ру4-дейтаграммы составляет 65 535 байт и ограничива- ется 16-разрядным полем полной длины, показанным на рис. А.1.1Р-заголо- вок’требует 20 байт, UDP-заголовок — 8 байт, и для пользовательских дан- ных остается не более 65 507 байт. В IPv6 (без поддержки джумбограмм) размер IP-заголовка составляет 40 байт, и под пользовательские данные отво- дится 65 487 байт. В листинге Д.З приведена новая версия dg cl i. Если забыть установить размер буфера отправки, Беркли-ядра возвратят из функции sendto ошибку EMSGSIZE, поскольку размер буфера отправки сокета обычно меньше, чем максимально возможный размер UDP-дейтаграммы (чтобы убедиться в этом, выполните упражнение 7.1). Листинг Д.З. Запись дейтаграммы UDP/IPv4 максимального размера //udpcl1serv/dgclibig с 1 #include ' unp h ’ 2 #undef MAXLINE 3 #define MAXLINE 65507 4 void 5 dg_cli(FILE *fp int sockfd const SA *pservaddr socklen t servlen)
Глава 9 1001 6 { 7 int size 8 char sendline[MAXLINE], recvline[MAXLINE + 1]. 9 ssize_t n 10 size = 70000 11 Setsockopt(sockfd S0L_S0CKET S0_SNDBUF &size sizeof(size)); 12 Setsockopt(sockfd SOLSOCKET SD_RCVBUF &size sizeof(size)): 13 Sendto(sockfd sendline MAXLINE 0 pservaddr servlen) 14 n = Recvfrom(sockfd recvline MAXLINE 0 NULL NULL). 15 printfCreceived £d bytesXen" n) 16 } Но если установить размеры буферов сокета клиента, как показано в листин- ге Д 3, и запустить программу, сервер ничего не возвратит. С помощью про- граммы tcpdump можно убедиться, что клиентская дейтаграмма отправляется серверу, но если в сервер поместить функцию printf, вызов функции recvfrom не возвратит дейтаграмму. Проблема заключается в том, что приемный буфер UDP-сокета сервера меньше, чем посланная нами дейтаграмма, поэтому дей- таграмма отбрасывается и не доставляется на сокет. В системах BSD/OS это можно проверить, запустив программу netstat -s и проверив счетчик, указы- вающий количество дейтаграмм, отброшенных из-за переполнения буферов сокета (dropped due to full socket buffers), до и после получения нашей длин- ной дейтаграммы. Решением является модификация сервера путем задания размеров буферов приема и отправки сокета. В большинстве сетей дейтаграмма длиной 65 535 байт фрагментируется Как отмечалось в разделе 2.9, IP-уровнем должен поддерживаться размер буфера для сборки фрагментов, равный всего лишь 576 байт. Поэтому некоторые узлы не получат дейтаграмму максимального размера, посылаемую в данном уп- ражнении. Кроме того, во многих Беркли-реалпзацнях, включая 4.4BSD-Lite2, имеется ошибка, связанная со знаковыми типами данных, которая не позво- ляет UDP принимать дейтаграммы, большие чем 32 767 байт (см. строку 95, с. 770 [105]). Глава 9 1. В листинге Д.4 приведена программа, вызывающая функцию gethostbyaddr. Листинг Д.4. Изменение листинга 9.1 для вызова функции gethostbyaddr //names/hostent2 с 1 #include 'unp h 2 int 3 main(int argc char **argv) 4 { 5 char *ptr **pptr 6 char str[INET6_ADDRSTRLEN] 7 struct hostent *hptr 8 while (--argc > 0) { продолжение &
1002 Приложение Д. Решения некоторых упражнений Листинг Д.4 (продолжение) 9 ptr = *++argv 10 if ( (hptr = gethostbyname(ptr)) == NULL) { 11 err_msg('gethostbyname error for host £s ' 12 ptr hstrerror(h_errno)) 13 continue 14 } 15 printf("official hostname £s\en' hptr->h_name) 16 for (pptr = hptr->h_aliases *pptr NULL pptr++) 17 printfC alias £s\en' *pptr) 18 switch (hptr >h_addrtype) { 19 case AFJNET 20 #ifdef AFJNET6 21 case AFJNET6 22 #endif 23 pptr = hptr->h_addrjist 24 for ( *pptr '= NULL pptr++) { 25 printf( \etaddress £s\en' 26 Inet_ntop(hptr->h_addrtype *pptr str. sizeof(str))j); 27 if ( (hptr = gethostbyaddr(*pptr hptr->hjength 28 ptr->h_addrtype)) == NULL) 29 printfC\et(gethostbyaddr failed)\en') 30 else if (hptr->h_name '= NULL) 31 printf('\etname = £s\en" hptr->h_name) 32 else 33 pnntf("\et(no hostname returned by gethostbyaddr)\en") 34 } 35 break 36 default 37 err_ret('unknown address type"). 38 break 39 } 40 } 41 exit(O) 42 } Эта программа корректно работает на узле с единственным IP-адресом. Если запустить программу из листинга 9.1 на узле с четырьмя IP-адресами, то по- лучим- solans % hostent gemini tuc.noao.edu official hostname gemim tuc noao edu address 140 252 8 54 address 140 252 4 54 address 140 252 3 54 address 140 252 1 11 Но если запустить программу из листинга Д.4 на том же узле, в выводе будет только первый 1Р-адрес: solans X hostent2 gemim.tuc noao edu official hostname gemim tuc noao edu address 140 252 8 54 name = gemim tuc noao edu Проблема заключается в том, что две функции, gethostbyname и gethostbyaddr, совместно используют одну и ту же структуру hostent, как было показано в раз-
Глава 9 1003 деле 11.14 Когда наша новая программа вызывает функцию gethostbyaddr, она повторно использует данную структуру вместе с областью памяти, на кото- рую структура указывает (массив указателей h_addr_l ist), стирая три остав- шиеся IP-адреса, возвращаемые функцией gethostbyname. 2. Если ваша система не поддерживает повторно входимую версию функции gethostbyaddr (см. раздел 11.15), то прежде чем вызывать функцию gethostbyaddr, вам следует создать копию массива указателей, возвращаемых функцией gethostbyname, и данных, на которые указывает этот массив. 3. Функция my_addr приведена в листинге Д.5, а функция man п — в листинге Д.6. Листинг Д.5. Версия листинга 9.3, вызывающая функцию gethostname //names/myaddrl с 1 include 'unp h' 2 include <sys/param h> 3 char ** 4 my_addrs(int *addrtype) 5 { 6 struct hostent *hptr. 7 char myname[MAXHOSTNAMELEN] 8 if (gethostname(myname sizeof(myname)) < 0) 9 return (NULL) 10 if ( (hptr = gethostbyname(myname)) = NULL) 11 return (NULL) 12 *addrtype = hptr->h_addrtype 13 return (hptr->h_addr_list) 14 } 4 Если функция gethostbyname возвращает структуру hostent, определяющую один или более адресов IPv6, значение h_length будет равно 16. Поэтому структура sockaddr in будет переполнена, и часть данных запишется поверх того, что было расположено в памяти далее за этой структурой Данный параметр рас- познавателя следует устанавливать только в том случае, если программа гото- ва работать с адресами IPv6, а наша программа для этого пе предназначена. Этот пример также объясняет, почему длина аргумента memcpy должна соот- ветствовать размеру получателя (в данном примере sizeof(struct in_addr)), а не размеру отправителя (hp -у h_length), даже если оба эти размера одина- ковы. 5. Сервер chargen отправляет клиенту данные до тех пор, пока клиент не закры- вает соединение (то есть пока вы не завершите выполнение клиента). Листинг Д.6. Тестовая программа для листингов 9 3 и Д.5 //names/prmyaddrsl с 1 #include 'unp h ’ 2 char **my_addrs(int *) 3 int 4 main(int argc. char **argv) 5 { продолжение
1004 Приложение Д. Решения некоторых упражнений Листинг Д.6 (продолжение) 6 int addrtype. 7 char **pptr. buf[INET6_ADDRSTRLEN], 8 if ( (pptr = my_addrs(&addrtype)) == NULL) 9 err_quit("my_addrs error"). 10 for ( *pptr '= NULL. pptr++) II pnntf("\etaddress £s\en", 12 Inet_ntop(addrtype. *pptr. buf. sizeof(buf))): 13 exit(O), 14 } 6. Как упоминалось в связи с рис. 9.3, это особенность новейших версий BIND. В листинге Д.7 приведена измененная версия. Порядок тестирования строки с именем узла имеет значение. Сначала мы вызываем функцию inet pton, по- скольку она обеспечивает быстрый тест «внутри памяти» (in-memory) для проверки, является ли строка допустимым IP-адресом в точечно-десятичной записи. Только если тест заканчивается неудачно, мы запускаем функцию gethostbyname, которая обычно требует некоторых сетевых ресурсов и времени. Если строка является допустимым IP-адресом в точечно-десятичной записи, мы создаем свой массив указателей (addrs) на один IP-адрес, оставив без из- менений цикл, использующий pptr. Поскольку адрес уже был переведен в двоичное представление в структуре адреса сокета, мы заменяем вызов функции memepy в листинге 9.4 на вызов функции memmove, так как при вводе IP-адреса в точечно-десятичной записи исходное и конечное поля в данном вызове одинаковые. Случай перекрыва- ния полей при работе с функциями memepy и memmove рассматривается в упраж- нении 30.3. Листинг Д.7. Допускаем как использование IP-адреса в точечно-десятичной записи, так и задание имени узла, номера порта или имени службы //names/daytinietcpcli2 с 1 #include "unp h" 2 int 3 main(int argc. char **argv) 4 { 5 int sockfd. n. 6 char recvlinefMAXLINE + 1]. 7 struct sockaddr_in servaddr. 8 struct in_addr **pptr. *addrs[2]: 9 struct hostent *hp. 10 struct servent *sp. 11 if (argc 3) 12 err_quit("usage daytimetcpcli2 <hostname> <service>"). 13 bzero(&servaddr. sizeof(servaddr)). 14 servaddr sin_family = AFJNET. 15 if (inet_pton(AF_INET, argv[l], &servaddr sin_addr) == 1) ( 16 addrsfO] = &servaddr sin_addr.
Глава 9 1005 17 addrsLU = NULL, 18 pptr = &addrs[0], 19 } else if ( (hp = gethostbynarre(argv[l])) !- NULL) { 20 pptr = (struct in_addr **) hp->h_addr_list. 21 } else 22 err_quit("hostname error for Xs Xs". argv[l], hstrerror(h_errno)); 23 if ( (n = atoi(argv[2])) > 0) 24 servaddr sin_port = htons(n). 25 else if ( (sp = getservbyname(argv[2]. "tep")) l= NULL) 26 servaddr sm_port = sp->s_port. 27 else 28 err_quit("getservbyname error for Xs". argv[2]): 29 for ( *pptr '= NULL. pptr++) { 30 sockfd = Socket(AF_INET. SOCK_STREAM. 0). 31 memmove(&servaddr sin_addr. *pptr, sizeoftstruct in_addr)); 32 pnntfftrying Xs\en". 33 Sock_ntop((SA *) Sservaddr, sizeof(servaddr))). 34 ' if (connecttsockfd, (SA *) &servaddr. sizeof(servaddr)) == 0) 35 . break. /* успех */ 36 г err_ret("connect error"), 37 close(sockfd), 38 } 39 if (*pptr == NULL) 40 err_quit("unable to connect"). 41 while ( (n = Readtsockfd. recvline MAXLINE)) > 0) { 42 recvline[n] = 0. /* завершающий нуль */ 43 Fputs(recvlme. stdout). 44 } 45 exit(0). 46 } 7. Программа приведена в листинге Д.8. Листинг Д.8. Изменение листинга 9.4 для работы с IPv4 и IPv6 //names/daytimetcpcl12 с 1 #include "unp h" 2 int 3 maintint argc. char **argv) 4 { 5 mt sockfd. n. 6 char recvl me[MAXLINE + 1]. 7 struct sockaddr_in servaddr. 8 struct sockaddr_m6 servaddr6: 9 struct sockaddr *sa. 10 socklen_t salen. 11 struct in_addr **pptr, 12 struct hostent *hp. 13 struct servent *sp. 14 if (argc '= 3) 15 err_quit("usage daytimetcpcli3 <hostname> <service>"); 16 if ( (hp = gethostbyname(argv[l])) == NULL)
1006 Приложение Д. Решения некоторых упражнений Листинг Д.8 (продолжение) 17 err_quit("hostname error for £s". argvfl], hstrerror(h_errno)): 18 if ( (sp = getservbyname(argv[2], "tcp")) == NULL) 19 err_quit("getservbyname error for £s”. argv[2]). 20 pptr = (struct in_addr **) hp->h_addr_list 21 for (, *pptr NULL, pptr++) { 22 sockfd = Socket(hp->h_addrtype, SOCK_STREAM. 0). 23 if (hp->h_addrtype == AF_INET) { 24 sa = (SA *) &servaddr. 25 salen = sizeof(servaddr). 26 } else if (hp->h_addrtype == AF_INET6) { 27 sa = (SA *) &servaddr6. 28 salen = sizeof(servaddr6). 29 } else 30 err_quit(”unknown addrtype fcd". hp->h_addrtype): 31 bzero(sa, salen). 32 sa->sa_family = hp->h_addrtype. 33 sock_set_port(sa. salen. sp->s_port). 34 sock_set_addr(sa. salen *pptr). 35 printfCtrying £s\en" Sock_ntop(sa. salen)), 36 if (connect(sockfd. sa salen) == 0) 37 break. /* успех */ 38 err_ret("connect error"). 39 close(sockfd). 40 } 41 if (*pptr = NULL) 42 err_quit("unable to connect"). 43 while ( (n = Read(sockfd recvline, MAXLINE)) > 0) { 44 recvline[n] =0. /* завершающий нуль */ 45 Fputs(recvline stdout). 46 } 47 exit(0) 48 } Используем значение h_addrtype, возвращаемое функцией gethostbyname, для определения типа адреса. Также используем функции sock_set_port и sock_ set_addr (см. раздел 3.8), чтобы установить два соответствующих поля в струк- туре адреса сокета. Эта программа работает, однако имеется два ограничения. Во-первых, мы долж- ны обрабатывать все различия, следя за h addrtype и задавая соответствую- щим образом sa или salen. Более удачным решением было бы иметь библио- течную функцию, которая не только просматривает имя узла и имя службы, но и заполняет всю структуру адреса сокета (например, getaddrinfo, см. раз- дел 11.2). Во-вторых, эта программа компилируется только на узлах с поддерж- кой IPv6. Чтобы ее можно было откомпилировать на узле, поддерживающем только IPv4, следует добавить в код огромное количество директив #i fdef, что, несомненно, усложнит программу. К вопросу независимости от протоколов мы вернемся в главе 11 и рассмот- рим более совершенные способы обеспечения этой независимости.
ГлаваИ 1007 Глава 10 1. Ниже приведен вывод, в котором некоторые фрагменты опущены (например, процедура входа и содержимое каталогов). solans % ftp bsdi Connected to bsdi.kohala com 220 bsdi.kohala com FTP server 230 Guest login ok. access restrictions apply. ftp> debug Debugging on (debug=l) ftp> dir ---> PORT 206.62 226.33.129.145 200 PORT command successful ---> LIST 150 Opening ASCII mode data connection for /bTh/ls. solans % ftp sunos Connected to sunos5 kohala com 220 sunos5 kohala com FTP server 230 Guest login ok. access restrictions apply. ftp> debug Debugging on (debug=l) ftp> dir —> LPRT 6.16,95.27.223.0.206.62.226 0,0.32,8.0.32 120.227,227.2,129.148 200 LPRT command successful ---> LIST 150 ASCII data connection for /Ып/ls (5flb dfOO ce3e e200 20 800:2078 еЗеЗ,31721 (fl bytes) Глава 11 1. Разместите в памяти большой буфер (превышающий по размеру любую струк- туру адреса сокета) и вызовите функцию getsockname. Третий аргумент явля- ется аргументом типа «значение-результат», возвращающим фактический раз- мер адресов протоколов. К сожалению, это допускают только структуры адреса сокета с фиксированной длиной (IPv4 и IPv6). Нет гарантии, что это будет работать с протоколами, которые могут вернуть структуру адреса сокета пере- менной длины (доменные сокеты Unix, см. главу 14). 2. Сначала размещаем в памяти массивы, содержащие имя узла и имя службы: char host[NI_MAXHOST]. serv[NI_MAXSERV]. После того как функция accept возвращает управление, вызываем вместо функ- ции sock _ntop функцию getnamei nfo: if (getnameinfolcliaddr. len. host. NI_MAXHOST serv. NI_MAXSERV. NI-NUMERIGHOST | NI_NUMERICSERV) == 0) pnntfCconnection from %s 2s\en" nost. serv). Поскольку это сервер, определяем флаги NI_NUMERICHDST и NI_NUMERICSERV, что- бы избежать поиска в DNS и /etc/services.
1008 Приложение Д. Решения некоторых упражнений 3. Первая проблема состоит в том, что второй сервер пе может связаться (bi nd) с тем же портом, что и первый сервер, поскольку пе установлен параметр со- кета SO REUSEADDR. Простейший способ справиться с такой ситуацией состоит в том, чтобы создать копию функции udp_server, переименовать ее в udp_server_ reuseaddr, сделать так, чтобы она установила параметр сокета, и вызывать ее в сервере. 4. Если бы переменная aipnext была глобальной, функция getaddrinfo не была бы безопасной в многопоточной среде. 5. Да, содержит. Обратившись по ссылке ftp://ftp.isi.edu/in-notes/iana/assignments/ port-numbers, можно увидеть, что имена служб cl/1 и 914с/д содержат косую черту. 6. Когда клиент выводит Trying 206 62 226 35 , функция gethostname возвращает IP-адрес. Пауза перед этим выводом означает, что распознаватель ищет имя узла. Вывод Connected to bsdi kohala com означает, что функция connect воз- вратила управление. Пауза между этими двумя выводами говорит о том, что функция connect пытается установить соединение. Глава 12 1. Закрытие дескрипторов в daemon_imt функцией close приводит к закрытию прослушиваемого TCP-сокета, созданного с помощью функции tcp_listen. Поскольку программа, написанная как демон, может быть запущена из неко- торого системного сценария, запускаемого при загрузке, не следует ожидать, что будет выведено какое-либо сообщение об ошибке. Все сообщения об ошиб- ках, даже ошибка загрузки, такая как неправильный аргумент командной стро- ки, должны сохраняться в файлах журнала с помощью функции syslog. 2. TCP-версии серверов echo, discard и chargen запускаются как дочерние про- цессы, после того как демон inetd вызовет функцию fork, поскольку эти три сервера работают, пока клиент не прервет соединение. Два других ТСР-серве- ра, time и dayt ime, не требуют использования функции fork, поскольку эти служ- бы легко реализовать (получить текущую дату, преобразовать ее, записать и закрыть соединение). Эти два сервера обрабатываются непосредственно де- моном inetd. Все пять UDP-служб обрабатываются без использования функ- ции fork, поскольку каждая из них генерирует единственную дейтаграмму в ответ на клиентскую дейтаграмму, которая запускает эту службу. Эти пять служб обрабатываются напрямую демоном i netd. 3. Это известная атака типа «отказ в обслуживании» [17]. Первая дейтаграмма с порта 7 заставляет сервер chargen отправить дейтаграмму обратно на порт 7. На эту дейтаграмму приходит эхо-ответ, и серверу chargen посылается другая дейтаграмма. Происходит зацикливание. Одним из решений, реализованным в системе BSD/OS, является игнорирование дейтаграмм, направленных лю- бому внутреннему серверу, если номер порта отправителя пришедшей дей- таграммы принадлежит одному из внутренних серверов. Другим решением может быть запрещение этих внутренних служб — либо с помощью демона 1 netd на каждом узле, либо на маршрутизаторе, связывающем внутреннюю сеть
Глава14 1009 4. IP-адрес и номер порта клиента могут быть получены из структуры адреса сокета, заполняемой функцией accept. Причина, по которой демон inetd не делает этого для UDP-сокета, состоит в том, что чтение дейтаграмм (recvfrom) осуществляется с помощью функции ехес сервером, а не самим демоном inetd. Демон inetd может считать дейтаграмму с флагом MSG_PEEK (см. раздел 13.7), только чтобы получить IP-адрес и номер порта клиента, но оставляет саму дейтаграмму для чтения серверу. Глава 13 1. Если не установлен обработчик, первый вызов функции signal будет йозвра- щать значение SIG_DFL, а вызов функции signal для восстановления обработ- чика просто вернет его в исходное состояние. 3. Приведем цикл for: for ( . ) ( if ( (n = Recvtsockfd, recvline MAXLINE, MSG_PEEK)) — 0) break /* сервер закрыл соединение */ Ioctl(sockfd FIONREAD Snpend) printfCW bytes from PEEK, £d bytes pending\en", n. npend): n = Readtsockfd, recvline, MAXLINE) recvline[n] = 0 /* завершающий нуль */ Fputs(recvline stdout), } 4. Данные продолжают выводиться, поскольку выход из функции main — это то же самое, что и возврат из этой функции. Функция main вызывается програм- мой запуска на языке С следующим образом: exit(main(argc argv)) Следовательно, вызывается функция exit, а затем и программа очистки стан- дартного ввода-вывода. Глава 14 1. Функция uni 1 nk удаляет имя файла из файловой системы, и когда клиент поз- же вызывает функцию connect, она не выполнится. Это не влияет на прослу- шиваемый сокет сервера, но клиенты не смогут выполнить функции connect после вызова функции unlink. 2. Клиент не сможет соединиться с сервером с помощью функции connect, даже если полное имя существует, поскольку для успешного соединения с помощью функции connect доменный сокет Unix должен быть открыт и связан с этим полным именем (см. раздел 14.4). 3. При выводе адреса протокола клиента путем вызова функции sockjitop мы по- лучим сообщение datagram from (no pathname bound) (дейтаграмма от (имя не за- дано)), поскольку по умолчанию с сокетом клиента не связывается никакое имя.
1010 Приложение Д. Решения некоторых упражнений Одним из решений является проверить доменный сокет Unix в функциях udp_cl 1 ent и udp_connect и связать с сокетом при помощи функции bi nd времен- ное полное имя. Это приведет к зависимости от протокола в библиотечной функции, но не в нашем приложении. 4. Даже если мы заставим сервер вернуть в функции write 1 байт па его 26-бай- товый ответ, использование функции sleep на стороне клиента гарантирует, что все 26 сегментов будут получены до вызова функции read, в результате чего функция read вернет полный ответ. Это еще одно подтверждение тому, что TCP является потоком байтов с отсутствием границ записи. Чтобы использовать доменные протоколы Unix, запускаем клиент и сервер с двумя аргументами командной строки /local (или /игл х) и /tmp/daytime (или любое другое временное имя, которое вы хотите использовать). Ничего не из- менится: 26 байт будут возвращаться функцией read каждый раз, когда будет запускаться клиент. Поскольку для каждой функции send сервер определяет флаг MSG_EOR, каждый байт рассматривается как логическая запись, и функция read при каждом вы- зове возвращает 1 байт. Причина в том, что Беркли-реализации поддержива- ют флаг MSG_EOR по умолчанию. Однако этот факт пе документирован и не может использоваться в серийном (production) коде. В данном примере мы исполь- зуем эту особенность, чтобы показать разницу между потоком байтов и ори- ентированным на записи протоколом. С точки зрения реализации, каждая операция вывода идет в mbuf (memory buffer — буфер памяти), и флаг MSG_EOR сохраняется ядром вместе с mbuf, когда mbuf переходит из отправляющего со- кета в приемный буфер принимающего сокета. Когда вызывается функция read, флаг MSG EOR все еще присоединен к каждому mbuf, так что основная подпро- грамма ядра read (поддерживающая флаг MSG_EOR, поскольку некоторые про- токолы используют этот флаг) сама возвращает каждый байт. Если бы вместо read мы использовали recvmsg, флаг MSG_EOR возвращался бы в поле msg_f 1 ags каждый раз, когда recvmsg возвращала бы 1 байт. Такой подход в TCP не сра- батывает, поскольку отправляющий TCP пе анализирует флаг MSG EOR в отсы- лаемом mbuf и в любом случае у нас нет возможности передать этот флаг при- нимающему TCP в TCP-заголовке. (Выражаем благодарность Мату Томасу (Matt Thomas) за то, что он указал нам это недокументированное «средство».) 5. В листинге Д.9 приведена реализация данной программы. Версия XTI приве- дена в листинге Д.14. Листинг Д.9. Определение фактического количества собранных в очередь соединений для различных значений аргумента backlog //debug//backl og с 1 #include "unp h' 2 #define PORT 9999 3 #define ADDR "127 001" 4 #define MAXBACKLOG 100 5 /* глобальные переменные */ 6 struct sockaddr_in serv. 7 pid_t pid. /* диччрпиЯ- процесЕ */
Глава 14 1011 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 int pipefd[2], #define pfd pipefd[l] /* сокет родительского процесса */ #define cfd pipefd[0] /* сокет дочернего процесса */ /* прототипы функций */ void do_parent(void). void do_child(void). int main(int argc. char **argv) { if (argc 1) err_quit("usage backlog"). Socketpair(AF_UNIX. SOCK STREAM. 0. pipefd); bzero(&serv. sizeof(serv)). serv sin_family = AF_INET. serv sin_port = htons(PORT). Inet_pton(AF_INET. ADDR. &serv sin_addr). if ( (pid = ForkO) == 0) do_child() else do_parent() exit(0). } void parent_alrm(int signo) { return /* прерывание блокированной функции connectO */ } void do_parent(void) { int backlog, j. k. junk. fd[MAXBACKLOG + 1], Close(cfd). Signal(SIGALRM parent_alrm). for (backlog = 0. backlog <= 14. backlog++) { pri ntf("backlog = $d " backlog) Write(pfd, ^backlog. sizeof(int)). /* сообщение значения дочернему процессу*/ Read(pfd &junk, sizeof(int)). /* ожидание дочернего процесса*/ for (j - 1 j <- MAXBACKLOG j++) { fd[j] - Socket(AF_INET SOCK_STREAM. 0) alarm(2). if (connect(fd[j] (SA *) &serv sizeof(serv)) < 0) { if (errno EINTR) err_sys("connect error, j - M'. j). printf("timeout. %d connections completedken”. j - 1); for (k - 1 k <- j. k++) Close(fd[kJ) break. /* следующее значение backlog */
1012 Приложение Д. Решения некоторых упражнений Листинг Д.9 (продолжение) 55 } 56 alarm(O). 57 } 58 if (J > MAXBACKLDG) 59 pnntfC'Xd connect ions? \ en". MAXBACKLOG). 60 } 61 backlog = -1. /* сообщаем дочернему процессу, что все сделано*/ 62 Write(pfd. &backlog. sizeof(int)). 63 } 64 void 65 do_child(void) 66 { 67 int listenfd. backlog, junk; 68 const int on - 1. 69 Close(pfd). 70 Read(cfd, &backlog. sizeof(int)). /* ожидание родительского процесса */ 71 while (backlog >= 0) { 72 listenfd = Socket(AFJNET. SOCKJTREAM. 0). 73 Setsockopt(listenfd. S0L_S0CKET SO REUSEADDR. &on sizeof(on)). 74 Bind(l istenfd. (SA *) &serv. sizeof(serv)). 75 Listen(listenfd. backlog) /* начало прослушивания */ 76 Write(cfd &junk sizeof(int)). /* сообщение родительскому прцессу*/ 77 Read(cfd. 8backlog. sizeof(int)), /* ожидание родительского процесса*/ 78 Close(listenfd). /* также закрывает все соединения в очереди */ 79 } 80 } Глава 15 1. Дескриптор используется совместно родительским и дочерним процессами, поэтому его счетчик ссылок равен 2. Если родительский процесс вызывает функцию close, счетчик ссылок уменьшается с 2 до 1, и пока он больше нуля, сегмент FIN не посылается. Еще одна цель вызова функции shutdown — послать сегмент FIN, даже если дескриптор больше нуля. 2. Родительский процесс продолжит запись в сокет, получивший сегмент FIN, а первый сегмент, посланный серверу, вызовет получение сегмента RST в от- вет. После этого функция write пошлет родительскому процессу сигнал SIGPIPE, как показано в разделе 5.12. 3. Когда дочерний процесс вызывает функцию getppid для отправки сигнала SIGTERM, возвращаемый идентификатор процесса будет равен 1. Это указывает на процесс imt, наследующий все продолжающие работать дочерние процес- сы, родительские процессы которых завершились. Дочерний процесс будет пытаться послать сигнал процессу imt, не имея необходимых прав доступа. Но если не исключается, что данный клиент будет запущен с правами приви- легированного пользователя, позволяющими посылать сигналы процессу imt, то возвращенное функцией getppid значение должно быть проверено перед ПТППЯПТОШ гнгпя ття
Глава 19 1013 4. Если удалить эти две строки, вызывается функция select. Но функция select немедленно завершится, поскольку соединение установлено и сокет открыт для записи. Эта проверка и оператор goto предотвращают ненужный вызов функции select. 5. Это может случиться, если сервер отправляет данные сразу, как только завер- шается его функция accept, и если узел клиента занят, когда приходит второй пакет трехэтапного рукопожатия для завершения соединения со стороны кли- ента (см. рис. 2.5). SMTP-серверы, например, немедленно отсылают клиенту сообщение по новому соединению, прежде чем произвести из него счи 1 ывание. Глава 16 1. Нет, это не имеет значения, поскольку первые три элемента объединения в листинге 16.1 являются структурами адреса сокета. Глава 17 1. Элемент sdl_nlen будет равен 5, а элемент sdl alen будет равен 8. Для этого требуется 21 байт, поэтому размер округляется до 24 байт [105, с. 89] в пред- положении, что используется 32-разрядная архитектура. 2. На этот сокет никогда не посылается ответ от ядра. Данный параметр сокета (SOJJSELOOPBACK) определяет, посылает ли ядро ответ отправляющему процес- су, как показано на с. 649-650 [105]. По умолчанию этот параметр включен, поскольку большинство процессов ожидают ответа. Но отключение данного параметра препятствует отправке ответов отправителю. Глава 18 1. Если вы получаете большое количество ответов, они могут следовать каждый раз в разном порядке. Правда, отправляющий узел обычно выводится первым, поскольку дейтаграммы, направленные к нему или от пего, не появляются в реальной сети. 2. Величина 1472 — это размер MTU Ethernet (1500) минус 20 байт IP-заголов- ка и минус 8 байт UDP-заголовка. 3. Когда в BSD/OS обрабо гчик сигналов записывает байт в канал, а затем завер- шается, функция sei ect возвращает ошибку EINTR. Она вызывается заново 11 при завершении сообщает о возможности чтения из канала. Глава 19 1. Если запустить программу, то вывод будет следующий: solans Ж udpc1i05 224.0.0.1 In from 206 62 226 34 Thu Jun 19 17 28 32 1997 from 206 62 226 43 Thu Jun 19 17 28 32 1997 from 206 62 226 42 Thu Jun 19 17 28 32 1997
1014 Приложение Д Решения некоторых упражнений from 206 62 226 40 Thu Jun 19 17 28 32 1997 from 206 62 226 35 Thu Jun 19 17 28 32 1997 Пять ответивших узлов работают под управлением AIX, BSD/OS, Digital Unix и Linux Единственные не ответившие узлы с поддержкой многоадресной пе- редачи — это узел в Solans и маршрутизатор Cisco Происходит следующее В адресе получателя UDP-дейтаграммы стоит 224 00 1 — это группа всех узлов, в которой должны состоять узлы, поддерживающие многоадресную передачу UDP-дейтаграмма посылается как многоадресный кадр Ethernet, и все узлы с поддержкой многоадресной передачи должны по- лучить ее поскольку все они входят в указанную i руппу Все отвечающие узлы передают полученную UDP-дейтаграмму серверу времени и даты (обычно он является частью демона inetd), даже если этот сокет не находится в группе Однако реализация Solans требует, чтобы сокет получателя вступил в группу для получения дейтаграммы Этот пример показывает, что UDP-программа, не предназначенная для отве тов на многоадресные дейтаграммы все же может эти дейта1 раммы получать Подобное мы видели в примере с сервером времени и даты из главы 18 UDP- программа, которая не была разработана для ответа на широковещательные дейтаграммы, могла их получать 2 В листинге ДЮ показаны простые изменения функции main для связывания (bind) с адресом многоадресной передачи и портом О Листинг Д. 10. Функция main UDP-клиента, осуществляющая связывание с адресом многоадресной передачи //mcast/udpcli06 с 1 #include unp h 2 int 3 maindnt argc char **argv) 4 { 5 int sockfd 6 socklen_t salen 7 struct sockaddr *cli *serv 8 if (argc ’= 2) 9 err_quit( usage udpcli06 <IPaddress> 1 10 sockfd = Udp_client(argv[l] daytime (void **) &serv &salen) 11 cli = Malloc(salen) 12 memcpy(cli serv salen) /* копируем структуру адреса сокета-*/ 13 sock_set_port(cli salen 0) /* и устанавливаем порт в 0 */ 14 Bind(sockfd cli salen) 15 dg_cli(stdin sockfd serv salen) 16 exit(O) П } К сожалению, все три системы, на которых проводилась проверка — BSD/OS, Digital Unix и Solans 2 5, — позволяют использовать функцию bind, а затем посылают UDP-дейтаграммы с IP-адресом многоадресной передачи отправи- теля Все пять отвечающих систем (те же, что и в предыдущем упражнении)
Глава 19 1015 в ответе переставляют IP-адреса отправителя и получателя, так что все пять ответов являются многоадресными! На узлах с поддержкой многоадресной передачи с получаемыми ответами ничего не происходит, поскольку порт по- лучателя в ответах является динамически назначаемым и при связывании с адресом многоадресной передачи выбирается ядром клиента, а с этим пор- том не связан никакой сокет ICMP-сообщения о недоступности порта в ответ на многоадресные UDP-дейтаграммы не генерируются 3 Если мы запустим программу pi ng для группы узлов 224 0 0 1 на нашем узле sol an s, получим следующий вывод solans t ping 224 0 0 1 PING 224 0 0 1 56 data bytes 64 bytes from solans kohala com (206 62 226 33) icmp_seq=0 time=4 ms 64 bytes from linux kohala com (206 62 226 40) icmp_seq=0 time-9 ms 64 bytes from aix kohala com (206 62 226 43) icmp_seq-0 time-11 ms 64 bytes from bsdi kohala com (206 62 226 35) icmp_seq-0 time=13 ms 64 bytes from alpha kohala com (206 62 226 42) icmp_seq-0 time-15 ms 64 bytes from sunos5 kohala com (206 62 226 36) icmp_seq=0 time-17 ms 64 bytes from bsdi2 kohala com (206 62 226 34) icmp_seq=0 time-54 ms 64 bytes from gw kohala com (206 62 226 62) icmp_seq=0 time-75 ms 224 0 0 1 PING Statistics 1 packets transmitted 8 packets received 8 00 times amplification round trip (ms) min/avg/max - 4/24/75 Отвечает каждый узел из верхней сети Ethernet, показанной на рис 1 7 (ко- нечно, включая отправителя), за исключением узла umxware, который не под- держивает многоадресную передачу 4 Поскольку ядро не поддерживает многоадресную передачу, оно воспринима- ет получателя как обычный IP-адрес Ядро ищет 224 0 0 1 в обычной таблице маршрутизации и выбирает маршрут по умолчанию, указывающий на марш- рутизатор gw (см рис 1 7) Этому маршрутизатору посылается направленный эхо-запрос ICMP с IP-адресом 224 0 0 1 и аппаратным адресом интерфейса Ethernet выбранного маршрутизатора (аппаратный адрес не является адре- сом многоадресной передачи) Маршрутизатор принимает полученный пакет, поскольку он адресован его интерфейсу, а IP-адрес отправителя является ад- ресом группы многоадресной передачи, к которой он принадлежит 5 Если запустить программу ping для группы всех маршрутизаторов 224 0 0 2, получим следующий вывод solans % ping 224 0 0 2 PING 224 0 0 2 56 data bytes 64 bytes from bsdi kohala com (206 62 226 35) icmp_seq=0 time-3 ms 64 bytes from gw kohala com (206 62 226 62) icmp_seq-0 time-24 ms 224 0 0 2 PING Statistics 1 packets transmitted 2 packets received 2 00 times amplification round trip (ms) min/avg/max - 3/13/24 Мы предполагали получить ответ от узла bsdi, поскольку он является марш- рутизатором многоадресной передачи в подсети с туннелем в Mbone (см раз- дел Б 2) и на нем запущен демон mrouted Маршрутизатор gw также отвечает, но он не работает как маршрутизатор многоадресной передачи
1016 Приложение Д Решения некоторых упражнений 7 В листинге 19 10 мы видим, что отметки времени NTP представляют собой количество секунд, прошедших с 1 января 1990 года В году содержится 31 356 000 секунд (356x24x60x60), так что два значения составляют около 96,7 лет (не считая високосные годы) и имеют смысл Кроме того, номер вер- сии данного объявления больше, чем идентификатор сеанса (который, как мы полагаем, был задан, когда сеанс был впервые анонсирован), что также имеет смысл 8. Величина 1 073 741 824 преобразуется в значение с плавающей точкой и де- лится на 4 294 967 296, что дает значение 0,250 В результате умножения на 1 000 000 получаем значение 250 000 в микросекундах, а это одна четверть се- кунды Наибольшая дробная часть получается при делении 4 294 967 295 на 429 4967 296, и составляет 0,99 999 999 976 716 935 634 Умножая это число на 1 000 000 и отбрасывая дробную часть, получаем 999 999 — наибольшее зна- чение количества микросекунд 9. При обсуждении этих двух параметров сокета в разделе 7 5 отмечалось, что параметр сокета SO_REUSEADDR эквивалентен параметру SO_REUSEPORT, если свя- зываемый IP-адрес является адресом многоадресной передачи Это упрощает переносимость нашего кода, поскольку в противном случае нам следовало бы написать #1 fdef SO_REUSEPORT Setsockopttfd SOL_SOCKET SOREUSEPORT &on sizeof(on)) #else Setsockopttfd SOL_SOCKET SO_REUSEADDR &on sizeof(on)) #endif 10 Некоторые системы не будут доставлять сокету многоадресные дейтаграммы, если сокет не включен в группу Одного только связывания с портом в таких системах недостаточно Это относится к Solans 2 5 Беркли-реализации, на- оборот, доставляют дейтаграммы всем подходящим сокетам, независимо от того, включен ли сокет в группу Напомним, что функция bi nd ubcast вызыва- ется из листинга 19 14 для связывания (bind) сокета с универсальным адре- сом, так что данный сокет к группе не присоединен И Будут получены двенадцать дополнительных дейтаграмм в листинге 19 22 посылаются три многоадресных дейтаграммы, и каждая из них доставляется четырем подходящим сокетам (три сокета многоадресной передачи плюс уни- версальный сокет) Глава 20 1 Повторный вызов функции sock_ntop использует свой собственный статичес- кий буфер для хранения результата Если мы вызовем ее дважды в качестве аргумента в вызове pnntf, второй вызов приведет к перезаписи результата первого вызова 2 Да, если ответ содержи г 0 байт пользовательских данных (например, структу- ра hdr) 3 Поскольку функция sel ect не изменяет структуру timeval, которая определя- ет ее ограничение по времени, нам следует заметить время отправки первого
Глава 20 1017 пакета (оно возвращается в миллисекундах функцией rtt_ts) Если функция sei ect сообщает, что сокет готов к чтению, заметьте текущее время, а если функ- ция recvmsg вызывается повторно, вычислите новый тайм-аут для функции select 4 Обычным решением будет создать по одному сокету на каждый адрес интер- фейса, как было сделано в разделах 19 11 и 20 6, и отправлять ответ с того же сокета, на который пришел запрос 5 Вызов функции getaddrinfo без аргумента имени узла и без флага AI_PASSIVE заставляет эту функцию считать, что используется локальный адрес 0 1 (для IPv6) или 127 0 01 (для IPv4) Напомним, что структура адреса сокета IPv6 возвращается функцией getaddri nfo перед структурой адреса сокета IPv4 при условии, что поддерживается протокол IPv6 Если узел поддерживает оба про- токола, вызов функции socket в udp_cl i ent закончится успешно при укзании семейства протоколов AF_INET6 В листинге ДИ приведена не зависящая от протокола версия программы Листинг Д. 11. Не зависящая от протокола версия программы из раздела 20 6 //advio/udpserv04 с 1 #i ncl ude unpifi h 2 void mydg_echo(int SA * socklen_t) 3 int 4 main(int argc char **argv) 5 { 6 int sockfd family port 7 const int on = 1 8 pid_t pid 9 socklen_t salen 10 struct sockaddr *sa *wild 11 struct ifi_info *ifi *ifihead 12 if (argc = 2) 13 sockfd = Udp_cllent(NULL argv[l] (void **) &sa &salen) 14 else if (argc -= 3) 15 sockfd = Udp_client(argv[l] argv[2] (void **) &sa &salen) 16 else 17 err_quit( usage udpservO4 [ <host> ] <service or port>’) 18 family = sa->sa_family 19 port = sock_get_port(sa salen) 20 Close(sockfd) /* хотим узнать семейство порт salen */ 21 for (ifihead = ifi = Get_ifi_info( family 1) 22 ifi NULL ifi = ifi >ifi_next) { 23 /* связывание с многоадресными адресами */ 24 sockfd = Socket(fami1y SOCK_DGRAM 0) 25 Setsockopt(sockfd SOL_SOCKET SO_REUSEADDR Son. sizeof(OrtO): 26 sock_set_port(ifi->ifi_addr salen port) 27 Bind(sockfd ifi->ifi_addr salen) 28 printf( bound £s\en Sock_ntop(ifi >ifi_addr salen)). 29 if ( (pid - ForkO) “ 0) { /* дочерний процесс */ продолжение
1018 Приложение Д Решения некоторых упражнений Листинг Д.11 (продолжение) 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 mydg_echo(sockfd ifi >ifi_addr salen) exit(O) /* никогда не выполняется */ } if (ifi >ifi_flags & IFF_BROADCAST) { /* попытка связывания с широковещательным адресом */ sockfd - Socket(famly SOCK_DGRAM 0) Setsockopt(sockfd SOL_SOCKET SO_REUSEADDR &on sizeof(on)) sock_set_port(ifi >ifi_brdaddr salen port) if (bind(sockfd ifi >ifi_brdaddr salen) < 0) { if (errno — EADDRINUSE) { printf( EADDRINUSE £s\en Sock_ntop(ifi->ifi_brdaddr salen)) Close(sockfd) continue } else err_sys( bind error for £s Sock_ntop(ifi >ifi_brdaddr salen)) } printf( bound ^s\en Sock_ntop(ifi >ifi_brdaddr salen)k if ( (pid - ForkO) == 0) { /* дочерний процесс */ mydg_echo(sockfd ifi >ifi_brdaddr salen) exit(0) /* никогда не выполняется */ } } } /* связывание с универсальным адресом */ sockfd = Socket(family SOCK_DGRAM 0) Setsockopt(sockfd SOL_SOCKET SO_REUSEADDR &on sizeof(on)) wild - Malloc(salen) menicpy(wild sa salen) /* копирует семейство и порт */ sock_set_wild(wild salen) Bind(sockfd wild salen) printf( bound ^s\en Sock_ntop(wild salen)) if ( (pid = ForkO) “ 0) { /* дочерний процесс */ mydg_echo(sockfd wild salen) exit(O) /* никогда не выполняется */ } exit(0) } void mydg_echo(int sockfd SA *myaddr socklen_t salen) ( int n char mesg[MAXLINE] socklen_t len struct sockaddr *cli cli - Malloc(salen) for ( ) {
Глава 21 1019 78 len - salen 79 n = Recvfrom(sockfd mesg MAXLINE 0 cli &len) 80 printf! child M datagram from 81 getpidO Sock_ntop(cli len)) 82 printfl to £s\en Sock_ntop(myaddr salen)) 83 Sendto(sockfd mesg n 0 cli len) 84 } 85 } Глава 21 1 Да, разница есть В первом примере 2 байта отсылаются с единственным сроч- ным указателем, который указывает на байт, следующий за b Во втором же примере (вызываются две функции) сначала отсылается символ а с указате- лем срочности, который указывает на следующий за ним байт, а за этим сег- ментом следует еще один TCP-сегмент, содержащий символ b с другим указа- телем срочности указывающим на следующий за ним байт 2 В листинге Д 12 приведена версия программы с использованием функции pol 1 Листинг Д. 12. Версия программы из листинга 21 4, использующая функцию poll вместо функции select //oob/tcprecv03p с 1 #include unp h 2 int 3 mainlint argc char **argv) 4 { 5 int listenfd connfd n justreadoob - 0,f 6 char buff[100] 7 struct pol1fd pollfd[l] 8 if (argc == 2) 9 1istenfd = Tcp_listen(NULL argv[l] NULL) 10 else if (argc == 3) 11 listenfd = Tcp_listen(argv[l] argv[2] NULL) 12 else 13 err_quit( usage tcprecv03p [ <host> ] <port#> ) 14 connfd = Accept(1istenfd NULL NULL) 15 pollfd[O] fd = connfd 16 pollfd[0] events = POLLRDNORM 17 for ( ) { 18 if (justreadoob = 0) 19 pollfd[O] events |- POLLRDBAND 20 PolKpollfd 1 INFTIM) 21 if (pollfd[0] revents & POLLRDBAND) { 22 n = Recv(connfd buff sizeof(buff) 1 MSG_OOB) 23 buff[n] = 0 /* завершающим нуль */ 24 printf( read M 00B byte $s\en n buff) 25 justreadoob - 1 26 pollfd[0] events &= -POLLRDBAND /* отключение бита */ 27 } 28 if (pollfdLO] revents & POLLRDNORM) {
1020 Приложение Д. Решения некоторых упражнений Листинг Д. 12 (продолжение) 29 if ( (n- Read(connfd buff, sizeof(buff) - D) == 0) { 30 printfCreceived E0F\en"), 31 exit(O). 32 } 33 buff[n] = 0 /* завершающий нуль */ 34 printfC'read И bytes $s\en” n buff) 35 justreadoob = 0 36 } 37 } 38 } Глава 22 Нет, такая модификация приведет к ошибке. Проблема состоит в том, что nqueu уменьшается до того, как завершается обработка элемента массива dg[iget], что позволяет обработчику сигналов считывать новую дейтаграмму в данный элемент массива. Глава 23 1. В примере с функцией fork будет использоваться 101 дескриптор, один про- слушиваемый сокет и 100 присоединенных сокетов. Но каждый из 101 про- цесса (один родительский и 100 дочерних) имеет только один открытый де- скриптор (игнорируем все остальные, такие как стандартный поток ввода, если сервер не является демоном). В случае сервера с потоками используется 101 де- скриптор для одного процесса. Каждым потоком (включая основной) обраба- тывается один дескриптор. 2. Обмена двумя последними сегментами завершения TCP-соединения (сегмент FIN сервера и сегмент АСК клиента в ответ на сегмент FIN сервера) не про- изойдет. Это переведет клиентский конец соединения в состояние FIN_ WAIT 2 (см. рис. 2.4). Беркли-реализации прервут работу клиентского конца, если он остался в этом состоянии, по тайм-ауту через 11 минут [105, с. 825- 827]. У сервера же в конце концов закончатся дескрипторы. 3. Это сообщение будет выводиться основным программным потоком в том слу- чае, когда он считывает из сокета признак конца файла и при этом другой по- ток продолжает работать. Простейший способ выполнить это — объявить дру- гую внешнюю переменную по имени done, инициализируемую нулем. Прежде чем функция copyto программного потока вернет управление, она установит эту переменную в 1. Основной программный поток проверит эту переменную, и если она равна нулю, выведет сообщение об ошибке. Поскольку значение переменной устанавливает только один программный поток, пет необходимо- сти в синхронизации. Глава 24 1. На стороне сервера ничего не изменится. Во-первых, маршрутизатор исполь- зует обычную модель системы с гибкой привязкой, поэтому он принимает вхо-
Глава 25 1021 дящие дейтаграммы из Ethernet, даже если адрес получателя является адре- сом другого его интерфейса. Во-вторых, при использовании маршрутизации IPv4 от отправителя перенаправляющий узел заменяет его адрес в списке на адрес исходящего интерфейса. Исходящим интерфейсом является интер- фейс Ethernet (206.62.226.62), независимо от того, на какой адрес отправ- лен пакет. 2. Ничего не изменится. Все системы являются соседями, поэтому гибкая марш- рутизация идентична жесткой. 3. Мы бы поместили EOL (нулевой байт) в конец буфера. 4. Поскольку программа ping создает символьный (неструкгурированный) со- кет (см. главу 25), она получает полный IP-заголовок, включая все 1Р-пара- метры, для каждой дейтаграммы, которую она считывает с помощью функции recvfrom. 5. Потому что сервер rlogind запускается демоном inetd (см. раздел 12.5). 6. Проблема заключается в том, что пятый аргумент функции setsockopt являет- ся указателем на длину, а не самой длиной. Эта ошибка, вероятно, была выяв- лена, когда впервые использовались прототипы ANSI С. Ошибка оказалась безвредной, поскольку, как отмечалось, для отключения параметра сокета IP_OPTIONS можно либо задать пустой указатель в качестве четвертого аргумента, либо установить нулевое значение в пятом аргументе (длине) [105, с. 269]. Глава 25 1. Недоступными являются поле номера версии и поле следующего заголовка в IPv6. Поле полезной длины доступно либо как аргумент одной из функций вывода, либо как возвращаемое значение одной из функций ввода, но если требуется параметр увеличенного поля данных (jumbo payload option), сам параметр приложению недоступен. Заголовок фрагментации также недосту- пен приложению. 2. В конце концов приемный буфер клиентского сокета заполнится, и при этом функция демона wri te будет заблокирована. Мы не хотим, чтобы это произош- ло, поскольку демон тогда перестанет обрабатывать данные на всех своих со- кетах. Простейшим решением является следующее: демон должен сделать свой конец соединения домена Unix с клиентом неблокируемым. Для этого демон должен вызывать функцию write вместо функции-обертки Write и игнориро- вать ошибку EWOULDBLDCK. 3. По умолчанию Беркли-ядра допускают широковещательную передачу через символьный сокет [105, с. 1057]. Поэтому параметр сокета S0J3R0ADCAST необ- ходимо определять только для UDP-сокетов. 4. Наша программа не проверяет адреса многоадресной передачи и не устанав- ливает параметр сокета IP_MULTICAST_IF. Следовательно, ядро выбирает ис- ходящий интерфейс, вероятно, просматривая таблицу маршрутизации для
1022 Приложение Д. Решения некоторых упражнений 224.0.0.1. Мы также не устанавливаем значение поля IP_MULTICAST_TTL, поэто- му по умолчанию оно равно 1, и это правильное значение. Глава 26 1. Этот флаг означает, что буфер перехода устанавливается функцией sigsetjmp (см. листинг 26.6). Хотя этот флаг может казаться лишним, существует веро- ятность, что сигнал может быть доставлен после того, как устанавливается обработчик ошибок, но перед тем, как вызывается функция sigsetjmp. Даже если программа не вызывает генерацию сигнала, сигнал все равно может быть сгенерирован другим путем (например, как в случае с командой kill). Глава 27 1. Родительский процесс оставляет прослушиваемый сокет открытым в том слу- чае, если ему позже будет необходимо создать дополнительный дочерний про- цесс с помощью функции fork (это будет расширением нашего кода). 2. Для передачи дескриптора действительно можно вместо потокового сокета использовать сокет дейтаграмм. В случае сокета дейтаграмм родительский процесс не получает признака конца файла на своем конце канала, когда до- черний процесс прерывается преждевременно, но для этих целей родительс- кий процесс может использовать сигнал SIGCHLD. Следует иметь в виду, что эта ситуация отличается от случая с применением нашего демона i cmpd (см. раз- дел 25.7): тогда между клиентом и сервером не было иерархических отноше- ний (родительский процесс — дочерний процесс), поэтому использование при- знака конца файла было единственным способом для сервера обнаружить исчезновение клиента. Глава 28 1. Как правило, не используется, поскольку единственным типом приложений, которые присваивают полю qlen отличное от нуля значение, является ориен- тированный на соединение сервер (например, TCP). Но серверы обычно свя- зываются с заранее известным портом, не позволяя системам выбирать дина- мически назначаемый порт. Исключением является сервер RPC (Remote Pro- cedure Call — удаленный вызов процедур), который связывается с динамически назначаемым портом, азатем регистрирует его с помощью программы отобра- жения портов RPC. 2. Получение сообщений ICMP о недоступности получателя в ответ на сегмент SYN не является фатальной ошибкой. TCP может передавать сегмент SYN определенное количество раз либо пока не истечет время, отведенное для по- вторных передач. 3. Функция wri te, предназначенная для записи в сокет, получивший сегмент RST, генерирует сигнал SIGPIPE. Если процесс ничего не делает с этим сигналом, по умолчанию процесс прерывается. Если процесс игнорирует сигнал, функция write возвращает ошибку EPIPE.
Глава 29 1023 Глава 29 1. Да, функция является безопасной в многопоточной среде Функция setnetconfig динамически выделяет память, используемую для храпения структуры net- config и массивов, па которые она указывает. Эта память освобождается с по- мощью функции endnetconfig. Следует быть внимательным и не ссылаться на возвращаемую функцией getnetconf i g структуру netconf i g после вызова функ- ции endnetconfig, поскольку отведенная ей память уже освобождена. 2. Программа приведена в листинге Д 13 Листинг Д.13. Сравнение T ALL вместо определения каждой структуры netbuf //debug/testO6 с 1 #include unpxti h 2 int 3 mainlint argc char **argv) 4 { 5 int fd 6 struct t_call *tcall 7 fd = T_open(XTI_TCP O_RDWR NULL) 8 tcall = T_alloc(fd T_CALL T_ALL) 9 printfC first t_alloc OK\en") 10 tcall = T_alloc(fd. T_CALL T_ADDR | T_OPT | TJJDATA) 11 printf( second t_alloc 0K\en') 12 exit(O) 13 } После запуска этой программы получим следующий вывод: alpha test06 first tai loc OK t_alloc error system error Invalid argument Точка доступа TCP не поддерживает пользовательские данные с запросом на соединение (значение -2 для строки, содержащей функцию connect в табл. 28.1). Когда определена функция T_ALL, функция t_al loc пропускает эти неподдер- живаемые структуры, и на рис. 29.2 мы видим, что значение udata 1 еп равно нулю, a udata buf — пустой указатель Но при указании любой из грех струк- тур netbuf в качестве третьего аргумента функции t ai 1 ос (а не функции T ALL) будет возвращена ошибка, если хоть одно из заданных полей не поддержива- ется. Это еще одна причина, по которой всегда следуег использовать функ- цию T_ALL. 3. Функция не может определить тип структуры только по ее указателю. Если функция t free освобождает только память, выделенную под структуру, этот аргумент не потребуется. Но поскольку она проходит через структуру и осво- бождает все буферы, на которые указывают структуры netbuf внутри нее, этой функции необходимо знать тип структуры 4. Порядок следования элементов этой структуры не гарантирован.
1024 Приложение Д. Решения некоторых упражнений Глава 30 1. Этот способ не работает, поскольку для записи целочисленного значения обычно используется 32 бита, в то время как указатели требуют 64 бита (см. табл. 1.5). 2. Константа РАТН_МАХ стандарта Posix. 1 не содержи г завершающего нулевого байта. 3. На рис. Д.7 мы показали 4-байтовый массив и два 2-бапговых поля с перекры- ванием. x[0] x[l] x[2] x[3] , Источник н----------н к---------и Приемник Рис. Д.7. Перекрывание областей памяти при копировании Если копирование выполняется от начала исходной области Д6 начала конеч- ной, как в следующем фрагменте программы на языке С: while (nbytes--) *dst++ = *src++. то тогда выполняются два оператора присваивания х[1] = х[2]. х[2] = х[3]. что корректно. Но если копирование делается в другом направлении, как в следующем фрагменте кода: src += nbytes dst += nbytes. while (nbytes--) *--dst = *--src. то два оператора присваивания будут выглядеть следующим образом: х[2] = х[3]. х[1] = х[2] Такое присваивание некорректно. Заранее не известно, в каком направлении функция memcpy осуществляет копирование, следовательно, при перекрывании должна вызываться функция memmove. Функция memmove обрабатывает перекры- вание путем копирования в «правильном» направлении, в зависимости от вза- имоотношений между исходной и конечной областью. 4. В листинге Д.14 приведены две функции: do_parent и do chi Id. Предваритель- но следует подключить в листинге Д.9 заголовочный файл unpxti h вместо unp h. Листинг Д. 14. Определение действительного номера поставленных в очередь соединений для различных значений qlen 35 void 36 doporent(void) 3/ {
Глава 30 1025 38 39 40 41 42 43 44 45 46 47 48 49 . 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 int qlen. j, к. junk, fd[MAXBACKL0G + 1]. struct t_call tcall. Close(cfd) Signal(SIGALRM, parent_alrm). for (qlen = 0. qlen <= 14. qlen++) { printfCqlen = 2d ". qlen), Write(pfd. &qlen. sizeof(int)). /* сообщает дочернему процессу значение */ Read(pfd. &junk. sizeof(int)), /* ждет дочерний процесс*/ for (J = 0. j <= MAXBACKLOG. J++) { fd[j] = T_open(XTl_TCP, 0_RDWR NULL); T_bind(fd[j], NULL. NULL), tcall addr maxlen = sizeof(serv). tcall addr len = sizeof(serv), tcall addr buf = &serv. tcall opt len = 0. tcall udata len = 0. alarm(2). if (t_connect(fd[j]. &tcall. NULL) < 0) { if (errno != EINTR) err_xti("t_connect error, j = И". j). printf("timeout, ^d connections completed\en”. j - 1); for (k = 1. к < j. k++) T_close(fd[k]). break. /* следующее значение qlen */ } alarm(O). } if (j > MAXBACKLOG) printfCfcd connectionsAen". MAXBACKLOG): } qlen = -1. /* сообщаем дочернему процессу, что все сделано */ Write(pfd. &qlen. sizeof(int)). } void do_child(void) { int listenfd qlen junk. • struct t_bind tbind, tbindret. f Close(pipefd[l]) .' Read(cfd &qlen sizeof(int)). /* ждет родительский процесс */ while (qlen >= 0) { listenfd = T_open(XTl_TCP. 0_RDWR NULL). tbind addr maxlen = sizeof(serv). tbind addr len = sizeof(serv); tbind addr.buf = &serv. tbind qlen = qlen. tbindret addr maxlen = 0. tbindret addr len = 0: продолжение
1026 Приложение Д. Решения некоторых упражнений Листинг Д. 14 (продолжение) 86 87 88 T_bind(listenfd. &tbind. &tbindret). printfCreturned qlen = %d. ". tbindret qlen). fflush(stdout). 89 Write(cfd &junk. sizeof(int)). /* сообщает родительскому процессу*/ 90 91 92 93 Read(cfd &qlen. sizeof(int)), /* ждет родительский процесс*/ T close!listenfd). /* также закрывает все установленные в очередь соединения */ } } Глава 33 Здесь предполагается, что по умолчанию для протокола осуществляется нор- мальное завершение при закрытии потока, и для TCP это правильно.
Литература 'Все документы RFC находятся в свободном доступе и могут быть получены по электронной почте, через анонимные FTP-серверы или WWW. Стартовая точка для поиска — http://www.ietf.org. Документы RFC расположены по адресу ftp:// ftp.isi.edu/in-notes. Отдельные документы RFC не снабжены адресами URL. Пункты, помеченные как «интернет-проект», — это еще не законченные раз- работки IETF (Internet Engineering Task Force — целевая группа инженерной под- держки Интернета). После выхода этой книги в свет эти проекты, возможно, изменятся или будут опубликованы как RFC. Они находятся в свободном досту- пе, как и документы RFC. Основное хранилище интернет-проектов — http:// www.ietf.org. Часть URL, содержащая имя файла, приведена рядом с названием каждого проекта, так как в ней содержится номер версии. Для книг, статей и других источников, имеющих электронные версии, указа- ны адреса сайтов. Они могут меняться, поэтому следите за списком обновлений на сайте автора книги http://www.kohala.com/~rstevens. 1. Albitz, Р., Liu, С. DNS and BIND, Second Edition //O'Reilly & Associates Sebasto- pol, Calif., 1997. 2. Almquist, P. Type of Service in the Internet Protocol Suite //RFC 1349 (July), 1992. Обсуждается использование поля тип сервиса в заголовке IPv4. 3. Atkinson, R.J. IP Authentication Header //RFC 1826 (Aug.), 1995. 4. Atkinson, R.J. IP Encapsulating Security Payload (ESP) //RFC 1827 (Aug.), 1995. 5. Baker, F. Requirements for IP Version 4 Routers //RFC 1812 (June), ed. 1995. 6. Boman, D. A. Frequency of RST Terminated Connections //January 30, 1997, end2end-interest mailing list. http://www.kohala.com/~rstevens/borman.97jan30.txt 7. Boman, D. A. TCP and UDP over IPv6 Jumbograms //RFC 2147 (May), 1997. 8. Boman, D. A. "Re: SYN/RST cookies", June 6,1997, tcp-impl mailing list. http://www.kohala.com/~rstevens/borman.97jun06.txt 9. Braden, R. T. Requirements for Internet Hosts — Communication Layers //RFC 1122 (Oct.), ed. 1989. Первая часть Host Requirements RFC: канальный уровень, IPv4, ICMPv4, IGMPv4, ARP, TCP и UDP. 10. Braden, R. T. TIME-WAIT Assassination Hazards in TCP //RFC 1337 (May), 1992. 11. Braden, R. T. Extending TCP for Transactions — Concepts //RFC 1379 (Nov.), 1992.
1028 Литература 12. Braden, R. T. TCP Extensions for High Performance: An Update, 1993. Интернет-проект http://www.kohala.com/--rstevens/tcplw-extensions.txt. Это об- новленная редакция документа RFC 1323 [45], которая никогда не печата- лась как RFC, по обновление этого документа должно когда-то появиться. 13. Braden, R. Т. Т Т/ТСР — TCP Extensions for Transactions. Functional Specifi- cation//RFC 1644 (July), 1994. 14. Braden, R. T„ Borman, D. A., Partridge, C. Computing the Internet Checksum //RFC 1071 (Sent.), 1988. 15. Bradner, S. O. The Internet Standards Process — Revision 3 //RFC 2026 (Oct.), 1996. 16. Butenhof, D. R. Programming with POSIX Threads //Addison-Wesley, Reading, Mass, 1997. 17. CERT 1996a. UDP Port Denial-of-Service Attack //Advisory CA-96.01, Compu- ter Emergency Response Team, Pittsburgh, Pa. (Feb.). ftp://info.cert.org/pub/cert_advisories/CA-96.01.UDP_service_deniai 18. CERT 1996b. TCP SYN Flooding and IP Spoofing Attacks //Advisory CA-96.21, Computer Emergency Response Team, Pittsburgh, Pa. (Sent.). ftp://info.cert.org/pub/cert_advisories/CA-96.21.tcp_syn_fiooding 19. Cheswick, W. R., Bellovin, S. M. Firewalls and Internet Security: Repelling the Wily Hacker //Addison-Wesley, Reading, Mass, 1994. 20. Comer, D. E., Lin, J. C. TCP Buffering and Performance Over an ATM Network //Purdue Technical Report CSD-TR 94-026, Purdue University, West Lafayette, Ind. (Mar.), 1994. ftp://gwen.cs.purdue.edU/pub/iin/TCP.atm.ps.Z 21. Conta, A., Deering, S. E. Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification //RFC 1885 (Dec.), 1995. 22. Crawford, M. A Method for the Transmission of IPv6 Packets over Ethernet Networks, //RFC 1972 (Aug.), 1996. 23. Crawford, M. A Method for the Transmission of IPv6 Packets over FDDI Networks //RFC 2019 (Oct.), 1996. 24. Deering, S. E. Host Extensions for IP Multicasting//RFC 1112 (Aug.), 1989. 25. Deering, S. E., Hinder, R. Internet Protocol, Version 6 (IPv6) Specification //RFC 1883 (Dec.), 1995. 26. Dewar, R. В. K„ Smosna, M. Microprocessors: A Programmer's View //McGraw- Hill, New York. 1990. 27. Eriksson, H. MBONE: The Multicast Backbone //Communications of the ACM, vol. 37, no. 8, pp. 54-60 (Aug.), 1994. 28. Fenner, W. С. Личное общение, 1997. 29. Fuller, V., Li, T., Yu,J. Y„ Varadhan, K. Classless Inter-Domain Routing (CIDR): An Address Assignment and Aggregation Strategy //RFC 1519 (Sent.), 1993. 30. Garfinkel, S.L., and Spafford, E. H. Practical UNIX and Internet Security //Second Edition, O'Reilly & Associates, Sebastopol, Calif., 1996.
Литература 1029 31. Gierth, А. Личное общение, 1996. 32. Gilligan, R. Е., Thomson, S., Bound,}., Stevens, ll7 R. Basic Socket Interface Extensi- ons for IPv6 //RFC 2133 (Apr.), 1997. 33. Handley, M. SAP: Session Announcement Protocol //Nov., 1996. Интернет-проект draft-ietf-mmusic-sap-OO.txt. 34. Handley, M.,Jacobson, V. SDP: Session Description Protocol //Mar., 1997. Интернет-проект draft-ietf-mmusic-sdp-03.txt. 35. Hinden, R., Deering, S. E. IP Version 6 Addressing Architecture //RFC 1884 (Dec.), 1995. 36. Hinden, R., Deering, S. E. IP Version 6 Addressing Architecture //July, 1997. Интернет-проект draft-ietf-ipngwg-addr-arch-v2-02.txt. Получив статус RFC, он должен заменить RFC 1884 [35]. 37. Hinden, R., Fink, R., Postel,J. B. IPv6 Testing Address Allocation //July, 1997. Интернет-проект draft-ietf-ipngwg-testv2-addralloc-01.txt. Получив статус RFC, он должен заменить RFC 1897 [39]. 38. Hinden, R., О Dell, M., Deering, S. E. An IPv6 Aggregatable Global Unicast Address Format //July, 1997. Интернет-проект draft-ietf-ipngwg-unicast-aggr-02.txt. Получив статус RFC, он должен заменить RFC 2073 [86]. 39. Hinden, R., Postel,J. B. IPv6 Testing Address Allocation //RFC 1897 (Jan.), 1996. 40. IEEE 1996. Information Technology — Portable Operating System Interface (POSIX) — Part 1: System Application Program Interface (API) [C Language] // IEEE Std 1003.1,1996 Edition, Institute of Electrical and Electronics Engineers, Piscataway, N. J. (July). Данная версия Posix.l (называемая также ISO/IEC 9945-1: 1996) содержит базовый интерфейс API (1990), расширения реального времени 1003.1b (1993), программные потоки Pthreads 1003.1с (1995) и технические поправки 1003. li (1995). Чтобы сделать заказ, обратитесь на сайт http://www.ieee.org. К сожа- лению, стандарты IEEE не распространяются свободно через Интернет. 41. IEEE 1997а. Information Technology — Portable Operating System Interface (POSIX) — Part xx: Protocol Independent Interfaces (PII) //P1003.1g/D6.6, Institute of Electrical and Electronics Engineers, Piscataway, N.J. (Mar.). Эта версия должна была быть окончательной версией Posix. 1g. К сожалению, стандарты IEEE не распространяются свободно через Интернет. 42. IEEE 1997b. Guidelines for 64-bit Global Identifier (EUI-64) Registration Autho- rity //Institute of Electrical and Electronics Engineers, Piscataway, N.J. http://standards.ieee.org/db/oui/tutoriaLs/EUI64.html 43. Jacobson, V., Congestion Avoidance and Control //Computer Communication Review, vol. 18, no. 4, pp. 314-329 (Aug.), 1988. ftp://ftp.ee.lbl.gov/papers/congavoid.ps.Z. Классическая статья, описывающая ал- горитмы медленного старта и предотвращения перегрузки сети для TCP.
1030 Литература 44. Jacobson, V. "Re: half baked anycastoff idea... ", 27 June, 1994, end2end-interest mailing list. http://www.kohala.com/~rstevens/vanj.94jun27.txt 45. Jacobson, V., Braden, R. T., Borman, D. A. TCP Extensions for High Performance // RFC 1323 (May.), 1992. Описываются параметр масштабирования окна, параметр отметки времени, алгоритм PAWS, а также приводятся причины необходимости этих модифи- каций. [12] представляет собой обновленную версию этого документа RFC. 46. Jacobson, V., Braden, R. T„ Zhang, L. TCP Extensions for High-Speed Paths, //RFC 1185 (Oct.), 1990. 47. Josey, A. Go Solo 2: The Authorized Guide to Version 2 of the Single UNIX Specification //Prentice Hall, Upper Saddle River, N.J., ed. 1997. 48. Joy, IF. N. Личное общение, 1994. 49. Karn, P., and Partridge, C. Improving Round-Trip Time Estimates in Reliable Transport Protocols //Computer Communication Review, vol. 17, no. 5, pp. 2-7 (Aug.), 1987. 50. Katz, D. Transmission of IP and ARP over FDDI Network //RFC 1390 Gan.), 1993. 51. Katz, D. IP Router Alert Option //RFC 2113 (Feb.), 1997. 52. Katz, D„ Atkinson, R.J., Partridge, C.,Jackson, A. IPv6 Router Alert Option //June, 1997. Интернет-проект draft-ietf-ipngwg-ipv6router-alert-02.txt. 53. Kent, S. T. U.S. Department of Defense Security Options for the Internet Protocol //RFC 1108 (Nov.), 1991. 54. Kent, S. T., Atkinson, R.J. IP Authentication Header,//July, 1997. Интернет-проект draft-ietf-ipsec-auth-header-01.txt. 55. Kent, S. T., Atkinson, R.J. IP Encapsulating Security Payload (ESP) //July, 1997. Интернет-проект draft-ietf-ipsec-esp-v2-00.txt. 56. Kemigjian, B. IF, Pike, R. The UNIX Programming Environment //Prentice Hall, Englewood Cliffs, N.J., 1984. 57. Kemighan, B. IF., Ritchie, D. M. The C Programming Language //Second Edition, Prentice Hall, Englewood Cliffs, N.J., 1988. 58. Kom, D. G., Vo, К. P. "SIFO:Safe/Fast String/File IO" //Proceedings of the 1991 Summer USENIX Conference, pp. 235-255, Nashville, Tenn.,1991. 59. Lanciani, D. "Re: sockets: AF_INET vs. PF INET", Message-ID: <3561@news. IPSWITCH.COM>, Usenet, comp.protocols.tcp-ip Newsgroup //Apr., 1996. http://www.kohala.com/~rstevens/lanciani.96aprl0.txt 60. Maslen, T. M. "Re: gethostbyXXXXQ and Threads", Message-ID <maslen.862463530 @shellx>, Usenet, comp.programming.threads Newsgroup //May, 1997. http://www.kohala.com/~rstevens/maslen.97may01.txt. 61. Maufer, T., Semeria, C. Introduction to IP Multicast Routing //May, 1997. Интернет-проект draft-ietf-mboned-intro-multicast-02.txt.
Литература 1031 62. McCann, J., Deerins,, S. E., Mosul, 1. C. Path MTU Discovery for IP version 6, // RFC 1981 (Aug.), 1996. 63. McCanne, S., Jacobson, V. The BSD Packet Filter: A New Architecture for User- Level Packet Capture //Proceedings of the 1993 Winter USENIX Conference, pp. 259-269, San Diego, Calif., 1993. ftp://ftp.ee.lbl.goV/papers/bpf-usenix93.ps.Z 64. McDonald, D. L. A Simple IP Security API Extension to BSD Sockets //Mar., 1997. Интернет-проект draft-mcdonald-simple-ipsec-api-01.txt. 65. McDonald, D. L., Phan, B. G., Atkinson, R.J. A Socket-Based Key Management API (and surrounding infrastructure) //Proceedings of the INET'96 Conference, pp. 53-63 (June), Montreal, Quebec, 1996. http: //www. cs. h ut.fi/ssh/сгу pto/pf-key. ps 66. McDonald, D.L., Metz, C. W., Phan, B. G. PF_KEY Key Management API, Version 2 //July, 1997. Интернет-проект draft-mcdonald-pf-key-v2-04.txt. 67. McKusick, M. K„ Bostic, K., Karels, M.J., Quarterman, J. S. Design and Implemen- tation of the 4.4BSD Operating System //Addison-Wesley, Reading, Mass, 1996. 68. Meyer, D. Administratively Scoped IP Multicast //June, 1997. Интернет-проект draft-ietf-mboned-admin-ip-space-03.txt. 69. Mills, D. L. Network Time Protocol (Version 3): Specification, Implementation, and Analysis //RFC 1305 (Mar.), 1992. 70. Mills, D. L. Simple Network Time Protocol (SNTP) Version 4 for IPv4, IPv6 and OSI //RFC 2030 (Oct.), 1996. 71. Mogul,J. C., Deering, S.E. Path MTU Discovery//RFC 1191 (Aug.), 1990. 72. Mogul, J. C., Postel, J. B. Internet Standard Subnetting Procedure //RFC 950 (Aug.), 1985. 73. Nemeth, E. Личное общение, 1997. 74. Open Group, The. CAE Specification, Networking Services (XNS), Issue 5 //The Open Group, Reading, Berkshire, U.K., 1997. Спецификация сокетов и XTI для Unix 98. Это руководство также содержит приложения, в которых описано использование XTI с NetBIOS, протоколов OSI, SNA, а также Netware IPX и SPX. Эти приложения охватывают исполь- зование сокетов и XTI с ATM. 75. Partridge, С., Mendez, Т., Milliken, IF. Host Anycasting Service //RFC 1546 (Nov.), 1993. 76. Partridge, C., Pink, S. A Faster UDP //IEEE/ACM Transactions on Networking, vol. 1, no. 4, pp. 429-440 (Aug.), 1993. 77. Paxson, V. End-to-End Routing Behavior in the Internet //Computer Communi- cation Review, vol. 26, no. 4, pp. 25-38 (Oct.), 1996. ftp://ftp.ee.lbl.g0v/papers/routing.SIGCOMM.ps.Z >.
1032 Литература 78. Piscitello, D. M. FTP Operation Over Big Address Records (FOOBAR) //RFC 1639 (June), 1994. 79. Plauger, P.J. The Standard C Library //Prentice Hall, Englewood Cliffs, NJ., 1992. 80. Postel,J. B. User Datagram Protocol //RFC 768 (Aug.), 1980. 81. Postel,J. B. Internet Protocol //RFC 791 (Sent.), ed. 1981a. 82. Postel,J. B. Internet Control Message Protocol //RFC 792 (Sent.), ed. 1981b. 83. Postel, J. B. Transmission Control Protocol //RFC 793 (Sent.), ed. 1981c. 84. Pusateri, T. IP Multicast Over Token-Ring Local Area Networks //RFC 1469 (June), 1993. 85. Rago, S. A. UNIX System V Network Programming //Addison-Wesley, Reading, Mass, 1993. 86. Rekhter, Y., Lothberg, P„ Hinden, R„ Deering, S. E., Postel, J. B. An IPv6 Provider- Based Unicast Address Format //RFC 2073 (Jan.), 1997. 87. Reynolds,}. K., Postel, J. B. Assigned Numbers //RFC 1700 (Oct.), 1994. Некоторая информация, содержащаяся в этом документе RFC, может уста- реть еще до момента выхода обновленной версии RFC. Все данные для этого документа RFC взяты из файлов, находящихся по адресу ftp://ftp.isi.edu/in- notes/iana/assignments, обновляемых по мере поступления новой информации. Этот документ RFC содержит ссылки на файлы в указанном каталоге, и за самой новой информацией рекомендуется обращаться к этим файлам. 88. Ritchie, D. М. A Stream Input-Output System //AT&T Bell Laboratories Technical Journal, vol. 63, no. 8, pp. 1897-1910 (Oct.), 1984. 89. Salus, P. H. A Quarter Century of Unix //Addison-Wesley, Reading, Mass, 1994. 90. Salus, P. H. Casting the Net: From ARPANET to Internet and Beyond //Addison- Wesley, Reading, Mass, 1995. 91. Schimmel, C. UNIX Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers //Addison-Wesley, Reading, Mass, 1994. 92. Srinivasan, R. XDR: External Data Representation Standard //RFC 1832 (Aug.), 1995. 93. Stevens, IF. R. Advanced Programming in the UNIX Environment //Addison- Wesley, Reading, Mass, 1992. Программирование в Unix — детальное описание. 94. Stevens, IF. R. TCP/IP Illustrated, Volume 1: The Protocols //Addison-Wesley, Reading, Mass, 1994. Введение в протоколы Интернета. 95. Stevens, IF. R. TCP/IP Illustrated, Volume 3: TCP for Transactions, HTTP, NNTP, and the UNIX Domain Protocols //Addison-Wesley, Reading, Mass, 1996. 96. Stevens, IF. R., Thomas, M. Advanced Sockets API for IPv6 //June, 1997. Интернет-проект draft-stevens-advanced-api-04.txt. 97. Tanenhaum, A. S. Operating Systems Design and Implementation //Prentice Hall, Englewood Cliffs, N.J., 1987.
Литература 1033 98. Thomas, S., Transmission of IPv6 Packets over Token Ring Networks //June, 1997. Интернет-проект draft-ietf-ipngwg-trans-tokenring-OO.txt. 99. Thomson, S., Huitema, C. DNS Extensions to Support IP version 6 //RFC 1886 (Dec.), 1995. 100. Torek, C. "Re: Delay in re-using TCP/IP port", Message-ID <199501010028. QAA16863@elf.bsdi.com>, Usenet, comp.unix.wizards Newsgroup (Dec.), 1994. http://www.kohala.com/~rstevens/torek.94dec31.txt 101. Unix International. Data Link Provider Interface Specification, Revision 2.0.0 //Unix International, Parsippany, N. J. (Aug.), 1991. http://www.kohala.eom/~rstevens/dlpi.2.0.0.ps. Более новая версия этой специ- фикации доступна по адресу http://www.rdg.opengroup.org/pubs/catalog/web.htm. 102. Unix International. Network Provider Interface Specification, Revision 2.0.0 //Unix International, Parsippany, N.J. (Aug.), 1992. http://www.kohala.eom/~rstevens/npi.2.0.0.ps 103. Unix International. Transport Provider Interface Specification, Revision 1.5 //Unix International, Parsippany, N.J. (Dec.), 1992. http://www.kohala.eom/~rstevens/tpi.l.5.ps. Более новая версия этой специфи- кации доступна по адресу http://www.rdg.opengroup.org/pubs/catalog/web.htm. 104. Vixie, Р. А. Личное общение, 1996. 105. Wright, G. R., Stevens, IT. R. TCP/IP Illustrated, Volume 2: The Implementation //Addison-Wesley, Reading, Mass, 1995. Реализация протоколов Интернета в операционной системе 4.4BSD-Lite.
Алфавитный указатель 1-9 4.1cBSD, 121 4.2BSD, 52,94, 103,121,122, 128, 286,401,418, 521, 524, 583,820 4.3BSD, 52, 78, 276,382,521 4.4BSD, 52,58, 66,98,100,120,126, 152, 159,193, 229, 234, 238, 241, 245,268,295,400,420,426,441, 471,482,491,499, 503,552, 587, 711,757, 794,857,956 4.4BSD-Lite, 52,1033 4.4BSD-Lite2, 52, 1001 64-разрядное выравнивание, 97. 946 64-разрядные архитектуры, 60,103, 176,822,876, 992, 1024 бЬопе (IPv6 backbone), 31, 54, 716, 739, 961 А Abell, V. А., 976 accept, функция, 28,46,68,89,93, 98, 127,130,136,144,148, 151, 159, 163, 165, 173,191, 202, 205, 223, 233, 256, 267, 279, 285,306, 309, 311, 320,333,334,336, 342, 385,386,388,389,413,427,435, 438,439,441,466,468, 622, 628, 630,652,659,688,696, 748, 782, 792,794,808,811,817,822, 855, 858,861, 937, 989, 998,1007, 1009, 1013 неблокируемая, 466 определение, 133 прерывание соединения, 166 Addis, J., 31 addr, элемент, 822,827,835,846, 857,867,880, 884,919, 932, 940 addr_fd, элемент, 567 addr_flags, элемент, 567 addr_ifname, элемент, 567, 571 ADDRlength, элемент, 918 ADDR offset, элемент, 918 addr_sa, элемент, 567, 571 addr salen, элемент, 567 addrinfo, структура, 121, 316,319, 325,332, 352,355, 357,361, 362, 364,367,462,719, 731,846 определение, 316 Addrs, структура, 567, 570, 576 AF_hPF_, 121 AF INET, константа, 39,97,107, 109,117,119, 259, 289, 291, 293, 313,322,351,355,357, 501, 719, 747, 849 AFINET6, константа, 63,97,107, 117, 119, 287, 289, 291, 293, 313, 322,351,355,357,484, 504, 719, 747,849, 1017 AFISO, константа, 120 AF_KEY, константа, 119 AF_LINK, константа, 98, 501, 507 AF LOCAL, константа, 59, 98, 119, 418,421,423,425 AF_NS, константа, 120 AFROUTE, константа, 119, 240, 470,490,496,501 AF_UNIX, константа, 59,120,418 AF UNSPEC, константа, 270,316, 322,329, 332,334, 340,351,486, 501 АН, заголовок аутентифи- кации, 698,1027,1030 ai_addr, элемент, 316,321, 367 ai_addrlen, элемент, 316,320, 846 AI_CANONNAME, константа, 316, 325,357,358, 364
Алфавитный указатель 1035 ai_canonname, элемент, 316,321, 358,363 AI CLONE, константа, 349, 361, 362,366 ai_family, элемент, 316,319,351 ai_flags, элемент, 316,363 ai_next, элемент, 316, 317,363,365, 369 AIPASSIVE, константа, 316,322, 332, 352, 367,616,1017 aiprotocol, элемент, 316,319,368 ai_socktype, элемент, 316, 319,362, 366 aioread function, 185 AIX, 31, 54, 102,132, 223,272, 277, 285,491,524,552,822,839,847, 874,902,987, 990,1014 alarm, функция, 392,415,439,525, 527,533,600,615,637,772 Albitz, P„ 283,301, 1027 Allman, E., 316 Almquist, P., 241,1027 ANSI (American National Standards Institute), 38 C, 27,40,47, 61, 95, 104,409,471. 659, 746, 984, 1021 API, программный интерфейс приложений, 26, 39 ARP (Address Resolution Protocol), 65, 123, 249,264,471,485, 502, 515,518,714, 763 операции с кэшем, функция ioctl, 485 arp, программа, 486 arp_flags, элемент, 485 arp_ha, элемент, 485 arp_pa, элемент, 485 <arpa/nameser.h>, заголовочный файл, 773 arpreq, структура, 471,485 AS (автономная система), 953 ASCII, 40,283, 990 asctime, функция, 662 asctime r, функция, 662 at, программа, 375 ATFCOM, константа, 485 ATF INUSE, константа, 485 ATF_PERM, константа, 485 ATF_PUBL, константа, 485 Atkinson, R. J., 31,120, 698, 700, 1027,1030 ATM (Asynchronous Transfer Mode), 234,1031 atoi, функция, 359,433 autoconf, программа, 102,981 AVP (Audio/Video Profile), 556 awk, программа, 32,57 В Baker, F„ 743, 1027 basename, программа, 57 Bellovin, S. M., 132,689,1028 Bentley, J. L., 32 BGP (Border Gateway Protocol), 88 BIND (Berkeley Internet Name Domain), 284, 287,291,294,317, 343,348,503, 1004 bind, функция, 26,46, 61, 68, 79,93, 95,96, 98, 100, 122, 123, 133, 136, 142,144,151,165,172,204, 228, 236, 239,251,257,260, 263,265, 267, 276, 278, 281,306, 313,317, 320,325,332,354,367, 382, 390, 413,418,438,547,554,562,570, 579,606,613, 710,713, 733,740, 744, 748,822,825,828, 931,955, 989,996,1008, 1010, 1014 определение, 123 bind ack, структура, 919 bind connect listen, функция, 239 bindincast, функция, 568 bind_req, структура, 918 bind_ubcast, функция, 568,1016 Blindheim, R., 30 BOOTP (Bootstrap Protocol), 84, 515 Borman, D. A., 70,84, 128,133,595, 725, 896,1027,1030
1036 Алфавитный указатель Bostic, К., 52,1031 Bound, J., 30,96, 242, 343, 508, 545, 1029 Bourne shell, 57 Bowe, G., 30 BPF, пакетный фильтр BSD, 64, 66,120, 757, 778 Braden, R. T„ 70, 75, 228, 253, 263, 412, 517, 557, 583, 595, 725,896, 951,1027,1030 Bradner, S., 60,1028 Briggs, A., 30 BSD (Berkeley Software Distribution), 52 история, 52 BSD/OS, 52,102,121,132,169, 193, 225, 241, 276,389,402,419,434, 435,475,478, 501, 517, 524,552, 588, 624, 631, 776, 783, 796, 798, >801, 809, 968,976,981, 988, 996, 1001,1008,1014 buf, элемент, 826, 912 BUFFSIZE, константа, определение, 980 BUFLEN, константа, 496 bufmod, модуль, 760 Butenhof, D. R., 31,653,1028 c С, стандарт C9X, 47 calloc, функция, 483,671 caplen, элемент, 777 CDE (Common Desktop Environment), 59 CERT (Computer Emergency Response Team), 132,1008 chargen, программа, 87, 214,302 390,1003,1008 check_dup, функция, 576 check_loop, функция, 576 Cheswick, W. R., 132, 689, 1028 Child, структура, 804,809 child.h, заголовочный файл, 804 child_main, функция, 794, 797, 808 child_make, функция, 793, 799,804 Cisco, 54 Clark, E., 32 Clark, J. J., 32 cleanup, функция, 768 cli, структура, 866,868,877 client, структура, 747, 748, 752 clock gettime, функция, 683 close, функция, 44,47, 68, 71, 89, 123,139,145,162,198,199, 215, 228, 251,346,452,467, 659, 686, 752,856,864,873, 925, 989, 994, 1008,1012 определение, 141 CLOSE_WAIT, состояние, 72 CLOSED, состояние, 72,89,123, 232 closelog, функция, 377 определение, 377 CLOSING, состояние, 72 Clouter, M., 31 cmsg_control, элемент, 408 CMSG_DATA, макрос, 407,432, 895 cmsg data, элемент, 406,432, 701, 704 CMSGJFIRSTHDR, макрос, 407, 408,585,895 CMSGLEN, константа, 979 CMSGLEN, макрос, 407,408 cmsg_len, элемент, 404,406,408, 705 cmsg_level, элемент, 404,406, 614, 703, 705 CMSGNXTHDR, макрос, 407, 408,585,895 CMSG SPACE, константа, 979 CMSG_SPACE, макрос, 407,408 cmsg type, элемент, 404, 614, 703, 705 cmsghdr, структура, 404,415,432, 613, 614, 701, 703, 705 определение, 406 CNAME (Canonical Name Record, DNS), 283,287, 289
Алфавитный указатель 1037 Comer, D. Е., 234,1028 config.h, заголовочный файл, 432, 981 configure, программа, 981 CONINDnumber, элемент, 918 conn_req, структура, 921 connect, функция, 26,39,43,45,58, 61, 68, 79, 89,93, 98,100,121,122, 126,131,142,144,150,151,153, 160, 165,172,178,233, 239, 252, 255, 256, 260,267, 276, 285, 299, 306,308, 309, 313,314, 317, 319, 325, 329,339, 354,373, 377, 393, 397,413, 415,422,427,439,441, 453, 454, 458,465,468, 672, 675, 685, 696, 710, 713, 740, 748, 792, 822,825,828,840,852,856,878, 883, 937, 965,968,995,1000, 1008, 1023 UDP, 267 неблокируемая, 453 определение, 121 прерванная, 457 тайм-аут, 393 connect, элемент, 822 connectnonb, функция, 454,458 исходный код, 455 connect_timeo, функция, 393 исходный код, 393 connld, модуль, 435 const, квалификатор, 104,125,188 Conta, А., 1028 copyto, функция, 657,1020 cpio, программа, 57 CPUVENDOROS, константа, 102 CR (возврат каретки), 41, 971, 990 Crawford, М., 536,1028 cron, программа, 375,377 CSRG (Computer Systems Research Group), 52 ctermid, функция, 662 ctime, функция, 47, 662, 864 ctime r, функция, 662 CTL_NET, константа, 501 D daemon_inetd, функция, 388 исходный код, 388 daemon init, функция, 378,388, 1008 исходный код, 380 daemon proc, переменная, 380, 388, 984 Davis, J., 31 daytime, программа, 87, 373,1008 DCE, 89 DCE (Distributed Computing Environment), 592 RPC, 89 Deering, S. E., 83, 243,534, 546, 700, 703, 706, 944, 952, 1028, 1031 DESTJength, элемент, 921 DEST offset, элемент, 921 /dev/bpf, устройство, 768 /dev/console, устройство, 375 /dev/icmp, устройство, 821,842 /dev/ipx, устройство, 842 /dev/klog, устройство, 375 /dev/kmem, устройство, 486,488 /dev/log, устройство, 375 /dev/nspx2, устройство, 842 /dev/null, устройство, 380, 679 /dev/rawip, устройство, 842 /dev/tcp, устройство, 821,842, 859, 901,916,965 /dev/ticlts, устройство, 821, 842 /dev/ticots, устройство, 821,842 /dev/ticotsord, устройство, 821,842 /dev/udp,устройство, 821,842, 901 /dev/xti/tcp, устройство, 902 /dev/xti/udp, устройство, 902 /dev/zero, устройство, 797, 802 Dewar, R. В. K„ 102,1028 DF, флаг запрета фрагментации (IP-заголовок), 83,449, 742, 943, 956 DG, структура, 645 dg cli, функция, 260, 261, 271, 272, 394,396,426, 521, 527,530, 533, 552,596, 743,1000
1038 Алфавитный указатель dg echo, функция, 257, 259, 273, 275,425, 586, 644,646 dg_send_recv, функция, 596, 600, 615 исходный код, 599 DHCP (Dynamic Host Configuration Protocol), 88 Digital Equipment Corp., 31 Digital Unix, 31,54, 93,132,170, 272, 277,345,348,491, 524, 552, 678, 783,801,802, 809,810,813, 822,874, 1014 Digital VAX, 103 discard, программа, 87,1008 discon, элемент, 822, 824,835, 933 DISCON_reason, элемент, 922 DISPLAY, переменная окружения, 417 DLATTACHREQ константа, 760 DLPI (Data Link Provider Interface), 64, 66,120, 757, 760, 778,910,1033 DLT EN10MB, константа, 776 DNI (Detailed Network Interface), 58 DNS (Domain Name System), 41, 84,89, 254, 282, 296,413 абсолютное имя, 282 альтернативы, 285 простое имя, 282 циклическое обслуживание, 788 do_child, функция, 1024 do get read, функция, 673, 684 doparent, функция, 1024 domfamily, элемент, 121 domain, структура, 121 Doupnik, J., 30 dup, функция, 795 dup2, функция, 385 Durst, W., 316 E EACCES, ошибка, 224,521,919 EADDRBUSY, ошибка, 919 EADDRINUSE, ошибка, 126,457, 607,996 EAFNOSUPPORT, ошибка, 107, 270 EAGAIN, ошибка, 441, 624,631, 654 EBUSY, ошибка, 760 echo, программа, 87, 170,390,1008 ECONNABORTED, ошибка, 166, 467 ECONNREFUSED, ошибка, 45, 122,272,422,456, 742, 885, 922, 956 ECONNRESET, ошибка, 168, 171, 226,831,840, 995 EDESTADDRREQ, ошибка, 268 EEXIST, ошибка, 500 EHOSTDOWN, ошибка, 957 EHOSTUNREACH, ошибка, 122, 171, 226, 742, 922,956 EINPROGRESS, ошибка, 441,453 EINTR, ошибка, 113,159,163,188, 207, 279,394,457,468, 523,532, 533, 635, 647, 737, 772, 1013 EINVAL, ошибка, 480, 587,620, 624, 746,989 EISCONN, ошибка, 268,456 EMSGSIZE, ошибка, 86, 524, 742, 956,1000 endnetconfig, функция, 842, 860, 1023 endnetpath, функция, 851,881 определение, 843 ENETUNREACH, ошибка, 122, 171,224 ENOBUFS, ошибка, 87 ENOMEM, ошибка, 501 ENOPROTOOPT, ошибка, 222, 414, 587, 956 ENOSPC, ошибка, 108 ENOTCONN, ошибка, 268,414,456 environ, переменная, 138 EOL (конец списка параметров), 687, 691, 1021 EOPNOTSUPP, ошибка, 587
Алфавитный указатель 1039 EPIPE, ошибка, 168,991,1022 EPROTO, ошибка, 166,467, 798 EPROTONOSUPPORT, ошибка, 368 Eriksson, Н., 959,1028 err_doit, функция исходный код, 986 err_dump, функция, 984 исходный код, 985 err_msg, функция, 382, 984 исходный код, 985 errquit, функция, 43,168, 390,984 исходный код, 985 err_ret, функция, 984 errsys, функция, 39,43,123,272, 631,836,984 исходный код, 985 err_xti, функция, 825, 984 err_xti_ret, функция, 984 errno, переменная, 43, 62,107,166, 168,191,194, 207, 209, 225, 265, 287, 321,345,376, 394,430,433, 456, 600, 653, 654, 740, 742, 746, 754,825,835,837,840,851,884, 956,984,987 error, элемент, 827,884 ERROR_prim, элемент, 918 ESRCH, ошибка, 500 ESTABLISHED, состояние, 72,89, 123,127,152, 165, 990 /etc/hosts, файл, 285 /etc/inetd.conf, файл, 383,389 /etc/irs.conf, файл, 285 /etc/netconfig, файл, 842,853,859, 965 /etc/netsvc.conf, файл, 285 /etc/networks, файл, 300 /etc/nsswitch.conf, файл, 285 /etc/rc, файл, 374,382 /etc/resolv.conf, файл, 269,285, 290,317 /etc/services, файл, 88, 296,319, 389,1007 /etc/svc.conf, файл, 285 /etc/syslog.conf, файл, 375, 377,390 ETH_P_ALL, константа, 761 ETHPARP, константа, 761 ETHPIP, константа, 761 ETH P IPV6, константа, 761 Ethernet, 65, 74,82,84, 90, 224, 234, 306,476,478,479,486,491,507, 517,518,524, 534,538, 761, 776, 822, 943, 953, 988, 1013 ETIME, ошибка, 683 ETIMEDOUT, ошибка, 45,122, 171, 226,394,456, 600, 922, 994, 999 ETSDU (срочный блок данных транспортного уровня), 822 etsdu, элемент, 822 EUI (расширенный интерфейс пользователя), 477,513, 952 EUI (расширенный уникальный идентификатор), 1029 event, структура, 213 events, элемент, 208, 935 EWOULDBLOCK, ошибка, 182, 229, 232,398,440,445,468, 620, 629, 639,649,1021 ехес, функция, 57, 115, 137,143, 173,311,383,385,426,430, 653, 791,818, 929,930,1009 execl, функция, 430 определение,137 execle, функция определение, 137 execlp, функция, 138 определение, 137 execv, функция определение, 137 execve, функция, 137 execvp, функция, 138 определение, 137 exit, функция, 41, 72,139,153,162, 252,411,416,433,612, 658,984, 1009 F F_CONNECTING, константа, 462, 465
1040 Алфавитный указатель FDONE, константа, 465, 675 f_flags, элемент, 463 F GETFL, константа, 248 F_GETOWN, константа, 248,473 F_JOINED, константа, 684 F READING, константа, 463,465 F_SETFL, константа, 248,473, 642 F SETOWN, константа, 248,473, 642 f_tid, элемент, 673 F_UNLCK, константа, 800 F WRLCK, константа, 800 FAQ (часто задаваемый вопрос), 169, 236 FASYNC, константа, 248 fattach, функция, 907 fc gid, элемент, 434 fc_groups, элемент, 434 fclogin, элемент, 434 fcngroups, элемент, 434 fcrgid, элемент, 434 fc_ruid, элемент, 434 fc_uid, элемент, 434 fcntl, функция, 138, 216, 248,251, 443,455,471,473, 619, 622, 642, 647, 799,926, 940 определение, 250 fcred, структура, 434 определение, 434 fd, элемент, 208 FD CLOEXEC, константа, 138 FD_CLR, макрос, 189 FD_ISSET, макрос, 189 FD_SET, макрос, 189,195, 675 fd_set тип данных, 187, 210 FD_SETSIZE, константа, 189,193, 203, 210 FDZERO, макрос, 195 FDDI (Fiber Distributed Data Interface), 65, 534 fdopen, функция, 410 Feng, W., 30 Fenner, W. C., 30,241,1028 fflush, функция, 410 fgets, функция, 47,146,150, 153, 167,168,193,260,411,442,523, 907,989, 998 FIFO (первым пришел, первым обслужен), 258,420,868 FILE, структура, 412, 657 file, структура, 459,464, 673, 685, 795 fileno, функция, 195,410 FIN, флаг завершения (ТСР- заголовок), 71,205,413, 758 FIN_WAIT_1, состояние, 72 FIN_WAIT_2, состояние, 72,153, 167,1020 Fink, R., 953, 1029 FIOASYNC, константа, 248,471, 642 FIOGETOWN, константа, 471 FIONBIO, константа, 248,471 FIONREAD, константа, 248,409, 416,471 FIOSETOWN, константа, 471 flags, элемент, 822, 827, 899 flock, структура, 800 FNDELAY, константа, 249 fopen, функция, 907 fork, функция, 27,48, 57,81,118, 136, 143,145,148,151, 157, 165, 201,258, 279,311,379,382,385, 390,426,430,439,450,468,558, 606,609,611, 652,656, 676,685, 696, 781, 785, 789,795, 797, 804, 809, 818, 865, 866,1008, 1020, 1022 определение, 136 fpathconf, функция, 235 fprintf, функция, 346,376,380, 382, 445,448 fputs, функция, 41,43, 146,150, 195, 260,411,442, 635, 657, 993 FQDN (полное доменное имя), 282, 289, 295,317,342 Franz, М., 32 free, функция, 369, 512,662,860 free_ifi_info, функция, 476, 484, 571 исходный код, 484
Алфавитный указатель 1041 freeaddrinfo, функция, 320,329, 347, 358,368 определение, 321 FreeBSD, 52, 241,413, 644, 688 freenetconfigent, функция, 851 Friesenhahn, R., 30 fseek, функция, 410 fsetpos, функция, 410 fstat, программа, 976 fstat, функция, 115 FTP (File Transfer Protocol), 41, 52, 89, 227, 239, 241, 296,311, 314, 377, 386, 639, 857, 988,1027,1032 Fuller, V., 948,1028 G ga_aistruct, функция, 355,357, 364, 373 ga_clone, функция, 361,362 gaecheck, функция, 351, 367 gansearch, функция, 351, 354 ga_port, функция, 360,361,367 ga_serv, функция, 358,361,367 ga_unix, функция, 351,363 gaihdr.h, заголовочный файл, 349 gai_strerror, функция, 321 определение, 321 Garfmkel, S. L., 47, 1028 Gari Software, 31 gated, программа, 225,490, 709 get_ifi_info, функция, 475,488,504, 562,565, 570,606 исходный код, 479, 505 get_rtaddrs, функция, 497, 507, 510 getaddrinfo, функция, 42,47,116, 301, 313, 315,334, 339, 341,345, 347,354,358,361,368,372,438, 616, 719, 744, 841, 846, 850, 852, 853,1006,1017 IPv6 и доменный сокет Unix, 322 примеры,325 реализация, 349 getc_unblocked, функция, 662 getcharunlocked, функция, 662 getconninfo, функция, 316 getgrid, функция, 662 getgridr, функция, 662 getgrnam, функция, 662 getgrnam r, функция, 662 gethostbyaddr, функция, 69, 282, 284, 290, 293,300, 313, 315, 343, 348,371,373, 662,841, 1001, 1003 определение, 293 поддержка IPv6,294 gethostbyaddrr, функция, 347 определение,347 gethostbyname, функция, 69, 282, 284,297,300,306,308,313, 315, 320,322,331,343,357, 361,373, 662, 822,841,854,1003,1006, 1008 определение, 287 gethostbyname_r, функция, 347 определение, 347 gethostbyname2, функция, 291, 30t, 322,344,357 определение, 291 gethostent, функция, 301 gethostname, функция, 295, 302, 766, 1003 определение, 295 getifaddrs, функция, 475 getlogin, функция, 662 getlogin_r, функция, 662 getmsg, функция, 181, 777,907',51'1, 918,921,964 определение, 912 getnameinfo, функция, 116, 301, 313, 315,320,334, 342, 345, 349, 369,372, 734, 853, 1007 определение, 342 реализация, 349 getnameinfo_timeo, функция, 373 getnetbyaddr, функция, 300 getnetbyname, функция, 300 getnetconfig, функция, 842, 843, 854, 859, 886,1023 getnetconfigent, функция, 851
1042 Алфавитный указатель getnetpath, функция, 844,851, 852, 859,880 определение, 843 getopt, функция, 695, 766 getpeername, функция, 79,93,98, 142, 173, 312, 330, 342, 388,456, 848 определение, 142 getpid, функция, 655 getpmsg, функция, 907, 911,913, 925 определение, 913 getppid, функция, 136,1012 getprotobyname, функция, 300 getprotobynumber, функция, 300 getpwnam, функция, 385,662 getpwnam_r, функция, 662 getpwuid, функция, 662 getpwuid_r, функция, 662 getrlimit, функция, 993 getrusage, функция, 790, 793 gets, функция, 47 getservbyaddr, функция, 300 getservbyname, функция, 282, 296, 331,345,360,361,385,841 определение, 296 getservbyport, функция, 282, 297, 345,372 определение, 297 getsockname, функция, 98, 126,142, 172,173, 237, 267, 276, 342,418, 741,751,848, 989, 1007 определение, 142 getsockopt, функция, 100,191, 216, 225, 241,244, 252, 312,456,465, 543, 587, 614, 688, 693,696, 707, 714, 893, 899,903,906 определение, 216 gettimeofday, функция, 562, 575, 602, 683, 721 getuid, функция, 768 gf_time, функция, 447 исходный код, 447 Gierth, А., 30,466,1029 GIF (Graphics Interchange Format), 459, 790 Gilliam, W., 31 Gilligan, R. E„ 60,96, 242,343,508, 545,1029 Glover, B., 31 gmtime, функция, 662 gmtime_r, функция, 662 gn_ipv46, функция, 370 gpic, программа, 32 Grandi, S., 31 grep, программа, 153,987 gtbl, программа, 32 H h addr, элемент, 286 h_addr_Iist, элемент, 286, 295,357, 1003 h_addrtype, элемент, 287, 294,1006 h_aliases, элемент, 286 h_cnt, элемент, 845 herrno, элемент, 287,347,357 h_host, элемент, 844 h_hostservs, элемент, 845 h_length, элемент, 286, 287, 292, 302, 1003 h_name, элемент, 286, 293,301 h_serv, элемент, 844 Handley, M., 553,1029 Hanson, D. R., 32 Hathaway, W., 30 Haug, J., 31 HAVE_MSGHDR_MSG_ CONTROL, константа, 432,982 HAVE_SOCKADDR_SA_LEN, константа, 93,982 hdr, структура, 598, 599,1016 heartbeat_cli, функция, 635, 637 исходный код, 636 heartbeat_serv, функция, 638 исходный код, 637 Hewlett-Packard, 31 Hinden, R., 243,536, 700, 703, 706, 952,1029,1032 HIPPI (High-Performance Parallel Interface), 82 Hofer, K., 30
Алфавитный указатель 1043 Hogue, J., 31 home page, функция, 460, 673 Host Requirements RFC, 1027 HOSTNOTFOUND, константа, 287 HOST_SELF, константа, 859 host_serv, функция, 327,462,692, 696, 719, 731, 768 исходный код, 327 определение, 327 hostent, структура, 286, 290,300, 347, 1003 определение, 286 hostent_data, структура, 348 HP-UX, 31, 54,102,132,272, 277, 285,345,348, 822, 839, 874 hstrerror, функция, 288 HTML (Hypertext Markup Language), 459, 790 htonl, функция, 103, 125, 179,992 определение, 103 htons, функция, 39, 297 определение, 103 HTTP (Hypertext Transfer Protocol), 41, 72,89, 126,129, 237, 413,457,461,465, 590, 675, 786, 790,974 Huitema, C„ 283,1033 I I PUSH, константа, 914, 965 I—RECVFD, константа, 426,435 I_SENDFD, константа, 426 I—SETSIG, константа, 914,933,935 I—STR, константа, 965 IANA (Internet Assigned Numbers Authority), 77,325, 373 IBM, 31 ICMP (Internet Control Message Protocol), 65, 89, 226, 265, 272, 709, 713, 715, 727, 884,974,997, 1000 демон сообщений, реализация, 740 ICMP (продолжение) заголовок, 956 поле кода, 956 поле типа, 956 ICMP-сообщение запрос адреса, 713,956 запрос маршрутизатору, 709, 956 запрос отметки времени, 713, 956 извещение маршрутизатора, 709, 714,956 изменение маршрутов, 490,501, 956 отключение отправителя, 742, 956 получатель недоступен, 171, 226, 265, 734,742, 746, 830, 838, 840, 883,922,956, 1022 получатель недоступен, необходима фрагментация, 742,956 порт недоступен, 265, 269, 272, 281,519, 727, 734, 742, 763, 780, 883,888, 956,1000,1015 превышено время передачи, 727, 734, 736, 742,956 проблема с параметром, 699 слишком большой пакет, 83, 742, 957 эхо-запрос, 709, 713, 715, 956, 1015 эхо-ответ, 709, 715, 956 ICMP6_FILTER, параметр сокета, 242, 714, 724 icmp6_filter, структура, 220, 242, 714 ICMP6FILTERSETBLOCK, макрос, 714 ICMP6_FILTER_ SETBLOCKALL, макрос, 714 ICMP6_FILTER_SETPASS, макрос, 714 ICMP6_FILTER_SETPASSALL, макрос, 714
1044 Алфавитный указатель ICMP6_FILTER_WILLBLOCK, макрос, 714 ICMP6_FILTER_WILLPASS, макрос, 714 icmpcode_v4, функция, 737 icmpcode_v6, функция, 737 icmpd, программа, 740, 743, 745, 885, 890,1022 icmpd.h, заголовочный файл, 746 icmpd_dest, элемент, 743 icmpderr, элемент, 742, 745, 755 icmpd_errno, элемент, 742 ICMPv4 (Internet Control Message Protocol v. 4), 65, 709, 714, 740, 943,955 заголовок, 727 заголовочный файл, 717 контрольная сумма, 711, 724, 775, 956 типы сообщений, 956 ICMPv6 (Internet Control Message Protocol v. 6), 65, 242, 709, 712, 740, 955 заголовочный файл, 717, 729 контрольная сумма, 712,725, 956 параметр сокета, 242 типы сообщений, 957 фильтрация, 714 IE С (International Electrotechnical Commission), 57, 1029 IEEE, 57,477, 513, 535, 952,1029 IEEEIX, 57 IETF (Internet Engineering Task Force), 60,952,1027 if_freenameindex, функция, 509 исходный код, 512 определение, 508 if_index, поле, 980 if_index, элемент, 509 ifindextoname, функция, 508,548, 588 исходный код, 510 определение, 508 if_msghdr, структура, 492,507 if_name, поле, 980 if name, элемент, 509, 512 if_nameindex, структура, 980 определение, 509 if_nameindex, функция, 491,509 исходный код, 511 определение, 508 if_nametoindex, функция, 491,508, 548 исходный код, 509 определение, 508 ifamsghdr, структура, 492 ifam_addrs, элемент, 492,499 ifc_buf, элемент, 474 ifc_len, элемент, 100,475,476 ifc_req, элемент, 474 ifconf, структура, 100,471 ifconfig, программа, 55,126, 249, 477,485 IFFBROADCAST, константа, 485 IFFPOINTOPOINT, константа, 485 IFF PROMISC, константа, 761 IFF_UP, константа, 485 IFI_ALIAS, константа, 568 ifi_hlen, элемент, 478,483,507 ifi_info, структура, 474,476,478, 481,483,489, 504, 507,567, 571, 606 ifi_next, элемент, 476 ifm_addrs, элемент, 492, 499 ifm_type, элемент, 506 IFNAMSIZ, константа, 508 ifr_addr, элемент, 474,485 ifr_broadaddr, элемент, 474,485, 488 ifr_data, элемент, 474 ifr_dstaddr, элемент, 474,485,488 ifr_flags, элемент, 474,485 ifr_metric, элемент, 474, 485 ifr_name, элемент, 476,485 ifreq, структура, 471,481,483,485, 488, 550
Алфавитный указатель 1045 IGMP (Internet Group Management Protocol), 65, 542, 709, 713,943 контрольная сумма, 725 ILP32, модель программиро- вания, 61 imr_interface, элемент, 544, 550 imr_multiaddr, элемент, 544 in-addr.arpa, домен, 283, 294 in.rdisc, программа, 709 inaddr, структура, 95, 220, 287, 293, 309, 544 определение,92 in_addr_t, тип данных, 94 in_cksum, функция, 725 исходный код, 726 in pcbdetach, функция, 166 in pktinfo, структура, 582, 585, 979 определение, 582 in_port_t, тип данных, 94 in6 addr, структура, 96, 220, 287, 293 IN6_IS_ADDR_LINKLOCAL, макрос, 310 IN6_IS_ADDR_LOOPBACK, макрос, 310 IN6_IS_ADDR_MC_GLOBAL, макрос, 310 IN6_IS_ADDR_MC_ LINKLOCAL, макрос, 310 IN6_IS_ADDR_MC_ NODELOCAL, макрос, 310 IN6_IS_ADDR_MC_ORGLOCAL, макрос, 310 IN6_IS_ADDR_MC_SITELOCAL, макрос, 310 IN6_IS_ADDR_MULTICAST, макрос, 310 IN6_IS_ADDR_SITELOCAL, макрос, 310 IN6ISADDRUNSPECIFIED, макрос, 310 IN6_IS_ADDR_V4COMPAT, макрос, 310 IN6_IS_ADDR_V4MAPPED, макрос, 306, 311,314, 719, 822 определение, 310 in6_pktinfo, структура, 582, 612, 706 определение, 612 m6addr_any, константа, 125, 955 IN6ADDRANYINIT, константа, 125, 320,322, 352,418, 613, 955 in6addr_Ioopback, константа, 954 IN6ADDRLOOPBACKJNIT, константа, 954 INADDR_ANY, константа, 46,80, 125,148,151, 240, 257, 320, 322, 352,418,543, 828, 916,950, 989 INADDRLOOPBACK, константа, 950 INADDR_MAX_LOCAL_ GROUP, константа, 989 INADDR_NONE, константа, 96, 978, 989 inet addr, функция, 40, 92,105,117 определение, 105 INET_ADDRSTRLEN, константа, 110, 978 inet_aton, функция, 105, 117 определение, 105 INET IP, константа, 895 inet ntoa, функция, 92,105, 345, 662 определение, 105 inet_ntop, функция, 92, 105,116, 135, 289,343, 345,371, 373,487, 588 версия только для IPv4, исходный код, 108 определение, 107 inet_pton, функция, 40,43, 92,105, 116,334,345,355, 1004 версия только для IPv4, исходный код, 108 определение, 107 inet_pton_Ioose, функция, 117 inet_srcrt add, функция, 691, 695