Книга
Книга
Часть 1. Введение и протоколы TCP/IP
Глава 1. Введение в сетевое программирование34
Глава 2. Транспортный уровень: TCP и UDP 63
Часть 2. Элементарные сокеты
Глава 3. Введение в сокеты 92
Глава 4. Элементарные сокеты TCP118
[лава 5. Пример TCP-соединения клиент-сервер146
Глава 6. Мультиплексирование ввода-вывода:■
функции select и poll ,180
Глава 7. Параметры сокетов216
Глава 8. Основные сведения о сокетах UDP254
Глава 9. Элементарные преобразования имен и адресов 282
Часть 3. Дополнительные возможности сокетов
Глава 10. Совместимость IPv4 и IPv6 . 304
Глава 11. Дополнительные преобразования имен и адресов 315
Глава 12. Процессы-демоны и суперсервер inetd374
Глава 13. Дополнительные функции ввода-вывода 392
Глава 14. Доменные протоколы Unix417
Глава 15. Неблокируемый ввод-вывод440
Глава 16. Операции функции ioctl 470
Глава 17. Маршрутизирующие сокеты 490
Глава 18. Широковещательная передача514
Глава 19. Многоадресная передача 534
Глава 20. Дополнительные сведения о сокетах UDP 581
Глава 21. Внеполосные данные 617
Глава 22. Управляемый сигналом ввод-вывод 641
Глава 23. Программные потоки 652
Глава 24. Параметры IP687
Глава 25. Символьные сокеты 709
Глава 26. Доступ к канальному уровню757
Глава 27. Альтернативное устройство клиента и сервера 781
Часть 4. XTI: транспортный интерфейс Х/Ореп
Глава 28. XTI: ТСР-клиенты 820
Глава 29. XTI: функции имен и адресов841
Глава 30. XTI: ТСР-серверы855
Глава 31. XTI: клиенты и серверы UDP 878
Глава 32. Параметры XTI891
Глава 33. Потоки907
Глава 34. XTI: дополнительные функции926
Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6942
Приложение Б. Виртуальные сети959
Приложение В. Техника отладки 964
Приложение 匚 Различные исходные коды977
Приложение Д. Решения некоторых упражнений987
Литература 1027
Алфавитный указатель1034
Содержание
Предисловие 26
Введение 26
Изменения по сравнению с первым изданием 27
Кому адресована эта книга 29
Исходный код и замеченные опечатки 30
Благодарности30
Об авторе32
От издательства 32
ЧАСТЬ 1. ВВЕДЕНИЕ И ПРОТОКОЛЫ TCP/IP
Глава 1. Введение в сетевое программирование34
Введение 34
Простой клиент времени и даты 37
Независимость от протокола 41
Обработка ошибок: функции-обертки43
Значение системной переменной Unix errno 44
Простой сервер времени и даты45
Список примеров технологии клиент-сервер, используемых в книге48
Модель OSI50
История сетей BSD 52
Сети и узлы, используемые в примерах 54
Определение топологии сети55
Стандарты Unix57
Posix 57
Open Group59
Internet Engineering Task Force60
Версии Unix и переносимость60
64-разрядные архитектуры 60
1.12. Резюме 61
Упражнения62
Глава 2. Транспортный уровень: TCP и UDP 63
Введение 63
Обзор протоколов TCP/IP 63
UDP: протокол пользовательских дейтаграмм66
TCP: протокол управления передачей66
Установление и завершение соединения TCP 68
Трехэтапное рукопожатие 68
Параметры TCP 70
Разрыв соединения TCP 71
Диаграмма состояний TCP72
Просмотр пакетов 73
Состояние TIME WAIT 75
Номера портов 77
Пара сокетов 79
Номера портов TCP и параллельные серверы 79
Размеры буфера и ограничения82
Отправка по TCP 84
Отправка по UDP 86
Стандартные службы Интернета 87
Использование протоколов приложениями Интернета 88
Резюме 89
Упражнения 90
ЧАСТЬ 2. ЭЛЕМЕНТАРНЫЕ СОКЕТЫ
Глава 3. Введение в сокеты 92
Введение 92
Структуры адреса сокетов 92
Структура адреса сокета IPv4 92
Универсальная структура адреса сокета 95
Структура адреса сокета IPv6 96
Сравнение структур адреса сокетов97
Аргументы типа «значение-результат»98
Функции определения порядка байтов100
Функции управления байтами103
Функции inet__aton, inet addr и inet_ntoa 105
Функции inet_pton и inet_ntop 107
Пример 108
Функция sock ntop и связанные с ней функции109
Функцииreadn, writenи readlineIll
Функция isfdtype 115
Резюме 116
Упражнения117
Глава 4. Элементарные сокеты TCP 118
Введение 118
Функция socket118
Сравнение AF_xxx и PF_xxx120
Функция connect 121
Функция bind 123
Функция listen126
Функция accept133
Пример: аргументы типа «значение-результат» 134
Функции fork и ехес136
Параллельные серверы 138
Функция close 141
Счетчик ссылок дескриптора141
Функции getsockname и getpeername142
Пример: получение семейства адресов сокета 144
Резюме 144
Упражнения 145
Глава 5. Пример TCP-соединения клиент-сервер 146
Введение 146
Эхо-сервер TCP: функция main 147
Эхо-сервер TCP: функция str echo 148
Эхо-клиент TCP: функция main - 149
Эхо-клиент TCP: функция str_cli 150
Нормальный запуск151
Нормальное завершение153
Обработка сигналов Posix 154
Функция signal 155
Семантика сигналов Posix157
Обработка сигналов SIGCHLD157
Обработка зомбированных процессов 157
Обработка прерванных системных вызовов159
Функции wait и waitpid 160
Различия между функциями wait и waitpid 161
Прерывание соединения перед завершением функции accept 165
Завершение процесса сервера 166
Сигнал SIGPIPE168
Сбой на узле сервера170
Сбой и перезагрузка на узле сервера 171
Выключение узла сервера 172
Итоговый пример TCP172
Формат данных174
Пример: передача текстовых строк между клиентом и сервером174
Пример: передача двоичных структур между клиентом и сервером ... 174
Резюме 177
Упражнения178
Глава 6. Мультиплексирование в вода-вы вода: функции select и poll 180
Введение 180
Модели ввода-вывода181
Модель блокируемого ввода-вывода181
Модель неблокируемого ввода-вывода 182
Модель мультиплексирования ввода-вывода 182
Модель ввода-вывода, управляемого сигналом184
Модель асинхронного ввода-вывода184
Сравнение моделей ввода-вывода 185
Сравнение синхронного и асинхронного ввода-вывода 186
Функция select187
При каких условиях дескриптор становится готовым? 190
Максимальное число дескрипторов для функции select192
Функция str cli (продолжение) 193
Пакетный ввод195
Функция shutdown 198
Функция stjcli (еще раз) 199
Эхо-сервер TCP (продолжение) 201
Атака типа «отказ в обслуживании» 206
Функция pselect 206
Функция poll 208
Эхо-сервер TCP (еще раз)211
Резюме 213
Упражнения214
Глава 7. Параметры сокетов216
Введение 216
Функции getsockopt и setsockopt 216
Проверка наличия параметра и получение значения по умолчанию .... 220
Состояния сокетов 223
Общие параметры сокетов224
Параметр сокета SO_BROADCAST 224
Параметр сокета SO DEBUG224
Параметр сокета SO DONTROUTE 224
Параметр сокета SO_ERROR 225
Параметр сокета SO_KEEPALIVE 225
Параметр сокета SO_LINGER227
Параметр сокета SO OOBINLINE 233
Параметры сокета SO_RECVBUF и SO_SNDBUF233
Параметры сокета SO RCVLOWAT и SOJSNDLOWAT235
Параметры сокета SCCRCVTIMEO и SOjSNDTIMEO 235
Параметры сокета SO REUSEADDR и SO_REUSEPORT236
Параметр сокета SO TYPE 239
Параметр сокета SO USELOOPBACK240
Параметры сокетов IPv4 240
Параметр сокета IP HRDINCL 240
Параметр сокета IP OPTIONS240
Параметр сокета IP RECVDSTADDR 241
Параметр сокета IP_RECVIF 241
Параметр сокета IP_TOS 241
Параметр сокета IP TTL241
Параметр сокета ICMPv6242
Параметр сокета ICMP6一FILTER242
Параметры сокетов IPv6 242
Параметр сокета IPv6_ADDRFORM 242
Параметр сокета IPv6__CHECKSUM 242
Параметр сокета IPv6_DSTOPTS 243
Параметр сокета IPv6_HOPLIMIT243
Параметр сокета IPv6_HOPOPTS243
IPv6_NEXTHOP …二 243
Параметр сокета IPv6_PKTINFO 243
Параметр сокета IPv6_PKTOPTIONS244
Параметр сокета IPv6_RTHDR 244
Параметр сокета IPv6_UNICAST_HOPS244
Параметры сокетов TCP 244
Параметр сокета TCP_KEEPALFVE244
Параметр сокета TCP_MAXRT 244
Параметр сокета TCP_MAXSEG245
Параметр сокета TCP_NODELAY245
Параметр сокета TCP_STDURG 248
Функция fcntl 248
Резюме 251
Упражнения252
254254Глава 8. Основные сведения о сокетах UDP
254
254
Функции recvfrom и sendtoЭхо-сервер UDP: функция main 84 ЭхО'сервер: функция dg_echoЭхо'клиент UDP: функция main86 Эхо'клиент UDP: функция dg_cliПотерянные дейтаграммы Проверка полученного ответаЗапуск клиента без запуска сервераИтоговый пример клиент-сервера UDPФункция connect для UDPМногократный вызов функции connect для сокета UDPПроизводительностьФункция dg_cli (продолжение)Отсутствие управления потоком в UDP Приемный буфер сокета UDPОпределение исходящего интерфейса для UDP Эхо-сервер TCP и UDP, использующий функцию selectФункции recvfrom и sendtoЭхо-сервер UDP: функция main 84 ЭхО'сервер: функция dg_echoЭхо'клиент UDP: функция main86 Эхо'клиент UDP: функция dg_cliПотерянные дейтаграммы Проверка полученного ответаЗапуск клиента без запуска сервераИтоговый пример клиент-сервера UDPФункция connect для UDPМногократный вызов функции connect для сокета UDPПроизводительностьФункция dg_cli (продолжение)Отсутствие управления потоком в UDP Приемный буфер сокета UDPОпределение исходящего интерфейса для UDP Эхо-сервер TCP и UDP, использующий функцию select5 6 7 95 5 5 52 2 2 25 6 7 95 5 5 52 2 2 20 114 5 76 6 6 6 6 62 2 2 2 2 20 114 5 76 6 6 6 6 62 2 2 2 2 27027027027027 7 7 7 72 2 2 2 27 7 7 7 72 2 2 2 212 5 6 712 5 6 7
Функции recvfrom и sendto
Эхо-сервер UDP: функция main
84 ЭхО'сервер: функция dg_echo
Эхо'клиент UDP: функция main
86 Эхо'клиент UDP: функция dg_cli
Потерянные дейтаграммы
Проверка полученного ответа
Запуск клиента без запуска сервера
Итоговый пример клиент-сервера UDP
Функция connect для UDP
Многократный вызов функции connect для сокета UDP
Производительность
Функция dg_cli (продолжение)
Отсутствие управления потоком в UDP
Приемный буфер сокета UDP
Определение исходящего интерфейса для UDP
Эхо-сервер TCP и UDP, использующий функцию select
Функции recvfrom и sendto
Эхо-сервер UDP: функция main
84 ЭхО'сервер: функция dg_echo
Эхо'клиент UDP: функция main
86 Эхо'клиент UDP: функция dg_cli
Потерянные дейтаграммы
Проверка полученного ответа
Запуск клиента без запуска сервера
Итоговый пример клиент-сервера UDP
Функция connect для UDP
Многократный вызов функции connect для сокета UDP
Производительность
Функция dg_cli (продолжение)
Отсутствие управления потоком в UDP
Приемный буфер сокета UDP
Определение исходящего интерфейса для UDP
Эхо-сервер TCP и UDP, использующий функцию select
5 6 7 9
5 5 5 5
2 2 2 2
5 6 7 9
5 5 5 5
2 2 2 2
0 114 5 7
6 6 6 6 6 6
2 2 2 2 2 2
0 114 5 7
6 6 6 6 6 6
2 2 2 2 2 2
70
2
70
2
70
2
70
2
7 7 7 7 7
2 2 2 2 2
7 7 7 7 7
2 2 2 2 2
12 5 6 7
12 5 6 7
280280Упражнения
280
280
Глава 9. Элементарные преобразования имен и адресов 282
Введение 282
Система доменных имен282
Записи ресурсов283
Распознаватели и серверы имен 284
Альтернативы DNS 285
Функция gethostbyname 285
Пример 288
Параметр распознавателя RES_USE_INET6 290
Функция gethostbyname2 и поддержка IPv6291
Функция gethostbyaddr 293
Функция gethostbyaddr и поддержка IPv6 294
Функция uname294
Пример: определение IP-адресов локального узла295
Функция gethostname 295
Функции getservbyname и getservbyport296
Пример: использование функций gethostbyname и getservbyname .... 297
Другая информация о сетях 300
Резюме 301
Упражнения301
ЧАСТЬ 3- ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ СОКЕТОВ
Глава 10. Совместимость IPv4 и IPv6 304
Введение 304
Клиент IPv4, сервер IPv6305
Клиент IPv6, сервер IPv4308
Резюме: совместимость IPv4 и IPv6 310
Макроопределения проверки адреса IPv6310
Параметр сокета IPv6_ADDRFORM 311
Переносимость исходного кода313
Резюме 314
Упражнения314
Глава 11. Дополнительные преобразования имен и адресов 315
Введение 315
Функция getaddrinfo315
Функция gai_strerror321
Функция freeaddrinfo 321
1L5. Функция getaddrinfo: IPv6 и доменный сокет Unix 322
Функция getaddrinfo: примеры 325
Функция host_serv327
Функция tcp__connect 328
Пример: клиент времени и даты 329
Функция tcp_listen 331
Пример: сервер времени и даты 333
Пример: сервер времени и даты с указанием протокола334
Функция udp_client 336
Пример: не зависящий от протокола клиент времени и даты337
Функция udp_connect 338
Функция udp_server339
Пример: не зависящий от протокола сервер времени и даты 341
Функция getnameinfb 341
Функции, допускающие повторное вхождение343
Функции gethostbyname_r и gethostbyaddr_r 347
Реализация функций getaddnnfo и getnameinfo 349
Создание структуры addnnfb для каждого адреса 357
Резюме372
Упражнения372
Глава 12. Процессы-демоны и суперсервер inetd .. .374
Введение 374
£22 Демон syslogd375
Функция syslog376
Функция daemon_init378
Пример: сервер времени и даты в качестве демона381
Демон inetd382
Функция daemon_inetd 388
Пример: сервер времени и даты, активизированный демоном inetd …388
Резюме 390
Упражнения390
Глава 13. Дополнительные функции ввода-вывода 392
13Л. Введение 392
Тайги-ауты сокета392
Тайм-аут для функции connect (сигналSIGALRM) 393
Тайм-аут для функции recvfrom (сигнал SIGALRM) 394
п» Тайм-аут для функции recvfrom (функция select) 395
Тайм-аут для функции recvfrom (параметр сокета SO RCVTIMEO) 397
£3.3. Функции recv и send 398
Функции readv и writev 400
Функции recvmsg и sendmsg400
Вспомогательные данные 405
Сколько данных находится в очереди?408
Сокеты и стандартный ввод-вывод 409
Пример: функция stjecho, использующая стандартный ввод-вывод410
Т/ТСР: TCP для транзакций 412
Резюме415
Упражнения 415
Глава 14. Доменные протоколы Unix417
Введение 417
Структура адреса доменного сокета Unix 418
Пример: функция bind и доменный сокет Unix418
Функция socketpair 420
Функции сокетов 421
Клиент и сервер потокового доменного протокола Unix 422
Клиент и сервер дейтаграммного доменного протокола Unix424
Передача дескрипторов 426
Пример передачи дескриптора 427
Получение информации об отправителе 434
Пример 435
Резюме 438
Упражнения438
Глава 15. Неблокируемый ввод-вы вод440
Введение 440
Неблокируемые чтение и запись: функция str_cli (продолжение) 441
Более простая версия функции str_cli 449
Сравнение времени выполнения различных версий функции strcli452
Неблокируемая функция connect453
Неблокируемая функция connect: клиент времени и даты 454
Прерванная функция connect 457
Неблокируемая функция connect: клиент Web 457
Эффективность одновременных соединений465
Неблокируемая функция accept 466
Резюме 468
Упражнения469
Глава 16. Операции функции ioctl 470
Введение 470
Функция ioctl 470
Операции с сокетами472
Операции с файлами473
Конфигурация интерфейса473
Функция get_ifi_info475
Операции с интерфейсами 484
Операции с кэшем ARP 485
Пример: вывод аппаратного адреса узла 486
Операции с таблицей маршрутизации 488
Резюме488
Упражнения 488
Глава 17. Маршрутизирующие сокеты490
Введение 490
Структура адреса сокета канального уровня491
Чтение и записьГ492
Пример: получение и вывод записи из таблицы маршрутизации 494
Операции функции sysctl 500
Пример: определяем» включены ли контрольные суммы UDP 503
Функция get ifi info504
Функции имени и индекса интерфейса 508
.、 Функция i^nametoindex509
Функция if_indextoname510
' Функция i 匚 nameindex511
Функция ijfreenameindex 512
Резюме 512
Упражнения513
Глава 18. Широковещательная передача514
Введение 514
Широковещательные адреса516
Сравнение направленной и широковещательной передач 517
Функция dg_cli при использовании широковещательной передачи ... 521
IP-фрагментация и широковещательная передача524
Ситуация гонок524
Применение IPC в обработчике сишала функции530
Резюме 532
Упражнения533
Глава 19. Многоадресная передача 534
Введение 534
Адрес многоадресной передачи 534
Адреса IPv4 класса D 534
Адреса многоадресной передачи IPv6536
Область действия адресов многоадресной передачи 536
Сравнение многоадресной и широковещательной передач в локальной сети 538
Многоадресная передача в глобальной сети 540
Параметры сокетов многоадресной передачи543
Функция mcastjoin и родственные функции 547
Пример: функция mcast_join 549
Пример: функция mcast_set_loop 551
Функция dg_cli, использующая многоадресную передачу552
Фрагментация IP и многоадресная передача 552
Получение анонсов сеанса МВопе 553
Отправка и получение 556
SNTP: простой синхронизирующий сетевой протокол560
SNTP (продолжение) 564
Резюме578
Упражнения579
Глава 20. Дополнительные сведения о сокетах UDP .. . 581
Введение 581
Получение флагов, IP-адреса получателя и индекса интерфейса 582
Пример: вывод IP-адреса получателя и уведомления о том, что дейтаграмма обрезана 586
Обрезанные дейтаграммы 589
Когда UDP оказывается предпочтительнее TCP 590
Добавление надежности приложению UDP 593
Пример 596
Связывание с адресами интерфейсов 604
Параллельные серверы UDP 609
Информация о пакете IPv6612
Исходящий и входящий интерфейс613
Адрес отправителя и адрес получателя IPv6 613
Задание и получение предельного количества транзитных узлов614
Задание адреса следующего транзитного узла614
Резюме 615
Упражнения615
Глава 21. Внеполосные данные617
Введение 617
Внеполосные данные протокола TCP617
Простой пример использования сигнала SIGURG 620
Простой пример использования функции select623
Функция sockatmark 625
Пример 626
Пример 628
Пример 631
Резюме по теме внеполосных данных TCP 632
Клиент-серверные функции проверки пульса 634
Резюме 639
Упражнения640
Глава 22. Управляемый сигналом ввод-вывод 641
2.1. Введение 641
12 2 4 11 2 2 33455568023 9250456 7 77947893678 9 9 о 646464646565№65656565656565656566666666676768686868^^^^^^^^^^^Оетоп12 2 4 11 2 2 33455568023 9250456 7 77947893678 9 9 о 646464646565№65656565656565656566666666676768686868^^^^^^^^^^^Оетоп
12 2 4 11 2 2 33455568023 9250456 7 77947893678 9 9 о 646464646565№65656565656565656566666666676768686868^^^^^^^^^^^Оетоп
12 2 4 11 2 2 33455568023 9250456 7 77947893678 9 9 о 646464646565№65656565656565656566666666676768686868^^^^^^^^^^^Оетоп
2 Управляемый сигналом ввод-вывод для сокетов
Сигнал SIGIO и сокеты UDP
Сигнал SIGIO и сокеты TCP
3 Эхо-сервер UDP с использованием сигнала SIGIO
22 4 Резюме
Упражнение
Глава 23. Программные потоки
1 Введение
2 Основные функции для работы с потоками создание и завершение потоков
Функция pthread_create
Функция pthread」oin
Функция pthread_self
'尸 Функция pthread_detach
Функция pthread exit
3 Использование потоков в функции str__cli
4 Использование потоков в эхо-сервере TCP
Передача аргументов новым потокам
Функции, безопасные в многопоточной среде
23 5 Собственные данные потоков
Пример функция readline, использующая собственные данные потока
23 6 Web-клиент и одновременное соединение (продолжение)
23 7 Взаимные исключения
23 8 Условные переменные
23 9 Web-клиент и одновременный доступ
23 10 Резюме
Упражнения
Глава 24. Параметры IP
24 1 Введение
24 2 Параметры IPv4
24 3 Параметры маршрута от отправителя IPv4
Пример
Уничтожение маршрута, полученного от отправителя
24 4 Заголовки расширения IPv6
24 5 Параметры транзитных узлов и параметры получателя IPv6
24 6 Заголовок маршрутизации IPv6
24 7 «Закрепленные» параметры IPv6
24 8 Резюме
Упражнения
Глава 25. Символьные сокеты
25 1 Введение
25 2 Создание символьных сокетов
25 3 Вывбд на символьном сокете710
Особенности символьного сокета версии IPv6711
Параметр сокета IPv6_CHECKSUM712
25 4 Ввод через символьный сокет712
Фильтрация ICMPv6714
25 5 Программа Ping715
25 6 Программа Traceroute727
Пример739
25 7 Демон сообщений ICMP740
Эхо-клиент UDP, использующий демон icmpd743
, Примеры эхо-клиента UDP746
Демон icmpd746
25 8 Резюме756
Упражнения756
Глава 26. Доступ к канальному уровню757
26 1 Введение757
26 2 BPF пакетный фильтр BSD757
263 DLPI интерфейс поставщика канального уровня760
264 Linux SOCK_PACKET761
26 5 Libcap библиотека для захвата пакетов762
26 6 Анализ поля контрольной суммы UDP762
Пример779
26 7 Резюме780
Упражнения780
Глава 27. Альтернативное устройство клиента и сервера 781
27 1 Введение781
27 2 Альтернативы для клиента TCP785
27 3 Тестовый клиент TCP785
27 4 Последовательный сервер TCP787
27 5 Параллельный сервер TCP один дочерний процесс для
каждого клиента.. 787
27 6 Сервер TCP с предварительным порождением процессов
без блокировки для вызова accept. г1 791
Реализация 4 4BSD.. . 794
Эффект наличия слишком большого количества дочерних'
процессов... 796
Распределение клиентских соединений между дочерними:
процессами796
Коллизии при вызове функции select797
27 7 Сервер TCP с предварительным порождением процессов
и защитой вызова accept блокировкой файла798
Эффект наличия слишком большого количества дочерних процессов 801
Распределение клиентских соединении между дочерними процессами801
27 8 Сервер TCP с предварительным порождением процессов и защитой вызова accept при помощи взаимного исключения802
27 9 Сервер TCP с предварительным порождением процессов передача дескриптора804
27 10 Параллельный сервер TCP один поток для каждого клиента809
27 И Сервер TCP с предварительным порождением потоков, каждый из которых вызывает accept811
27 12 Сервер с предварительным порождением потоков основной поток вызывает функцию accept813
27 13 Резюме817
Упражнения818
ЧАСТЬ 4. XTI: ТРАНСПОРТНЫЙ ИНТЕРФЕЙС X/OPEN
Глава 28. XTI: ТСР-клиенты820
28 1 Введение820
28 2 Функция t_open821
28 3 Функции t_error и t_strerror825
28 4 Структуры netbuf и структуры протокола XTI826
28 5 Функция t_bind827
28 6 Функция t_connect829
28 7 Функции t_rcv и t__snd830
28 8 Функция tjook832
289 Функции t_sndrel и t__rcvrel833
28 10 Функции t_snddis и t_rcvdis834
28 И Клиент времени и даты для протоколов XTI и TCP835
Возможность взаимодействия сокетов и XTI838
28 12 ①ункин* xti_rdwr838
28 13 Резюме840
Упражнения840
Глава 29. XTI: функции имен и адресов 841
29 1 Введение841
29 2 Файл /etc/netconfig и функции netconfig841
29 3 Переменная NETPATH и функция netpath843
29 4 Функции netdir844
29 5 ①ункцин t_alloc и t_free846
29 6 Функции t_getprotaddr848
29 7 Функция xti_ntop848
29 8 Функция tcp_connect849
Пример852
29 9 Резюме853
Упражнения854
Глава 30. XTI: ТСР-серверы
855
30 1 Введение
855
30 2 Функция t_listen
857
30 3 Функция tcp listen
858
304 Функция t_accept
860
305 Функция xti accept
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
31 2 Функции t_rcvudata и t__sndudata
878
31 3 Функция udp__chent
879
Пример клиент времени и даты
882
31 4 Функция t_rcvuderr асинхронные ошибки
883
Пример сообщение ICMP о недоступности порта
884
31 5 Функция udp_server
886
Пример сервер времени и даты
887
31 6 Чтение дейтаграммы по частям
888
31 7 Резюме
890
Глава 32. Параметры XTI
891
32 1 Введение
891
32 2 Структура t_opthdr
894
32 3 Параметры XTI
895
Параметр XTIDEBUG
895
Параметр XTILINGER
896
Параметры xfi_RCVBUF и XT1RCVLOWAT
896
Параметры XTl^SNDBUF и XTI SNDLOWAT
896
Параметр T IP BROADCAST
896
Параметр T„IP_DONTROUTE
896
Параметр Т IP OPTIONS
896
Параметр T_IP_REUSEADDR
897
Параметр TIPTOS
897
Параметр TIPTTL
897
Параметр Т ТСР КЕЕРALIVE
898
Параметр T_TCP_MAXSEG
898
Параметр T_TCP_NODELAY
898
Параметр T_UDP_CHECKSUM
898
32 4 Функция t_optmgmt
899
.5. Проверка наличия параметра и получение значения по умолчанию ... 899
.6. Получение и установка значений параметров XTI903
Функция xtigetopt 903
Функция xti_setopt 904
Пример 906
.7. Резюме 906
Глава 33. Потоки907
Введение 907
332 Обзор *907
Типы сообщений 910
Функции getmsg и putmsg912
Функции getpmsg и putpmsg913
Функция ioctl 914
TPI: интерфейс поставщика транспортных служб 914
Резюме 925
Упражнение925
Глава 34. XTI: дополнительные функции926
Введение 926
Неблокируемый ввод-вывод926
Функция t_rcvconnect 927
Функция t getinfo 928
Функция t getstate 928
Функция t_sync 929
Функция t_unbind 931
Функции t_rcvv и t_rcwudata931
Функции t_sndv и t_sndvudata 932
Функции t rcvreldata и t_sndreldata932
Управляемый сигналом ввод-вывод 933
Внеполосные данные 934
Пример использования сигнала SIGPOLL 935
Пример использования функции poll938
Поставщики транспортных служб закольцовки 939
Резюме940
Приложение А. Протоколы IPv4, IPv6, ICMPv4 и ICMPv6 942
АД. Введение942
А.2. Заголовок IPv4942
А.З. Заголовок IPv6 944
А.4. Адресация IPv4946
Бесклассовые адреса и CIDR947
Адреса подсетей 948
Адрес закольцовки950
Неопределенный адрес950
Многоинтерфейсность и псевдонимы адресов 950
А.5. Адресация IPv6 951
Объединяемые глобальные индивидуальные адреса952
Тестовые адреса бЬопе 953
Адреса IPv4, преобразованные к виду IPv6953
Адреса IPv4, совместимые с IPv6 954
Адрес закольцовки954
Неопределенный адрес 954
Адрес локальной связи 955
Адрес, локальный на уровне сайта955
А.6. ICMPv4 и ICMPv6: протокол управляющих сообщений в сети Интернет 955
Приложение Б. Виртуальные сети 959
Б.1. Введение959
Б.2. МВопе 959
Б.З. бЬопе961
Приложение В. Техника отладки964
Трассировка системного вызова 964
Библиотека потоковых сокетов SVR4 964
Потоковая XTI-библиотека SVR4 966
Сокеты ядра BSD 968
Сокеты ядра Solaris 2.6 968
Стандартные службы Интернета 969
Программа sock969
Небольшие тестовые программы 972
Пример: определение полосы приоритета внеполосных данных XTI 972
Пример: определение события для получения внеполосных (срочных) данных XTI973
Программа tcpdump 974
Программа netstat 975
Программа Isof 975
Приложение Г. Различные исходные коды 977
Г.1. Заголовочный файл unp.h 977
Г.2. Заголовочный файл config.h 981
Г.З. Заголовочный файл unpxti.h 982
Г.4. Стандартные функции обработки ошибок983
Приложение Д. Решения некоторых упражнений ,.. 987
Глава 1 987
Глава 2 988
Глава 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
Глава 33 1026
Литература1027
Алфавитныйуказатель1034
Салли, Биллу, Элен и Дэвиду Aloha nui loa
Предисловие
Введение
Сетевое программирование подразумевает написание программ, взаимодействующих через сеть. Одна из этих программ обычно называется клиентом, а другая —сервером. В большинстве операционных систем имеются предварительно скомпилированные программы, взаимодействующие через сеть — в мире TCP/IP наиболее типичным примером таких программ являются web-клиенты (браузеры) и web-серверы, а также клиенты и серверы FTP и Telnet, — однако в этой книге рассказывается о том, как писать собственные сетевые приложения.
Сетевые приложения пишутся с использованием программного интерфейса приложений, или API (Application Program Interface). В этой книге мы рассматриваем два API для сетевого программирования:
Сокеты, иногда называемые Беркли-сокетами (Berkeley), что указывает на их связь с Berkeley Unix.
XTI (X/Open Transport Interface — транспортный интерфейс группы X/Open), являющийся модификацией TLI (Transport Layer Interfoce — интерфейс транспортного уровня), разработанного группой 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 и т. д.). При понимании того, как реализована та или иная функциональная возможность, ее применение в приложениях становится более осмысленным.
Изменения по сравнению с первым изданием
Второе издание этой книги содержит очень много изменений. Все они стали результатом моего опыта преподавания данного материала и чтения материалов сетевых конференций Usenet в период с 1990 по 1996 год — это дало мне возможность выявить те темы и концепции сетевого программирования, которые регулярно оказываются неверно понятыми. Ниже перечислены основные изменения, внесенные во второе издание:
Для всех примеров в этом издании используется ANSI С.
Старые главы 6 («Berkeley Sockets» — «Сокеты Беркли») и 8 («Library Routins» —«Библиотечные функции») были расширены, и теперь соответствующий материал занимает 25 глав. Это семикратное увеличение (из расчета количества слов), вероятно, является самым значительным изменением со времени первого издания. Большинство разделов прежней главы 6 теперь выросли в отдельные главы, при этом было увеличено количество примеров.
Части прежней главы 6, посвященные TCP и UDP, теперь разделены. Сначала мы рассматриваем функции TCP и все, что относится к клиент-серверному взаимодействию TCP, а затем — функции UDP и взаимодействие клиента и сервера UDP. Для новичков в этой области такой подход будет проще, чем, например, подробное изучение всех особенностей функции connect с различной семантикой в случае UDP и TCP.
Старая глава 7 («System V Transport Layer Interface» — «Интерфейс транспортного уровня System V») была расширена, и теперь соответствующий материал занимает 7 глав. Мы также рассматриваем более новую технологию XTI, пришедшую на смену технологии ТЫ, о которой шла речь в первом издании. Глава 2 первого издания («The Unix Model» — «Модель Unix») не вошла во второе издание. В этой главе содержался обзор системы Unix, и занимала она 75 страниц. В 1990 году эта глава была необходима, поскольку было не так много книг, адекватно описывающих основной программный интерфейс Unix (в частности, существовавшие на тот период различия между реализациями Беркли и System V). В настоящее время гораздо больше читателей имеют представление о Unix, и поэтому такие понятия, как идентификатор пользователя, файлы паролей, каталоги и идентификаторы групп пользователей, уже не нуждаются в особом пояснении. (Для читателей, которым необходима дополнительная информация в области программирования под Unix, предназначена моя книга [93], содержащая 700 страниц материала по этим вопросам.)
Некоторые более сложные темы из прежней главы 2, предназначенные для опытных программистов, вошли и во второе издание, но они рассматриваются параллельно с применением соответствующей функциональности. Например, при рассмотрении нашего первого параллельного сервера (раздел 4.8) мы подробно описываем функцию fork. Когда мы описываем обработку сигнала SIGCHLD, мы рассказываем о многих дополнительных функциональных возможностях обработки сигналов Posix (зомбированные процессы, прерванные сис
темные вызовы и т. д.).
Везде в книге мы стремились по возможности использовать стандарт Posix. (Более подробно о семействе стандартов Posix говорится в разделе 1.10.) Сюда относятся не только стандарт Posix. 1 для основных функций Unix (управление процессами, сигналы и т. п.), но и более новый стандарт Posix. 1g для сокетов и XTI и стандарт Posix. 1 1996 года для потоков.
При описании таких функций, как socket и connect, термин «системный вызов» был заменен на термин «функция» Это связано с принятым в Posix соглашением о том, что различие между системным вызовом и функцией — это подробность реализации, как правило, не имеющая значения для программиста.
Прежние главы 4 («А Network Primer» — «Сетевой букварь») и 5 («Communication 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).
Виртуальные сети и туннелирование (приложение Б).
Технологии отладки сетевых программ (приложение В).
К первому изданию было сделано столько добавлений, что, к сожалению, получившийся материал не может войти в одну книгу. Поэтому планируется выпустить еще как минимум два тома.
Второй том1 получит подзаголовок «Взаимодействие процессов» (IPC: Interprocess Communications) и станет расширением главы 3 первого издания. В него также войдет описание механизмов IPC реального времени по стандарту Posix.l.
Третий том будет иметь подзаголовок «Приложения» («Applications») и будет представлять собой расширение глав 9-18 первого издания.
Большинство сетевых приложений будет рассматриваться в третьем томе, но некоторые специальные приложения рассматриваются в этом томе, а именно программы Ping, Traceroute и демон metd.
Кому адресована эта книга
Эту книгу можно использовать и как учебное пособие по сетевому программированию, и как справочник для более опытных программистов. При использовании его как учебника или для ознакомления с сетевым программированием следует уделить особое внимание второй части («Элементарные сокеты», главы 3-9), после чего можно переходить к чтению тех глав, которые представляют наибольший интерес. Во второй части рассказывается об основных функциях сокетов — как для TCP, так и для UDP; кроме того, рассматриваются мультиплексирование ввода-вывода, параметры сокетов и основные преобразования имен и адресов. Всем читателям следует прочесть главу 1, в особенности раздел 1.4, так как в нем описаны некоторые функции-обертки, используемые далее во всей книге. Глава 2 и, возможно, приложение А могут быть использованы по мере необходимости для получения справочных сведений в зависимости от уровня подготовки читателя. Большинство глав в третьей части («Дополнительные возможности сокетов») могут быть прочитаны независимо от других содержащихся в этой же части.
Для тех, кто собирается использовать эту книгу в качестве справочного пособия, имеется подробный предметный указатель. Для тех, кто будет читать только выборочные главы в произвольном порядке, в книге имеются ссылки на те места, где обсуждаются близкие темы.
Хотя API сокетов стал фактическим стандартом сетевого программирования, наравне с ним используется и XTI, иногда с отличным от TCP/IP набором протоколов. Интерфейсу XTI посвящена четвертая часть. Он описан не так подробно, как интерфейс сокетов во второй и третьей частях. Это объясняется тем, что основные концепции интерфейса сокетов применимы и к XTL Например, все кон-
Этот том также переведен н выпущен издательством «Питер» У. Стивенс UNIX взаимодействие процессов. — СПб: Питер, 2002.
цепции интерфейса сокетов, относящиеся к использованию неблокируемого ввода-вывода, широковещательной и многоадресной передачи, управляемого сигналом ввода-вывода, внеполосных данных и потоков, остаются в силе и для интерфейса XTI. Действительно, многие аспекты сетевого программирования схожи в своей основе независимо от того, сокеты или XTI вы используете в своих программах, и вряд ли есть какие-то задачи, которые один API позволяет решить, а другой нет. Концепции остаются неизменными — меняются лишь имена функций и их аргументы.
Исходный код и замеченные опечатки
Исходный код для всех примеров расположен на моей домашней странице, адрес которой указан в конце предисловия. Чтобы научиться сетевому программированию, лучше всего будет взять эти программы, изменить их и расширить. На самом деле написание программ таким образом является единственным способом овладеть изученными технологиями. В конце каждой главы приводятся упражнения, а ответы на большинство из них содержатся в приложении Г.
Список найденных опечаток по этой книге также находится на моей домашней странице.
Благодарности
Для каждого автора самой существенной является поддержка со стороны семьи — без этого не было бы написано ни одной книги! Я благодарен всем членам моей семьи — Салли, Биллу, Элен и Дэвиду — за их поддержку и понимание при написании моей первой книги (первого издания этой книги) и за их терпение, проявленное во время создания этой «маленькой» переделки. Благодаря их любви, поддержке и одобрению стало возможным появление этой книги.
Многочисленные рецензенты снабдили меня ценными замечаниями и указаниями (всего более 190 страниц текста, или 70 000 слов), обращая мое внимание на многочисленные ошибки и те области, которые требовали более подробного изложения, а также предложили альтернативные варианты формулировок, изложения материала и самих программ. Это Рагнвалд Блайндхейм (Ragnvald Blindheim), Джим Баунд (Jim Bound), Гэвин Боуи (Gavin Bowe), Аллеи Бриггс (Alien Briggs), Джо Дупник (Joe Doupnik), Вучан Фен (Wuchang Feng), Билл Феннер (Bill Fenner), Боб Фриснан (Bob Friesenhahn), Эндрю Гиерт (Andrew Gierth), Вайн Хэтвей (Wayne Hathaway), Кент Хофер (Kent Hofer), Сьюги Джа- мин (Sugih Jamin), Скотт Джонсон (Scottjohnson), Рик Джонс (Rick Jones), Ma- кеш Кэкер (Mukesh Каскег), Марк Лампо (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), Андреас Терзис (Andreas Terzis) и Дейв Телер (Dave Thaler).
Особую благодарность я выражаю С. Джамину и его студентам по курсу 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 Rafeky) и замечательной команде Gari Software за многочисленные интересные обсуждения технических деталей. Спасибо тебе, Ларри!
Многие люди н те организации, в которых они работали, шли мне навстречу, предоставляя программное обеспечение или доступ к системе, необходимые для тестирования некоторых примеров к книге.
Мег МакРобертс (Meg McPoberts) из SCO предоставил последние выпуски UnixWare, а Дион Джонсон (Dion Johnson), Ясмин Куреши (Yasmin Kureshi), Майкл Таунсенд (Michael Townsend) и Брайан Зиел (Brian Ziel) отвечали на мои многочисленные вопросы.
Макет Кэкер (Mukesh Kacker) из 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) отвечал на мои многочисленные вопросы по этой теме.
Многие помогали мне при работе с Интернетом. Еще раз хочу поблагодарить сотрудников Национальной оптической астрономической обсерватории (NOAO)— Сиднея Вольфа (Sidney Wolff), Ричарда Вольфа (Richard Wolf) и Стива Гранди (Steve Grandi) — за обеспечение доступа к их сетям и узлам. Дейв Сигал (Dave Siegel), Джастус Аддис (Justus Addiss) и Поль Лачино (Paul Lucchina) отвечали на мои многочисленные вопросы, Фил Кэсло (Phil Kaslo) и Джим Дэвис (Jim Davis) обеспечивали соединение с Mbone, Рэн Аткинсон (Ran Atkinson) и Педро Маркес (Perdo Marques) обеспечивали соединение с бЬопе, а Крейг Метц (Craig Metz) много помогал мне при работе с DNS.
Сотрудники издательства Prentice Hall, особенно мой редактор Мэри Франц (Mary Franz) вместе с Норин Реджин (Noreen Regina), Софи Папаниколау (Sophie Papamkolaou) и Эйлин Кларк (Eileen Clark), — чудесная находка для автора Я очень признателен за то, что имел возможность столь многое сделать именно так, как мне хотелось
Как обычно (но в противоположность общепринятым технологиям), я сделал оригинал-макет книги, используя замечательный пакет groff, написанный Джеймсом Кларком (James Clark) Я набрал все 291 972 слова, используя редактор vi, создал 201 иллюстрацию с помощью программы gpic (используя многие из макросов Гари Райта), сделал 81 таблицу с помощью программы gtbl, составил предметный указатель и подготовил окончательный макет страниц Программа Дейва Хансона (Dave Hanson) loom и некоторые сценарии Гари Райта (Gary Wright) использовались для включения кода программ в книгу Набор сценариев на языке dwk, написанный Джоном Бентли (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 Illustrated»
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу электронной по* ты comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение,
Исходные коды всех программ, приведенных в книге, вы можете найти по ад~
ресу http.//www.piter.com/downLoad
На web сайте издательства http://www.piter.com вы найдете подробную информацию о наших книгах
ЧАСТЬ 1
ВВЕДЕНИЕ И ПРОТОКОЛЫ TCP/IP
ГЛАВА 1
Введение в сетевое программирование
Введение
Большинство сетевых приложений можно разделить на две группы: клиенты и серверы. Связь между ними демонстрирует рис. 1.1.
Рис. 1.1. Сетевое приложение: клиент и сервер
Можно привести множество примеров клиентов и серверов, с которыми, вероятно, читатель знаком: web-браузер (клиент), соединяющийся с web-сервером; клиент FTP, получающий файлы с сервера FTP; клиент Telnet, используемый нами для того, чтобы войти на удаленный узел через сервер Telnet на этом удаленном узле.
Обычно за один сеанс клиенты устанавливают соединение с одним сервером, хотя если говорить о web-браузере, мы можем соединиться со множеством различных web-серверов, скажем, в течение 10 минут. Однако с точки зрения сервера нет ничего удивительного в том, что в любой момент времени он соединяется
Рис. 1.2. Сервер, обслуживающий одновременно множество клиентов
со множеством клиентов (рис. 1.2). Далее в этой главе будут рассмотрены различные возможности взаимодействия сервера одновременно со множеством клиентов.
Взаимодействие клиентского и серверного приложений невозможно без сетевых протоколов. В этой книге мы сосредоточимся на наборе (стеке) протоколов TCP/IP, называемом также набором протоколов Интернета. Так, например, клиенты и серверы Web устанавливают соединения, используя протокол TCP. TCP, в свою очередь, использует протокол IP, а протокол IP устанавливает соединение с тем или иным протоколом канального уровня. Например, если и клиент, и сервер находятся в одной сети Ethernet, взаимодействие между ними будет соответствовать показанному на рис. 1.3.
Пол ьзовател ьски й процессПол ьзовател ьски й процессСтек протоколов внутри ядраСтек протоколов внутри ядраУровень приложенияУровень приложенияТранспортный уровеньТранспортный уровеньСетевой уровеньСетевой уровеньКанальный уровеньКанальный уровеньEthernetРис. 1.3. Клиент и сервер в сети Ethernet, соединен иные по протоколу TCPEthernetРис. 1.3. Клиент и сервер в сети Ethernet, соединен иные по протоколу TCP
Пол ьзовател ьски й процесс
Пол ьзовател ьски й процесс
Стек протоколов внутри ядра
Стек протоколов внутри ядра
Уровень приложения
Уровень приложения
Транспортный уровень
Транспортный уровень
Сетевой уровень
Сетевой уровень
Канальный уровень
Канальный уровень
Ethernet
Рис. 1.3. Клиент и сервер в сети Ethernet, соединен иные по протоколу TCP
Ethernet
Рис. 1.3. Клиент и сервер в сети Ethernet, соединен иные по протоколу TCP
Несмотря на то что клиент и сервер устанавливают соединение с использованием протокола уровня приложений, транспортные уровни устанавливают соединение, используя TCP, и т. д., нужно отметить, что действительный поток информации между клиентом и сервером идет вниз по стеку протоколов на стороне клиента, затем по сети и, наконец, вверх по стеку протоколов на стороне сервера.
Отметим также, что клиент и сервер являются типично пользовательскими процессами, в то время как TCP и протоколы IP обычно являются частью стека протоколов внутри ядра. На рис. 1.3 мы выделили четыре уровня процессов.
Мы будем обсуждать нс только протоколы TCP и IP. Некоторые клиенты и серверы вместо протокола TCP используют UDP, и оба эти протокола более подробно обсуждаются в главе 2. Употребляя термин IP, мы подразумеваем последнюю официальную версию этого протокола — IPv4. Новая версия этого протокола, IP версии 6, была разработана в середине 90-х и, возможно, со временем заменит протокол IPv4. Начальные реализации протокола IPv6 были доступны на момент
написания этой книги, и в тексте описана разработка сетевых приложений как под IPv4, так и под IPv6. В приложении А приводится сравнение протоколов IPv4 н IPv6 с другими протоколами, с которыми вы встретитесь.
Вовсе не обязательно, чтобы клиент и сервер находились, как показано на рис. 1.3, в одной и той нелокальной сети {localarea network, LAN). Напротив, клиент и сервер могут относиться к разным локальным сетям (рис. 1.4), при этом обе локальных сети должны быть соединены в глобальной сети (wide-area network, WAN) с использованием маршрутизаторов.
Рис. 1.4. Клиент и сервер в различных локальных сетях, соединенных через глобальную сеть
Маршрутизаторы — это блоки, из коюрых строится гюба.чьная сеть. На сегодняшний день наибольшей глобальной сетью является Интернет, хотя многие компании создают свои собственные глобальные сети, п эги частные сети могут быть, а могут и не быть подключены к Интернету.
Оставшаяся часть этой главы представляет собой обзор различных тем, которые более подробно раскрываются далее по тексту книги. Мы начнем с полного, хотя и простого примера TCP-клиента, демонстрирующего вызовы многих функции и понятия, с которыми мы встретимся в книге. Клиент рабо гает только с протоколом IP версии 4. Мы покажем изменения, необходимые для работы с про го- колом IP версии 6. Разумнее всего создавать не зависящие от протокола клиенты и серверы, и это решение будет рассмотрено нами в главе 11. В этой главе также показан пример сервера TCP, работающего с нашим клиентом.
Чтобы упростить написанный нами код, мы определяем наши собсгвенпые функции-обертки Wrapper functions) для большинства системных функций, которые будем вызывагь. В большинстве случаев мы будем использовать функции- оберт ки для поиска ошибок, вывода соответствующих сообщений п завершения работы при обнаружении ошибки.
Кроме того, в этой главе мы подробно расскажем о сети, использовавшейся для тестирования примеров из книги, приведем имена узлов, их IP-адреса и названия операционных систем, под управлением которых они работают.
В настоящее время нельзя говорить о Unix, не упомянув Posix — стандарт, принятый большинством производителей. Мы опишем историю стандарта Posix и расскажем, каким образом он определяет API, рассматриваемые в этой книге наряду с другими конкурирующими стандартами.
Простой клиент времени и даты
Рассматривая этот конкретный пример, мы введем многие понятия и термины, с которыми будем встречаться в процессе изучения этой книги. В листинге 1.1 представлена реализация клиента времени н даты, подключающегося к серверу по протоколу TCP. Эют клиент устанавливает ТСР-соединение с сервером, а сервер просто посылает клиенту время и дату в удобочитаемом формате.
Листинг 1.1. ТСР-клиентдля определения времени и даты //Tntro/daytimetcpcl i с
#】nclude "unp h"
int
mam(irt argc, char **argv)
{
mt sockfd. n
charrecvl】ne[MAXLINE + 1]:
struct sockaddr_】n servaddr:
if (argc J 2)
e”工1111("11S旳e a out <IPaddress>,,).
if ( (sockfd - socket(AFJNET SOCK_STREAM. Q)) <0〉
err_sys("socket error*5.
bzero(&servaddr. sizeof(servaddr))
servaddr sin_family = AF_INET.
servaddr sin_port = htons(13). /* сервер для определения врененм М даты */
if (inet_pton(AF_INET. argv[l], &servaddr sin_addr) <= 0)
err_quit( *metjDton error for %s". argv[l])
if (connect(sockfd. (SA *) &servaddr, sizeof(servaddr)) < 0)
err_sys("connect error")
while ( (n = read(sockfd. recvline. MAXLINE)) > 0) {
recvline[n] = 0./* завершающий нуль */
if (fputs(recvline. stdout) == EOF)
err_sys(Mfputs error").
}
if (n < 0)
err_sys(Mread error"),
продолжение 按
Листинг 1.1 (продолжение)
exit(O).
}
ПРИМЕЧАНИЕ
Этот формат мы используем для всех вставок исходного кода в тексте. Каждая непустая строка пронумерована. Абзац текста, описывающий некоторую часть кода, начинается с двух номеров — начального и конечного номеров тех строк, о которых идет речь в данном абзаце. Как правило, абзацу предшествует короткий заголовок, в котором резюмируется содержание описываемого кода.
В начале фрагмента кода указано имя файла исходного кода: в данном примере это файл daytimetcpcli.c в каталоге intro. Поскольку исходный код всех примеров является свободно доступным (см. предисловие), вы можете найти соответствующие исходные файлы. Наилучший способ изучить концепции сетевого программ и рован ия — компилировать, запускать и особенно модифицировать эти программы в ходе чтения книги.
ПРИМЕЧАНИЕ
Примечания наподобие этого мы будем использовать для описания особенностей реализации и исторических справок.
Если мы откомпилируем эту программу в определенный по умолчанию файл a.out и выполним его, на выходе мы получим следующее:
solans % a.out 206.62.226.35 наш ввод
Fn Jan 12 14 27 52 1996 вывод программы
ПРИМЕЧАНИЕ
Отображая интерактивный ввод и вывод, мы выделяем то, что вводим, полужирным шрифтом. Комментарии идут справа от вывода курсивом. Название системы мы включаем в приглашение оболочки (в данном примере Solaris), чтобы показать, на каком узле выполняе гея команда. На рис. 1.7 показаны системы, используемые для выполнения большинства примеров этой книги. Имена узлов обычно соответствуют операционным системам.
Теперь мы можем рассмотреть подробности этой программы, состоящей из 27 строк. Здесь мы лишь кратко опишем их — на тот случай, если это первая сетевая программа, с которой вы встретились, — а более полное представление о ней вы сможете получить далее по мере чтения книги.
Подключение собственного заголовочного файла
Мы подключили наш собственный заголовочный файл, unp.h, текст которого приведен в разделе Г.1. Этот заголовочный файл подключает различные системные заголовочные файлы, необходимые большинству сетевых программ, и определяет используемые нами константы (например, MAXLINE).
Аргументы командной строки
2-3 Это определение функции mam вместе с аргументами командной строки. При написании кода примеров к этой книге подразумевалось, что для его компиляции должен использоваться компилятор ANSI С (American National Standards Institute — Американский национальный институт стандартов). _,.,,‘
Создание сокета 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 сокетов. В предыдущем абзаце мы упоминали функцию socket, входщую в API сокетов. Там же мы говорили и о сокете TCP, что является синонимом точки доступа TCP (TCP endpoint).
Если вызов функции socket оказывается неудачным, мы прерываем выполнение программы с помощью вызова функции err_sys. Она выдает сообщение об ошибке и ее описание (например, одна из возможных ошибок функции socket связана с отсутствием поддержки протокола) и прерывает выполнение процесса. Эта функция, как и некоторые другие созданные нами функции, начинающиеся с егг_, встречаются далее по тексту книги. Они будут описаны в разделе Г.4.
Задание IP-адреса и порта сервера
12-16 Мы заполняем структуру адреса сокета Интернета (структура sock add r_i п с именем servaddr) IP-адресом и номером порта сервера. Сначала мы заполняем структуру нулями, используя функцию bzero, затем устанавливаем номер порта, равный 13 (это номер заранее известного порта (well-known port) сервера времени и даты на любом узле TCP/IP, на котором работает эта служба, — табл. 2.1), а также присваиваем IP-адресу значение, определенное первым аргументом командной строки (argv[l]). В этой структуре поля IP-адреса и номера портов должны иметь определенный формат: мы вызываем библиотечную функцию htons (host to network short), чтобы преобразовать двоичный номер порта в требуемый формат, и библиотечную функцию met_pton (presentation to numeric — преобразо-
вать в двоичное представление), чтобы преобразовать аргумент колганднои строки в символах ASCII (например, 206 62 226 35 при выполнении данною примера) в двоичный формат
ПРИМЕЧАНИЕ
Функция b7ero нс является фулкциен ANSI С — она происходит от более раннего кода сетевого npoi раммирования Беркли Тем лс менее мы исиопьз^см именно се, а пс функцию ANSI С mtmsct (с тремя apjуметами), потому что с 巾)икцией Ь/сго рабогать проще Почти каждый производитель, поддерживающий API сокетов также реализует и функцию bzero а на случаи, если такой реа^шзации ист мы JipiiBO/iiiM ее макроопределение в нашем зат оловочном файле unp h
Сам автор допустил ошибку, поменяв местами второй и третий api'VMeiiTbi функции nicniset в 10 местах в первой редакции |95| Компилятор С нс можс! распозпагь эту ошибку, лоскопьку оба аргумента принадлежат к одному 1 ипу В дспствш епыюс ги второй api умент принадлежит кт ил v mt, атрс гий — si7e_t — обычно имеет тип unsigned int (то есть це^юс без знака), по заданные значения, соогвегствелно 0 и 16 яв шютсл допустимыми для обоих типов api умента Вы юв функции memset вес равно осуществляйся но функция факт ически ничего ис делала, поскольку задавалось нулевое число инициализируемых баи гов ПрО1 рамма рабслала, потому что голько некоторые функции Сокелов деисгвигслыю требуют, нюбы иос刀еднне 8 баиюв в структуре адреса соке i а были нулевыми Тем нс менее э го ошибка, и се можно избежать, используя функцию bzero поскольку перестановка двух аргументов функции b/сю будег Bcti да выявлена компилятором С, если используются про го типы функции
Возможно, вы впервые встречаете функцию inet__pton Она яв 1ястся новой, и появи- чась она в протоко ic IPv6 (о ко юром бо icc noxipo6no мы потоворим в приложении А) В более старом коде для преобразования точечпо-десятичлои записи ASCII (dotted- decimal string) в необходимым формат используется функция inct_addr, по у нее есть ряд офаничснии, коюрых не имеет функция inet_pton Нс беспокоитесь, если ваша система (пока еще) не поддерживает л у функцию, — ее реализация приведена в разделе 3 7
Установление соединения с сервером
7 18 Функция connect, применяемая к сокету TCP, устанавливает соединение по протоколу TCP с сервером, используя адрес сокета из структуры, на которую указывает второй аргумент Мы также должны задать длину структуры адреса в качестве третьего аргумента функции connect, а для структур адреса сокета Интернета мы всегда предоставляем вычисление длины компилятору, используя опера гор Csizeof
ПРИМЕЧАНИЕ
В заголовочном файле unp h мы используем директиву #define SA, чтобы определить SA как struct sockadd г, что соогвстствуег общей ci руктурс адреса соке i а Когда одна m функций сокетов требует указателя па< груктуру адреса сокст а, этот указатель должен • быть преобразован в указатель на общую структуру адреса сокета Это происходит потому, что функции сокетов появились раньше чем стандарт ANSI С Cooiвсгстценно, тип указан еля void * еще нс бы ч досту лен в начале 80-х, koi да эти функции разрабатывались Проблема состоит в том, что выражение struct sockadd г занимает 15 симво- * лов и часто заоавляст выходить с гроку исходно! о кода за правую границу жрана (или границы книги), — поэтому мы сократили ее до SA Более подробно мы исследуем структуры с адресами сокетов общею тина с помощью лисгишаЗ 2
Чтение и отображение ответа сервера
19-25 Мы чи1 аем ответ сервера и отображаем результаг с помощью ci андартноп функции ввода вывода fputs Нужно быть внимательным при использовании TCP, поскольку это потоковый (byte-stream) протокол без границ между записями Обычно ответом сервера яв тяется 26-байтовая строка следующе! о формата
Fri Jan 丄2 14 27 52 1996\r\n
где \r — это возврат каретки, а \п — перевод строки (в символах ASCII) В случае потоковою протокола эти 26 байтов можно поручить в нескольких вариантах в виде отдельного сегмента TCP, содержащего все 26 байтов данных, или в виде 26 сегментов каждый из которых содержит по одному байту данных, или в виде любой другом комбинации, в сумме дающей 26 бантов Обычно возвращается один сегмент, содержащий все 26 байтов, но при больших объемах данных мы не можем рассчитывать, что ответ сервера будет получен с помощью одного вызова read Следовательно, при чтении из сокета TCP нужно всегда вызывать функцию read циклически и прерывать цикл либо когда функция возвращает 0 (например, соединение было разорвано другой сгоронои) либо когда возвращенное значение оказывается меньше нуля (функция возвращает ошибку)
В приведенном примере конец записи обозначается сервером закрывающим соединение Эта технология используется также протоколом передачи гипертекста (Hypeitext Transfer Protocol HTTP) Однако есть и другие способы обозначения конца записи Например, протокол передачи файлов (File Transfer Protocol, FTP) и простой upoioKon передачи почты (Simple Mail Transfer Protocol, SMTP) обозначают конец записи 2-баи говой пос яедоватепыюстыо» состоящей из ASCII- символов возврата карзткн и перевода с гроки С 1уяс6а вызова удаленных процедур (Remote Piocedure Call, RPC) и система именования доменов (Domain Name System, DNS) помещают перед каждой записью, отсыпаемой по протоколу TCP, двоичное число, соотвегствующее длине этом записи Здесь важно осознать, я го протокол TCP сам по себе не предоставляет никаких меток записей если приложение требует отде чять записи одну от дру1 ой, оно должно сделать это самое! оя- тельно, и для этого имею гея стандартные методы
Завершение программы
26 Функция exit завершает программу Unix всех да закрывае! все о гкрытые дескрипторы при завершении процесса, поэтому теперь наш TCP-сокет закрыт
Как уже отмечалось, далее в тексте книги вы найдете значительно более подробное обсуж/(еиис моментов, о которых мы здесь только упомянуло
Независимость от протокола
Наша программа, представленная в листинге 1 1, является зависящей от прото- кочa (protocol dependent) IP версии 4 (IPv4) Мы выделяем и инициализируем структуру sockaddr^i п, определяем адрес как относящийся к семейству AF_INET и устанавливаем первый аргумент функции socket равным AF」NET
Если мы хотим, чтобы программа работала по протоколу IP версии 6 (IPv6), необходимо модифицировать код В листинге 1 2 показана версия программы,
работающая по протоколу IPv6, а внесенные в нее изменения отмечены полужирным шрифтом.
Листинг 1.2. Версия программы из листинга 1.1 для IPv6 //1ntro/daytimetcpcl дv6 с
1 include "unp h"
2 mt
3 main(int argc char **argv)
4 {
5intsockfd. n,
6charrecvline[MAXLINE + 1].
7 struct sockaddr_in6 servaddr.'
8 if (argc 「 2)
9err_quit( "usage a out <IPaddress>,').
10 if ( (sockfd = socket(AF_INET6 SOCK_STREAM, 0)) < 0)
11err_sys("socket error"?,
12 bzero(&servaddr. sizeof(servaddr)).
13 servaddr sin6_family 工 AFINET6,
14 servaddr sin6_port = htons(13)./* сервер для определение времени и даты */
15 if (1net_pton?AF_INET6. drgv[l] &servaddr sin6_addr) <= 0)
16err_qu11("inet_pton error for %s" argvflj)
17 if (connect(sockfd (SA *) &servaddr. sizeof(servaddr)) < 0)
18 err_sys('connect error")
19 while ( (n = read(sockfd recvlme MAXLINE)) > 0) {
20 recvl ine[n] = 0/* завершающий нуль */
21 if (fputs(recvline stdout) == EOF)
22 errsysC^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). Людям проще работать с именами, чем с числами (например, laptop.kohala com или просто laptop). В главах 9 и 11 мы обсудим функции, обеспечивающие преобразование имен узлов в IP-адреса и имен служб в номера портов. Мы специально откладываем описание этих функций, продолжая использовать IP-адреса и номера портов, и поэтому точно знаем, что именно входит
в структуры адресов сокетов, которые мы должны заполнить н проверить. Это также упрощает наши объяснения сетевого программирования, снимая необходимость описывать в подробностях еще один набор функций.
Обработка ошибок: функции-обертки
В любой реальной программе существенным моментом является проверка каждого вызова функции на предмет возвращаемой ошибки. В листинге 1.1 мы проводим поиск ошибок в вызовах функций socket, inet_pton, connect, read и fputs, и когда ошибка случается, мы вызываем своп собственные функции err quit и err_sys для вывода сообщения об ошибке и для прерывания выполнения программы. В отдельных случаях, когда функция возвращает ошибку, бывает необходимо выполнить еще какие-либо действия помимо прерывания программы (см,, например, листинг 5.9, где требуется проверить прерванный системный вызов).
Поскольку прерывание программы из-за ошибки — типичное явление, мы сократим наши программы, определив функгщи-обертки, которые будут вызывать сами рабочие функции, проверять возвращаемые ими значения и прерывать программу при возникновении ошибки. Соглашение, используемое нами, заключается в том, что название функции-обертки пишется с заглавной буквы, например: sockfd = Socket(AF_INET. SOCK_STREAM. 0).
Наша функция-обертка для функции socket показана в листинге 1.3.
Листинг 1.3. Функция-обертка для функции socket //lib/wrapsock с 172 mt
173 Socket(1 nt family, int type, int protocol)
174 {
175 int n.
176 if ( (n = socketCfamly type, protocol)) < 0)
177err_sys("socket error").
178 return (n).
179 }
ВНИМАНИЕ
В тексте книги вам будут встречаться функции с именами, начинающимися с заглавной буквы Это будут паши собственные функци и -обертки, предназпаче! н гые для вызова функций с теми же именами, но начинающимися со строчной буквы.
При описании исходного кода в тексте книги мы всегда ссылаемся на вызываемую функцию низшего уровпя (например, socket), а не на функцию-обертку (например, Socket).
Хотя вам может показаться, что использование функций-оберток не дает больших преимуществ, на самом деле это не так. Когда мы будем обсуждать потоки в главе 23, вы обнаружите, что при возникновении ошибки функции потоков не присваивают стандартной переменной Unix errno определенную константу, специфическую для произошедшей ошибки. Вместо этого значение переменной errno просто возвращается функцией. Эго значит, что каждый раз, когда мы вызываем
одну из функций pthread, мы должны разместить в памяти переменную, сохранить возвращаемое значение в этой переменной и присвоить его переменно!! errno перед вызовом err_sys. Чтобы избежать загромождения кода скобками, мы можем использовать оператор языка С запятая для объединения присваивания переменной е「гпо и вызова err_sys следующим образом:
int п.
if ( (n = pthread_mutex_lock(&ndone_mutex)) != 0) errno = n. err_sys("pthread_mutex_lock error"), В качестве альтернативы мы можем определить новую функцию выдачи сообщений об ошибках, которая в качестве аргумента получает системный номер ошибки. Однако проще всего будет выглядеть код с использованием функции- обертки, если мы определим нашу собственную функцию-обертку, как показано в листинге 1.4:
Pthread jnutex_l ock г Sridone^mutex).
Листинг 1.4. Функция-обертка для функции pthread_mutex_lock //lib/wrappthread с 72 void
73 Pthread_mutex_lock(pthread_mutex_t *mptr)
74 {—~一 一
75 mt n
76 if ( (n = pth»~ead_niutex_lock(mptr)) == 0)
77 return
78 errro = n
7。 err_sys("pt;hreadjnutex_Jock error').
80 }—~~
ПРИМЕЧАНИЕ
Если аккуратно программировать на С, можно использовать вместо функций макросы, что обеспечивает небольшой выигрыш в нрои ^водиюлыюсти, однако фулкпии- обертки крайне редко бывают причиной недос гаточной производительности программ. Наш выбор — первая заглавная буква в названии функции — является компромиссом. Мы рассмотрели множество других стилей: подстановка префикса ”е" (как сделано на с. 182 [56]), добавление суффикса '^с'* к имени функции и т. д. Выбранный стиль кажется наименее категоричным, и в тоже время он представляет визуальное указание на то, что на самом деле вызывается какая-то другая функция.
Эта технология имеет, кроме того, полезный побочный эффект: она позволяет проверять возникновение ошибок при выполнении таких функций, ошибки в которых часто остаются незамеченными, например close и listen.
Мы всегда будем использовать функции-обертки, кроме тех случаев, когда нам понадобится проверить ошибку явно и обрабатывать ее другим, отличным от прерывания программы способом. Мы не приводим исходный код для всех наших функций -оберток, но он полностью доступен (см. предисловие).
Значение системной переменной Unix errno
Когда при выполнении функции Unix (например, одной из функций сокетов) происходит ошибка, глобальной переменной errno присваивается положитель-
ное значение, указывающее на тип ошибки, а возвращаемое значение функции при этом обычно равно -1. Наша функция err_sys проверяет значение переменной errno и выводит строку с соответствующим сообщением об ошибке (например, об истечении времени соединения, если значение функции errno равно ETIMEDOUT). ~
Переменной errno присваивается определенное значение, только если при выполнении функции произошла какая-либо ошибка. Ее значение не определено, если функция не возвращает ошибки. Все положительные коды ошибок являются константами с именами в верхнем регистре, начинающимися с «Е»,п обычно определяются в заголовке <sys/errno h>. Ни одна ошибка не имеет кода 0.
Переменную errno нельзя хранить как глобальную переменную, если имеется множество программных потоков, у которых все глобальные переменные являются общими. О решении этой проблемы мы расскажем в главе 23.
Говоря для краткости, что «функция connect возвращает ECONNREFUSED», мы подразумеваем, что при выполнении функции произошла ошибка (обычно при этом возвращаемое значение функции равно -1)и значение переменной errno в настоящий момент равно указанной константе.
Простой сервер времени и даты
Напишем простую версию сервера TCP для определения времени и даты, который будет работать с клиентом, описанным в разделе 1.2. Воспользуемся функциями-обертками, описанными в предыдущем разделе. Сервер представлен в листинге 1.5.
Листинг 1.5. TCP-cepi^ep для определения времени и даты //intro/daytTmetcpsrv с
include"ипр п"
fmclude<time h>
mt
mamCint argc char **drgv)
{
int1istenfd. connfd.
structsockdddr_in servaddr:
charbufffMAXLINE]
time_tticks.
listenfd^ Socket(AFJNET. SOCK_STREAM.0):
bzeroC&servaddr sizeof(servaddr)).
servaddrsin_fannly = AF_IHET.
servaddrsin_addr s_addr = htonl(INADDR_ANY).
servaddr sinport = htons(13) /* сервер времени и даты */
Bind(hstenfd. (SA *) &servaddr sizeof(servaddr)):
ListenClistenfd LISTENQ).
for (..) {
connfd = Accept(listenfd. (SA *) NULL. NULL)
Листинг 1.5 (продолжение)
ticks = time(NULL):
20snprintflbuff. sizeof(buff).n% 24s\er\en", ct7me(&t?cks)).
21Write(connfd, buff,strlen(buff)).
22Close(connfd)
23},
24 }
Создание сокета TCP
10 Создание сокета TCP аналогично созданию клиентского кода.
Связывание заранее известного порта сервера с сокетом
Ы5 Заранее известный порт сервера (порт номер 13 в случае сервера времени и даты) связывается с сокетом путем заполнения структуры адреса сокета и вызова функции bind. Мы задаем IP-адреса как INADDR_ANY> что позволяет серверу устанавливать соединение с клиентом на любом интерфейсе в том случае, если узел сервера имеет несколько интерфейсов. Далее мы рассмотрим, как можно ограничить список интерфейсов, через которые может осуществляться подключение клиентов.
Преобразование сокета в прослушиваемый сокет
16 С помощью вызова функции listen сокет преобразуется в прослушиваемый сокет, на котором входящие соединения от клиентов принимаются ядром. Вызов функций socket, bind и 11sten — это обычная последователыюсть шагов для любого сервера TCP, позволяющая создать прослушиваемый дескриптор (listening descriptor); в нашем примере это переменная 11 stenfd.
Константа LISTENQ взята из нашего заголовочного файла unp h. Она задает максимальное количество клиентских соединений, которые ядро ставит в очередь на прослушиваемом сокете. Более подробно создание эгих очередей мы рассмотрим в разделе 4.5.
Принятие клиентского соединения, отправка ответа
7~21 Обычно процесс сервера блокируется при вызове функции accept и ждет подключения клиента. Соединение TCP устанавливается с помощью так называемого трехэтапного рукопожатия (three-way handshake). Когда рукопожатие состоялось, функция accept возвращает значение, и это значение является новым дескриптором (connfd), который называется присоединенным дескриптором {connected descriptiof). Этот новый дескриптор используется для связи с новым клиентом. Новый дескриптор возвращается функцией accept для каждого клиента, соединяющегося с нашим сервером.
ПРИМЕЧАНИЕ
В книге используется следующий способ обозначения бесконечного пнкла, for ( .){
Текущее время и дата определяются с помощью библиотечной функции time, возвращающей количество секунд с начала эпохи Unix: 00:00:00 1 января 1970 года UTC (Universal Time Coordinated — унивресальное скоординированное время, среднее время по Гринвичу). Следующая библиотечная функция, dime, преобразует целое значение в строку следующего формата, удобного для человеческого восприятия:
Fri Jan 12 14 27 52 1996
Символ возврата каретки и пустую строку добавляет к строке функция snpri ntf, а функция write сообщает результат клиенту.
.ПРИМЕЧАНИЕ
Возможно, вы впервые встречаетесь с функцией snprintf. Во многих случаях используется другая функция — sprintf, но она не в состоянии обеспечить проверку на переполнение буфера получателя. Функция snprintf, наоборот, требует, чтобы в качестве второго аргумента указывался размер буфера получателя, что позволяет избежать переполнения буфера.
Функция snprintf еще не является частью стандарта ANSI С, но существует вероятность ее включения в обловленный стандарт, в настоящее время называемый С9Х. Тем нс мепсс многие поставщики программного обеспечения уже сейчас включают эту функцию в стандартную библиотеку языка С. Во многих примерах в нашей книге мы используем нашу собственную версию функции snprintf, содержащую обращение к функции sprintf.
Отмстим, что во многих случаях проникнуть в систему хакерам помогало то, что в рс~ зультате отправки данных серверу вызовы функции sprintf переполняли буфер сервера. Также следует проявлять осторожность при использовании функций gets, strcat и strepy — вместо них лучше использовать fgets, strncat и strnepy. Дополнительную 初f- формацию по написанию защищенных сетевых программ вы найдете в [30].
Завершение соединения
22 Сервер закрывает соединение с клиентом, вызывая функцию cl ose. Это инициирует обычную последовательность прерывания соединения TCP: пакет FIN посылается в обоих направлениях, и каждый пакет FIN распознается на другом конце соединения. Более подробно трехэтапное рукопожатие и четыре пакета TCP, используемые для прерывания соединения, будут описаны в разделе 2.5.
Как и в случае клиента в предыдущем разделе, мы только кратко рассмотрели этот сервер, оставив подробности на потом. Запомните следующие моменты:
Как и клиент, сервер зависит от протокола IPv4. В листинге 11.5 мы покажем не зависящую от протокола версию, которая использует функцию getaddri nfo. Наш сервер обрабатывает только один запрос клиента за один раз. Если приблизительно в одно время происходит множество клиентских соединений, ядро ставит их в очередь, максимальная длина которой регламентирована, и передает эти соединения функции accept по одному за один вызов. Наш сервер времени и даты, который требует вызова двух библиотечных функций — time и cti me, — является достаточно быстрым. Но если у сервера обслуживание каждого клиента занимает больше времени (допустим, несколько секунд или ми-
нуту), нам придется некоторым образом организовать одновременное обслуживание нескольких клиентов. Сервер, показанный в листинге 2.5, называется последовательным, или итеративным, сервером (itemtwesewe^, поскольку он обслуживает клиентов последовательно. Существует несколько технологий написания параллельного сервера (concurrent sewer), обслуживающего множество клиентов одновременно. Самой простои технологией является вызов функции Unix fork (раздел 4.7), когда для каждого клиента создается по одному дочернему процессу. Другой способ — использование npoi раммных потоков (threads) вместо функции fork (раздел 23.4) или порождение фиксированного количества дочерних процессов с помощью функции fork в начале работы (раздел 27.6).•
? Если мы запустим подобный сервер из командной строки, может no i ребовать- ся, чтобы он работал достаточно долго, поскольку часто серверы должны работать, пока работает система. Для этого нужно модифицировать код сервера таким образом, чтобы он корректно работал как демон {daemon) Unix — процесс, способный работать в фоновом режиме, отдельно от терминала. Это решение подробно описано в разделе 12 4.
Список примеров технологии клиент- сервер, используемых в книге
На протяжении всей книги мы иллюстрируем 1ехнологии сетевого программирования двумя основными приложениями модели клиент-сервер.
клиент и сервер времени и даты (их описание мы начали в листингах 1.1, 1.2 и 1.5);
эхо-клиент и эхо~сервер (их описание начнется в главе 5).
Чтобы обеспечить удобный поиск различных тем, которых мы касаемся в этой книге, мы составили список разработанных нами программ. В табл. 1.1 перечислены версии клиента для определения времени п даты, две из которых вы уже видели. В табл. 1.2 перечисляются версии сервера для определения времени и даты. В габл. 1 3 представлены версии эхо-клиента, а в табл. 1.4 ~ версии зхо-сер- вера.
Таблица 1.1. Различные версии клиента времени и даты, рассматриваемые в данной книге
«"■"■ Z< Л9К«М«»КК'Ж<'»«»» MSmZMi»» <ОЛ4«К А* »»«<♦••»>«■ «WMtWWWXWOWKXVKiOWWibWWXWMMWWWWSNWsMMW»»»»" »»W«^4W^'4W»>^X««4«<««fc«>W»CK«««J««C<~»?,«««W»W«K«KWw4«*'4«»WXWWW«
Листинг
Описание
11
12
94
」
TCP/IPv4, зависящий о г протокола
TCP/IPv6, зависящим от протокола
TCP/IPv4 зависящий от протоко^ш вызывает функции gethostbvname 、 и getservbyname
ИЗ 118
157
28 2
29 2
» у TCP, не зависящий от протокола, вызывает функции geracldnnfo и tep connect UDP, не зависящий от протокола, вызывает функции getacldnnfo и uclp_conneQt< 1СР, использует псблокируемую функцию connect,
TCP/IPv4, ХТ1, зависяищй от протокола
TCP, XTI, ие зависящим от протокола, вызывает netdn gctbynainc н Up connect
X»^»MM««W*««<>>KM««WWWW«<««frH4«««4fr^»M«M«<^WWW«<^-X««W«4WWWW«<«^«WW«vM«^>wc«v4^^
Листинг
Описание
312
313
316
33 2
〃1
Д2
Д7
UDP. XTI, не зависяиши от протокола, вызывает netdir_getbyname и udp_client UDP, XTI, не зависящим от протокола, получает асинхронные ошибки UDP, XTI, не зависящий от протокола, читает дейтаграммы по частям TCP, зависящий от протокола, использует TPI вместо сокетов или XTI TCP, зависящий от протокола, генерирует SIGPIPE
TCP, зависящий от протокола, выводит размер буфера сокета и MSS
TCP, зависящий от протокола, допускает использование имени узла (функция gethostbyname) или IP-адреса
Д8
TCP не зависящий от протокола, допускает использование имени узла (функция gethostbyname)
Таблица 1.2. Различные версии сервера времени и даты, рассматриваемые в данной книге
Листинг
Описание
15
TCP/IPv4, зависяиши от протокола
115
TCP, не зависящий от протокола, вызывает gctaddnnfo и tcpjisten
116
TCP. не зависящий от протокола, вызывает gctaddnnfo и tcp_hsten
И 11
UDP, не зависящий от протокола, вызывает getaddnnfo и udp_server
12 2
TCP, не зависящий от протокола, выполняется как автономный демон
12 4
TCP, не завися щип от протокола, порожденный демоном inetd
30 3
TCP, XTI, ле зависящий от про юкола, вызывает netdi r_getbynaine и tcpjisten
315
UDP, ХТ1, не зависящип от протокола, вызывает netdir_gctbyname и udp_scrver
Таблица 1.3. Различные версии эхо-клиента, рассматриваемые в данной книге
Листинг
Описание
53
61
62
83
85
87
13 2
134
13 5
TCP/IPv4, зависящий oi протокола
TCP, использует функцию select
TCP, испол1>зует функцию select и работает в пакетном режиме
UDP/IPv4, зависящим от протокола
UDP, проверяет адрес сервера
UDP, вызывает функцию connect для получения асинхронных ошибок
UDP, тайм-аут при чтении ответа сервера с использованием сигнала SIGALRM
UDP, тайм-аут при чтении ответа сервера с использованием функции select UDP, таим-ayi при чтении ответа сервера с использованием параметра сокета SO_RCVTIMEO
14 4
146
151
156
15 14 U
181 V
182
183
Доменный сокет Unix, зависящий от протокола
Дейтаграмма домена Unix, зависит от протокола
TCP, использует неблокируемую модель ввода-вывода
TCP, использует два процесса (функцию fork)
TCP, устанавливает соединение, затем посылает сегмент RST
UDP, широковещательным, ситуация гонок
UDP, широковешательныи, ситуация гонок
UDP, широковеи1атс^1ьный, для устранения ситуации юнок используется функция pselcct
Таблица 1.3 (продолжение)
Листинг Описание
18.5
UDP, широковещательный, для устранения ситуации гонок используются функции sigseqmp и 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, зависящий от протокола, корректно обрабатывает завершение всех дочерних iipoueccoB
6.3
TCP/IPv4, зависящий от протокола, использует функцию select, один процесс обрабатывает все клиенты
6.5
TCP/IPv4, зависящий от протокола, использует функцию poll, один процесс обрабатывает все клиенты
8.1
8.14
136
14 3
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, связывает все адреса интерфейсов
Модель OSI
Самым общим способом описания уровней сети является предложенная Международной организацией по стандартизации (International Standards Organization, ISO) модель взаимодействия открытых систем {Open Systems Interconnection, 057). Эта семиуровневая модель взаимодействия показана на рис. 1.5, где она сравнивается со стеком протоколов Интернета.
Уровень приложения
Уровень представления
УровеньсеансаУровеньсеансаТранспортныйуровень
Уровеньсеанса
Уровеньсеанса
Сетевой уровень
Канальныйуровень
Физический уровень
Набор протоколов ИнтернетаНабор протоколов ИнтернетаРазличныеРазличныесоставляющиесоставляющиеканалаканаласвязисвязиМодель OSI
Набор протоколов Интернета
Набор протоколов Интернета
Различные
Различные
составляющие
составляющие
канала
канала
связи
связи
Рис. 1.5. Уровни модели OSI и набор протоколов Интернета
Два нижних уровня модели OSI соответствуют аппаратному обеспечению драйверов устройств и сетей, которое поддерживается системой. Обычно нам не нужно заботиться об этих уровнях, так как достаточно знать только некоторые свойства канального уровня — например, что MTU (максимальная единица передачи) Ethernet, описываемая в разделе 2.9, имеет размер 1500 байт.
Сетевой уровень управляется протоколами IPv4 и IPv6. Оба они описываются в приложении А. Из протоколов транспортного уровня мы можем выбирать TCP или UDP (о них рассказывается в главе 2). На рис. 1.5 между TCP и UDP изображен разрыв, означающий, что, возможно, приложение минует транспортный уровень и будет использовать непосредственно IPv4 или IPv6. В таких случаях говорят о символьном, или неструктурированном1, сокете (mw socket), который будет описан в главе 25.
Три верхних уровня модели OSI соответствуют уровню приложений. Приложением может быть web-клиент {браузер), клиент Telnet, web-сервер, сервер FTP или любое другое используемое нами приложение. В случае протоколов Интернета три верхних уровня модели OSI разделяются очень редко.
Два программных интерфейса, которые мы описываем в этой книге, — сокеты и XTI — являются интерфейсами между верхними тремя уровнями (уровнем приложений) и транспортным уровнем. Это один из важнейших вопросов книги: как создавать приложения, используя либо сокеты, либо XTI, которые используют, в свою очередь, либо TCP, либо UDP. Мы уже упоминали о символьных сокетах, и в главе 26 мы увидим, что можем даже полностью миновать уровень IP, чтобы читать и записывать свои собственные кадры канального уровня.
Встречаются также термины «сырой сокет», «необработанный сокет» и «дейтаграмм-орнентирован- ный соке г». — Примеч. перев
Почему и сокеты, и XTI предоставляют интерфейс между верхними тремя уровнями модели OSI и транспортным уровнем? Для подобной организации модели OSI имеются две причины, которые мы отобразили на правой стороне рис. 1.5. Прежде всего, три верхних уровня отвечают за все детали приложения (например, FTP, Telnet, HTTP), но мало «знают» о составных частях соединения. Четыре нижних уровня, напротив, мало «знают» о приложении, но отвечают за все составляющие соединения: отиравку данных, ожидание рас!гознаван«я, упорядочивание данных, приходящих не в должном порядке, расчет и проверку контрольных сумм и т. д. Вторая причина: верхние три уровня часто формируют то, что носит название пользовательского процесса (user process), в то время как четыре нижних уровня обычно поставляются как часть ядра операционной системы. Unix, как п многие современные операционные системы, обеспечивает разделение пользовательского процесса п ядра. Следовательно, интерфейс между уровнями 4 и 5 является естественным местом для создания программного интерфейса приложения (API).
История сетей BSD
Интерфейсы сокетов происходят от системы 4.2BSD (Berkeley Software Distribution —программный продукт Кал ифорнийского университета, в данном случае это адаптированная для Интернета реализация операционной системы 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, а 1акжс интерфейс сокетов), и приложения (такие, как клиенты и серверы Telnet н FTP), — был разработан независимо от кода Unix, созданного AT&T. Поэтому начиная с 1989 года университет Беркли представил первые реализации системы BSD, не ограниченные лицензией на исходный код Unix. Эти реализации распространялись свободно и, в конечном итоге, стали доступны с помощью анонимных FTP любому пользователю Интернета.
Последними реализациями Беркли стали 4.4BSD-Lite в 1994 n 4.4BSD-Lite2 в 1995 году. Нужно отметить, что эти две реализации были затем использованы в качестве основы для других систем: BSD/OS, FreeBSD, NetBSD и OpenBSD, и все четыре до сих пор активно развиваются и совершено гвуются. Более подробную информацию о различных реализациях BSD, а также общую историю развития различных систем Unix можно найти в главе 1 [67].
Многие системы Unix начинались с некоторой версии сетевого кода BSD, включая API сокетов. Мы называем их реализациями, происходящими。根 Беркли, или Беркли -реализациями (Berkeley-derived implementations). Многие другие ком-
мерческие версии Unix основаны на Unix System V Release 4, и некоторые из них содержат сетевые программы, разработанные в университете Беркли (например, UnixWare 2.x), в то время как сетевой код других систем, основанных на SVR4, был разработан независимо (например, Solaris 2.x). Мы также должны отметить, что система Linux, популярная и доступная реализация Unix, не относится к классу происходящих от Беркли: ее сетевой код и API сокетов были разработаны «с нуля».
4.2BSD (1983)
Первые широко доступныереализации TCP/IP и API сокетов
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
V
4.4BSD Tahoe (1993)Многоадресная передача,модификации каналас повышенной вместимостью
4.4BSD-Lite (1994)
BSD/OS FreeBSDNetBSDOpenBSDBSD/OS FreeBSDNetBSDOpenBSD4.4BSD-Lite2(1995)4.4BSD-Lite2(1995)В тексте обозначается как Net/1
BSD/OS FreeBSD
NetBSD
OpenBSD
BSD/OS FreeBSD
NetBSD
OpenBSD
4.4BSD-Lite2(1995)
4.4BSD-Lite2(1995)
Рис. 1.6. История различных реализаций BSD
Сети и узлы, используемые в примерах
На рис. 1.7 показаны различные сети и узлы, используемые в примерах книги. Для каждого узла мы указываем операционную систему и тип аппаратного обеспечения (потому что некоторые операционные системы работают более чем на одном типе аппаратного обеспечения). Имя в каждой рамке — это имя узла, встречающееся в тексте. 一
МаршрутизаторМаршрутизаторalphaalpha.42.42AIX 4.2 (w/IPv6) Power PCAIX 4.2 (w/IPv6) Power PCDigital Unix4.0B (w/IPv6)AlphaDigital Unix4.0B (w/IPv6)AlphaBSD/OS 3.0(4.4BSD-Lrte2)Intel x86BSD/OS 3.0(4.4BSD-Lrte2)Intel x86UnixWare2 1.2Intel x86UnixWare2 1.2Intel x86bsdi2bsdi2• 34• 34unixwareunixwareBSD/OS 3 0 酒 (4 4BSD-Lite2)BSD/OS 3 0 酒 (4 4BSD-Lite2)Intel x86МаршрутизаторMbone .Intel x86МаршрутизаторMbone .bsdibsdi.66.66linuxlinuxSolarisSolarissunos5sunos5.40.40.33.33.36.36solaris26solaris26ИнтернетИнтернет192.6.38.59192.6.38.59Solaris 2 5.1 (w/IPv6) SparcSolaris 2 5.1 (w/IPv6) SparcSolans 2.6 (beta) SparcSolans 2.6 (beta) Sparc6 boneSolaris 2.5.1 (w/IPv6) Sparc6 boneSolaris 2.5.1 (w/IPv6) SparcLinux 2 0 30 (RedHat 4.2) Intel x86Linux 2 0 30 (RedHat 4.2) Intel x86Маршрутизатор CiscoМаршрутизатор CiscoSunOS 4.1 3 SparcSunOS 4.1 3 SparcПодсеть 206.62.226.32/27Префикс Ipv6 5f lb: сеЗе: e200:20: : /64Домен kohala. comПодсеть 206.62.226.32/27Префикс Ipv6 5f lb: сеЗе: e200:20: : /64Домен kohala. comПодсеть 206.62.226.64/27Домен kohala. comПодсеть 206.62.226.64/27Домен kohala. com.65.65laptoplaptopIntel x86Intel x86.37.37206.85.40.74206.85.40.74HP-UX 10 30(beta)PA-RISCHP-UX 10 30(beta)PA-RISC192.9.5.9192.9.5.9geminigemini.11.11[140.252.104.1< • \gateway Маршрутизатор、Cisco.4[140.252.104.1< • \gateway Маршрутизатор、Cisco.4
Маршрутизатор
Маршрутизатор
alpha
alpha
.42
.42
AIX 4.2 (w/IPv6) Power PC
AIX 4.2 (w/IPv6) Power PC
Digital Unix4.0B (w/IPv6)Alpha
Digital Unix4.0B (w/IPv6)Alpha
BSD/OS 3.0(4.4BSD-Lrte2)Intel x86
BSD/OS 3.0(4.4BSD-Lrte2)Intel x86
UnixWare2 1.2Intel x86
UnixWare2 1.2Intel x86
bsdi2
bsdi2
• 34
• 34
unixware
unixware
BSD/OS 3 0 酒 (4 4BSD-Lite2)
BSD/OS 3 0 酒 (4 4BSD-Lite2)
Intel x86Маршрутизатор
Mbone .
Intel x86Маршрутизатор
Mbone .
bsdi
bsdi
.66
.66
linux
linux
Solaris
Solaris
sunos5
sunos5
.40
.40
.33
.33
.36
.36
solaris26
solaris26
Интернет
Интернет
192.6.38.59
192.6.38.59
Solaris 2 5.1 (w/IPv6) Sparc
Solaris 2 5.1 (w/IPv6) Sparc
Solans 2.6 (beta) Sparc
Solans 2.6 (beta) Sparc
6 bone
Solaris 2.5.1 (w/IPv6) Sparc
6 bone
Solaris 2.5.1 (w/IPv6) Sparc
Linux 2 0 30 (RedHat 4.2) Intel x86
Linux 2 0 30 (RedHat 4.2) Intel x86
Маршрутизатор Cisco
Маршрутизатор Cisco
SunOS 4.1 3 Sparc
SunOS 4.1 3 Sparc
Подсеть 206.62.226.32/27
Префикс Ipv6 5f lb: сеЗе: e200:20: : /64Домен kohala. com
Подсеть 206.62.226.32/27
Префикс Ipv6 5f lb: сеЗе: e200:20: : /64Домен kohala. com
Подсеть 206.62.226.64/27
Домен kohala. com
Подсеть 206.62.226.64/27
Домен kohala. com
.65
.65
laptop
laptop
Intel x86
Intel x86
.37
.37
206.85.40.74
206.85.40.74
HP-UX 10 30(beta)PA-RISC
HP-UX 10 30(beta)PA-RISC
192.9.5.9
192.9.5.9
gemini
gemini
.11
.11
[140.252.104.1
< • \
gateway Маршрутизатор
、Cisco
.4
[140.252.104.1
< • \
gateway Маршрутизатор
、Cisco
.4
Подсеть 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.7 мы по-разному обозначили узлы, работающие как маршрутизаторы (прямоугольники со скругленными углами), и узлы, которые являются обычными компьютерами (обычные прямоугольники). Этих обозначений мы будем придерживаться па протяжении всей книги, поскольку иногда нужно четко отделять обычный узел от маршрутизатора.
ПРИМЕЧАНИЕ
Хотим подчеркнуть, что настоящее имя операционной системы Sun — SunOS 5 х, а не Solans 2.x, однако все называют се Solans.
Определение топологии сети
На рис. 1.7 мы показываем топологию сети, состоящей из улов, используемых в качестве примеров книги, но вам нужно знать топологию вашей собственной сети, чтобы запускать в ней примеры и выполнять упражнения. Хотя в настоящее время нет стандартов Unix в отношении сетевой конфигурации и администрирования, большинство систем Unix предоставляют две основные команды, которые можно использовать для определения подробностей построения сети: netstat и ifconfig. Мы приводим примеры в различных системах, представленных на рис. 1.7. Изучите руководство, в котором описаны эти команды для ваших систем, чтобы понять различия в той информации, которую вы получите на выходе. Также имейте в виду, что некоторые производители помещают эти команды в административный каталог, например /sbiп или /usr/sbin, вместо обычного /usr/bin, и этих каталогов может не быть в обычном пути поиска (PATH).
. 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 32000 32000 BLRU
ethO 1500 0 483929000 449881000 BRU
Интерфейс закольцовки называется lo, a Ethernet называется ethO. В следующем примере показан узел с поддержкой IPv6.
alpha
% netstat -m
Name
Mtu
Network
Address
Ipkts Ier「s
Opkts
Oerrs
Coll
1г0
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
<Lwk>
L?nk#4
0
0
0
0
0
turn。
576
IPv6
206 62 226 42
0
0
0
<.o
0
netstat -г показывает таблицу маршрутизации, которая тоже позволяет определить интерфейсы. Обычно мы задаем флаг -п для вывода численных адресов При этом также приводится IP-адрес маршру гизатора, заданного по умолчанию:9…
dix % netstat -rn〃
Routing tables
DestinationGatewayFl ags
Refs
Use
MTU
Netif Expire
Route tree for Protocol Family 2 (Internet) default206 62 226 62UG
0
0
enO
127/B127 0 0 1U
0
0
-
loO
206 62 226 32/27 206 62 226 43U
4
475
-
enO
Route tree for Protocol Family 24 (Internet /960000UC
v6)
0
0
1480
sitO
defaultfe80 2 0 800 207В еЗеЗ UG
0
0
-
enO
11UH
0
0
16B96
loO
5flb dfOO ce3e e200 20 /80
link#2 £UC
0
0
1500
enO
fe80 /1611 nk#2 , ,UC
0
0
1500
enO
fe80 2 0 800 2078 еЗеЗ,
link#2”' UHDL
1
0
1500
enO
ffOl /161U
0
0
-
loO
ff02 /16 fe80 800 5afc 2b36 U
1
3
1500
enO
ffll /161U
0
0
-
loO
ffl2 /16 fe80 800 5afc 2b36 U
0
0
1500
enO
Длинные строки мы разбили па части, чтобы умеет шь их на странице.
Имея имена интерфейсов, мы выполняем команду if config, чтобы получить подробную информацию для каждого ннгерфейса.
linux 弘 ifeonfig ethO
ethO Link encap 10Mbps Ethernet HWaddr 00 AO 24 9C 43 34
met 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 弘 ifeonfig InO
InO flags=c63 <UP BROADCAST NOTRAILERS RUNNING MULTICAST SIMPLEX〉
met 206 62 226 42 netmask ffffffeO broadcast 206 62 226 63 ipmtu 1500
В некоторых реализациях предоставляется флаг -а, при указании которого выводится информация обо всех сконфигурированных интерфейсах.
Одним из способов определить IP-адрсс узлов в локальной сети является проверка широковешательного адреса (найденного нами на предыдущем шаге) ' с помощью программы pi ng.
bsdi % ping 206.62.226.63』
PING 206 62 226 63 (206 62 226 63) 56 data bytes
64 bytes64 bytes64 bytes64 bytes64 bytes64 bytes64 bytes *?64 bytes64 bytes64 bytes64 bytes64 bytes64 bytes64 bytes *?from 206 from 206 from 206 from 206 from 206 from 206 from 206from 206 from 206 from 206 from 206 from 206 from 206 from 206…206 62 226 63…206 62 226 6362 226 35 icmp_seq=0 tt1=255 time=0 316 ms62 226 40 icmp_seq=0 ttl =64 time==l 369 ms (DUP1)62 226 34 icmp_seq=0 ttl =255 time=l 822 ms (DUP1)62 226 42 icmp_seq=0 ttl=64 time=2 27 ms (DUP1)62 226 37 icmp_seq=0 ttl=64 time=2 717 ms (DUP1)62 226 33 icmp_seq=0 ttl=255 time=3 281 ms (DUP1) 62 226 62 icn)p_seq=0 ttl =255 time=3 731 ms (DIJpi) вводим символ np叩нванмя (DEL) ping statistics …62 226 35 icmp_seq=0 tt1=255 time=0 316 ms62 226 40 icmp_seq=0 ttl =64 time==l 369 ms (DUP1)62 226 34 icmp_seq=0 ttl =255 time=l 822 ms (DUP1)62 226 42 icmp_seq=0 ttl=64 time=2 27 ms (DUP1)62 226 37 icmp_seq=0 ttl=64 time=2 717 ms (DUP1)62 226 33 icmp_seq=0 ttl=255 time=3 281 ms (DUP1) 62 226 62 icn)p_seq=0 ttl =255 time=3 731 ms (DIJpi) вводим символ np叩нванмя (DEL) ping statistics …1 packets transmitted 1 packets received1 packets transmitted 1 packets receivedround-trip min/avg/max = 0 316/2 215/3 731 msround-trip min/avg/max = 0 316/2 215/3 731 ms+6 duplicates+6 duplicates0% packet loss0% packet loss
64 bytes
64 bytes
64 bytes
64 bytes
64 bytes
64 bytes
64 bytes *?
64 bytes
64 bytes
64 bytes
64 bytes
64 bytes
64 bytes
64 bytes *?
from 206 from 206 from 206 from 206 from 206 from 206 from 206
from 206 from 206 from 206 from 206 from 206 from 206 from 206
…206 62 226 63
…206 62 226 63
62 226 35 icmp_seq=0 tt1=255 time=0 316 ms
62 226 40 icmp_seq=0 ttl =64 time==l 369 ms (DUP1)
62 226 34 icmp_seq=0 ttl =255 time=l 822 ms (DUP1)
62 226 42 icmp_seq=0 ttl=64 time=2 27 ms (DUP1)
62 226 37 icmp_seq=0 ttl=64 time=2 717 ms (DUP1)
62 226 33 icmp_seq=0 ttl=255 time=3 281 ms (DUP1) 62 226 62 icn)p_seq=0 ttl =255 time=3 731 ms (DIJpi) вводим символ np叩нванмя (DEL) ping statistics …
62 226 35 icmp_seq=0 tt1=255 time=0 316 ms
62 226 40 icmp_seq=0 ttl =64 time==l 369 ms (DUP1)
62 226 34 icmp_seq=0 ttl =255 time=l 822 ms (DUP1)
62 226 42 icmp_seq=0 ttl=64 time=2 27 ms (DUP1)
62 226 37 icmp_seq=0 ttl=64 time=2 717 ms (DUP1)
62 226 33 icmp_seq=0 ttl=255 time=3 281 ms (DUP1) 62 226 62 icn)p_seq=0 ttl =255 time=3 731 ms (DIJpi) вводим символ np叩нванмя (DEL) ping statistics …
1 packets transmitted 1 packets received
1 packets transmitted 1 packets received
round-trip min/avg/max = 0 316/2 215/3 731 ms
round-trip min/avg/max = 0 316/2 215/3 731 ms
+6 duplicates
+6 duplicates
0% packet loss
0% packet loss
Стандарты Unix
Стандарты Unix на сегодняшний день определяются Posix и The Open Отопри “
Posix
Название Posix представляет собой сокращение от «Portable Operating System Interface» (интерфейс переносимых операционных систем). Posix не является одиночным стандартом — это семейство стандартов, разрабатываемых организацией IEEE (Institute of Electrical and Electronics Engineers — Институт инженеров по электротехнике и радиоэлект ронике). Стандарты Posix также приняты в качестве международных стандартов ISO (International Standards Organization — Международная организация по стандартизации) и TEC (International Electrotechnical 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.
Далее появился стандарт IEEE Std 1003. lb-1993, ранее известный как IEEE 1003.4. Он стал дополнением стандарта 1003.1-1990 и включал расширения реального времени, разработанные группой Р1003.4. Стандарт 1003.1b-1993 добавил к стандарту 1990 года следующие пункты: синхронизация файлов, асинхронный ввод- вывод, семафоры, управление памятью (функция mmap и разделение памяти), планирование выполнения, часы, таймеры и очереди сообщений. Стандарт 1003.1b- 1993 имел объем 590 страниц.
Следующий стандарт Posix — IEEE Std 1003.1, редакция 1996 года [40], который включает 1003.1-1990 (базовый программный интерфейс приложений), 1003.1Ь-1993 (расширения реального времени), 1003.1с-1995 (функции Pthread) и 1003.И-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 (РИ) (интерфейсы, нс зависящие от протокола), выпущенный рабочей группой P1003.1g. Это сетевой стандарт API, определяющий два API, называемые DNI (Detailed Network Interfaces — подробные сетевые интерфейсы):
DNI/Socket, основанный на API сокетов 4.4BSD.
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.1 g. Например, третий аргумент функции connect (см. раздел 4.3) представлен типом данных sockl en_t, хотя он является новым в Posix. 1g. Аналогично мы описываем новую функцию Posix. lg sockatmark
(раздел 21.3) и предоставляем ее реализацию, используя функцию i octi. Мы также используем обозначение AF_LOCAL, принятое в протоколе Posix. 1g, вместо AF_UNIX для сокетов домена Unix. Различия между используемыми на сегодняшний день стандартами и Posix. 1g в книге оговариваются. На сегодня ни один из производителей не поддерживает Posix. 1g (поскольку это не окончательная реализация), но когда стандарт будет завершен, его поддержка производителями будет обеспечена.
Работа над всеми стандартами Posix продолжается, и попытки описать эти стандарты можно сравнить со стрельбой по движущейся мишени. О текущем состоянии различных стандартов Posix можно узнать на сайте http://www.pasc.org/ standing/sdll.html.
Open Group
Open Group (открытая группа) была создана в 1996 году при объединении организаций X/Open Group Company (основанной в 1984 году) и Open Software Foundation (OSF — Фонд открытого программного обеспечения, основанный в 1988 году). Это международный консорциум производителей и конечных потребителей в сфере промышленности, правительства и образовательных учреж~ дений.
Группа Х/Open в 1989 году выпустила руководство X/Open Portability Guide, Выпуск 3 (XPG3). Выпуск 4 вышел в 1992 году, а его вторая версия последовала в 1994 году. Эта последняя версия была известна также под именем Спецификации 1170 (Spec 1170), и «волшебное» число 1170 было суммой количества системных интерфейсов (926), числа заголовочных файлов (70) и числа команд (174). Последнее название этого набора спецификаций — «Х/Open Single Unix Specification» (Единая спецификация 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.
ПРИМЕЧАНИЕ
К сожалепию, X/Open обозначает свои сетевые стандарты с помощью аббревиатуры XNS: X/Open Networking Services. Например, версия этого документа, в которой определяются сокеты н технологии XTI для Unix 98 [74], называется «XNS Issue 5». Дело в том, что в мире сетевых технологий аббревиатура XNS всегда служила акронимом для Xerox Network Systems (сетевые системы Xerox). Поэтому мы избегаем использования акронима XNS и называем документ X/Open просто сетевым стандартом API Unix 98.”
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.
разрядные архитектуры
С середины до конца 90-х годов развивается тенденция к переходу на 64-разряд- ные архитектуры и 64-разрядное программное обеспечение. Одной из причин является более значительная по размеру адресация внутри процесса (например, 64-разрядные указатели), необходимая при использовании больших объемов памяти (более 232 байт). Обычная модель программирования для существующих 32-разрядных систем Unix называется ILP32. Ее название указывает на то, что целые числа (I), длинные целые числа (L) и указатели (Р) занимают 32 бита. В 64-разрядиых системах Unix сейчас завоевывает популярность модель LP64, Ее название говорит о том, что 64 бита требуется только для длинных целых чисел (L) и указателей (Р). В табл. 1.5 приводится сравнение этих двух моделей.
С точки зрения программирования модель LP64 означает, что мы не можем считать, что указатель хранится как целое число. Мы также должны учитывать влияние модели LP64 на существующие API.
Таблица 1.5. Сравнение количества битов для хранения различных типов данных
в моделях ILP32 и LP64
«mm^twA^wimammmjv^mm^е»»е«<»^лэдо^»>««««^^о»»、” ■мвшл <№>%
tMSSWMCXW ЧМММоМ »4»aM4e№9KW4«eSCM4eW9»»»»4MSMMS6r4«<> 08» 姪 «дМКШЧшМКЗОЧтк»
Модель LP64
Тип данных
Модель ILP32
char
8
8
short
16
16
mt
32
32
long
32
64
указатель
32
64
В ANSI введен тип данных size t, который используется, например, в качестве аргумента функции malloc (количество байтов, которое данная функция выделяет в памяти для размещения какого-либо объекта), а также как третий аргумент для функций read и write (число считываемых или записываемых байтов). В 32-разрядной системе size_t являегся 32-бптовым значением, но в 64-разрядной системе он должен иметь 64-разрядное значение, чтобы использовать преимущество больше/i модели адресации. Это означает, что в 64-разрядной системе, возможно, size」будет иметь тип unsigned long (целое число без знака, занимающее 32 бита). Проблемой сетевого API является то, что в некоторых проектах по Posix. 1g определено, что аргументы функции, содержащие размер структур адресов сокета, должны иметь тип size_t (например, третий аргумент в функциях bi nd и connect). Некоторые элементы структуры XTI также имели тин данных long (например, структуры t_info и t_opthdr). Если они оставались неизменными, то должны были изменять 32-разрядпые значения на 64-разрядные, когда система Unix менялась с модели ILP32 на LP64. В обоих случаях не было необходимости в 64-разрядных типах данных: длина структуры адресов сокета занимает максимум несколько сотен байтов, и использование типа данных 】ong для элементов структуры XTI было ошибкой.
Мы рассмотрим иовые типы данных, введенные для решения подобных проблем. API сокетов, для того чтобы хранить длину структур адресов сокетов, использует тип данных sock】en_t, a XTI использует типы данных t_scalar_t и t_uscalar_t. Причина, по которой эти 32-разрядные значения не заменяются на 64-разрядные, заключается в том, что таким образом упрощается двоичная совместимость с новыми 64-разрядиыми системами для приложений, скомпилированных в 32-разрядных системах.
1.12. Резюме
В листинге 1.1 показан полностью рабочий, хотя и простой клиент TCP, получающий текущее время и дату с заданного сервера. В листинге 1.5 представлена полная версия сервера. Эти примеры помогают ввести многие из терминов и понятий, которые в дальнейшем рассматриваются в книге.
Наш клиент зависел от протокола. Мы изменили его, чтобы он использовал IPv6, но при этом получили лишь еще одну зависящую от протокола программу. В главе 11 мы разработаем некоторые функции, которые позволят нам написать код, не зависящий от протокола. Это важно, поскольку в Интернете начинает использоваться протокол IPv6.
В книге мы будем использовать функции-обертки, созданные в разделе 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.9, чтобы получить информацию о топологии вашей сети.
Найдите исходный код для примеров из текста (см. предисловие). Откомпилируйте и протестируйте клиент времени и даты, представленный в листинге 1.1. Запустите программу несколько раз, задавая каждый раз различные IP- адреса в командной строке.
Замените первый аргумент функции socket, представленной в листинге 1.1, на 9999. Откомпилируйте и запустите программу. Что происходит? Найдите значение errno, соответствующее выданной ошибке. Как вы можете получить дополнительную информацию по этой ошибке?
Измените листинг 1.1 — поместите в цикл while счетчик, который будет считать, сколько раз функция read возвращает значение больше нуля. Выведите значение счетчика перед завершением. Откомпилируйте и запустите свой собственный клиент.
Измените листинг 1.5 следующим образом. Сначала поменяйте номер порта, задаваемый при вызове функции sw_port, с 13 на 9999. Затем замените один вызов функции write на циклический, при котором функция write вызывается для каждого байта результирующей строки. Откомпилируйте полученный сервер и запустите его в фоновом режиме. Затем измените клиент из предыдущего упражнения (в котором выводится счетчик перед завершением программы), изменив номер порта, заданный функции sin_port, с 13 на 9999. Запустите этот клиент, задав в качестве аргумента командной строки IP-адрес узла, на котором работает измененный сервер. Какое значение клиентского счетчика будет выведено? Если есть возможность, попробуйте также запустить клиент и сервер на разных узлах.
ГЛАВА 2
Транспортный уровень: TCP и UDP
Введение
В этой главе приводится обзор протоколов семейства 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 уровнем сокетов и т. д.
Обзор протоколов TCP/IP
Хотя набор протоколов и называется TCP/IP, это семейство состоит не только из собственно протоколов TCP и IP. На рис. 2.1 представлен обзор этих протоколов.
1Ру4-лриложение AF_INET sockaddr_in{}
|ру6-лрипожение AF_INET6 sockaddr_in6{}
Рис. 2.1. Обзор протоколов семейства TCP/IP
На этом рисунке представлены и IPv4, и IPv6. Если рассматривать этот рисунок справа налево, то четыре приложения справа используют IPv6, а о константе AF_INET6 и структуре sockaddr_iпб мы будем говорить в главе 3. Следующие пять приложений используют IPv4.
Приложение tcpdump, находящееся в самой левой части рисунка, соединяется непосредственно с канапьным уровнем* используя либо BPF (BSD Packet Filter — фильтр пакетов BSD), либо DLPI (Data Link Provider Interface — интерфейс поставщика канального уровня). Мы обозначили штриховую горизонтальную линию под девятью приложениями (интерфейс) как API, что обычно соответствует сокетам или XTI. Интерфейс BPF и DLPI не использует ни сокеты, ни XTI.
ПРИМЕЧАНИЕ
Здесь существует исключение, описанное в главе 25' Linux предоставляет доступ к канальному уровню с помощью сокета специального типа, называемого SOCK_ PACKET.—
На рис. 2.1 мы также отмечаем, что программа trace route использует два сокета: один для IP, другой для ICMP. В главе 25 мы создадим версии IPv4 и IPv6 утилит ping и traceroute.
А сейчас мы опишем каждый из протоколов, представленных на рисунке.
Протокол Интернета версии 4. IPv4 (Internet Protocol, version 4), который мы часто обозначаем просто как IP, был «рабочей лошадкой» набора протоколов Интернета с начала 80-х. Он использует 32-разрядную адресацию (см. раздел А.4). IPv4 предоставляет сервис доставки пакетов для протоколов TCP, UDP, ICMP и IGMR
Протокол Интернета версии 6. IPv6 (Internet Protocol, version 6) был разработан в середине 90-х как замена протокола IPv4. Главным изменением является увеличение размера адреса, в случае IPv6 равного 128 битам (см. раздел А.5) для работы с бурно развивающимся в 90-е годы Интернетом. IPv6 предоставляет сервис доставки пакетов для протоколов TCP, UDP и ICMPv6.
Когда нет необходимости различать IPv4 и IPv6, мы используем аббревиатуру/Р в словах типа «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 Message Protocol) обеспечивает передачу управляющей информации и сведений об ошибках между маршрутизаторами и узлами. Эти сообщения обычно генерируются и обрабатываются самостоятельно сетевым программным обеспечением TCP/IP, а не пользовательскими процессами, хотя мы и приводим в качестве примера программу Ping, использующую ICMP. Иногда мы будем называть этот протокол ICMPv4, чтобы отделить его от ICMPv6.
Протокол управления группами Интернета. IGMP (Internet Group Management 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.
Фильтр пакетов BSD. Этот интерфейс предоставляет доступ к канальному уровню для процесса. Обычно он поддерживается ядрами, произошедшими от BSD.
Интерфейс поставщика канального уровня. DLPI (Data Link Provider Interface) предоставляет доступ к канальному уровню и обычно поставляется с SVR4 (System V Release 4).
Все протоколы Интернета определяются в документах RFC (Request For Comments), которые играют роль формальной спецификации. Решение к упражнению 2.1 показывает, как можно получить документы RFC.
Узел, поддерживающий как IPv4, так и IPv6, мы будем называть узлом IPv4/ IPv6 (IPv4/IPv6 host) или двухстековым узлом {dual-stack host).
Дополнительные подробности собственно по протоколам TCP/IP можно найти в [94]. Реализация TCP/IP в 4.4BSD описывается в [105].
UDP: протокол пользовательских дейтаграмм
UDP — это простой протокол транспортного уровня. Он описывается в RFC 768 [80]. Приложение записывает в сокет UDP дейтаграмму {datagram), которая инкапсулируется (encapsulate)у или, иначе говоря, упаковывается либо в дейтаграмму IPv4, либо в дейтаграмму IPv6 и затем посылается получателю. При этом не гарантируется, что дейтаграмма UDP когда-нибудь дойдет до указанного получателя.
Проблема, с которой мы сталкиваемся в процессе сетевого программирования с использованием UDP, заключается в его недостаточной надежности. Если нам нужна уверенность в том, что дейтаграмма дошла до получателя, мы должны встроить в наше приложение множество функций: подтверждение приема, таймауты, повторные передачи и т. п.
Каждая дейтаграмма UDP имеет конкретную длину, и мы можем рассматривать дейтаграмму как запись (record). Если дейтаграмма корректно доходит до конечного получателя (то есть пакет приходит без ошибки контрольной суммы), длина дейтаграммы передается принимающему приложению. Мы уже отмечапи, что TCP является потоковым {byte-stream) протоколом, без каких бы то ни было границ записей (см. раздел 1.2), что отличает его от UDP.
Мы также отметили, что UDP предоставляет сервис, не ориентированный на установление соединения (connectionless)f поскольку в установлении долгосрочной связи между клиентом и сервером UDP нет необходимости. Например, клиент UDP может создать сокет и послать дейтаграмму данному серверу, а затем срезу же послать через тот же сокет дейтаграмму другому серверу. Аналогично сервер UDP может получить пять дейтаграмм подряд через один и тот же сокет UDP от пяти различных клиентов.
TCP: протокол управления передачей
Сервис, предоставляемый приложению протоколом TCP, отличается от сервиса протокола UDP. (TCP описывается в RFC 793 [83].) Прежде всего TCP обеспечивает установление соединении (^connections) между клиентами и серверами.
Клиент 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, тайм-ауты или повторные передачи. Если дейтаграмма UDР дублируется в сети, на принимающий узел мо!ут быть доставлены оба экземпляра. Также если клиент UDP отправляет две дейтаграммы одному и тому же получателю, их порядок может быть изменен сетью, и они будут доставлены с нарушением исходного порядка. Приложения UDP должны обрабатывать все подобные случаи, как это показано в разделе 20.5.
TCP обеспечивает управление потоком (flow control), TCP всегда сообщает своему собеседнику, сколько именно байтов он хочет получить от него. Это называется объявлением окна (window). В любой момент времени окно соответствует свободному пространству в приемном буфере. Таким образом гарантируется, что отправитель не переполнит буфер получателя. Окно изменяется динамически
с течением времени: по мере того как приходят данные от отправителя, размер окна уменьшается, но по мере считывания данных принимающим приложением окно увеличивается. Окно может стать нулевым: если приемный буфер TCP для данного сокета заполнен, он должен подождать, когда приложение считает данные из буфера, перед тем как сможет получать другие данные.
ПРИМЕЧАНИЕ
• UDP не обеспечивает управления потоком. Быстрый отправитель UDP может с лег, костью передавать дейтаграммы с такой скоростью, с которой не может работать полу- 笃 чатель UDP, как это показано в разделе 8.13.
Наконец, соединение TCP также является двусторонним (full-duplex). Это значит, что приложение может отправлять и принимать данные в обоих направлениях через заданное соединение в любой момент времени. Поэтому TCP должен отслеживать состояние таких характеристик, как порядковые номера и размеры окна, для каждого направления потока данных: отправки и приема.
ПРИМЕЧАНИЕ
UDP может быть (а может и не быть) двусторонним.
Установление и завершение соединения TCP
Чтобы упростить понимание функций connect, accept и cl ose и чтобы нам было легче отлаживать приложения TCP с помощью программы netstat, необходимо разобраться с тем, как устанавливаются и разрываются соединения TCP. Также необходимо познакомиться с диаграммой перехода состояний TCP. Это будет примером того, как глубокое знание протоколов способно помочь в сетевом программировании.
Трехэтапное рукопожатие
При установлении соединения TCP действия развиваются по следующему сценарию:
Сервер должен быть подготовлен для того, чтобы принять входящее соединение. Обычно это достигается путем вызова функций socket, bind и li sten и называется пассивным открытием (passive open).
Клиент выполняет активное открытие {active open), вызывая функцию connect. Это заставляет клиент TCP послать сегмент SYN (от слова «synchronize» — синхронизировать), чтобы сообщить серверу начальный порядковый номер данных, которые клиент будет посылать по соединению. Обычно с сегментом SYN не посылается никаких данных: он содержит только заголовок IP, заголовок TCP и, возможно, параметры TCP (о которых мы вскоре поговорим).
Сервер должен подтвердить получение клиентского сегмента SYN, а также послать свой собственный сегмент SYN, содержащий начальный порядковый номер для данных, которые сервер будет посылать по соединению. Сервер
посылает SYN и АСК — подтверждение приема (от слова «acknowledgement» ) клиентского SYN — в виде единого сегмента.
Клиент должен подтвердить получение сегмента SYN от сервера.
Для подобного обмена нужно как минимум три пакета, поэтому он называется трехэтапным рукопожатием TCP ( ГСР three-way handshake). На рис. 2.2 представлена схема такого обмена.
Рис. 2.2. Трехэтапное рукопожатие TCP
На рис. 2.2 J — это начальный порядковый номер клиента, а К — начальный порядковый номер сервера. Номер подтверждения в сегменте АСК — это следующий предполагаемый порядковый номер на том конце связи, который отправил сегмент АСК. Поскольку сегмент SYN занимает 1 байт пространства порядковых номеров, номер подтверждения в сегменте АСК каждого сешента SYN — это начальный порядковый номер, увеличенный на единицу. Аналогично сегмент АСК каждого сегмента FIN — это увеличенный на единицу порядковый номер сегмента FIN.
ПРИМЕЧАНИЕ
Повседневной аналогией установления соединения TCP может служить система телефонной связи [73]. Функция socket эквивалентна включению используемого телефона. Функция bind дает возможность другим узнать ваш телефонный номер, чтобы они могли позвонить вам. Функция listen включает звонок, и вы можете услышать, когда происходит входящий звонок. Функция connect требует, чтобы мы знали чей-то номер телефона и могли до него дозвониться. Функция accept ——аналогия ответа на входящий звонок. Идентифицирующие данные, возвращаемые функцией accept (это IP- адрес н номер порта клиента), аналогичны телефонному номеру звонящего по телефону. Однако имеется отличие, состоящее в том, что функция accept возвращает идентифицирующие данные клиента только после того, как соединение установлено, а во время телефонного звонка после определения номера телефона звонящего мы можем выбрать, отвечать на звонок или нет. Если используется система DNS (см. главу 9), она предоставляет услуги, аналогичные телефонной книге. Действие функции getsockbyname аналогично отысканию номера телефона по конкретному имени, а действие функции getsockbyaddr можно сравнить с отысканием имени но упорядоченному списку телефонных номеров.
Параметры 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 величина окна должна быть масштабирована — сдвинута влево наО-14 бит1, предоставляя максимально возможное окно размером почти в гигабайт (65 535 х 2й). Для использования параметра масштабирования окна в соединении необходима его поддержка обоими связывающимися узлами Мы увидим, как задействовать этот параметр с помощью параметра сокета SO_RCVBUF (см. раздел 7.5).
ПРИМЕЧАНИЕ
Чтобы обеспечить совместимость с более ранними реализациями, в которых не под- & держивается этот параметр, применяются следующие правила. TCP может отправить \ параметр со своим сегментом SYN в процессе активного открытия сокета Но он может * масштабировать свое окно, только если с другого конца связи также будет отправлен соответствующий параметр со своим сегментом SYN Эта логика предполагает, что недоступные в данной реализации параметры просто игнорируются. Это общее и необходимое требование, но, к сожалению, его выполнение не гарантировано для всех реализаций
В Отметка времени (Timestamp option). Этот параметр необходим для высокоскоростных соединений, чтобы предотвратить возможное повреждение данных, вызванное потерей пакетов. Поскольку это один из недавно появившихся ? параметров, его обработка производится аналогично параметру масштабирования окна. Программисту сетевых приложений не нужно беспокоиться об этом параметре.
Параметр MSS поддерживается в большинстве реализаций, в то время как параметр масштабирования окна и временная метка являются более новыми. Последние два параметра иногда называются «параметрами RFC 1323», поскольку описываются в этом документе [45]. Они также часто называются параметрами для «канала с повышенной вместимостью», поскольку сеть с широкой полосой пропускания либо с большой задержкой называется каналом с повышенной вместимостью (long fat pipe). В главе 24 [94] эти новые параметры описаны более подробно.
Что соответствует умножению на 2" — Примеч научн ред
Разрыв соединения TCP
В то время как для установления соединения необходимо три сегмента, для его разрыва требуется четыре сегмента,
Одно из приложений первым вызывает функцию close, и мы в этом случае говорим, что данный узел1 выполняет активное закрытие (active close). TCP этого узла отправляет сегмент FIN, обозначающий прекращение передачи данных.
Другой узел, получающий сегмент FIN, выполняет пассивное закрытие (passive close). Полученный сегмент FIN подтверждается TCP. Полученный сегмент FIN передается приложению как признак конца файла (после данных, которые уже стоят в очереди, ожидая приема приложением), поскольку получение сегмента FIN означает для приложения то, что оно уже не получит никаких данных по этому соединению.
Через некоторое время после того как приложение получило признак конца файла, оно вызывает функцию cl ose для закрытия своего сокета. При этом его TCP отправляет сегмент FIN.
TCP системы, получающей окончательный сегмент FIN (то есть того узла, на 」 котором произошло активное закрытие), подтверждает получение сегмента FIN.
Поскольку сегменты FIN и АСК передаются в обоих направлениях, обычно требуется четыре сегмента. Мы говорим «обычно», поскольку в ряде сценариев сегмент FIN на первом шаге отправляется вместе с данными. Кроме того, оба сегмента, отправляемых на шаге 2 и 3, отправляются с узла, выполняющего пассивное закрытие, и могут быть объединены в один сегмент. Эти пакеты изображены на рис. 2.3
Рис. 2.3. Обмен пакетов при закрытии соединения TCP
Сегмент FIN занимает 1 байт пространства порядковых номеров аналогично SYN. Следовательно, сегмент АСК каждого сегмента FIN — это порядковый номер FIN, увеличенный на единицу.
Здесь под узлом понимается точка доступа TCP (TCP endpoint) — Примеч научи реЗ
Возможно, что между шагами 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 перед получением признака конца файла (активное закрытие), происходит переход к состоянию FIN__WAIT_1 {ожидание FIN 1). Но если приложение получает сегмент FIN в состоянии ESTABLISHED (пассивное закрытие), происходит переход в состояние CLOSE_WAIT {ожидание закрытия).
Мы отмечаем нормальные переходы клиента с помощью обычной линии, а нормальные переходы сервера — с помощью штриховой линии. Необходимо отметить, что существует два перехода, о которых мы ранее не говорили: одновременное открытие (когда оба конца связи отправляют сегменты SYN приблизительно в одно время, и эти сегменты пересекаются в сети) и одновременное закрытие (когда оба конца связи отправляют сегменты FIN). В главе 18 [94] имеются примеры и описания этих сценариев, которые встречаются достаточно редко.
Одна из причин, по которым мы приводим здесь диаграмму перехода состояний, заключается в том, что мы хотим показать все 11 состояний TCP и их названия. Эти состояния отображаются программой netstat, которая является полезным средством отладки клиент-серверных приложений. Для отслеживания изменений состояния мы также используем программу netstat (см. главу 5).
Начальная точка
LISTENLISTENSYN RCVD XSYN RCVD Xrecv: ACK send: <nothing>recv: ACK send: <nothing>CLOSEDCLOSEDclosecloseSYN_SENTSYN_SENTESTABLISHEDESTABLISHEDrecv: FIN sendTACK-recv: FIN sendTACK-appl. пассивное окно send: <nothing>appl. пассивное окно send: <nothing>recv:SYN send: SYN, ACK одновременное открытиеrecv:SYN send: SYN, ACK одновременное открытиеПассивное открытиеПассивное открытиеFIN WAIT」FIN WAIT」CLOSINGCLOSINGTIME WAITTIME WAITrecv: ACK send:〈nothing〉recv: ACK send:〈nothing〉recv: FIN send: ACKrecv: FIN send: ACKСостояниепередачи данныхСостояниепередачи данныхААrecv: FINsend. АСКrecv: FINsend. АСКтайм-Активное открытие ауттайм-Активное открытие аутappl. closesend: FINappl. closesend: FINrecv: ACKrecv: ACKLAST ACKLAST ACKили тайм- 'аут Пассивное открытиеили тайм- 'аут Пассивное открытиеAA
LISTEN
LISTEN
SYN RCVD X
SYN RCVD X
recv: ACK send: <nothing>
recv: ACK send: <nothing>
CLOSED
CLOSED
close
close
SYN_SENT
SYN_SENT
ESTABLISHED
ESTABLISHED
recv: FIN sendTACK-
recv: FIN sendTACK-
appl. пассивное окно send: <nothing>
appl. пассивное окно send: <nothing>
recv:SYN send: SYN, ACK одновременное открытие
recv:SYN send: SYN, ACK одновременное открытие
Пассивное открытие
Пассивное открытие
FIN WAIT」
FIN WAIT」
CLOSING
CLOSING
TIME WAIT
TIME WAIT
recv: ACK send:〈nothing〉
recv: ACK send:〈nothing〉
recv: FIN send: ACK
recv: FIN send: ACK
Состояниепередачи данных
Состояниепередачи данных
А
А
recv: FINsend. АСК
recv: FINsend. АСК
тайм-
Активное открытие аут
тайм-
Активное открытие аут
appl. closesend: FIN
appl. closesend: FIN
recv: ACK
recv: ACK
LAST ACK
LAST ACK
или тайм- 'аут Пассивное открытие
или тайм- 'аут Пассивное открытие
A
A
А Нормальные переходы для клиента
> Нормальные переходы для сервера
appl:Переходы между состояниями, возникающие,
когда приложение выполняет указанную операцию
recv:Переходы между состояниями, возникающие,
когда получен указанный сегмент
send:Указывает; что отправляется при этом переходе
Рис. 2.4. Диаграмма состояний TCP
Просмотр пакетов
На рис. 2.5 представлен реальный обмен пакетами, происходящий во время соединения TCP: установление соединения, передача данных и разрыв соединения. Показаны также состояния TCP, через которые проходит каждый узел.
В этом примере клиент объявляет максимальный размер сегмента (MSS) 1460 байт (обычное значение для IPv4 в Ethernet), а сервер — 1024 байт (обычное значение для более поздних Беркли-реализаций в Ethernet). MSS в каждом направлении отличаются (см. также упражнение 2.5).
Как только соединение установлено, клиент формирует запрос и посылает его серверу. Мы считаем, что этот запрос соответствует одиночному сегменту TCP (то есть его размер меньше 1024 байт — анонсированного размера MSS сервера). Сервер обрабатывает запрос и отправляет ответ, и мы также считаем, что ответ соответствует одиночному сегменту (в данном примере меньше 1460 байт).
Оба сешента данных мы отобразили более жирными линиями. Обратите внимание, что подтверждение запроса отправляется клиенту вместе с ответом сервера. Это называется вложенным подтверждением (piggybacking) и обычно проис-
ходит, когда время, требуемое серверу для обработки запроса и генерации ответа, меньше приблизительно 200 миллисекунд. Если серверу требуется больше времени —скажем, 1 секунда — ответ будет приходить после подтверждения. (Динамика потока данных TCP подробно описана в главах 19 и 20 [94].)
Затем мы показываем четыре сегмента, закрывающих соединение. Обратите внимание, что узел, выполняющий активное закрытие (в данном сценарии клиент), входит в состояние TIME_WAIT. Мы рассмотрим это в следующем разделе.
Относительно рис. 2.5 важно отметить, что если целью данного соединения было отправить запрос, занимающий один сегмент, и получить ответ, также занимающий один сегмент, то при использовании TCP будет задействовано всего восемь сегментов. Если же используется UDP, произойдет обмен только двумя сегментами: запрос и ответ. Но при переходе от TCP к UDP теряется надежность, которую TCP предоставляет приложению, и множество задач по обеспечению надежности транспортировки данных переходит с транспортного уровня (TCP) на уровень приложения. Другое важное свойство, предоставляемое протоколом TCP, — это управление в условиях перегрузки, которое в случае протокола UDP также должно принимать на себя приложение. Тем не менее важно понимать, что для многих приложений, обменивающихся небольшими объемами данных, использование UDP позволяет избежать накладных расходов, возникающих при установлении и разрыве соединения TCP.
ПРИМЕЧАНИЕ
Альтернативой UDP в этом сценарии служит Т/ТСР — TCP для транзакций. Его мы описываем в разделе 139
Состояние 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.
В результате различных аномалий, происходящих в объединенных сетях, пакеты обычно теряются. Если возникает сбой на маршрутизаторе или отключается связь между двумя маршрутизаторами, то для стабилизации и поиска альтернативного пути требуются секунды или минуты. В течение этого периода могут возникать петли маршрутизации (маршрутизатор А отправляет пакеты маршрутизатору В, а маршрутизатор В отправляет их обратно маршрутизатору А), и пакеты теряются в этих петлях. Если потерянный пакет — это сегмент TCP, то по истечении установленного времени ожидания отправляющий узел снова передает пакет, и этот заново переданный пакет доходит до конечного получателя по некоему альтернативному пути. Но если спустя некоторое время (не превосходящее MSL) после начала передачи потерянного пакета петля маршрутизации исправляется, пакет, потерянный в петле, отправляется к конечному получателю. Начальный пакет называется потерянной копией или дубликатом (lost duplicate), а также блуждающей копией или дубликатом (wandering duplicate). TCP должен обрабатывать потерянные пакеты.
Состояние TIME WAIT позволяет добиться двух целей:
Обеспечить надежность разрыва двустороннего соединения TCP.
Подождать, когда истечет время жизни в сети старых дублированных сегментов.
Первую причину можно объяснить, обратившись к рис. 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 время жизни в сети всех старых дубликатов от предыдущих воплощений этого соединения уже истекло.
ПРИМЕЧАНИЕ
Из этого правила существует исключение. Реализации, происходящие от Беркли, инициируют новое воплощение соединения, которое в настоящий момент находится в состоянии TIME_WAIT, если приходящий сегмент SYN имеет порядковый номер «больше» конечного номера из предыдущего воплощения. На с. 958-959 [105] об этом рассказано более подробно. Для этого требуется, чтобы сервер выполнил активное закрытие, поскольку состояние TIME WAIT должно существовать на узле, получающем следующий сегмент SYN. Эта возможность используется командой rsh. В RFC 1185 [46] рассказывается о некоторых ловушках, которые могут вас при этом подстерегать.
Номера портов
В любой момент времени множество процессов может использовать либо протокол 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. Номера портов делятся на три диапазона:
Заранее известные порты: отОдо 1023. Эти номера портов управляются и присваиваются агентством IANA. Когда это возможно, один и тот же номер порта присваивается данному сервису и для TCP, и для UDP. Например, порт 80 присвоен web-серверу для обоих протоколов, хотя в настоящее время все реализации используют только TCP.
Зарегистрированные порты, от 1024 до 49 151. Они не управляются IANA, но IANA регистрирует и составляет списки использования этих портов для удобства потребителей. Когда это возможно, один и тот же порт присваивается данному сервису и для TCP, и для UDP. Например, порты с номерами от 6000 до 6063 присвоены серверу X Window для обоих протоколов, но в настоящее время все реализации используют только TCP. Верхний предел (49 151) для этих портов является новым, так как в RFC 1700 [87] он был равен 65 535.
Динамически назначаемые, или частные^ порты: от 49 152 до 65 535. IANA ничего не говорит об этих портах. Эти порты иногда называются эфемерными. («Волшебное» число 49 152 составляет три четверти от 65 536.)
На рис. 2.6 показано разделение портов на диапазоны и общее распределение номеров портов.
Заранее известные порты IANAЗареМтрироМННЫв Порты IANA►] 1^- 1023 1024 ДинамическиЗаранее известные порты IANAЗареМтрироМННЫв Порты IANA►] 1^- 1023 1024 ДинамическиДинамические,или частные,порты IANA—н ин49151 4915265535Динамические,или частные,порты IANA—н ин49151 4915265535Зарезервированные назначаемые порты BSDпорты BSD卜 М И►! КЗарезервированные назначаемые порты BSDпорты BSD卜 М И►! К1023 10241023 10245000 50015000 5001rresvportrresvport513 1023513 1023BSD-серверы(непривилегированные)BSD-серверы(непривилегированные)Динамически назначаемые порты SolarisДинамически назначаемые порты Solaris655356553532768327683276832768
Заранее известные порты IANAЗареМтрироМННЫв Порты IANA
►] 1^- 1023 1024 Динамически
Заранее известные порты IANAЗареМтрироМННЫв Порты IANA
►] 1^- 1023 1024 Динамически
Динамические,или частные,порты IANA
—н ин
49151 4915265535
Динамические,или частные,порты IANA
—н ин
49151 4915265535
Зарезервированные назначаемые порты BSDпорты BSD
卜 М И►! К
Зарезервированные назначаемые порты BSDпорты BSD
卜 М И►! К
1023 1024
1023 1024
5000 5001
5000 5001
rresvport
rresvport
513 1023
513 1023
BSD-серверы(непривилегированные)
BSD-серверы(непривилегированные)
Динамически назначаемые порты Solaris
Динамически назначаемые порты Solaris
65535
65535
32768
32768
32768
32768
Рис. 2.6. Распределение номеров портов
На этом рисунке мы отмечаем следующие моменты:
V В системах Unix имеется понятие зарезервированного порта {reserved port)— это порт с номером меньше 1024. Эти порты может присвоить сокету только привилегированный процесс. Все заранее известные порты IANA являются зарезервированными портами; следовательно, сервер, желающий использовать этот порт (такой, как сервер FTP), должен обладать правами привилегированного пользователя.
Исторически сложилось так, что Беркли-реализации (начиная с 4.3BSD) позволяют динамически выделять порты в диапазоне от 1024 до 5000. Это было приемлемо в начале 80-х, когда серверы не могли обрабатывать много клиентов одновременно, но сегодня можно легко найти сервер, поддерживающий более 3977 клиентов в любой момент времени. Поэтому некоторые системы выделяют динамически назначаемые порты по-другому (например, Solaris, как показано на рис. 2.6), чтобы предоставить больше динамически назначаемых портов.
ПРИМЕЧАНИЕ
Как выяснилось, значение 5000 для верхнего предела динамически назначаемых портов, реализованное в настоящее время во многих системах, было типографской ошибкой [6]. Этот предел должен был быть равен 50 000
扌 Существует несколько клиентов (не серверов), которые запрашивают зарезервированный порт для аутентификации в режиме клиент-сервер: типичным примером могут служить клиенты rlogin и rsh. Эти клиенты вызывают библиотечную функцию rresvport для создания сокета TCP и присваивают сокету неиспользованный номер порта из диапазона от 513 до 1023 Эта функция
обычно пытается связаться с портом 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.
Номера портов TCP и параллельные серверы
Что случится при наличии параллельного сервера, когда основным циклом сервера порождается дочерний процесс для обработки каждого нового соединения, если дочерний процесс будет продолжать использовать заранее известный номер порта при обслуживании длительного запроса? Проанализируем типичную последовательность. Пусть сервер запускается на узле bsdi (рис. 1.7), который является многоинтерфейсным (mutlihomed), то есть имеет несколько сетевых интерфейсов с IP-адресами 206.62.226.35 и 206.62.226.66, и выполняет пассивное открытие, используя свой заранее известный номер порта (в данном примере 21). Теперь он ждет запроса клиента. Эта ситуация изображена на рнс. 2.7.
Для указания пары сокетов сервера мы используем обозначение {*.21, *.*}. Сервер ждет запроса соединения на порте 21 любого локального интерфейса (первая звездочка). Удаленный IP-адрес и удаленный порт не заданы, и мы обознача-
Сервер
{*.21,*.*} —;А Прослушиваемый
」 сокет
Рис. 2.7. Сервер TCP с пассивным открытием на порте 21
ем их *.*. В данном случае можно сказать, что речь идет о прослушиваемом сокете (listening socket).
ПРИМЕЧАНИЕ
Чтобы отделить IP-адрес от номера порта, мы используем точку, поскольку это обозначение принято в программе netstat. Иногда это вызывает путаницу, поскольку десятичные точки используются и в доменных именах (solaris.kohala.com. 21), ив точечно-десятичной записи IPv4 (206 62.226.33.21).
Локальный IP-адрес, задаваемый с помощью звездочки (символа подстановки}, wildcard), — называется универсальным адресом. Если узел, на котором запущен сервер, является многоинтерфейсным (как в нашем примере), сервер может указать, что он хочет принимать входящие соединения, которые приходят только для одного определенного локального интерфейса. Сервер должен либо выбрать один определенный интерфейс, либо принимать запросы от всех интерфейсов, то есть он не может задать список, состоящий из нескольких адресов. Локальный адрес, заданный с помощью символов подстановки, соответствует выбору произвольного адреса из определенного множества. В листинге 1.5 перед вызовом функции bi nd IP-anpec в структуре адреса сокета задан с помощью константы INADDR_ANY.
Позже клиент запускается на узле с IP-адресом 198.69.10.2 и выполняет активное открытие соединения с IP-адресом сервера 206.62.226.35. В этом примере мы считаем, что динамически назначаемый порт, выбранный клиентом TCP,— это порт 1500, что отражено на рис. 2.8. Под клиентом мы показываем его пару сокетов.
198.69.10.2198.69.10.2КлиентКлиент{198.69.10.2.1500,206.62.226.35.21}{198.69.10.2.1500,206.62.226.35.21}Запрос на соединение 206.62.226.35 port 21Запрос на соединение 206.62.226.35 port 21206.62.226.35206.62.226.66206.62.226.35206.62.226.66> Сервер> Сервер{*.21,*.*}{*.21,*.*}
198.69.10.2
198.69.10.2
Клиент
Клиент
{198.69.10.2.1500,
206.62.226.35.21}
{198.69.10.2.1500,
206.62.226.35.21}
Запрос на соединение 206.62.226.35 port 21
Запрос на соединение 206.62.226.35 port 21
206.62.226.35
206.62.226.66
206.62.226.35
206.62.226.66
> Сервер
> Сервер
{*.21,*.*}
{*.21,*.*}
Рис. 2.8. Запрос на соединение от клиента к серверу
Для термина «wildcard» пока не существует устоявшегося перевода Встречаются такие варианты, как «метасимвол», «символ замены», «символ подстановки», «расширитель», «универсальный символ», «генератор», «групповой символ» и даже «джокер», «уальдкард» и «дикая карта» Под этими разнообразными названиями подразумевается специфический символ (как правило, звездочка), который заменяет любой символ или группу символов — Примеч перев
Когда сервер получает и принимает соединение клиента, он создает с помощью функции fork свою копию, давая возможность дочернему процессу обработать запрос клиента, как показано на рис. 2.9 (функцию fork мы описываем в разделе 4.7).
206.62.226.35
Рис. 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Д0.2, порт 1501, и предназначен для IP-адреса 206.62.226.35, порт 21, он доставляется второму дочернему процессу. Все другие сегменты TCP, предназначенные для порта 21, доставляются исходному серверу с прослушиваемым со
кетом.
"•,厶 nt»“ I .—л
198.69.10.2198.69.10.2206.62.226.35206.62.226.66206.62.226.35206.62.226.66Рис. 2.10. Второе соединение клиента с тем же серверомРис. 2.10. Второе соединение клиента с тем же сервером
198.69.10.2
198.69.10.2
206.62.226.35
206.62.226.66
206.62.226.35
206.62.226.66
Рис. 2.10. Второе соединение клиента с тем же сервером
Рис. 2.10. Второе соединение клиента с тем же сервером
Размеры буфера и ограничения
Существует несколько ограничений, влияющих на размер дейтаграмм 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-
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 выполняют фрагментацию (fragmentatiori). Фрагменты не соединяются заново (reassemble), пока они не достигнут конечного получателя. Узлы IPv4 выполняют фрагментацию генерируемых дейтаграмм, а маршрутизаторы IPv4 выполняют фрагментацию передаваемых дейтаграмм. Но в случае IPv6 дейтаграммы фрагментируются только узлами, а маршрутизаторы IPv6 не фрагментируют передаваемые ими дейтаграммы.
ПРИМЕЧАНИЕ
Будьте внимательны при использовании данной терминологии. Узел, помеченный как маршрутизатор IPv6, может все равно выполнять фрагментацию, но только для дейтаграмм, которые этот маршрутизатор генерирует сам, однако он никогда не фрагментирует передаваемые им дейтаграммы. Когда этот узел генерирует дейтаграммы IPv6, он на самом деле выступает в роли узла (а не маршрутизатора). Например, большинство маршрутизаторов поддерживают протокол Telnet, используемый администраторами для настройки. Дейтаграммы IP, генерируемые сервером Telnet маршрутизатора, генерируются маршрутизатором, поэтому он может выполнять их фрагментацию
Вы можете заметить, что в заголовке IPv4 (рис. А 1) существуют поля для выполнения 1Р¥4-фрагментации, но в заголовке IPv6 (рис. А.2) полей для фрагментации нет. Поскольку фрагментация скорее исключение, чем правило, IPv6 может содержать дополнительный заголовок с информацией о фрагментации.
Если бит DF (Don't Fragment — не фрагментировать) в заголовке IPv4 установлен (см. рис. А.1), это означает, что данная дейтаграмма не должна быть фрагментирована ни отправляющим узлом, ни каким-либо маршрутизатором на ее пути. Маршрутизатор, получающий дейтаграмму IPv4 с установленным битом DF, размер которой превышает MTU исходящей линии, генерирует сообщение ICMPv4 о недоступности получателя Fragmentation 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
1981 [62] для IPv6). Например, если TCP использует эту технологию с IPv4, он отправляет все дейтаграммы с установленным битом DF. Если же какой- нибудь промежуточный маршрутизатор возвращает сообщение ICMP Fragmentation needed but DF bit set (Необходима фрагментация, но установлен бит DF), TCP уменьшает количество данных, которые он отправляет в каждой дейтаграмме, и передает их повторно. Обнаружение транспортной MTU не обязательно для IPv4, но должно поддерживаться всеми реализациями IPv6. IPv4 и IPv6 определяют минимальный размер буфера сборки (minimum reassembly buff er size): минимальный размер дейтаграммы, который гарантированно поддерживает любая реализация. Для IPv4 этот размер равен 576 байт, для IPv6 он увеличен до 1500 байт. Например, в случае IPv4 мы не знаем, может ли данный получатель принять дейтаграмму в 577 байт. Поэтому многие приложения IPv4, использующие UDP (DNS, RIP, TFTP, ВООТР, SNMP) предотвращают генерацию дейтаграмм IP, превышающих этот размер.
緣 Для протокола ТСР определен максималъныйразмер сегмента (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.
ПриложениеБуфер приложения (любого размера)
writeПользовательски访 процесс
-- ~ 痴0
TCPБуфер отправки сокета (S0_SNDBUF)
Сегменты TCR имеющие размер MSS
V MSS обычно < MTU-40 (IPv4) или MTU-60 (IPv6:
Пакеты IPv4 или IPv6, имеющие размер MTU
V
Очередь
вывода
Канальный
уровень
Рис. 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 может выполнить фрагментацию перед пере-
дачей дейтаграммы, но, как мы отмечали выше, одна из целей параметра MSS — не допустить фрагментации; более новые реализации также используют обнаружение транспортной MTU. У каждого канального соединения имеется очередь вывода, и если она заполнена, пакет игнорируется и вверх по стеку протоколов возвращается ошибка: от канального уровня к IP и затем от IP к TCP. TCP учтет эту ошибку и попытается отправить сегмент позже. Приложение не информируется об этом временном состоянии.
Отправка по UDP
На рис. 2Д2 показано, что происходит, когда приложение записывает данные в сокет UDP. На этот раз буфер отправки сокета избражен пунктирными линиями, поскольку он (буфер) на самом деле не существует. У сокета UDP есть размер буфера отправки (который мы можем изменить с помощью функции сокета SO_SNDBUF, см. раздел 7.5), но это просто верхний предел размера дейтаграммы UDP, которая может быть записана в сокет. Если приложение записывает дейтаграмму размером больше буфера отправки сокета, возвращается сообщение об ошибке EMSGSIZE. Поскольку протокол UDP не является надежным, ему не нужно хранить копию данных приложения. Ему также не нужно иметь настоящий буфер отправки (данные приложения обычно копируются в буфер ядра по мере их движения вниз по стеку протоколов, но эта копия сбрасывается канальным уровнем после передачи данных).
UDP просто добавляет свой 8-байтовый заголовок и передает дейтаграмму протоколу IP. IPv4 или IPv6 добавляет свой заголовок, определяет исходящий интерфейс, выполняя функцию маршрутизации, а затем либо добавляет дейтаграмму в очередь вывода канального уровня (если размер дейтаграммы не превосходит MTU), либо фрагментирует дейтаграмму и добавляет каждый фрагмент в очередь вывода канального уровня.
Рис. 2.12. Этапы записи данных в сокет UDP и буферы, используемые при этой записи'
Если приложение UDP отправляет большие дейтаграммы (например, 2000байтовые), существует гораздо большая вероятность фрагментации, чем в случае TCP, поскольку TCP разбивает данные приложения на порции, равные по размеру MSS, которому нет аналога в UDP.
Успешное завершение функции записи в сокет UDP говорит о том, что либо дейтаграмма, либо фрагменты дейтаграммы были добавлены к очереди вывода канального уровня. Если недостаточно места для дейтаграммы или одного из ее фрагментов, приложению часто возвращается сообщение ENOBUFS.
ПРИМЕЧАНИЕ
К сожалению, некоторые реализации не возвращают этой ошибки, не предоставляя приложению никаких указаний на то, что дейтаграмма была проигнорирована еще до начала передачи.
Стандартные службы Интернета
В табл. 2.1 перечислены некоторые стандартные службы, предоставляемые большинством реализаций TCP/IP. Обратите внимание, что все они предоставляются с использованием и TCP, и UDP, и номер порта для обоих протоколов один и тот же.
Таблица 2.1. Стандартные службы TCP/JR предоставляемые в большинстве реализаций
»»т^«хк<вт-»км(
Имя
Порт 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-разрядного числа. Это число представляет собой количество секунд, прошедших с момента ОО.ОО'ОО 1 января 1900 года (итс>
Эти службы часто предоставляются демоном inetd на узлах Unix (см. раздел 12.5). Ставдартные службы обеспечивают возможность простого тестирования при использовании стандартного клиента Telnet. Вот, например, тесты для сервера, определяющего время и дату, и для эхо-сервера.
1 UTC — Universal Time Coordinated, универсальное скоординированное время (среднее время по Гринвичу). — Примеч. перев.
solans Z telnet bsdi daytime Trying 206 62 226 35Connected to bsdi kohala com Escape character is '*]'Tue Mar 19 11 06 49 1996Connection closed by foreign hostsolans Z telnet bsdi daytime Trying 206 62 226 35Connected to bsdi kohala com Escape character is '*]'Tue Mar 19 11 06 49 1996Connection closed by foreign hostsolans % telnet bsdi echoTrying 206 62 226 35Connected to bsdi kohala com Escape character is ‘人]'. hello, world hello worldtelnet> quitConnection closedsolans % telnet bsdi echoTrying 206 62 226 35Connected to bsdi kohala com Escape character is ‘人]'. hello, world hello worldtelnet> quitConnection closedвыходные данные -клиента Telnet выходные данные клиента Telnet выходные данные клиента Telnet выходные данные сервера daytime выходные данные клиента Telnet (сервер закрывает соединение)
solans Z 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 Z 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
telnet> quit
Connection closed
solans % telnet bsdi echo
Trying 206 62 226 35
Connected to bsdi kohala com Escape character is ‘人]'. hello, world hello world
telnet> quit
Connection closed
выходные данные клиента Telnet
выходные данные клиента Telnet
выходные данные клиента Telnet
это набираем мы
это возвращает сервер
мы вводим символы л« Jдля обращения к клиент Telnet 〃 сообщаем ему, что закончили
на этот раз клиент закрывает соединение
В этих двух примерах мы вводим имя узла и название службы (daytime и echo). Названия служб сопоставляются с номерами портов, показанными в табл. 2.1, с помощью файла /etc/services, о котором речь пойдет в разделе 9.9.
Обратите внимание, что когда мы соединяемся с сервером daytime, сервер выполняет активное закрытие. В случае эхо-сервера клиент выполняет активное закрытие. Вспомним рис. 2.4, где показано, что узел, выполняющий активное закрытие, —это узел, проходящий через состояние TIME_WAIT.
Использование протоколов приложениями Интернета
Таблица 2.2 иллюстрирует использование протоколов типичными приложениями Интернета.
Таблица 2.2. Использование протоколов типичными приложениями Интернета
Приложение
IP
ICMP
Udp
TCP
Pmg
•
Traceroute
•
•
OSFP (протокол маршрутизации)
•
RIP (протокол маршрутизации)
•
BGP (протокол маршрутизации)
•
ВООТР (протокол bootstrap — протокол ~
•
дистанционной загрузки и запуска устройств в сети)
DHCP (протокол bootstrap)
•
NTP (синхронизирующий сетевой протокол)
•
TFTP (простейший протокол передачи фдйлов) , t
•
SNMP (управление сетью)
•
SMTP (электронная почта)
•
Telnet (удаленный вход в систему)
•
Приложение\РICMP UDPTCP
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.
Резюме
UDP является простым ненадежным протоколом, не ориентированным на установление соединения, в то время как TCP — это сложный, надежный, ориентированный на установление соединения протокол. Хотя большинство приложений в Интернете используют протокол TCP (Web, Telnet, FTP, электронная почта), существует необходимость в обоих протоколах. В разделе 20.4 мы рассматрим причины, по которым иногда вместо TCP выбирается UDP.
TCP устанавливает соединение с помощью трехэтапного рукопожатия и разрывает соединение, используя обмен четырьмя пакетами. Когда соединение ТСР установлено, оно переходит из состояния CLOSED в состояние ESTABLISHED. При разрыве соединения оно переходит обратно в состояние CLOSED. Существует всего 11 состояний, в которых может находиться соединение TCP. Правила перемещения между этими состояниями определяет диаграмма переходов. Понимание этой диаграммы существенно для диагностики проблем при использовании программы netstat. Важно также понимать, что происходит, когда мы вызываем такие функции, как connect, accept и close.
Состояние TCP TIME_WAIT — неиссякаемый источник путаницы, возникающей у разработчиков сетевых приложений. Это состояние существует для того, чтобы реализовать разрыв двустороннего соединения TCP (например, для решения проблем, возникающих в случае потери последнего сегмента АСК), а также чтобы дождаться, когда истечет время жизни старых дублированных сегментов в сети.
Упражнения
Мы говорили о протоколах IPv4 и IPv6 А что произошло с версией 5, и каковы были версии 0, 1, 2 и 3? (Подсказка найдите последний документ RFC «Assigned numbers» («Присвоенные номера») Можете сразу переходить к решению (приложение Д), если вы не знаете, как получать документы RFC в электронном виде )
Где вы будете искать дополнительную информацию о протоколе, которому присвоено название «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
Введение в сокеты
Введение
Эта глава начинается с описания программного интерфейса приложения (API) сокетов. Мы начнем со структур адресов сокетов, которые будут встречаться почти в каждом примере на протяжении всей книги. Эти структуры можно передавать в двух направлениях: от процесса к ядру и от ядра к процессу. Последний случай — пример аргумента, через который передается возвращаемое значение, и далее в книге мы встретимся с другими примерами таких аргументов.
Перевод текстового представления адреса в двоичное значение, входящее в структуру адреса сокета, осуществляется функциями преобразования адресов. В большей части существующего кода IPv4 используются функции inet_add「 и 1 net_ntoa, но две новых функции, i net pton и inet_ntop, работают и с IPv4, и с IPv6.
Одной из проблем этих функций является то, что они зависят от протокола, так как для них имеет значение тип преобразуемого адреса — IPv4 или IPv6. Мы разработали набор функций, названия которых начинаются с sock_, работающих со структурами адресов сокетов независимо от протокола. Эти функции мы и будем использовать, чтобы сделать наш код не зависящим от протокола.
Структуры адреса сокетов
Большинство функций сокетов используют в качестве аргумента указатель на структуру адреса сокета. Каждый набор протоколов определяет свою собственную структуру адреса сокетов. Имена этих структур начинаются с sockaddг_ и заканчиваются уникальным суффиксом для каждого набора протоколов.
Структура адреса сокета IPv4
Структура адреса сокета IPv4, обычно называемая структурой адреса сокета Интернета, именуется sockaddr_in и определяется в заголовочном файле <netinet/ in h>. В листинге 3.11 представлено определение Posix. 1g.
Листинг 3.1. Структура адреса сокета Интернета (IPv4): sockadd гjn struct in_addr { in_dddr_t s_addr /* 32-разрядный адрес IPv4 */ /* порядок байтов в сети */
Вес исходные коды программ, опубликованные в згой книге, вы можете найти по адресу http:// www piter com/download
}.
struct sockaddr in {
uint8_t
sin_len.
/*
длина структуры
(16) */
sa_farmly_t
sin_family.
/*
AFJNET */
in_port_t
sin_port.
/* /*
16-битовый номер порядок байтов в
порта TCP или UDP */ сети */
struct in_addr
sin_dddr.
/* /*
32-битовый адрес порядок байтов в
IPv4 */ сети */
char
s】n_zero[8].
/*
не используется
*/
}.
Есть несколько моментов, касающихся структур адреса сокета в целом, которые мы покажем на примере.
> Элемент длины si п_1 еп появился в версии 4.3BSD-Reno, когда была добавлена поддержка протоколов OSI (см. рис. 1.6). До этой реализации первым элементом был sin_family, который исторически имел тип unsigned short (целое без знака). Не все производители поддерживают поле длины для структур адреса сокета, и в Posix.lg, например, не требуется наличия этого элемента. Типы данных, подобные innt8_t, введены в Posix.lg (табл. 3.1). Наличие поля длины упрощает обработку структур адреса сокета с переменной длиной.
q Даже если поле длины присутствует, нам не придется устанавливать и проверять его значение, пока мы не имеем дела с маршрутизирующими сокетами (см. главу 17). Оно используется внутри ядра процедурами, работающими со структурами адресов сокетов из различных семейств протоколов (например, код таблицы маршрутизации).
ПРИМЕЧАНИЕ
Четыре функции, передающие структуру адреса сокета от процесса к ядру, — bind, connect, sendto и sendmsg — используют функцию sockargs в реализациях, ведущих происхождение от Беркли [105, с. 452 ]. Эта функция копирует структуру адреса сокета из процесса и затем явно присваивает элементу sin len значение размера структуры, переданной в качестве аргумента этим четырем функциям Пять функций, передающих структуру адреса сокета от ядра к процессу, — accept, recvfrom, recvmsg, getpeername и getsockname — устанавливают элемент sin_len перед возвращением управления процессу. К сожалению, обычно не существует простого теста, выполняемого в процессе компиляции и определяющего, задает ли реализация поле длины для своих структур адреса сокета. В нашем коде мы тестируем собственную константу HAVE_SOCKADDR_SA_ LEN, (листинг Г.2), но для того чтобы определить, задавать эту константу или нет, требуется откомпилировать простую тестовую программу, использующую необязательный элемент структуры, и проверить, успешно ли выполнена компиляция. В листинге 3.3 мы увидим, что от реализаций IPv6 требуется задавать SIN6_LEN, если структура адреса сокета имеет поле длины. В некоторых реализациях IPv4 (например, Digital Unix) поле длины предоставляется для приложений, основанных на параметре времени компиляции (например, _SOCKADDR_LEN), Это свойство обеспечивает совместимость с другими более ранними программами.
Posix.lg требует наличия только трех элементов структуры: sin_fanii 1у, si n_addr и si n_port. Posix-совместимая реализация может определять дополнительные
элементы структуры, и это норма для структуры адреса сокета Интернета. Почти все реализации добавляют элемент sin_zero, так что все структуры адреса сокета имеют размер как минимум 16 байт.
緻 Типы элементов s_addr, sin_fami 1у и si n_port мы указываем согласно Posix.lg. Тип данных 1 n_addr_t соответствует целому числу без знака длиной как минимум 32 бита, in_port_t — целому числу без знака длиной как минимум 16 бит, a sa_family_t — это произвольное целое число без знака. Последнее обычно представляет собой 8-разрядное целое без знака, если реализация поддерживает поле длины, либо 16-разрядное целое, если поле длины не поддерживается. В табл. 3.1 перечислены эти три типа данных Posix вместе с некоторыми другими типами данных Posix. 1g, с которыми мы встретимся.
Таблица 3.1. Типы данных, требуемые Posix. 1д
Тип данных
Описание
Заголовочный файл
mt8_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>
in_addr_t
1Ру4-адрес, обычно типа uint32_t
<netinet/in.h>
in_port_t
порт TCP или UDP, обычно типа uintl6_t
<netinet/in h>
Вы также встретите типы данных u char, u_short, u_i nt и u_long, которые не имеют знака. Posix. 1g определяет их с замечанием, что они устарели. Они предоставляются в целях обратной совместимости.
И адрес IPv4, и номер порта TCP и UDP всегда хранятся в структуре в соответствии с порядком байтов, определенным в сети. Об этом нужно помнить при использовании этих элементов (более подробно о разнице между порядком байтов узла и порядком байтов в сети мы поговорим в разделе 3.4),
ПРИМЕЧАНИЕ —
Причина того, что sin_addr является структурой, а не просто целым числом без знака, носит исторический характер. В более ранних реализациях (например, 4.2BSD) структура in ad dr определялась как объединение (union) различных структур, чтобы сделать возможным доступ к каждому из четырех байтов 32-разрядного 1Ру4-адреса, а также к обоим входящим в него 16-разрядным значениям. Эта возможность использовалась в адресах классов А, В и С для выборки соответствующих байтов адреса. Но с появлением подсетей и последующим исчезновением различных классов адресов (см. раздел А.4) и введением бесклассовой адресации (classless addressing) необходимость в объединении структур отпала. В настоящее время большинство систем отказались от использования объединения и просто определяют m_addr как структуру, содержащую один элемент типа unsigned long.
г К 32-разрядному адресу IPv4 можно обратиться двумя путями. Например, если serv — это структура адреса сокета Интернета, то serv sin_addr указывает на 32-разрядный адрес IPv4 как на структуру in_addr, в то время как se「v.sin_ 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 sd_fannly. /* семейство адресов: константа AF_xxx */ char sa_data[14], /* адрес. специфичный для протокола */ 一
Функции сокетов определяются таким образом, что их аргументом является указатель на общую структуру адреса сокета, как показано в прототипе функции bind (ANSI С): “’
int bind(mt, struct sockaddr *. socklen_t).
При этом требуется, чтобы для любых вызовов этих функций указатель на структуру адреса сокета, специфичную для протокола, был преобразован в указатель на универсальную структуру адреса сокета. Например:
struct sockaddr_in serv. /* структура адреса сокета IPv4 */ /* заполняем serv{) */
b】nd(sockfd. (struct sockaddr *) &serv, sizeof(serv));
Если мы не выполним преобразование (struct sockaddr *), компилятор С сгенерирует предупреждение в форме nWarm ng passing arg 2 of'bind' from incompatible poi nter type1' (Передается указатель несовместимого типа). Здесь мы предполагаем, что в системных заголовочных файлах имеется прототип ANSI С для функции bi nd.
С точки зрения разработчика приложений универсальная структура адреса сокета используется только для преобразования указателей на структуры адресов конкретных протоколов.
ПРИМЕЧАНИЕ
Вспомните, что в нашем заголовочном файле unp.h (см. раздел 1.2) мы определили SA у как строку "struct sockaddr", чтобы сократить код, который мы написали для преобра- 'зования этих указателей.
С точки зрения ядра основанием использовать в качестве аргументов указатели на универсальные структуры адреса сокетов является то, что ядро должно получать указа- , тель вызывающей функции, преобразовывать его в struct sockadd г, а затем по значе- {/ нию элемента sa/family определять тип структуры. Но разработчику приложений было бы проще работать с указателем void *, поскольку это избавило бы его от необходимости выполнять явное преобразование указателя.
Структура адреса сокета IPv6
Структура адреса сокета IPv6 задается при помощи включения заголовочного файла <netinet/in h>, как показано в листинге 3.3.
Листинг 3.3. Структура адреса сокета IPv6: sockaddr_in6
uint8_t s6_addr[16]:/* 128-разрядный адрес IPv6 */}•/* порядок байтов сетевой */#define SIN6_LEN/* требуется для проверки во время конпиляиии */struct sockaddr_in6 { uint8_t sin6_len;s a_fami 1 y_ts i n6_fami 1 y;in_port_t sin6_port./* длина этой структуры (24) *//* AF」NET6 *//* номер порта транспортного уровня *//* порядок байтов сетевой */иint32_tsin6_们 owinfo./* приоритет и метка потока */ /* порядок байтов сетевой */struct in6_addr sin6_add「,/* IPv6-адрес *//* порядок байтов сетевой */uint8_t s6_addr[16]:/* 128-разрядный адрес IPv6 */}•/* порядок байтов сетевой */#define SIN6_LEN/* требуется для проверки во время конпиляиии */struct sockaddr_in6 { uint8_t sin6_len;s a_fami 1 y_ts i n6_fami 1 y;in_port_t sin6_port./* длина этой структуры (24) *//* AF」NET6 *//* номер порта транспортного уровня *//* порядок байтов сетевой */иint32_tsin6_们 owinfo./* приоритет и метка потока */ /* порядок байтов сетевой */struct in6_addr sin6_add「,/* IPv6-адрес *//* порядок байтов сетевой */struct in6_addr {
uint8_t s6_addr[16]:
/* 128-разрядный адрес IPv6 */
}•
/* порядок байтов сетевой */
#define SIN6_LEN
/* требуется для проверки во время конпиляиии */
struct sockaddr_in6 { uint8_t sin6_len;
s a_fami 1 y_ts i n6_fami 1 y;
in_port_t sin6_port.
/* длина этой структуры (24) */
/* AF」NET6 */
/* номер порта транспортного уровня */
/* порядок байтов сетевой */
иint32_tsin6_们 owinfo.
/* приоритет и метка потока */ /* порядок байтов сетевой */
struct in6_addr sin6_add「,
/* IPv6-адрес */
/* порядок байтов сетевой */
uint8_t s6_addr[16]:
/* 128-разрядный адрес IPv6 */
}•
/* порядок байтов сетевой */
#define SIN6_LEN
/* требуется для проверки во время конпиляиии */
struct sockaddr_in6 { uint8_t sin6_len;
s a_fami 1 y_ts i n6_fami 1 y;
in_port_t sin6_port.
/* длина этой структуры (24) */
/* AF」NET6 */
/* номер порта транспортного уровня */
/* порядок байтов сетевой */
иint32_tsin6_们 owinfo.
/* приоритет и метка потока */ /* порядок байтов сетевой */
struct in6_addr sin6_add「,
/* IPv6-адрес */
/* порядок байтов сетевой */
ПРИМЕЧАНИЕ
Расширения API сокетов для IPv6 описаны в RFC 2133 [32]. В Posix. 1g ничего не сказано об IPv6. Однако некоторые типы данных в листинге 3.3 отличаются от приведенных в RFC, поскольку используемые нами типы данных были окончательно определены в проекте Posix. 1g, опубликованном после выхода RFC 2133.
Отметим следующие моменты относительно листинга 3.3:
詣 Константа SIN6 J_EN должна быть задана, если система поддерживает поле длины для структур адреса сокета.
端 Семейством IPv6 является AF_INET6, в то время как семейство IPv4 — AF__INET.
藻 Элементы в структуре упорядочены таким образом, что если структура sockaddr_i пб выровнена по 64 битам, то так же выровнен и 128~разрядный элемент sin6_addr. На некоторых 64-разрядных процессорах доступ к данным с 64- разрядными значениями оптимизирован, если данные выровнены так, что их адрес кратен 64.
戀 Элемент si n6_fl owi nfo разделен на три поля:
24 бита младшего порядка — это метка потока;
следующие 4 бита обозначают приоритет;
следующие 4 бита зарезервированы.
Поле метки потока и поле приоритета рассматриваются в описании рис. А.2. Отметим, что использование поля приоритета еще не определено.
Сравнение структур адреса сокетов
На рис. 3.1 показано сравнение четырех структур адресов сокетов, с которыми мы встретимся в тексте, предназначенных для IPv4, IPv6, доменного сокета Unix и канального уровня (см. листинг 17.1). Подразумевается, что все структуры адреса сокета содержат 1-байтовое поле длины, поле семейства также занимает 1 байт и длина любого поля, размер которого охраничен снизу, в точности равна этому ограничению. Две структуры адреса сокета имеют фиксированную длину, а структура доменного сокета Unix и структура канального уровня — переменную. При обработке структур переменной длины мы передаем функциям сокетов
IPv4sockaddr_in { }АСИНЗ I AF一工NET16-битовый порт32-битовыйIPv4-aflpecIPv4sockaddr_in { }АСИНЗ I AF一工NET16-битовый порт32-битовыйIPv4-aflpecIPv6sockaddr_in6 { }Длина -r匸1症正16-битовый порт32-битоваяметка потокаIPv6sockaddr_in6 { }Длина -r匸1症正16-битовый порт32-битоваяметка потокаUnix sockad.dr_un{ } Длина~|af localUnix sockad.dr_un{ } Длина~|af local(Не используется)(Не используется)Фиксированная длина(16 байт)Листинг 3.1Фиксированная длина(16 байт)Листинг 3.1128-битовыйIPv4-3flpec128-битовыйIPv4-3flpecПолное имя(не более104 байт)Полное имя(не более104 байт)ДлинаAF—LINKИндекс интерфейсаТипДлина имениДлина адресаДлина селектораИмя интерфейса и адрес канального уровняДлинаAF—LINKИндекс интерфейсаТипДлина имениДлина адресаДлина селектораИмя интерфейса и адрес канального уровняКанальный уровень sockaddr_dl{}Канальный уровень sockaddr_dl{}Переменная длина Листинг 17.1Переменная длина Листинг 17.1Фиксированная длина(24 байта)Листинг 3.4Фиксированная длина(24 байта)Листинг 3.4
IPv4sockaddr_in { }АСИНЗ I AF一工NET16-битовый порт32-битовыйIPv4-aflpec
IPv4sockaddr_in { }АСИНЗ I AF一工NET16-битовый порт32-битовыйIPv4-aflpec
IPv6sockaddr_in6 { }Длина -r匸1症正16-битовый порт32-битоваяметка потока
IPv6sockaddr_in6 { }Длина -r匸1症正16-битовый порт32-битоваяметка потока
Unix sockad.dr_un{ } Длина~|af local
Unix sockad.dr_un{ } Длина~|af local
(Не используется)
(Не используется)
Фиксированная длина(16 байт)Листинг 3.1
Фиксированная длина(16 байт)Листинг 3.1
128-битовыйIPv4-3flpec
128-битовыйIPv4-3flpec
Полное имя(не более104 байт)
Полное имя(не более104 байт)
Длина
AF—LINK
Индекс интерфейса
Тип
Длина имени
Длина адреса
Длина селектора
Имя интерфейса и адрес канального уровня
Длина
AF—LINK
Индекс интерфейса
Тип
Длина имени
Длина адреса
Длина селектора
Имя интерфейса и адрес канального уровня
Канальный уровень sockaddr_dl{}
Канальный уровень sockaddr_dl{}
Переменная длина Листинг 17.1
Переменная длина Листинг 17.1
Фиксированная длина(24 байта)Листинг 3.4
Фиксированная длина(24 байта)Листинг 3.4
Переменная длина
Листинг 14.1
Рис. 3.1. Сравнение различных структур адресов сокетов
указатель на структуру адреса сокета, а в другом аргументе передаем длину этой структуры. Под каждой структурой фиксированной длины мы показываем ее размер в байтах (для реализации 4.4BSD).
ПРИМЕЧАНИЕ
Сама структура sockaddr_un имеет фиксированную длину, но объем информации в ней — длина полного имени (pathname) — может быть переменным. Передавая указатели на эти структуры, следует соблюдать аккуратность при обработке поля длины — как длины в структуре адреса сокета (если поле длины поддерживается данной реализацией), так и длины данных, передаваемых ядру и принимаемых от него
Этот рисунок служит также иллюстрацией стиля, которого мы придерживаемся в этой книге: названия структур на рисунках всегда выделяются полужирным шрифтом, а за ними следуют фигурные скобки.
Ранее отмечалось, что в реализации 4.3BSD Reno ко всем структурам адресов сокетов было добавлено поле длины. Если бы поле длины присутствовало в оригинальной реализации сокетов, то не возникло бы необходимости передавать аргумент длины функциям сокетов (третий аргумент функций bind и connect) Вместо этого размер структуры мог бы храниться в поле длины структуры.
Аргументы типа «значение-результат»
Мы отмечали, что когда структура адреса сокета передается какой-либо из функций сокетов, она всегда передается по ссылке, то есть в качестве аргумента передается указатель на структуру. Длина структуры также передается в качестве аргумента. Но способ, которым передается длина, зависит от того, в каком направлении передается структура: от процесса к ядру или наоборот.
Три функции — bind, connect и sendto — передают структуру адреса сокета от процесса к ядру. Один из аргументов этих функций — указатель на структуру адреса сокета, другой аргумент — это целочисленный размер структуры, как показано в следующем примере:.
struct sockaddr^in serv, /* заполняем serv{} */ connect(sockfd (SA *) &serv, sizeof(serv))
Поскольку ядру передается и указатель, и размер структуры, на которую он указывает, становится точно известно, какое количество данных нужно скопировать из процесса в ядро. На рис. 3.2 показан этот сценарий.
В следующей главе мы увидим, что размер структуры адреса сокета в действительности имеет тип socklen_t, а не int, но Posix.lg рекомендует, чтобы использовался тип umt32_t.
Четыре функции — accept, recvfrom, getsockname и get pee rname — передают структуру адреса сокета от ядра к процессу. Этим функциям передается указатель на структуру адреса сокета и указатель на целое число, содержащее размер структуры, как показано в следующем примере:
struct sockaddr_un cli,/* домен Unix */
socklen_t len.
len = sizeof(cli)/* len- это значение */
getpeername(unixfd, (SA *) &cli, &1en), /* значение len могло измениться */
Пользовательский процесс
tn1tn1
tn1
tn1
ДлинаДлинаosxcsx^osxcsx^ЯдроЯдро
Длина
Длина
osxcsx^
osxcsx^
Ядро
Ядро
Структураадресасокета
Aqpec, специфичныйдля данного протокола
Рис. 3.2. Структура адреса сокета, передаваемая от процесса к ядру
Причина замены типа для аргумента «длина» с целочисленного на указатель состоит в том, что размер является и значением при вызове функции (сообщает ядру размер структуры, так что ядро при заполнении структуры знает, где нужно остановиться), и результатом, когда функция возвращает значение (сообщает процессу, какой объем информации ядро действительно сохранило в этой структуре). Такой тип аргумента называется аргументом типа ^значение-результат^ (value-result argument). На рис. 3.3 представлен этот сценарий.
Пользовательский процесс
int*
ДлинаДлинаФХХФ5Х0ФХХФ5Х0IBlALTAm ①丘IBlALTAm ①丘СтруктураадресасокетаСтруктураадресасокета
Длина
Длина
ФХХФ5Х0
ФХХФ5Х0
IBlALTAm ①丘
IBlALTAm ①丘
Структураадресасокета
Структураадресасокета
Адрес, специфичный
для данного протокола
Рис. 3・3. Структура адреса сокета, передаваемая от ядра к процессуРис. 3・3. Структура адреса сокета, передаваемая от ядра к процессу
Рис. 3・3. Структура адреса сокета, передаваемая от ядра к процессу
Рис. 3・3. Структура адреса сокета, передаваемая от ядра к процессу
Пример аргументов типа «значение-результат» вы увидите в листинге 4.2.
Если при использовании аргумента типа «значение-результат» для длины структуры структура адреса сокета имеет фиксирован ную длину (рис. 3.1), то значение, возвращаемое ядром, будет всетда равно этому фиксированному раз-
меру: 16 для sockaddr_m IPv4 и 24 для sockaddr_m6 IPv6. Для структуры адреса сокета переменной длины (например, sockaddr_un домена Unix) возвращаемое значение может быть меньше максимального размера структуры (вы увидите это в листинге 14.2).
ПРИМЕЧАНИЕ
Мы говорили о структурах адресов сокетов, передаваемых между процессом и ядром. Для такой реализации, как 4.4BSD, где все функции сокетов являются системными вызовами внутри ядра, это верно. Но в некоторых реализациях, особенно в System V, функции сокетов являются лишь библиотечными функциями, которые выполняются как часть обычного пользовательского процесса. То, как эти функции взаимодействуют со стеком протоколов в ядре, относится к деталям реализации, которые обычно нас не волнуют. Тем не менее для простоты изложения мы будем продолжать говорить об этих структурах как о передаваемых между процессом и ядром такими функциями, как bind и connect. (В разделе В.1 вы увидите, что реализации System V действительно передают пользовательские структуры адресов сокетов между процессом и ядром, по как часть сообщений потоков.)
Существует еще две функции, передающие структуры адресов сокетов: это reevmsg и sendmsg (см. раздел 13.5). Однако при их вызове поле длины не является отдельным аргументом функции, а передается как одно из полей структуры.
В сетевом программировании наиболее общим примером аргумента типа «значение-результат» может служить длина возвращаемой структуры адреса сокета. Вы встретите и другие аргументы типа «значение-результат»:
殮 Три средних аргумента функции sei ect (см. раздел 6.3).
嬲 Аргумент длины для функции getsockopt (см. раздел 7.2).
舞 Элементы msg_r)amelen и msg_controllen структуры msghdr при использовании с функцией reevmsg (см. раздел 13.5).
誨 Элемент i fc_l еп структуры i fconf (см. листинг 16.1).
W Первый из двух аргументов длины в функции sysctl (см. раздел 17.4).
Функции определения порядка байтов
Рассмотрим 16-разрядное целое число, состоящее из двух байтов. Возможны два способа хранения двух байтов в памяти. Такое расположение, когда первым идет младший байт, называется прямым порядком байтов (little-endian)t а когда первым расположен старший байт — обратным порядком байтов (big-endian). На рис. 3.4 показаны оба варианта.
Сверху на этом рисунке изображены адреса, возрастающие справа налево, а снизу — слева направо. Старший бит (most significant bit, MSB) является в 16-раз- рядном числе крайним слева, г младший бит (least significant bit, LSB) — крайним справа.
ПРИМЕЧАНИЕ
Термины «прямой порядок байтов» и «обратный порядок байтов» указывают, какой конец многобайтового значения — младший байт или старший — хранится в качестве начального адреса значения.
Возрастание адресов в памяти
Адрес А+1 Адрес А
П^мой порядок байтов:П^мой порядок байтов:Старший байтМладший байт
П^мой порядок байтов:
П^мой порядок байтов:
| MSB 16-битовое значение LSB |
Обратный порядок байтов. [ Старший 6а济丁 | Млащцц访 6а宓丁 |
АдресААдресААдрес А+1Адрес А+1
АдресА
АдресА
Адрес А+1
Адрес А+1
Возрастание адресов в памяти►
Рис. 3.4. Прямой и обратный порядок байтов для 16-битового целого числа
К сожалению, не существует единого стандарта порядка байтов, и можно встретить системы, использующие оба формата. Способ упорядочивания байтов, используемый в конкретной системе, мы называем порядком байтов узла (host byte order). Программа, представленная в листинге 3.4, выдает порядок байтов узла.
Листинг 3.4. Программа для определения порядка байтов узла //intro/byteorder с
#】nclude "unp h”
int
4 5 6 7 84 5 6 7 8main(int агде. char **argv)
4 5 6 7 8
4 5 6 7 8
union { short s.'
charcEsizeof(short)];
} un.
un s = 0x0102.
pEntf(“%s “ CPU_VENDOR_OS).
if (sizeof(short) == 2) {
if (un c[0] == 1 && un.c[l] == 2)
printf("big-endiап\епи),
else if (un.c[0] = 2 && un c[l] == 1)
printfClittle-endianXen'1),
else
pri ntf (,,unknown\en,')
} else
printf("sizeof(short) = %d\en“, sizeof(short)):
20 exit(0)
21 }
Мы помещаем двухбайтовое значение 0x0102 в переменную типа short (короткое целое) и проверяем значения двух байтов этой переменной: с [0] (адрес Л на рис. 3.4) и с [1] (адрес Л+У на рис. 3.4), чтобы определить порядок байтов.
Константа CPU_VENDOR_OS определяется программой GNU (аббревиатура GNU раскрывается рекурсивно — GNU^ Not Unix) autoconf в процессе конфигурации, необходимой для выполнения программ из этой книги. В этой константе хранится тип центрального процессора, а также сведения о производителе и реализации операционной системы На рис. 1.7 представлены некоторые примеры вывода этой программы при запуске ее в различных системах, dix % byteorder powerpc-ibm-aix4 200 big-endian
alpha % byteorder
alpha-dec-osf4 0 little-endian
bsdi % byteorder
i386-pc-bsdi3 0 little-endian
gemim % byteorder
sparc-sun-sunos4 1 4 Mg-endian
hpux % byteorder
hppal 1-hp-hpuxlO 30 big-endian
linux % byteorder
i586-pc~linux-gnu little-endian
solans % byteorder
sparc-sun-solaris2 5 1 big-endian
unixware % byteorder
1386-umvel-sysv4 2Mp little-endian
Все, что было сказано об определении порядка байтов 16-разрядного целого числа, конечно, справедливо и в отношении 32-разрядного целого.
ПРИМЕЧАНИЕ
Существуют системы, в которых возможен переход от прямого к обратному порядку байтов [26] либо при перезапуске системы (MIPS 2000), либо в любой момент выполнения программы (Intel i860).
Разработчикам сетевых приложений приходится обрабатывать различия в определении порядка байтов, поскольку в сетевых протоколах используется сетевой порядок байтов (network byte order). Например, в сегменте TCP есть 16-разряд- ный номер порта и 32-разрядный адрес IPv4. Стеки отправляющего и принимающего протоколов должны согласовывать порядок, в котором передаются байты этих многобайтовых полей. Протоколы Интернета используют обратный порядок байтов.
Теоретически реализация Unix могла бы хранить поля структуры адреса сокета в порядке байтов узла, а затем выполнять необходимые преобразования при перемещении полей в заголовки протоколов и обратно, позволяя нам не беспокоиться об этом. Но исторически и с точки зрения Posix. 1g определяется, что для некоторых полей в структуре адреса сокета порядок байтов всегда должен быть сетевым. Поэтому наша задача — выполнить преобразование из порядка байтов узла в сетевой порядок и обратно. Для этого мы используем следующие четыре функции:
#include <netmet/m h>
uintl6_t htons(umtl6_t hostlGbitvalue)
uint32_t htonl(umt32_t host3^bitvdlue)
Обе функции возвращают значение, записанное в сетевом порядке байтов
uintl6_t ntohs(uintl6_t netl6bitvdlue)
uint32_t ntohl (uwt32_t net3?bitvdlue)
Обе функции возвращают значение, записанное в порядке байтов узла
В названиях этих функций h обозначает узел, п обозначает сеть, s — тип short, 1 — тип long. Термины shore и /объявляются наследием времен реализации 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 с обратным порядком байтов), было забыть вызвать одну из указанных четырех функций. На этих рабочих станциях программы работали нормально, но при переходе на машины с прямым порядком байтов они переставали работать.
Функции управления байтами
Существует две группы функций, работающих с многобайтовыми полями без преобразования данных и без интерпретации их как строк языка С с завершающим нулем. Они необходимы нам при обработке структур адресов сокетов, поскольку такие поля этих структур, как IP-адреса, могут содержать нулевые бай-
ты, но при этом не являются строками С. Строки с завершающим нулем обрабатываются функциями языка С, имена которых начинаются с аббревиатуры str. Эти функции подключаются с помощью файла <string h>.
Первая группа функций, названия которых начинаются с b (от слова «byte» ~ «байт»), взята из реализации 4.2BSD и все еще предоставляется практически любой системой, поддерживающей функции сокетов. Вторая группа функций, названия которых начинаются с mem (от слова《memory» — память), взята из стандарта ANSI С и доступна в любой системе, обеспечивающей поддержку библиотеки ANSI С.
Сначала мы представим функции, которые берут начало от реализации Беркли, хотя в книге мы будем использовать только одну из них — bzero. (Дело в том, что она имеет только два аргумента и ее проще запомнить, чем функцию memset с тремя аргументами, как объяснялось в разделе 1.2.) Две другие функции, Ьсору и bcmp, могут встретиться вам в существующих приложениях.
#include <strings h>
void bzero(void *dest. size t nbytes),
void bcopy(const void *src, void *dest. size_t nbytes),
Tnt bcmp(const void *p£rl const void *ptr2, size」 碩ytes):
Возвращает 0 в случае равенства, ненулевое значение в случае неравенства
ПРИМЕЧАНИЕ
Мы впервые встречаемся с квалификатором1 const. В приведенном примере он служит признаком того, что значения, на которые указывает указатель, то есть src, ptrl и ptr2, не изменяются функцией. Другими словами, область памяти, на которую указывает указатель с квалификатором const, считывается функцией, но не изменяется.
Функция bzero обнуляет заданное число байтов в указанной области памяти. Мы часто используем эту функцию для инициализации структуры адреса сокета нулевым значением. Функция Ьсору перемещает заданное число байтов из источника в место назначения. Функция bcmp сравнивает две произвольных последовательности байтов н возвращает нулевое значение, если две байтовых строки идентичны, и ненулевое в противном случае.
Следующие функции являются функциями ANSI С:
#include <string h>
void *memset(void *dest, int c. size_t Zen)
void *memcpy(void *dest, const void *src, size_t nbytes).
mt memcmp(const void *ptrl const void *ptr2 size_t nbytes).
Возвращает 0 в случае равенства, значение <0 или >0 в случае неравенства (см текст)
Функция memset присваивает заданному числу байтов значение с. Функция memcpy аналогична функции Ьсору, но имеет другой порядок двух аргументов.
Встречается и другой перевод термина «qualifier» — «определитель». — Примеч перев.
функция bcopy корректно обрабатывает перекрывающиеся поля, в то время как поведение функции memcpy не определено, если источник и место назначения перекрываются. В случае перекрывания полей должна использоваться функция ANSI С memmove (упражнение 30.3).
ПРИМЕЧАНИЕ
Чтобы запомнить порядок аргументов функции memcpy, подумайте о том, что он совпадает с порядком аргументов в операторе присваивания (справа — оригинал, слева — копия).
dest = src.
Последним аргументом этой функции всегда является длина области памяти.
Функция memcmp сравнивает две произвольных последовательности байтов и возвращает нуль, если они идентичны. В противном случае знак возвращаемого значения определяется знаком разности между первыми несовпадающими байтами, на которые указываютptr1 и ptr2. Предполагается, что сравниваемые байты принадлежат к типу unsigned chars.
Функции inet_aton, inet_addr и inet_ntoa
Существует две группы функций преобразования адресов, которые мы рассматриваем в этом и следующем разделах. Они выполняют преобразование адресов Интернета из строк ASCII (удобных для человеческого восприятия) в двоичные значения с сетевым порядком байтов (эти значения хранятся в структурах адресов сокетов).
Функции inet__aton, inet_ntoa и i net_addr преобразуют адрес IPv4 из точечнодесятичной записи (например, 206.62.226.33) в 32-разрядное двоичное значение в сетевом порядке байтов. Возможно, бы встретите эти функции в многочисленных существующих программах.
Более новые функции 】net_pton и 1 net ntop работают и с адресами IPv4, и с адресами IPv6. Эти функции, описываемые в следующем разделе, мы используем в книге.
include <arpa/inet h>
mt inet_aton(const char *strptr struct in_dddr ^addrptr),
Возвращает 1, если строка преобразована успешно. О в случае ошибки
in_dddr_t met_addr(const char *strptr).
Возвращает 32-разрядный адрес IPv4 в сетевом порядке байтов; INADDR_NONE в случае ошибки,
char *inet_ntoa(struct in_addr inaddr),
Возвращает указатель на строку с адресом в точен но-дес я тич ной записи
Первая из названных функций, i net_aton, преобразует строку, на которую указывает strpt г, в 32-разрядное двоичное число, записанное в сетевом порядке байтов, передаваемое через указатель addrptr. При успешном выполнении возвращаемое значение равно 1, иначе возвращается нуль.
ПРИМЕЧАНИЕ
Функция inet_aton обладает одним иедоку мент ированным свойством: если addrptr — пустой указатель (null pointer), функция все равно выполняет проверку допустимости адреса, содержащегося во входной строке, но нс сохраняет результата
Функция met_addr выполняет то же преобразование, возвращая в качестве значения 32-разрядное двоичное число в сетевом порядке байтов. Проблема при использовании этой функции состоит в том, что все 232 возможных двоичных значения являются действительными IP-адресами (от 0.0.0.0 до 255.255.255.255), но в случае возникновения ошибки функция возвращает константу INADDR_NUNE (обычно представленную двоичным числом, состоящим из 32 битов, установленных в единицу). Это означает, что точечно-десятпчная запись 255.255.255 255 (ограниченный адрес для широковещательной передачи IPv4, см. раздел 18.2) не может быть обработана этой функцией, поскольку ее двоичное значение выглядит как указание на сбой при выполнении функции.
ПРИМЕЧАНИЕ
Многие ранние версии утилиты Ping выводят ошибку Host unknown, если мы пытаемся выполнить команду ping 255 255 255 255 Причина возникновения этой некорректной ошибки в том, что функция met addr выполняется неверно, после чего программа пытается интерпретировать IP-ддрес в точечно-десячичной записи как имя узла, что тоже не срабатывает.
Другой возможной проблемой, сопровождающей выполнение функции met_addr, мо- жег стать то, что, как утверждается в некоторых руководст вах, в случае ошибки опа возвращает значение -1 вместо INADDR NONE Это может вызвать проблемы при сравнении возвращаемого значения функции (значение без знака) с отрицательной константой, зависящие от компилятора С
На сегодняшний день функция т net_addr является нерекомендуемой, или устаревшей, и в создаваемом коде вместо нее должна использоваться функция 1 net_aton. Еще лучше использовать более новые функции, описанные в следующем разделе, работающие и с IPv4, и с IPv6.
Функция inet_ntoa преобразует 32-разрядный двоичный адрес IPv4, хранящийся в сетевом порядке байтов, в точечно-десятичную строку. Строка, на которую указывает возвращаемый функцией указатель, находится в статической памяти. Это означает, что функция не допускает повторного вхождения, то есть не является повторно входимой (reentrant), что мы обсудим в разделе 11 14. Наконец, отметим, что эта функция принимает в качестве аргумента структуру, а не указатель на структуру.
ПРИМЕЧАНИЕ
Функции, принимающие действующие структуры в качестве аргуменгов, встречаются редко. Более общим способом является передача указателя на структуру
Функции inet_pton и inet_ntop
Эти функции появились с IPv6 и работают как с адресами IPv4, так и с адресами IPv6. Их мы будем использовать в книге. Символы ри п обозначают соответственно формат представления и численный формат. Формат представления адреса часто является строкой ASCII, а численный формат — это двоичное значение, входящее в структуру адреса сокета.
#inc】ude <arpa/inet h>,
int inet_pton(int farm ly const char *strptr void *addrptr)
Возвращает 1 в случае успешного выполнения функции 0 если входная строка икеда неверный формат представления -1 в случае ошибки
const char *inet_ntop(mt farm ly const void *addrptr char *strptr size_t Jen):
Возвращает указатель на результат если выполнение функции прошло успешно NULL в случае ошибки•
Значением аргумента farm 1 у для обеих функций может быть либо AF_I NET, либо AF_INET6. Если family не поддерживается, обе функции возвращают ошибку со значением переменной errno, равным EAFNOSUPPORT.
Первая функция пытается преобразовать строку, на которую указывает strpt г, сохраняя двоичный результат с помощью указателя add「pt「. При успешном выполнении ее возвращаемое значение равно 1 Если входная строка находится в неверном формате представления для заданного семейства (fam 1у), возвращается нуль.
in_addr{} 32-битовый двоичный адрес IPv4in_addr{} 32-битовый двоичный адрес IPv4Численный форматЧисленный форматTn_addr{}128-битовыйдвоичныйадрес IPv6Tn_addr{}128-битовыйдвоичныйадрес IPv6Функция 】net_ntop выполняет обратное преобразование — из численного формата (addrptr) в формат представления (strptr) Аргумент 1еп — это размер при-
in_addr{} 32-битовый двоичный адрес IPv4
in_addr{} 32-битовый двоичный адрес IPv4
Численный формат
Численный формат
Tn_addr{}128-битовыйдвоичныйадрес IPv6
Tn_addr{}128-битовыйдвоичныйадрес IPv6
in6_addr{}128-битовыйдвоичный IPv4-aflpec,преобразованныйк виду IPv64 илисовместимый с ним
ФорматпредставленияФорматпредставлениявози а①ит— L——11——•▼ -HMZM g<dowu р① ит м^^^—^фан 'UO4BJ3UT pNNIId<UoqdIWUTвози а①ит— L——11——•▼ -HMZM g<dowu р① ит м^^^—^фан 'UO4BJ3UT pNNIId<UoqdIWUTIPv4-aflpecв точечно-десятичнойзаписиIPv4-aflpecв точечно-десятичнойзаписиg6g6VVx:x:x:x:x:x:a.b.c.dx:x:x:x:x:x:a.b.c.d(9JNNI g<сок(9JNNI g<сок(9INNI1UW) doauJBUT(9INNI1UW) doauJBUTX*v#v*vev*v*v*vX*v#v*vev*v*v*v
Форматпредставления
Форматпредставления
вози а①ит
— L——11——•▼ -HMZM g<dowu р① ит м^^^—^фан 'UO4BJ3UT pNNIId<UoqdIWUT
вози а①ит
— L——11——•▼ -HMZM g<dowu р① ит м^^^—^фан 'UO4BJ3UT pNNIId<UoqdIWUT
IPv4-aflpecв точечно-десятичнойзаписи
IPv4-aflpecв точечно-десятичнойзаписи
g
6
g
6
V
V
x:x:x:x:x:x:a.b.c.d
x:x:x:x:x:x:a.b.c.d
(9JNNI g<сок
(9JNNI g<сок
(9INNI1UW) doauJBUT
(9INNI1UW) doauJBUT
X*v#v*vev*v*v*v
X*v#v*vev*v*v*v
Рис. 3.5. Функции преобразования адресов
нимающей строки, который передается, чтобы функция не переполнила буфер вызывающего процесса. Чтобы облегчить задание этого размера, в заголовочный файл <netinet/in h> включаются следующие определения:
#define INET—ADDRSTRLEN16/* для точечно-десятичной записи IPv4-aflpeca */
#define INET6_AD0RSTRLEN 46/* для шестнадцатеричной записи IPv6-адреса*/
Если аргумент 1 еп слишком мал для хранения результирующего формата представления вместе с символом конца строки (terminating null), возвращается пустой указатель и переменной еггпо присваивается значение ENOSPC.
Аргумент strptr функции inet_ntop не может быть пустым указателем. Вызывающий процесс должен выделить память для хранения преобразованного значения и задать ее размер. При успешном выполнении функции возвращаемым значением является этот указатель.
На рис. 3.5 приведена схема действия пяти функций, описанных в этом и предыдущем разделах.
Пример
Даже если ваша система еще не включает поддержку IPv6, вы можете использовать новые функции, заменив вызовы вида foo sin_addr s_dddr = inet_dddr(cp).厂
на inet_pton(AF」NET, ср. &foo sm_addr), > а также заменив вызовы вида ptг = 1net_ntoa(foo sin_addr). на char str[INET_ADDRSTRLEN];
ptr = inet_ntop(AF_INET. &foo sin_addr. str. sizeof(str)).
В листинге 3.5 представлено простое определение функции 】net_pton, поддерживающее только IPv4, а в листинге 3.6 — версия inet_ntop, поддерживающая только IPv4.
Листинг 3.5. Простая версия функции inet_pton, поддерживающая только IPv4 //11 bfree/inet_pton_ipv4 с 10 mt 11 met_ptori(int family, const char *strptr void *addrptr) 12 {— 13 if (family == AF_INET) { 14structin_dddrin_val.
if (met_aton(strptr. &inj/dl)) {
memcpyladdrptr, &in_val sizeof(struct in_addr)):
return (1),
IB}
return(0),
}
errno = EAFNOSUPPORT.
return (-1), 23 }
Листинг 3.6 Простая версия функции met_ntop, поддерживающая только IPv4 , //11bfree/inet_ntop_ipv4 с
const char *
met_ntop(mt family, const void *addrptr. char *strptr, size_t len)
{"—
const u_chdr *p = (const u_char *) addrptr,
if (family == AF_INET) {
char temp[INET_ADDRSTRLEN],
snpnntf(temp, sizeof(temp), "%d %d %d %d".
15р[0]. р[1]. p[2]. p[3]),
if (strlen(temp)>=len) {
e「rno = ENOSPC.
return (NULL).
}
strcpy(strptr. temp).
return (strptr).
}
errno = EAFNOSUPPORT.
return (NULL).
}
.8. Функция sock_ntop и связанные с ней функции
Основная проблема, связанная с функцией i net_ntop, состоит в том, что вызывающий процесс должен передать ей указатель на двоичный адрес. Этот адрес обычно содержится в структуре адреса сокета, поэтому вызывающему процессу необходимо знать формат структуры и семейство адресов. Следовательно, чтобы использовать эту функцию, нужно написать код следующего вида для IPv4: struct sockdddr_in addг.
inet_ntQp(AF」NET &addr sin addr. str. sizeof(str))
или такого вида для IPv6:
struct sockaddr_iпб addr6.
1 net_ntop(AF_INET6, &dddr6 sm6__addr. str. sizeof(str)).
Как видите, код становится зависящим от протокола.
Чтобы решить эту проблему, напишем собственную функцию и назовем ее sock_ntop. Она получает указатель на структуру адреса сокета, исследует эту структуру и вызывает соответствующую функцию для того, чтобы возвратить формат представления адреса.
#include "unp И"
char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen):
Возвращает непустой указатель, если функция выполнена успешно. NULL в случае ошибки sockaddr указывает на структуру адреса сокета, длина которой равна значению add「l еп. Функция sock_ntop использует свой собственный статический буфер для хранения результата и возвращает указатель на этот буфер.
ПРИМЕЧАНИЕ
Обратите внимание, что при статическом хранении результата функция не допускает повторного вхождения (не является повторно входимой) и не может быть использована несколькими программными потоками (не является безопасной в многопоточной среде — 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
//lib/sock_ntop с
char *
sock_ntop(const struct sockaddr *sa. socklen_t salen)
{-一
char portstr[7];
static char str[128]/* максимальный домен Unix */
switch (sa->sa_family) {
case AFJNET「
struct sockaddr m *sin = (struct sockaddjin *) sa:
if (inet^ntop(AF_INET, &sin->sin_addr. str. sizeof(str)) == NULL)
/ return (NULL).
' if (ntohs(sin->sin_port) >= 0) {
snprintf(portstr sizeof(portstr),"况”.ntohs(sin->sin_port)):
strcat(str. portstr).
}
return (str),
}
Для работы 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):
Возвращает 0 если адреса относятся к одному семейству и порты совпадают, ненулевое значение в противном случае
mt 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. mt port).
void sock_set_wi1d(struct sockaddr *sockaddr, socklen_t addrlen),
Функция sock_bind_wild связывает универсальный адрес и динамически назначаемый порт с сокетом. Функция sock_cmp_addr сравнивает адресные части двух структур адреса сокета, а функция sock_cmp_port сравнивает номера портов из них. Функция sock_get_port возвращает только номер порта, а функция sock_ntop_host преобразует к формату представления только ту часть структуры адреса сокета, которая относится к узлу (все, кроме порта, то есть IP-адрес узла). Функция sock_set_addr присваивает адресной части структуры значение, указанное аргументом pt г, а функция sock_set_port задает в структуре адреса сокета только номер порта. Функция sock_set_wi 1 d задает адресную часть структуры через символы подстановки. Как обычно, мы предоставляем для всех этих функций функции- обертки, которые возвращают значение, отличное от типа void, и в наших программах обычно вызываем именно обертки. Мы не приводим в данной книге исходный код для этих функций, так как он свободно доступен (см. предисловие).
.9. Функции readn, writen и readline
Потоковые сокеты (например, сокеты TCP) демонстрируют с функциями read и wri te поведение, отличное от обычного ввода-вывода файлов. Функция read или write на потоковом сокете может ввести или вывести немного меньше байтов, чем запрашивалось, но это не будет ошибкой. Причиной может быть достижение границ буфера для сокета в ядре. Все, что требуется в этой ситуации, — чтобы процесс повторил вызов функции read или write для ввода или вывода оставшихся байтов. (Некоторые версии Unix ведут себя аналогично при записи в канал (pipe) более 4096 байт.) Этот сценарий всегда возможен на потоковом сокете при выполнении функции read, но с функцией write он обычно наблюдается только если сокет неблокируемый. Тем не менее вместо write мы всегда вызываем функцию wn ten на тот случай, если в данной реализации возможно возвращение меньшего количества данных, чем мы запрашиваем.
Введем три функции для чтения и записи в потоковый сокет.
include "unp h"
ssizjt readn(int filedes. void *buff, size_t nbytes).
ssize_t writen(int filedes const void *buff size_t nbytes)
ssize_t read!ine(int filedes, void ★buff. size_t mdxlen)
Все функции возвращают количество считанных или записанных байтов -1 в случае ошибки
В листинге 3.8 представлена функция readn, в листинге 3.9 — функция writen, а в листинге 3.10 — функция readl i ne.
Листинг 3.8. Функция readn: считывание п байтов из дескриптора
//lib/readn с
1 #include "unp h'
ssize_t/* Считывает n байтов из дескриптора */
readnfint fd. void *vptr. size_t n)
{一
size」nleft.
ssize_t nread
char 一 *ptr,'
ptr =vptr.
nleft= n.
while(nleft > 0) {
if ( (nread = read(fd ptr nleft)) < 0) {
if (errno == EINTR)
nread = 0,/* и вызывает снова функцию readО */
14else
15return (-1).
16} else if (nread== 0)
17break./* EOF */
18nleft -= nread.
19ptr += nread.
20}
21 return (n - nleft) /* возвращает значение >= 0 */
22 }
Листинг 3.9. Функция wnten: запись п байтов в дескриптор
//lib/writen с
1 #mclude "unp h"
2 ssize_t /* Записывает n байтов в дескриптор */
writen(int fd const void *vptr size_t n)
{-
size_t nleft.
ssizjt nwritten,
const char *ptr.
89101112131415168910111213141516ptr = vptr.
8
9
10
11
12
13
14
15
16
8
9
10
11
12
13
14
15
16
nleft = n.
while (nleft > 0) {
if ( (nwritten = write(fd. ptr. nleft)) <= 0) {
if (errno == EINTR)
nw门tten = 0./* и снова вызывает функцию writeO */
else
return (-1),/* ошибка */
17nleft -= nwritten,
18ptr += nwritten.
19}
20return (n)
21 }
Листинг 3.10. Функция readline: считывание следующей строки из дескриптора; по 1 байту за один раз
//test/readlinel с
1 #include "ипр h'
ssize_t
readlineCint fd. void *vptr. s】ze_t maxlen)
{~
ssize_t n rc
charc *ptr
789101112131415161718192021222324789101112131415161718192021222324ptr = vptrptr = vptrfor (n = 1. n v maxien n++) {for (n = 1. n v maxien n++) {againagain?f ( (rc = readlfd &c. 1)) == 1) {?f ( (rc = readlfd &c. 1)) == 1) {*ptr++ = c.if (c == '\en') break/*} else if (rc == 0) { if (n == 1)return (0)./*elsebreak/*} else {if (errno == EINTR)*ptr++ = c.if (c == '\en') break/*} else if (rc == 0) { if (n == 1)return (0)./*elsebreak/*} else {if (errno == EINTR)записан символEOF данные неEOF некоторыезаписан символEOF данные неEOF некоторыеновой строки как в fgets() */считаны */данные были считаны */новой строки как в fgets() */считаны */данные были считаны */goto again return (-1) }}goto again return (-1) }}/* ошибка errno задается функцией readО *//* ошибка errno задается функцией readО */
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ptr = vptr
ptr = vptr
for (n = 1. n v maxien n++) {
for (n = 1. n v maxien n++) {
again
again
?f ( (rc = readlfd &c. 1)) == 1) {
?f ( (rc = readlfd &c. 1)) == 1) {
*ptr++ = c.
if (c == '\en') break/*
} else if (rc == 0) { if (n == 1)
return (0)./*
else
break/*
} else {
if (errno == EINTR)
*ptr++ = c.
if (c == '\en') break/*
} else if (rc == 0) { if (n == 1)
return (0)./*
else
break/*
} else {
if (errno == EINTR)
записан символ
EOF данные не
EOF некоторые
записан символ
EOF данные не
EOF некоторые
новой строки как в fgets() */
считаны */
данные были считаны */
новой строки как в fgets() */
считаны */
данные были считаны */
goto again return (-1) }
}
goto again return (-1) }
}
/* ошибка errno задается функцией readО */
/* ошибка errno задается функцией readО */
25*ptr = О26return (n)27 }25*ptr = О26return (n)27 }/* завершаем нулем как в fgetsO */
25*ptr = О
26return (n)
27 }
25*ptr = О
26return (n)
27 }
Если функция чтения или записи (read или wri te) возвращает ошибку, то наши функции проверяют, не совпадает ли код ошибки с EINTR (прерывание системного вызова сигналом, см. раздел 5.9). В этом случае прерванная функция вызывается повторно. Мы обрабатываем ошибку, не позволяя вызывающему процессу снова вызвать read или write, поскольку целью наших функций является предотвратить обработку нехватки данных вызывающим процессом.
В листинге 13.3 мы покажем, что вызов функции recv с флагом MSG_WAITALL позволяет обойтись без использования отдельной функции readn.
Заметим, что наша функция readl те вызывает системную функцию read один раз для каждого байта данных. Это неэффективно, что показано в разделе 3.9 [93]. Нам хотелось бы буферизовать данные с помощью вызова функции read, чтобы получить столько данных, сколько мы можем, а затем проверять буфер по 1 байту за один раз. Одно из решений заключается в использовании стандартной библиотеки ввода-вывода, о чем пойдет речь в разделе 13.8.
В листинге 3.11 представлена более быстрая версия функции readline, которая считывает до MAXLINE байтов за один вызов, а затем возвращает по одному символу за один вызов.
Листинг 3.11. Улучшенная версия функции readline
//lib/readline с
1 include "unp h"
2 3 4 5 6 72 3 4 5 6 78910111213141516171819202189101112131415161718192021static ss】ze_t
2 3 4 5 6 7
2 3 4 5 6 7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
8
9
10
11
12
13
14
15
16
17
18
19
20
21
my^readCint fd. char *ptr)
static int read_cnt = 0, static char *read_ptr. static char read buf[MAXLINE]:
if (read_cnt <= 0) {
again
if ( (read_cnt = read(fd, read^buf, s1zeof(read_buf))) < 0) { if (errno *= EINTR)
goto again,
return (・1).
} else if (read_cnt == 0)
return (0),
read_ptr = read_buf.
}"-
read_cnt--
*ptr = *read_ptr++,
return (1).
22 ssize_t
23 readline(int fd. void *vptr. size t maxlen)
24 {-
25ssize t n, rc.
26char c, *ptr.
27
pt г
=vptr.
28
29
30
31
for
11
(n = 1. n < maxlen, n++) {
F ( (rc = my_read(fd. &c)) == 1) { *ptr++ = c.
if (c == '\en')
32
33
34
35
36
}
break, else if (rc == 0) if (n ,= 1)
return (0); else
/* записан символ новой строки. {
/* EOF. данные не считаны */
как в fgets() */
37
38
}
break, else
/* EOF было считано некоторое количество данных */
39
40
}
return (-1).
/* ошибка, errno задается readO
*/
41 *ptr = 0/* завершающий нуль, как в fgets() */
42 return (n).
43 }
2-21 Внутренняя функция my_read считывает до MAXLINE символов заЪдин вйзов и затем возвращает их по одному.
29 Единственное изменение самой функции readlme заключается в том, что теперь она вызывает функцию my_read вместо read.
Такая незначительная модификация функции readlme приводит к большим изменениям в результатах ее работы. Если мы измерим время, требуемое старой и новой версиям для чтения файла, состоящего из 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.
Функция isfdtype
Бывают ситуации, когда нужно протестировать дескриптор, чтобы определить, относится ли он к некоторому заданному типу. Исторически это осуществлялось с помощью вызова функции Posix. 1 fstat и последующего тестирования возвращенного значения st_mode одним из макросов S_ISxrr [93, с. 73-76 ]. Многие, но не все реализации определяют макрос S_ISSOCK, проверяющий, является ли дескриптор сокетом. Поскольку существует несколько реализаций, не способных сообщить, является ли дескриптор сокетом, на основании только той информации, которую возвращает функция fstat, Posix. 1g предоставляет новую функцию isfdtype.
findude <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 с
include "unp h”
#ifndef SJFSOCK
Листинг 3.12 (продолжение)
#error S_IFSOCK not defined
#endif
int
isfdtype(int fd int fdtype)
{
struct stat buf
if (fstaKfd &buf) < 0)
return(-1)
if ( (buf st_mode & S__IFMT) «- fdtype)
return(1).
else
return(0),
}
Существуют другие константы S_IFxxx, определяемые в заголовочном файле <sys/stat h>, и наша реализация позволяет их использовать. Однако в Posix.lg указано, что эта функция работает только когда fdtype имеет тип S_IFSOCK.
Резюме
Структуры адресов сокетов являются неотъемлемой частью каждой сетевой программы. Мы выделяем для них место в памяти, заполняем их и передаем указате・ ли на них различным функциям сокетов. Иногда мы передаем указатель на одну из этих структур функции сокета, и она сама заполняет поля структуры. Мы все~ гда передаем эти структуры по ссылке (то есть передаем указатель на структуру, а не саму структуру) и всегда передаем размер структуры в качестве дополнительного аргумента. Когда функция сокета заполняет структуру, длина также передается по ссылке, и ее значение может быть изменено функцией, поэтому мы называем такой аргумент «значение-результат» (value-result).
Структуры адресов сокетов являются самоопределяющимися, поскольку они всегда начинаются с поля family, которое идентифицирует семейство адресов, содержащихся в структуре. Более новые реализации, поддерживающие структуры адресов сокетов переменной длины, также содержат поле, которое определяет длину всей структуры.
Две функции, преобразующие IP-адрес из формата представления (который мы записываем в виде последовательности символов ASCII) в численный формат (который входит в структуру адреса соке га) и обратно — это i net_pton и т net_ ntop. Эти функции являются зависящими от протокола. Более совершенной методикой является работа со структурами адресов сокетов как с непрозрачными (opaque) объектами, когда известны лишь указатель на структуру и ее размер. Мы разработали набор функций sock_, которые помогут сделать наши программы не зависящими от протокола. Создание наших не зависящих от протокола средств мы завершим в главе 11 функциями getaddnnfo и getnameinfo.
Сокеты TCP предоставляют приложению поток байтов, лишенный маркеров записей. Возвращаемое значение функции read может быть меньше запрашиваемого, но это не обязательно является ошибкой. Чтобы упростить считывание и запись потока байтов, мы разработали три функции — readn, w门 ten и read] i ne,— которое мы использ ем в книге.
Упражнения
Почему аргументы типа «значение-результат», такие как длина структуры адреса сокета, должны передаваться по ссылке?
Почему и функция readn, и функция wnten копируют указатель void* в указатель char*?
Функции inet_dton и inet_addr характеризуются традиционно нестрогим отношением к тому, что они принимают в качестве точечно-десятичной записи адреса IPv4: допускаются от одного до четырех десятичных чисел, разделенных точками; также допускается задавать шестнадцатеричное число с помощью начального Ох или восьмеричное число с помощью начального 0 (выполните команду tel net Охе, чтобы увидеть поведение этих функций). Функция т net_pton намного более строга в отношении адреса IPv4 и требует наличия именно четырех чисел, разделенных точками, каждое из которых является десятичным числом от 0 до 255. Функция inet_pton не разрешает задавать точечно-десятичный формат записи адреса, если семейство адресов — AF INET6, хотя существует мнение, что это можно было бы разрешить, и тогда возвращаемое значение было бы адресом IPv4, преобразованным к виду IPv6 (рис. А.8). Напишите новую функцию met_pton_l oose, реализующую такой сценарий: если используется семейство адресов AF_INET и функция i net pton возвращает нуль, вызовите функцию т net_aton и посмотрите, успешно ли она выполнится. Аналогично, если используется семейство адресов AF INET6 и функция т net_pton возвращает нуль, вызовите функцию i net_aton, и если она выполнится успешно, возвратите адрес IPv4, преобразованный к виду IPv6.
ГЛАВА4
Элементарные сокеты TCP
Введение
В этой главе описываются элементарные функции сокетов, необходимые для написания полностью работоспособного клиента и сервера TCP. Сначала мы опишем все элементарные функции сокетов, которые будем использовать, а затем в следующей главе создадим клиент и сервер. С этими приложениями мы будем работать на протяжении всей книги, постоянно их совершенствуя (см. табл. 1 3 и табл. 1 4).
Мы также опишем параллельные (concurrent) серверы — типичную технологию Unix, когда множество клиентов одновременно соединяются с одним и тем же сервером. Подключение очередного клиента заставляет сервер выполнить функцию fork, порождающую новый серверный процесс именно для этого клиента. В этой главе применительно к использованию функции fork мы будем рассматривать модель «каждому клиенту — один процесс», а в главе 23 при обсуждении программных потоков расскажем о модели «каждому клиенту — один потока.
На рис. 4.1 представлен типичный сценарий взаимодействия, происходящего между клиентом и сервером. Сначала запускается сервер, затем спустя некоторое время запускается клиент, который соединяется с сервером. Предполагается, что клиент посылает серверу запрос, сервер этот запрос обрабатывает и посылает клиенту ответ. Так продолжается, пока клиентская сторона не закроет соединение, посылая при этом серверу признак конца файла. Затем сервер закрывает свой конец соединения и либо завершает работу, либо ждет подключения нового клиента.
Функция socket
Чтобы обеспечить сетевой ввод-вывод, процесс должен начать работу с вызова функции socket, задав тип желаемого протокола (TCP с использованием IPv4, UDP с использованием IPv6, доменный сокет Unix и т. д.).
include <sys/socket h>
int socket(int family int type int protocol)
Возвращает неотрицательный дескриптор, если функция выполнена успешно -1 в случае ошибки
Константа family задает семейство протоколов. Ее возможные значения приведены в табл. 4.1. Значения константы type (тип) перечислены среди других кон-
TCP-серверTCP-серверРис. 4.1. Функции сокетов для элементарного клиент-серверного соединения TCPРис. 4.1. Функции сокетов для элементарного клиент-серверного соединения TCP
TCP-сервер
TCP-сервер
Рис. 4.1. Функции сокетов для элементарного клиент-серверного соединения TCP
Рис. 4.1. Функции сокетов для элементарного клиент-серверного соединения TCP
стант в табл. 4.2. Обычно аргумент protocol функции socket равен нулю, за исключением символьных сокетов. Их мы рассмотрим в главе 25.
Не все сочетания констант fami 1у и type допустимы. В табл. 4 3 показаны допустимые сочетания, а также протокол, соответствующий каждой паре. Ячейки таблицы, содержащие «Да», соответствуют допустимым комбинациям, для которых нет удобных сокращений Пустая ячейка означает, что данное сочетание не поддерживается.
Таблица 4.1. Константы протокола (family) для функции socket
Семейство сокетов (family)Описание
AF_INET AFJNET6 AF_LOCAL AF_ROUTE AF—KEYAF_INET AFJNET6 AF_LOCAL AF_ROUTE AF—KEYПротоколы IPv4
AF_INET AFJNET6 AF_LOCAL AF_ROUTE AF—KEY
AF_INET AFJNET6 AF_LOCAL AF_ROUTE AF—KEY
Протоколы IPv6
Протоколы доменных сокетов Unix (см главу 14)
Маршрутизирующие сокеты (см главу 17)
Ключевой сокет
Таблица 4.2. Тип сокета для функции socket
Тип (type)
Описание
SOCK_STREAM
Потоковый сокет
SOCK_DGRAM
Сокет дейта! рамм
SOCK_RAW
Символьный (неструктурированный) соке г
Таблица 4.3. Сочетания констант family и type для функции socket
AFJNET
AFJNET6
AF—LOCALAF_ROUTE
AF_KEY
SOCK_STREAM
TCP
TCPДа
SOCK—DGRAM
UDP
UDPДа
SOCK_RAW IPv4
IPv6
Да
Да
ПРИМЕЧАНИЕ
В качестве первого аргумента функции socket вы также можете встретить константу PF xxx Подробнее об этом мы расскажем в конце данного раздела
Кроме того, вам можег встретиться название AF_UNIX (исторически сложившееся в Unix) вместо AF LOCAL (название из Posix 1g), и более подробно мы поговорим об этом в главе 14
Для ар1ументов family и сурс существуют и дру! ие значения Например, 4 4BSD под- 入 держивает и AF_NS (протокола Xerox NS, часто называемые XNS), и AF ISO (протоколы OSI) Но сегодня очень немногие используют какой-либо из этих протоколов Аналогично значение type для SOCK SEQPACKET, сокета последовательных пакетов, реализуется и протоколами 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_ — в структу-
pax адресов сокетов Но в действительности семейства протоколов, поддерживающего множество семейств адресов, никогда не существовало, и поэтому в заголовочном файле vsys/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 Первый элемент этой структуры назывался sp_family, и его значение было одним из значений PF_ Второй элемент, sp_protocol, был номером протокола аналогично третьему аргументу нынешней функции socket Единственный способ задать семейство протоколов заключался в том, чтобы задать эту структуру Следовательно, в этой системе значения PF_ использовались как элементы для задания семейства протоколов в структуре sockproto Значения AF_ играли роль элементов д刀я задания семейства адресов в структурах адресов сокетов Структура sockproto еще присутствует в 4 4BSD [105, с 626627J, по служит только для вну греннего использования ядром Начальное определение содержало для элемента 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, либо в структуре адреса сокета
Функция connect
Функция connect используется клиентом TCP для установления соединения с сервером TCP
#include <sys/socket h>
1nt connect(1 nt sockfd const struct sockaddr &servaddr socklen_t addrlen)
Возвращает 0 в случае успешного выполнения функции -1 в случае ошибки
Аргумент sockfd — это дескриптор сокета, возвращенный функцией socket. Второй и третий аргументы 一 это указатель на структуру адреса сокета и ее размер, (см. раздел 3.3). Структура адреса сокета должна содержать IP-адрес и номер порта сервера. Пример применения этой функции был представлен в листинге 1.1.
Клиенту нет необходимости вызывать функцию bind (которую мы описываем в следующем разделе) до вызова функции connect: при необходимости ядро выберет и динамически назначаемый порт, и IP-адрес отправителя.
В случае сокета TCP функция connect инициирует трехэтапное рукопожатие TCP (см. раздел 2.5). Функция возвращает значение только если установлено соединение или произошла ошибка. Возможно несколько ошибок:
Если клиент TCP не получает ответа на свой сегмент SYN, возвращается сообщение ETIMEDOUT. 4.4BSD, например, отправляет один сегмент SYN, когда вызывается функция connect, второй — 6 секунд спустя, и еще один — через 24 секунды [105, с. 828]. Если ответ не получен в течение 75 секунд, возвращается ошибка.
Некоторые системы позволяют администратору устанавливать значение времени ожидания; см. приложение Е [94].
Если на сегмент SYN сервер отвечает сегментом RST, это означает, что ни один процесс на узле сервера не находится в ожидании подключения к указанному нами порту (например, нужный процесс может быть не запущен). Это устойчивая неисправность (hard error), и клиенту возвращается сообщение ECONNRE- FUSED сразу же по получении им сегмента RST.
RST (от «reset》一сброс) — это сегмент TCP, отправляемый собеседнику при возникновении ошибок. Вот три условия, при которых генерируется RST: сегмент SYN приходит для порта, не имеющего прослушивающего сервера (что мы только что описали); TCP хочет разорвать существующее соединение; TCP получает сешент для несуществующего соединения (дополнительная информация содержится на с. 246-250 [94]).
Если сегмент SYN клиента приводит к получению сообщения ICMP о недоступности получателя от какого-либо промежуточного маршрутизатора, это считается случайным сбоем (softemf). Клиентское ядро сохраняет сообщение об ошибке, но продолжает отправлять сегменты SYN с теми же временными интервалами, что и в первом сценарии. Но если по истечении определенного фиксированного времени (75 секунд для 4.4BSD) ответ не получен, сохраненная ошибка ICMP возвращается процессу либо как EHOSTUNREACH, либо как ENETUNREACH.
ПРИМЕЧАНИЕ
Многие более ранние системы, такие как 4.2BSD, некорректно прерывали попытки установления соединения при получении сообщения ICMP о недоступности получателя. Это было неверно, поскольку данная ошибка ICMP может указывать на временную неисправность. Например, может быть так, что эта ошибка вызвана проблемой маршрутизации, которая исправляется в течение 15 секунд.
Обратите внимание, что ENETUNREACH в табл. А.З не включается даже когда ошибка указывает, что сеть получателя недоступна. Нед остугп юсть сети считается устаревшей ошибкой, и даже если 4.4BSD получает такое сообщение, приложению возвращается EHOSTUNREACH.
Эти ошибки мы можем наблюдать на примере нашего простого клиента, созданного в листинге 1.1. Сначала мы указываем адрес нашего собственного узла (127.0.0.1), на котором работает сервер времени и даты, и видим обычный вывод: solans 弘 daytimetcpcl 1 127.0.0.1 Tue Jan 16 16.45 07 1996
Чтобы увидеть возвращаемый ответ в другом формате, мы обращаемся к локальному маршрутизатору Cisco (см. рис. 1.7):
solans % daytimetcpcl 1 206.62.226.62
Tuesday, May 7, 1996 U-01.33-MST
Затем мы задаем IP-адрес в локальной подсети (206.62.226) с несуществующим адресом узла (55). Когда клиент посылает запросы ARP (запрашивая аппаратный адрес узла), он не получает никакого ответа:
solans % daytimetcpcl 1 206.62.226.55
connect error Connection timed out*
Мы получаем сообщение об ошибке только по истечении времени выполнения функции connect (которое, как мы говорили, для Solaris 2.5 составляет 3 минуты). Обратите внимание, что наша функция err_sys выдает текстовое сообщение, соответствующее коду ошибки ETIMEDOUT.
В следующем примере мы пытаемся обратиться к шлюзу (gateway), представляющему собой маршрутизатор Cisco, на котором не запущен сервер времени и даты: solans % daytimetcpcl! 140.252.1.4 connect error Connection refused
Сервер немедленно отвечает, отправляя сегмент RST.
В последнем примере мы пытаемся обратиться к недоступному адресу из сети Интернет. Просмотрев пакеты с помощью программы tcpdump, мы увидим, что маршрутизатор через шесть повторных передач возвращает сообщение ICMP о недоступности узла:
solans % daytimetcpcl 1 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.
Функция bind
Функция bind связывает сокет с локальным адресом протокола. В случае протоколов Интернета адрес протокола — это комбинация 32-разрядного адреса IPv4 ТТ ■ 4 Q Q. с КС Т ] 14d — ■公 ― — —-":' ^ ' “、 ▼ ” " '、/'
#include <sys/socket h>
int bind(int sockfd, const struct sockaddr ★rnyaddr, 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
приведены все возможные значения, которые присваиваются аргументам si n_addr и si n_port либо sin6_addr и sin6_port в зависимости от желаемого результата.
Таблица 4.4. Результаты задания IP-адреса и/или номера порта в функции bind
xMtwwwwKWMWMM^vxmv^^^^MMKWKemamiMMKviKvnmimKexaiitvMvxt^i^^-MvMi^WK1^^
Процесс задает:Результат
IP-адресПорт
Универсальный
0
Ядро выбирает IP-адрес и порт
Универсальный
Ненулевое значение
Ядро выбирает IP-адрес, процесс задает порт
Локальный
,0
Процесс задает IP-адрес, ядро выбирает порт
Локальный
Ненулевое значение
Процесс задает IP-адрес и пор г
Если мы зададим нулевой номер порта, то при вызове функции bind ядро выберет динамически назначаемый порт. Но если мы зададим IP-адрес с помощью символов подстановки, ядро не выберет локальный IP-адрес, пока либо к сокету не присоединится клиент (TCP), либо на сокет не будет отправлена дейтаграмма (UDP).
В случае IPv4 универсальный адрес, состоящий из символов подстановки (wildcard), задается константой INADDR_ANY, значение которой обычно нулевое. Это указывает ядру на необходимость выбора IP-адреса. Такое пример вы видели в листинге 1.5: struct sockaddr_in servaddr,
servaddr sm_addr s_addr = htonl (INADDR^ANY)./* универсальный */
Этот прием работает с IPv4, где IP-адрес является 32-разрядным значением, которое можно представить как простую численную константу (в данном случае 0), но воспользоваться им при работе с IPv6 мы не можем, поскольку 128-раз- рядный адрес IPv6 хранится в структуре. (В языке С мы не можем поместить структуру в правой части оператора присваивания.) Эта проблема решается следующим образом: struct sockaddr_in6 serv serv sin6_addr = in6addr_any. /* универсальный */
Система выделяет место в памяти и инициализирует переменную in6dddr_dny, присваивая ей значение константы IN6A00R_ANY_INIT. Объявление внешней константы in6addr_any содержится в заголовочном файле <netinet/in.h>
Значение INADDR_ANY (0) не зависит от порядка байтов, поэтому использование функции htonl в действительности не требуется. Но поскольку все константы INAODR_, определенные в заголовочном файле <netinet/in.h>, задаются в порядке байтов узла, с любой из этих констант следует использовать функцию htonl.
Если мы поручаем ядру выбрать для нашего сокета номер динамически назначаемого порта, то функция bind не возвращает выбранное значение. В самом деле, она не может возвратить это значение, поскольку второй аргумент функции bind имеет квалификатор const. Чтобы получить значение динамически назначаемого порта, заданного ядром, потребуется вызвать функцию getsockname, которая возвращает локальный адрес протокола.
Типичным примером процесса, связывающего с сокетом конкретный IP-адрес, служит узел, на котором работают web-серверы нескольких организаций (см. раздел 14.2 [95]). Прежде всего у каждой организации есть свое собственное доменное имя, например www.organization.com. Доменному имени каждой организации сопоставляется некоторый IP-адрес; различным организациям сопоставляются различные адреса, но обычно из одной и той же подсети. Например, если адрес подсети 198.69.10, то IP-адресом первой организации может быть 198.69.10.128, следующей — 198.69.10.129 и т. д. Все эти IP-адреса затем становятся псевдонимами, или альтернативными именами (alias), одного сетевого интерфейса (например, при использовании параметра alias команды if config в 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 является EADDRINUSE, указывающая на то, что адрес уже используется. Более подробно мы поговорим об этом в разделе 7.5, когда будем рассматривать параметры сокетов SD_REUSEADDR и SO_REUSEPORT.
Функция listen
Функция listen вызывается только сервером TCP и выполняет два действия.
Когда сокет создается с помощью функции socket, считается, что это активный сокет, то есть клиентский сокет, который запустит функцию connect. Функция ]1 sten преобразует неприсоединенный сокет в пассивный сокет, запросы на подключение к которому начинают приниматься ядром. В терминах диаграммы перехода между состояниями TCP (см. рис. 2.4) вызов функции 11 sten переводит сокет из состояния CLOSED в состояние LISTEN.
Второй аргумент этой функции задает максимальное число соединений, которые ядро может помещать в очередь этого сокета.
#1 ndude <sys/socket.h>
int listenCmt sockfd. int backlog}.
Эта функция обычно вызывается после функций socket и bind. Она должна вызываться перед вызовом функции accept.
Чтобы уяснить смысл аргумента backlog, необходимо понять, что для данного прослушиваемого сокета ядро поддерживает две очереди:
Очередь не полностью установленных соединений (incomplete connection queue), содержащая запись для каждого сегмента SYN, пришедшего от клиента, для которого сервер ждет завершения трехэтапного рукопожатия TCP. Эти сокеты находятся в состоянии SYN_RCVD (см. рис. 2.4).
Очередь полностью установленных соединений {complete connection queue}, содержащая запись для каждого клиента, с которым завершилось трехэтапное рукопожатие TCP. Эти сокеты находятся в состоянии ESTABLISHED (см. рис. 2.4).
На рис. 4.2 представлены обе эти очереди для прослушиваемого сокета.
□□Очередь не полностью установленных соединений (состояние SYN_RCVD)Очередь не полностью установленных соединений (состояние SYN_RCVD)Сумма обеих очередей не может превосходить значения аргумента backlogСумма обеих очередей не может превосходить значения аргумента backlogОчередь полностью установленных соединений (состояние ESTABLISHED)Очередь полностью установленных соединений (состояние ESTABLISHED)Прибытие сегмента SYNПрибытие сегмента SYNРис. 4.2. Две очереди, поддерживаемые прослушиваемым сокетом TCPРис. 4.2. Две очереди, поддерживаемые прослушиваемым сокетом TCP
□
□
Очередь не полностью установленных соединений (состояние SYN_RCVD)
Очередь не полностью установленных соединений (состояние SYN_RCVD)
Сумма обеих очередей не может превосходить значения аргумента backlog
Сумма обеих очередей не может превосходить значения аргумента backlog
Очередь полностью установленных соединений (состояние ESTABLISHED)
Очередь полностью установленных соединений (состояние ESTABLISHED)
Прибытие сегмента SYN
Прибытие сегмента SYN
Рис. 4.2. Две очереди, поддерживаемые прослушиваемым сокетом TCP
Рис. 4.2. Две очереди, поддерживаемые прослушиваемым сокетом TCP
На рис. 4.3 показан обмен пакетами во время установления соединения с использованием этих очередей.
Когда от клиента приходит сегмент SYN, TCP создает новую запись в очереди не полностью установленных соединений, а затем отвечает вторым сегментом трехэтапного рукопожатия, посылая сегмент SYN вместе с сегментом АСК, подтверждающим прием клиентского сегмента SYN (см. раздел 2.5). Эта запись останется в очереди не полностью установленных соединений, пока не придет третий сегмент трехэтапного рукопожатия (клиентский сегмент АСК для сегмента сервера SYN) или пока не истечет время жизни этой записи. (В реализациях, происходящих от Беркли, время ожидания (тайм-аут) для элементов очереди не
установленных, функция accept
1 * 飞может завершиться
Рис. 4.3. Обмен пакетами в процессе установления соединения с применением очередей
полностью установленных соединений равно 75 секундам ) Если трехэтапное рукопожатие завершается нормально, запись переходит из очереди не полностью установленных соединений в конец очереди полностью установленных соединений Когда процесс вызывает функцию accept (о которой мы поговорим в следующем разделе), ему возвращается первая запись из очереди полностью установленных соединений, а если очередь пуста, процесс переходит в состояние ожидания до появления записи в ней
Есть несколько важных моментов, которые нужно учитывать при работе с этими очередями
Аргумент backlog функции 11 sten исторически задавал максимальное суммарное значение для обеих очередей
ПРИМЕЧАНИЕ
Формального определения аргумента backlog никогда не существовало В руководстве 4 2BSD сказано, что «он определяет максимальную длину, до которой может вырасти очередь не полностью установленных соединений» Многие руководства и даже Posix 1g копируют это определение дословно, но в нем не говорится, в каком состоянии должно находиться соединение — в состоянии SYN RCVD, ESTABLISHED (до вызова accept) или же в любом из них Определение, приведенное выше, относится к реализации Бер кли, пакет 4 2BSD, и копируется многими другими реализациями
Беркли-реализации включают поправочный множитель для аргумента backl од, равный 1,5 [94, с 257], [105, с 462] Например, при типичном значении аргумента backlog = 5 в таких системах допускается до восьми записей в очередях, как показано в табл 4 6
ПРИМЕЧАНИЕ
Причина возникновения этого множителя теряется в истории [48] Но если мы рассматриваем backlog как способ задания максимального числа установленных соединений, которые ядро помещает в очередь прослушиваемого сокета (об этом вскоре будет рассказано) этот множитель нужен для учета не полносгью установленных соединений, находящихся в очереди [8]
Не следует задавать нулевое значение аргументу backlog, поскольку различные реализации ин1 ерпретируют это по-разному (табл 4 6) Некоторые реализации допускают помещение в очередь одного соединения, в то время как в других вообще невозможно помещать соединения в очередь Если вы не хоти ге, чтобы какие-либо клиенты соединялись с вашим прослушиваемым сокетом, просто закройте прослушиваемый сокет
农 Если трехэтапное рукопожатие завершается нормально (то есть без потерянных сегментов и повторных передач), запись остается в очереди не полностью установленных соединений на время одного периода обращения (round-trip time, RTT), какое бы значение ни имел этот параметр для конкретного соединения между клиентом и сервером В разделе 14 4 [95] показано, что для одного web-сервера средний период RTT равен 187 миллисекундам (Это среднее значение, и отдельные значения могут намного его превосходить ) В коде примеров всегда используется традиционное значение backlog, равное 5, поскольку это было максимальное значение, которое поддерживалось в системе 4 2BSD Это было актуально в 80-х, когда загруженные серверы могли обрабатывать только несколько сотен соединений в день Но с ростом Сети (WWW), когда серверы обрабатывают миллионы соединении в день, столь малое число стало абсолютно неприемлемым [95, с 187-192] Серверам HTTP необходимо намного большее значение аргумента backlog, и новые ядра должны поддержи* вать такие значения
ПРИМЕЧАНИЕ
В настоящее время многие сиоемы позволяю г адмннис i раторам изменять максимальное значение аргумента backlog
Возникает вопрос какое значение аргумента backlog должно задавать приложение, если значение 5 часто является неадекватным*^ На этот вопрос нет простого ответа Серверы HTTP сейчас задают большее значение, но если заданное значение является в исходном коде константой, то для увеличения константы требуется перекомпиляция сервера Другой способ — принять некоторое значение по умолчанию и предоставить возможность изменять его с помощью параметра командной строки или переменной окружения Всегда можно задавать значение больше того, которое поддерживается ядром, так как ядро допжно обрезать значение до максимального, не возвращая при этом ошибку [105, с 456]
Мы приводим простое решение этой проблемы, изменив нашу функцию-обертку для функции listen В листинге 4 I1 представлен действующий код Переменная окружения LISTENQ позволяет переопределить значение по умолчанию.
Листинг 4.1. функция-обертка для функции listen, позволяющая переменной окружения переопределить аргумент backlog
//lib/wrapsock с
74 void
продолжение 夕
Вес исходные коды npoi рамм опубликованные в этой khhi е, вы можете найти по адресу http // www piter com/download
Листинг 4.1 (продолжение)
75 Listen(int fd・ int backlog)
76 {
77 char *ptr,
78/* может заменить вт叩0济 аргумент на переменную окружения */
79 】f ( (ptr = getenvCIISIENQ")) !- NULL)
80backlog = atoi(ptr)
81 if (listen(fd. backlog) < 0)
82err_sys("listen error"),
83 }-
巡 Традиционно в руководствах и книгах утверждалось, что помещение фиксированного числа соединений в очередь позволяет обрабатывать случай загруженного серверного процесса между последовательными вызовами функции accept. При этом подразумевается, что из двух очередей больше записей будет содержаться, вероятнее всего, в очереди полностью установленных соединений. Но оказалось, что для действительно загруженных web-серверов это не так. Причина задания большего значения backlog в том, что очередь не полностью установленных соединений растет по мере поступления сегментов SYN от клиентов; элементы очереди находятся в состоянии ожидания завершения трехэтапного рукопожатия.
В табл. 4.5 показано действительное число записей в каждой очереди, измеренное на умеренно загруженном web-сервере. Эти значения были получены при проверке двух счетчиков для прослушиваемого сокета HTTP приблизительно каждые 84 миллисекунды в течение 2 часов в середине рабочего дня.
Таблица 4.5. Количество записей в очереди полностью и не полностью установленных соединений
<iiWAWiW>>K«MV«>w^A^^
Количество записан в очереди
«х«л^»»№л^<^л^л^лим<л«л^л>^
Сколько раз это количество наблюдалось для очереди не полностью установленных соединений
уяю»^лкг<^и^лттсл)ктк4кык<лы«м^^
Сколько раз это количество наблюдалось для очареди полностью установленных соединений
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
Количество записей в очереди
Сколько раз это количество наблюдалось для очереди не полностью установленных соединений
Сколько раз это количество наблюдалось для очереди полностью установленных соединений
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.lg разрешает оба варианта реагирования на эту ситуацию: игнорирование нового сегмента SYN или ответ на новый сегмент SYN с помошыо RST. Все реализации, происходящие от Беркли, игнорировали новый сегмент SYN.
Данные, которые приходят после завершения трехэтапного рукопожатия, но до того, как сервер вызывает функцию accept, должны быть помещены в очередь TCP-сервера, пока не будет заполнен приемный буфер.
В табл 4 6 показано действительное число установленных в очередь соединений для различных значений аргумента backlog в различных операционных системах, показанных на рис 1 7 Девять различных оп^ационных систем помещены в шесть различных колонок, что иллюстрирует многообразие значении аргумента backlog
Таблица 4.6. Действительное количество соединений в очереди для различных значений аргумента backlog
backlog
AIX 4.2,
BSD/OS 3.0
DUnix4.0,
Linux 2.0.27,
Uware 2.1.2
HR-UX 10.30
SunOs 4. t .4 Solans 2.5.1 Solaris 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-адрес хакера Очередь не полностью установленных со-
единений заполняется ложными сегментами SYN, в результате чего для подлинных сегмен гов SYN в нем не хватает места Таким образом, происходит отказ в обслуживании (denial of service) нормальных клиентов Существует два типичных способа противостояния jthm атакам [8] Но самое интересное в этом примечании — это еще одно обращение к вопросу о i ом, чюпа самом деле означае! api умент backlog функции listen Он должен задана】ь максимальное число усгановлспиых соединений для данного со* кета, которые ядро помещает в очередь Oi раничснис количества установленных соединений имеет целью приостановить получение ядром новых запросов на соединение для данного сокета, когда их не принимает приложение (по любой причине) Если система реализует именно такую интерпретацию, как, например, BSD/OS 3 0, то приложению нс нужно задавать большие значения ар1умепта backlog только потому, что сервер обраблываег множество клиентских запросов (например, занятый ”сЬ-ссрвер), или для защиты от «наводнения» SYN (лавиппои адресации сешенга SYN) Ядро обрабатывает множество не полностью установленных сос хипении вне зависимости от то! о, являются ли они законными или приходят от хакера Но даже в такой интерпретации мы видим (см табл 4 5), что имеют место сценарии, когда очередь полное!ью установленных соединении накапливает записи (в данном случае до 15), и значения 5 гуг явно недостаточно
Функция accept
Функция accept вызывается сервером TCP для возвращения следующего установленною соединения из начала очереди полностью установленных соединении (см рис 4 2) Если очередь полностью установленных соединении пуста, процесс переходит в состояние ожидания (по умолчанию предполагается блокируемый сокет)
#inc】ude <sys/socket h>
int accept(int sockfd struct sockaddr * chaddr socklen_t ★addrlen}
Возвращает неотрицательный дескриптор в случае успешного выполнения функции 1 в случае ошибки
Аргументы ch addr и addrl еп используются для возвращения адреса протокола подключившегося процесса (клиента) Аргумент addrl ел — это аргумент типа «значение-результат» (см раздел 3 3) Перед вызовом мы присваиваем целому числу на которое указывает *addlen, размер структуры адреса сокета, на которую указывает аргумент cl i addr, и по завершении функции это целое число содержит действительное число байтов, хранимых ядром в структуре адреса сокета
Если выполнение функции accept прошло успешно, он возвращает новый дескриптор, автоматически созданный ядром Этот дескриптор используется для обращения к соединению TCP с конкретным клиентом При описании функции accept мы называем ее первый аргумент прослушиваемым сокетом (listemngsocket)— это дескриптор, созданный функцией socket и затем используемый в качестве аргумента для функций bind и listen, — а значение, возвращаемое этой функцией, мы называем присоединенным сокетом (connectedsocket) Сервер обычно создает только один прослушиваемый сокет, который существует в течение времени жизни сервера Затем ядро создает по одному присоединенному сокету для каждого клиентского соединения, принятого с помощью функции accept (для которого
завершено трехэтапное рукопожатие TCP). Когда сервер заканчивает предоставление сервиса данному клиенту, сокет закрывается.
Эта функция возвращает до трех значений: целое число, которое является либо дескриптором сокета, либо кодом ошибки, а также адрес протокола клиентского процесса (через указатель cl i addr) и размер адреса (через указатель addrl еп). Если нам не нужно, чтобы был возвращен адрес протокола клиента, следует сделать указатели cliaddr и add г len пустыми указателями.
В листинге 1.5 показаны эти моменты. Присоединенный сокет закрывается при каждом прохождении цикла, но прослушиваемый сокет остается открытым в течение времени жизни сервера. Мы также видим, что второй и третий аргументы функции accept являются пустыми указателями, поскольку нам не нужно идентифицировать клиент.
Пример: аргументы типа «значение-результат»
В листинге 4.2 представлен измененный код листинга 1.5 (используемого для вывода IP-адреса и номера порта клиента), обрабатывающий аргумент типа «значение-результат» функции accept.
Листинг 4.2. Сервер определения времени и даты, сообщающий IP-адрес и номер порта клиента
//1nt ro/daytimetcps rvl c
#include'unp h"
include<time h>
int
main(int argc. char **argv)
{
int listenfd connfd.
socklen_t len.
struct sockaddr_in servaddr cliaddr.
charbuff[MAXLINE]
time_t ticks
listenfd = Socket(AF_INET SOCKJTREAM, 0)
bzero(&servaddr, sizeof(servaddr)).
servaddr sin_family = AF_INET
servaddr s】n_addr s_addr = htonl(INADDR_ANY)
servaddr sin_port = htons(13) /* daytime server */
Bind(listenfd (SA *) &servaddr sizeof(servaddr)).
L】sten(listenfd LISTENQ),
for (. ) {
len = sizeof(chadclr)
connfd = Accept(listenfd. (SA *) &c】iaddr. &len).
printf("connection from Zs. port ^d\en*'.
Inet_ntop(AF_INET &cl】add「 sin addr buff sizeof(buff)).
ntohs(cliaddr sin_port)).
ticks = time(NULL)
25snpnntfCbuff sizeof(buff)."岩 24s\er\en"ct】me(&ticks)).
26Write(connfd, buff, strlen(buff)).
27Close(connfd).
28}
29 }
Новые объявления
7-8 Мы определяем две новых переменных: 1еп, которая будет переменной типа «значение-результат», и cliaddr, которая будет содержать адрес протокола клиента.
Принятие соединения и вывод адреса клиента
19-23 Мы инициализируем переменную 1еп, присвоив ей значение, равное размеру структуры адреса сокета, и передаем указатель на структуру cl iaddr и указатель на 1еп в качестве второго и третьего аргументов функции accept. Мы вызываем функцию ?net_ntcp (см. раздел 3.7) для преобразования 32-битового IP-адреса в структуре адреса сокета в строку ASCII (точечно-десятичную запись), а затем вызываем функцию ntohs (см. раздел 3.4) для преобразования сетевого порядка байтов в 16-битовом номере порта в порядок байтов узла.
ПРИМЕЧАНИЕ
При вызове функции sock_ntop вместо inet_ntop наш сервер станет меньше зависеть от протокола, однако он все равно зависит от IPv4. Мы покажем версию этого сервера, не зависящего от протокола, н листинге 11.5.
Если мы запустим наш новый сервер, а затем запустим клиент на том же узле, то, дважды соединившись с сервером, мы получим от клиента следующий вывод: solans Z daytimetcpcli 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).
На конечном шаге приглашение оболочки изменяется на знак # — это приглашение к вводу команды для привилегированного пользователя. Наш сервер должен обладать правами привилегированного пользователя, чтобы с помощью функ-
ции bind связать зарезервированный порт 13. Если у нас нет прав привилегированного пользователя, вызов функции bi nd оказывается неудачным:
solans 岂 daytimetcpsrvl bind error Permission denied
Функции fork и exec
Прежде чем рассматривать создание параллельного сервера (что мы сделаем в следующем разделе), необходимо описать функцию Unix fork. Эта функция является единственным способом создания нового процесса в Unix.
#include <unistd h>
pid_t fork(void).
Возвращает 0 в дочернем процессе идентификатор дочернего процесса в родительском процессе. -1 в случае ошибки
Если вы никогда не встречались с этой функцией, трудным для понимания может оказаться то, что она вызывается один раз, а возвращает два значения. Одно значение эта функция возвращает в вызывающем процессе (который также называется родительским процессом) — этим значением является идентификатор созданного процесса (который также называется дочерним процессом). Второе значение (нуль) она возвращает в дочернем процессе. Следовательно, по возвращаемому значению можно определить, является ли данный процесс родительским или дочерним.
Причина того, что функция fork возвращает в дочернем процессе нуль, а не идентификатор родительского процесса, заключается в том, что у дочернего процесса есть только один родитель, и дочерний процесс всегда может получить идентификатор родительского, вызвыв функцию getppid. У родителя же может быть любое количество дочерних процессов, и способа получить их идентификаторы не существует. Если родительскому процессу требуется отслеживать идентификаторы своих дочерних процессов, он должен записывать возвращаемые значения функции fork.
Все дескрипторы, открытые в родительском процессе перед вызовом функции fork, становятся доступными дочерним процессам после ее завершения. Вы увидите, как это свойство используется сетевыми серверами: родительский процесс вызывает функцию accept, а затем функцию fork. Затем присоединенный сокет совместно используется родительским и дочерним процессами. Обычно дочерний процесс использует присоединенный сокет для чтения и записи, а родительский процесс только закрывает присоединенный сокет.
Существует два типичных случая применения функции fork*
Процесс создает свою копию таким образом, что каждая из копий может обрабатывать одно задание. Это типичная ситуация для сетевых серверов. Далее в тексте вы увидите множество подобных примеров.
Процесс хочет запустить другую программу. Поскольку единственный способ создать новый процесс — это вызвать функцию fork, процесс сначала вызывает функцию fork, чтобы создать свою копию, а затем одна из копий (обычно дочерний процесс) вызывает функцию ехес (ее описание следует за описани-
ем функции fork), чтобы заменить себя новой программой. Этот сценарий типичен для таких программ, как интерпретаторы командной строки.
Единственный способ запустить в Unix на выполнение какой-либо файл — вызвать функцию ехес (Мы будем часто использовать общее выражение «функция ехес», когда неважно, какая из шести функций ехесхг вызывается.) Функция ехес заменяет копию текущего процесса новым программным файлом, причем в новой программе обычно запускается функция main. Идентификатор процесса при этом не изменяется. Процесс, вызывающий функцию ехес, мы будем называть вызывающим процессом, а выполняемую при этом программу — новой программой.
В старых описаниях и книгах новая программа ошибочно называется новым процессом. Это неверно, поскольку новый процесс не создается.
Различие между шестью функциями ехес заключается в том, что они допускают различные способы задания аргументов.
выполняемый программный файл может быть задан или именем файла (filename), или полным именем (pathname)}
' аргументы новой программы перечисляются один за другим, либо на них имеется ссылка через массив указателей;
новой программе либо передается окружение вызывающего процесса, либо задается новое окружение.
#include <umstd h>
int execl(const char *pathname const char *argO /* (char *) 0 */ ),
int execv(const char ^pathname char *const arg"]).
int execle(const char ^pathname const char *argO /* (char *) 0. char *const envp[] */ )
int execve(const char *pathndme char *const drgv[] char *const envpE]).
mt execlp(const char *f? 1 ename const char *argO /* (char *) 0 */ )»
int execvp(const char *f? lenanie char *const *gi/[])
Все шесть функций возвращают -1 в случае йвибкм если же функция выполнена успешно то ничего не возвращается
Эти функции возвращают вызывающему процессу значение -1 только если происходит ошибка. Иначе управление передается в начало новой программы, обычно функции main.
Отношения между этими шестью функциями показаны на рис. 4.4. Обычно только функция exeeve является системным вызовом внутри ядра, а остальные представляют собой библиотечные функции, вызывающие exeeve.
Отметим различия между этими функциями:
Три верхние функции (рис. 4.4) принимают каждую строку как О1дель- ный аргумент, причем перечень аргументов завершается пустым указа гелем (так как их количество может быть различным). У трех нижних (функций
имени файла в полное имявызов
Рис- 4.4. Отношения между шестью функциями ехес
имеется массив argv, содержащий указатели на строки. Этот массив должен содержать пустой указатель, определяющий конец массива, поскольку размер массива не задается.
Две функции в левой колонке получают аргумент filename. Он преобразуется в pathname с использованием текущей переменной окружения PATH. Если аргумент filename функции execlp или execvp содержит косую черту (/) в любом месте строки, переменная PATH не используется. Четыре функции в двух правых колонках получают полностью определенный аргумент pathname.
Четыре функции в двух левых колонках не получают точного указателя окружения. Вместо этого с помощью текущего значения внешней переменной envi гоп создается список окружения, который передается новой программе. Две функции в правой колонке получают точный список окружения. Массив указателей envp должен быть завершен пустым указателем.
Дескрипторы, открытые в процессе перед вызовом функции ехес, обычно остаются открытыми во время ее выполнения. Мы говорим «обычно», поскольку это свойство может быть отключено при использовании функции fcntl для установки флага дескриптора FD_CLDEXEC・ Оно необходимо серверу inetd, о котором пойдет речь в разделе 12.5.
Параллельные серверы
Сервер, представленный в листинге 4.2, является последовательным (итеративным) сервером. Для такого простого сервера, как сервер времени и даты, это допустимо. Но когда обработка запроса клиента занимает больше времени, мы не можем связывать один сервер с одним клиентом, поскольку нам хотелось бы обрабатывать множество клиентов одновременно. Простейшим способом написать параллельный сервер под Unix является вызов функции fork, порождающей дочерний процесс для каждого клиента. В листинге 4.3 представлен типичный параллельный сервер.
Листинг 4.3. Типичный параллельный сервер p】d_t pid.
mt listenfd. connfd.
1istenfd = Socket(.):
/* записываем в sockaddr_m{} п叩аметрм заранее известного порта сервера */ BindClistenfd. ).
Listen(listenfd. LISTENQ).
for ( . . ) {
if ( (pid = ForkO) Close(listenfd). doit(connfd). Close(connfd), exit(O).}if ( (pid = ForkO) Close(listenfd). doit(connfd). Close(connfd), exit(O).}Close(connfd).Close(connfd).connfd = Acceptdistenfd, ):/* вероятно, блокировка */
if ( (pid = ForkO) Close(listenfd). doit(connfd). Close(connfd), exit(O).
}
if ( (pid = ForkO) Close(listenfd). doit(connfd). Close(connfd), exit(O).
}
Close(connfd).
Close(connfd).
;0) {
/* дочерний процесс закрывает 叩оспу出пваемь麻 сокет */
/* обработка запроса */
/* с этим клиентом закончено */
/* дочерний процесс завершен */
/* родительский процесс закрывает присоединенный сокет */
}
Когда соединение установлено, функция accept возвращает управление, сервер вызывает функцию fork и затем дочерний процесс занимается обслуживанием клиента (по присоединенному сокету connfd), а родительский процесс ждет другого соединения (на прослушиваемом сокете 11 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 и от клиента приходит запрос на соединение.
КлиентКлиентconnect ()connect ()Рис. 4.5. Состояние соединения клиент-сервер перед завершением вызванной функции acceptРис. 4.5. Состояние соединения клиент-сервер перед завершением вызванной функции accept
Клиент
Клиент
connect ()
connect ()
Рис. 4.5. Состояние соединения клиент-сервер перед завершением вызванной функции accept
Рис. 4.5. Состояние соединения клиент-сервер перед завершением вызванной функции accept
Сразу же после завершения функции accept мы получаем сценарий, изображенный на рис. 4.6. Соединение принимается ядром и создается новый сокет — connfd. Это присоединенный сокет, и теперь данные могут считываться и записываться по этому соединению.
Клиент
Рис. 4.6. Состояние соединения клиент-сервер после завершения функции accept
Следующим шагом параллельного сервера является вызов функции fork.
На рис. 4.7 показано состояние соединения после вызова функции fork.
Рис. 4.7. Состояние соединения клиент-сервер после вызова функции fork
Обратите внимание, что оба дескриптора, 1 】stenfd и connfd, совместно используются родительским и дочерним процессами.
Далее родительский процесс закрывает присоединенный сокет, а дочерний процесс закрывает прослушиваемый сокет. Это показано на рис. 4.8.
Желательно, чтобы это было последнее состояние сокетов. Дочерний процесс управляет соединением с клиентом, а родительский процесс может снова вызвать функцию accept на прослушиваемом сокете, чтобы обрабатывать следующее клн- ентское соединение.
КлиентКлиентСервер (родительский)Сервер (родительский)Рис. 4.8. Состояние соединения клиент-сервер после закрытия родительским и дочерним процессами соответствующих сокетовРис. 4.8. Состояние соединения клиент-сервер после закрытия родительским и дочерним процессами соответствующих сокетов
Клиент
Клиент
Сервер (родительский)
Сервер (родительский)
Рис. 4.8. Состояние соединения клиент-сервер после закрытия родительским и дочерним процессами соответствующих сокетов
Рис. 4.8. Состояние соединения клиент-сервер после закрытия родительским и дочерним процессами соответствующих сокетов
Функция close
Обычная функция Unix close также используется для закрытия сокета и завершения соединения TCP.
#include <umstd h>
int close(int sockfd)..
По умолчанию функция close отмечает сокет TCP как закрытый и немедленно возвращает управление процессу. Дескриптор сокета больше не используется процессом и не может быть передан в качестве аргумента функции read или wri te. Но TCP попытается отправить данные, которые уже установлены в очередь, и после того как отправка произойдет, осуществится нормальная последовательность завершения соединения TCP (см. раздел 2.5).
В разделе 7.5 рассказывается о параметре сокета SO_LINGER, который позволяет нам изменять действие с сокетом TCP, выполняемое по умолчанию. В этом разделе мы также назовем действия, благодаря которым приложение TCP может получить гарантию того, что приложение-собеседник получило данные, поставленные в очередь на отправку, но еще не отправленные.
Счетчик ссылок дескриптора
В конце раздела 4.8 мы отметили, что когда родительский процесс на нашем параллельном сервере закрывает присоединенный сокет с помощью функции cl ose, счетчик ссылок дескриптора уменьшается лишь на единицу. Поскольку счетчик ссылок при этом все еще оставался больше нуля, вызов функции cl ose не инициировал последовательность завершения TCP-соединения, состоящую из четырех пакетов. Нам нужно, чтобы наш параллельный сервер с присоединенным сокетом, разделяемым между родительским и дочерним процессами, работал по такр- му принципу.
142 ГЬава 4. Элементарнне сокеты TCP
Если мы хотим отправить сегмент FIN по соединению TCP, вместо функции close должна использоваться функция shutdown (см. раздел 6.6). Причины мы рассмотрим в разделе 6.5.
Необходимо также знать, что происходит с нашим параллельным сервером, если родительский процесс не вызывает функцию cl ose для каждого присоединенного сокета, возвращаемого функцией accept. Прежде всего, родительский процесс в какой-то момент израсходует все дескрипторы, поскольку обычно число дескрипторов, которые могут быть открыты процессом, ограничено. Но что более важно, ни одно из клиентских соединений не будет завершено. Когда дочерний процесс закрывает присоединенный сокет, его счетчик ссылок уменьшается с 2 до 1 и остается равным 1, поскольку родительский процесс не закрывает присоединенный сокет с помощью функции close. Это помешает выполнить последовательность завершения соединения TCP, и соединение останется открытым,
Функции getsockname и getpeername
Эти две функции возвращают либо локальный (функция getsockname), либо удаленный (функция getpeername) адрес протокола, связанный с сокетом.
#include <sys/socket h>
int getsocknan)e(int sockfd struct sockaddr *loca1add socklen_t *addrlen),
int getpeernameCmt 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), как только устанавливается соединение с кли-
ентом (функция accept успешно выполнена), может вызвать функцию getsockname, чтобы получить локальный IP-адрес соединения. Аргумент sockfd (дескриптор сокета) в этом вызове должен содержать дескриптор присоединенного, а не прослушиваемого сокета.
Когда сервер запускается с помощью функции ехес процессом, вызывающим функцию accept, он может идентифицировать клиент только одним способом — вызвать функцию getpeername. Это происходит, когда функция inetd (см. раздел 12.5) вызывает функции fork и ехес для создания сервера TCP. Этот сценарий представлен на рис. 4.9. Функция т netd вызывает функцию accept (верхняя левая рамка), после чего возвращаются два значения: дескриптор присоединенного сокета connfd (это возвращаемое значение функции), а также IP-адрес и номер порта клиента, отмеченные на рисунке небольшой рамкой с подписью «адрес собеседника» (структура адреса сокета Интернета). Далее вызывается функция fork и создается дочерний процесс функции metd. Поскольку дочерний процесс запускается с копией содержимого памяти родительского процесса, структура адреса сокета доступна дочернему процессу, как и дескриптор присоединенного сокета (так как дескрипторы совместно используются родительским и дочерним процессами). Но когда дочерний процесс с помощью функции ехес запускает выполнение реального сервера (скажем, сервера Telnet), содержимое памяти дочернего процесса заменяется новым программным файлом для сервера Telnet (то есть структура адреса сокета, содержащая адрес собеседника, теряется). Однако во время выполнения функции ехес дескриптор присоединенного сокета остается открытым. Один из первых вызовов функции, который выполняет сервер Telnet, — это вызов функции getpeername для получения IP-адреса и номера порта клиента.
Сервер Telnet
connfd
Рис- 4.9. Пример функции metd, порождающей сервер
Очевидно, что в приведенном примере сервер Telnet при запуске должен знать значение функции connfd. Этого можно достичь двумя способами Во-первых,
процесс, вызывающий функцию ехес, может отформатировать номер дескриптора как символьную строку и передать ее в виде аргумента командной строки программе, выполняемой с помощью функции ехес. Во-вторых, можно заключит ь соглашение относительно определенных дескрипторов: некоторый дескриптор всегда присваивается присоединенному сокету перед вызовом функции ехес. Последний случай соответствует действию функции т netd — она всегда присваивает дескрипторы 0, 1 и 2 присоединенным сокетам.
Пример: получение семейства адресов сокета
Функция sockfd_to_fann 1у, представленная в листинге 4.4, возвращает семейство адресов сокета.
Листинг 4.4. Возвращаемое семейство адресов сокета
//11Ь/sockfd_to__fami 1у с
#1nclude "unp h"
mt
sockfd_to_fami1у(int sockfd)
{— 一
union {
struct sockaddr sa.
char data[MAXSOCKADDRJ;
} un.
socklen_t len.
len = MAXSOCKADDR
if (getsockname(sockfd (SA *) un.data. &len) < 0)
return (-1).
return (un sa sa_family).
M }-
Выделение пространства для наибольшей структуры адреса сокета
5-8 Поскольку мы не знаем, какой тип структуры адреса сокета нужно будет разместить в памяти, мы используем в нашем заголовочном файле unp h константу MAXSOCKADDR, которая представляет собой размер наибольшей структуры адреса сокета в байтах. Мы определяем массив типа char такого размера в объединении, включающем универсальную структуру адреса сокета.
Вызов функции getsockname
.0-13 Мы вызываем функцию getsockname и возвращаем семейство адресов.
Поскольку Posix. 1g позволяет вызывать функцию getsockname на пеприсоеди- ненном сокете, эта функция должна работать для любого дескриптора открытого сокета.
Резюме
Все клиенты и серверы начинают работу с вызова функции socket, возвращающей дескриптор сокета. Затем клиенты вызывают функцию connect, в то время как серверы вызывают функции bi nd, 11 sten и accept. Сокеты обычно закрывают-
ся с помощью стандартной функции cl ose, хотя в разделе 6.6 вы увидите другой способ закрытия, реализуемый с помощью функции shutdown. Мы также проверим влияние параметра сокета SO_LINGER (см. раздел 7.5).
Большинство серверов TCP являются параллельными. При этом для каждого клиентского соединения, которым управляет сервер, вызывается функция fork. Вы увидите, что большинство серверов UDP являются последовательными. Хотя обе эти модели успешно использовались на протяжении ряда лет, имеются и другие параметры создания серверов с использованием программных потоков и процессов, которые мы рассмотрим в главе 27.
Упражнения
В разделе 4.4 мы утверждали, что константы INADDR_, определенные в заголовочном файле <netmet/in h>, расположены в порядке байтов узла. Каким образом мы можем это определить?
Измените листинг 1.1 так, чтобы вызвать функцию getsockname после успешного завершения функции connect. Выведите локальный IP-адрес и локальный порт, присвоенный сокету TCP, используя функцию sock_ntop. В каком диапазоне (см. рис. 2.6) будут находиться динамически назначаемые порты вашей системы?
Предположим, что на параллельном сервере после вызова функции fork запускается дочерний процесс, который завершает обслуживание клиента перед тем, как результат выполнения функции fork возвращается родительскому процессу. Что происходит при этих двух вызовах функции cl ose в листинге 4.3?
В листинге 4.2 сначала измените порт сервера с 13 на 9999 (так, чтобы для запуска программы вам не потребовались права привилегированного пользователя). Удалите вызов функции listen. Что происходит?
Продолжайте предыдущее упражнение. Удалите вызов функции bind, но оставьте вызов функции listen. Что происходит?
ГЛАВА 5
Пример ТСР-соединения клиент-сервер
Введение
Напишем простой пример пары клиент-сервер, используя элементарные функции из предыдущей главы. Наш простой пример — это эхо-сервер, функционирующий следующим образом:
Клиент считывает строку текста из стандартного потока ввода и отправляет ее серверу.
Сервер считывает строку из своего сетевого ввода и отсылает эту строку обратно клиенту.
Клиент считывает отраженную строку и помещает ее в свой стандартный поток вывода.
На рис. 5.1 изображена пара клиент-сервер вместе с функциями, используемыми для ввода м вывода.
fgets stdin а
stdout Ч
fputs
TCP- клиент
writenreadline
TCP- сервер
"readlinewriten
Рис. 5.1. Простой эхо-клиент и эхо-сервер
Между клиентом и сервером мы показали две стрелки, но на самом деле это одно двустороннее соединение TCP. Функции fgets и fputs имеются в стандартной библиотеке ввода-вывода, а функции w门 ten и readline приведены в разделе 3.9.
Мы разрабатываем нашу собственную реализацию эхо-сервера, однако большинство реализаций TCP/IP предоставляют готовый эхо-сервер, используя как TCP, так и UDP (см. раздел 2.10). С нашим собственным клиентом мы также будем использовать и готовый сервер.
Соединение клиент-сервер, отражающее вводимые строки, является корректным, хотя и простым примером сетевого приложения. На этом примере можно проиллюстрировать все основные действия, необходимые для реализации соединения клиент-сервер. Все, что вам нужно сделать, чтобы применить его к вашему приложению, — это изменить операции, которые выполняет сервер с принимаемыми ог клиентов данными. .
С помощью этого примера мы можем не только проанализировать запуск нашего клиента и сервера в нормальном режиме (ввести строку и посмотреть, как она отражается), но и исследовать множество «граничных условий»: выяснить, что происходит в момент запуска клиента и сервера; что происходит, когда клиент нормальным образом завершает работу; что происходит с клиентом, если процесс сервера завершается до завершения клиента или если возникает сбой на узле сервера, и т. д. Рассмотрев эти сценарии, мы сможем понять, что происходит на уровне сети и как это представляется для API-сокетов, и научиться писать приложения так, чтобы они умели обрабатывать подобные ситуации.
Во всех рассматриваемых далее примерах присутствуют специфичные для протоколов жестко заданные (hard coded) константы, такие как адреса и порты. Это обусловлено двумя причинами. Во-первых, нам необходимо точно понимать, что нужно хранить в специфичных для протоколов структурах адресов. Во-вторых, мы еще не рассмотрели библиотечные функции, которые сделали бы наши программы более переносимыми. Эти функции рассматриваются в главах 9и 11.
В последующих главах код клиента и сервера будет претерпевать многочисленные изменения, по мере того как вы будете больше узнавать о сетевом программировании (см. табл. 1.2 и 1.3).
Эхо-сервер TCP: функция main
Наши клиент и сервер TCP используют функции, показанные на рис. 4.1. Программа параллельного сервера представлена в листинге 5.11.*
Листинг 5.1. Эхо-сервер TCP (улучшенный в листинге 5.9)
//tcpdiserv/tcpservOl с
#include "unp h"
2 3 4 5 6 7 82 3 4 5 6 7 8mt
2 3 4 5 6 7 8
2 3 4 5 6 7 8
main(mt argc, char **argv)
{
mt listenfd. connfd.
pid_t chi1 dpid.
socklen_t cl lien.
struct sockaddr in cliaddr. servaddr.
listenfd = Socket(AFJNET. SOCK_STREAM. 0).
bzero(&servaddr. sizeof(servaddr)).
servaddrsm_family = AF_INET.
servaddrsm^addr s_dddr~= htonl(INADDR_ANY):
servaddrsi report ="htons(SERV_PORT).
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
ListenClistenfd. LISTENQ).
for (..)(
продолжение 夕продолжение 夕cllien = sizeof(cliaddr).
продолжение 夕
продолжение 夕
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter.com/download.
Листинг 5.1 (продолжение)
connfd = Accept(listenfd, (SA *) &cliaddr &clilen)
if ( (childpid = ForkO) == 0) {/* дочерний npouecc */
Close(listenfd) /* закрываем прослушиваемь楊 сокет */
str_echo(connfd) /* обрабатываем запрос */
exit(0)
}
Close(connfd) /* родительский процесс закрывает присоедпненнь血 сокет */
}
}
Создание сокета, связывание с известным портом сервера
3 15 Создается сокет TCP. В структуру адреса сокета Интернета записывается универсальный адрес (I NADDR_ANY) и номер заранее известного порта сервера (SERVP0RT, который определен как 9877 в нашем заголовочном файле unp h). В результате связывания с универсальным адресом системе сообщается, что мы примем соединение, предназначенное для любого локального интерфейса в том случае, если система имеет несколько сетевых интерфейсов. Наш выбор номера порта TCP основан на рис. 2.6. Он должен быть больше 1023 (нам не нужен зарезервированный порт), больше 5000 (чтобы не допустить конфликта с динамически назначаемыми портами, которые выделяются многими реализациями, происходящими от Беркли), меньше 49 152 (чтобы избежать конфликта с «правильным» диапазоном динамически назначаемых портов) и не должен конфликтовать ни с одним зарегистрированным портом. Сокет преобразуется в прослушиваемый при помощи функции 11 sten.
Ожидание завершения клиентского соединения
7-18 Сервер блокируется в результате вызова функции accept, ожидая подключения клиента
Параллельный сервер
9-24 Для каждого клиента функция fork порождает дочерний процесс, и дочерний процесс обслуживает запрос этого клиента. Как мы говорили в разделе 4.8, дочерний процесс закрывает прослушиваемый сокет, а родительский процесс закрывает присоединенный сокет. Затем дочерний процесс вызывает функцию str_echo (см. листинг 5.2) для обработки запроса клиента.
Эхо-сервер TCP: функция str_echo
Функция str_echo, показанная в листинге 5.2, выполняет серверную обработку запроса каждого клиента — считывание строк от клиента и отражение их обратно клиенту.
Листинг 5.2. функция str_echo: отраженные строки на сокете //lib/str_echo с
1 #1 nclude 'unp h'
str echo(int sockfd)
{ ~
ssize_t n
charline[MAXLINE]
for ( ) {
if ( (n = Readline(sockfd line MAXLINE)) == 0)
return/* соединение закрыто с другого конца */
Writen(sockfd line n)
}
}
Чтение строки и ее отражение
7-11 Функция readl те считывает очередную строку из соке^, после чего строка отражается обрат но клиенту с помощью функции wnten. Если клиент закрывает соединение (нормальный сценарий), то при получении клиентского сегмента FIN функция дочернего процесса readline возвращает нуль. После этого происходит возврат из функции str_echo, и далее завершается дочерний процесс, приведенный в листинге 5.1.
Эхо-клиент TCP: функция main
В листинге 5 3 показана функция main TCP-клиента. ,
Листинг 5.3. Эхо~клиентТСР
//tcpc11serv/tcpcl101 с
#i nd ude unp h"
mt
main(int argc char **argv)
{
mt sockfd
struct sockaddr_m servaddr.
if (argc』2)
err_quit( *usage tcpcli <IPaddress>,).
sockfd 工 Socket(AF_INET SOCK_STREAM 0)
bzero(&servaddr sizeof(servaddr))
servaddr sin_family = AF_INET
servaddr sin_port = htons(SERV PORT)
Inet_pton(AF_INET argv[l] &servaddr sin_addr)
Connect(sockfd (SA *) &servaddr sizeof(servaddr))
str_di(stdin sockfd) /* зта функция выполняет все необходимые действия со стороны клиента */
exit(0):
D }
Создание сокета, заполнение структуры его адреса
9-13 Создается сокет TCP, и структура адреса сокета заполняется IP-адресом сервера и номером порта. IP-адрес сервера мы берем из командной строки, а известный номер порта сервера (SERV_PORT) — из нашего заголовочного файла unp.h.
Соединение с сервером
14-15 Функция connect устанавливает соединение с сервером. Затем функция str_cl 1 (см. листинг 5.4) выполняет все необходимые действия со стороны клиента.
Эхо-клиент TCP: функция str_cli
Эта функция, показанная в листинге 5.4, обеспечивает отправку запроса клиента н прием ответа сервера в цикле 0. Функция считывает строку текста из стандартного потока ввода, отправляет ее серверу н считывает отраженный ответ сервера, после чего помещает отраженную строку в стандартный поток вывода.
Листинг 5.4. Функция str_cli: цикл формирования запроса клиента //lib/str_cli с
#include "unp h"
void. ।
str_cli(FILE *fp. int sockfd)
4「
char sendline[MAXLINE], recvline[MAXLINE]
while (Fgets(sendlme, MAXLINE, fp) != NULL) {
Wnten(sockfd. sendline strlen(sendline)).
if (Readline(sockfd. recvline, MAXLINE) == 0)
err_quit("str_cli server terminated prematurely*'):
Fputs(recvline. stdout),
)
}
Считывание строки, отправка серверу
5-7 Функция fgets считывает строку текста, а функция wn ten отправляет эту строку серверу.
Считывание отраженной сервером строки, запись в стандартный поток вывода
-10 Функция readlme принимает отраженную сервером строку, а функция fputs записывает ее в стандартный поток вывода.
Возврат в функцию main
1-12 Цикл завершается, когда функция fgets возвращает пустой указатель, что происходит, когда либо достигается конец файла, либо происходит ошибка. Наша функция-обертка Fgets проверяет наличие ошибки, и если ошибка обнаружена,
прерывает выполнение программы, то есть функция Fgets возвращает пустой указатель только при достижении конца файла.
5.6. Нормальный запуск
Наш небольшой пример TCP (около 150 строк кода для функций mam, str_echo, str_cl i, readlinen wri ten) позволяет нам понять, как запускаются н завершаются клиент н сервер и, что наиболее важно, как развиваются события, если произошел сбой на узле клиента или в клиентском процессе, потеряна связь в сети и т. д. Только при понимании этих «граничных условий» н их взаимодействия с протоколами TCP/IP мы сможем обеспечить устойчивость клиентов н серверов, которые смогут справляться с подобными ситуациями.
Сначала мы запускаем сервер в фоновом режиме на узле bsdi.
bsd? Z tcpservOl &!
21130
Когда сервер запускается, он вызывает функции socket, bind, listen и 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 % tcpcliOl 127.0.0.1
Клиент вызывает функции socket и connect, последняя вызывает трехэтапное рукопожатие TCP. Когда рукопожатие TCP завершается, функция connect возвращает управление процессу-клиенту, а функция accept — процессу-серверу. Соединение установлено. Затем выполняются следующие шаги:
Клиент вызывает функцию str_cl i, которая блокируется в вызове функции fgets, поскольку мы еще ничего не ввели.
Когда функция accept возвращает управление процессу-серверу, последний вызывает функцию fork, а дочерний процесс вызывает функцию str_echo. Она вызывает функцию readl т пе, которая, в свою очередь, вызывает функцию read, блокируемую в ожидании получения данных от клиента.
Родительский процесс сервера снова вызывает функцию accept н блокируется в ожидании подключения следующего клиента.
У нас имеется три процесса, н все они находятся в состоянии ожидания (блокированы): клиент, родительский процесс сервера и дочерний процесс сервера.
ПРИМЕЧАНИЕ
Мы специально поставили первым пунктом (после завершения трехэтапного рукопожатия) вызов функции str cli, происходящий ira стороне клиента, а затем уже перечислили действия на стороне сервера Причину объясняет рис. 2.5: функция connect возвращает управление, когда клиент получает второй сегмент рукопожатия. Однако функция accept не возвращает управление до тех пор, пока сервер не получит третий сегмент рукопожатия, то есть пока не пройдет половина периода RTT после завершения функции connect.
Мы намеренно запускаем и клиент, и сервер на одном узле — так проще всего экспериментировать с клиент-серверными приложениями. Поскольку клиент и сервер запущены на одном узле, функция netstat отображает теперь две дополнительные строки вывода, соответствующие соединению TCP: bsdi % netstat -а
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tep 00localhost 9877 local host 1052 ESTABLISHED
tep 00localhost 1052 local host 9877 ESTABLISHED
tep 00 * 98〃* *LISTEN
Первая из строк состояния ESTABLISHED соответствует дочернему сокету сервера, поскольку локальным портом является порт 9877. Вторая строка ESTABLISHED — это клиентский сокет, поскольку локальный порт — порт 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
H
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). В колонках РЮи PPID показаны отношения между родительским и дочерним процессами. Можно точно сказать, что первая строка tcpservOl соответствует родительскому процессу, а вторая строка tcpservOl — дочернему, поскольку PPID дочернего процесса — это PID родительского. Кроме того, PPID родительского процесса совпадает с PID интерпретатора команд (ksh).
Колонка STAT для всех трех сетевых процессов отмечена символом I. Это означает, что процессы находятся в состоянии ожидания (idle). Знак + в конце двух ячеек STAT говорит о том, что данный процесс находится в группе основных процессов его управляющего терминала. Если процесс находится в состоянии ожидания, колонка WCHAN сообщит нам о том, чем он занят В 4.4BSD значение netcon
выводится, если процесс блокируется функцией accept или connect, значение netio — если процесс блокируется при вводе или выводе через сокет, a ttym или ttyout — если процесс блокируется при терминальном вводе-выводе. Значения WCHAN имеют смысл для наших трех сетевых процессов.
Нормальное завершение
На этом этапе соединение установлено, н все, что мы ни вводили бы на стороне клиента, отражается обратно.
bsdi 弘 tcpcliOl 127.0.0.1 эту строку м& показывали раньше
hello, worldнаш ввод
hello, worldотраженная сервером строка
good bye
good bye
AD Ctrl-D- Haw завершающий символ для обозначения конца файла
Мы вводим две строки, каждая из них отражается, затем мы вводим символ конца файла (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, чтобы вывести только строки с заранее известным портом нашего сервера. Но при этом также удаляется строка заголовка.)
Перечислим этапы нормального завершения работы нашего клиента н сервера.
Когда мы набираем символ EOF, функция fgets возвращает пустой указатель, н функция str_c】1 возвращает управление (см. листинг 5.4).
Когда функция str_cli возвращает управление клиентской функции main (см. листинг 5.3), последняя завершает работу, вызывая функцию exit.
При завершении процесса выполняется закрытие всех открытых дескрипторов, так что клиентский сокет закрывается ядром. При этом серверу посылается сегмент FIN, на который TCP сервера отвечает сегментом АСК. Это первая половина последовательности завершения работы соединения TCP. На этом этапе сокет сервера находится в состоянии CLOSE_WAIT, а клиентский сокет —в состоянии FIN WAIT—2 (см. рис. 2.4 н 2.5).
Когда TCP сервера получает сегмент FIN, дочерний процесс сервера блокируется в вызове функции readlme (см. листинг 5.2), а затем функция readhne возвращает нуль. Это заставляет функцию str_echo вернуть управление функции mai п дочернего процесса сервера.
Дочерний процесс сервера завершается с помощью вызова функции exi t (листинг 5.1).
Все открытые дескрипторы в дочернем процессе сервера закрываются. Закрытие присоединенного сокета дочерним процессом вызывает отправку двух последних сегментов завершения соединения TCP: FIN от сервера клиенту
н АСК от клиента (см. рис. 2.5). На этом этапе соединение полностью завершается. Клиентский сокет входит в состояние TIME_WAIT.
Другая часть завершения процесса относится к сигналу SIGCHLD. Он отправляется родительскому процессу, когда завершается дочерний процесс. Это происходит н в нашем примере, но мы не перехватываем данный сигнал в коде н по умолчанию он игнорируется. Дочерний процесс входит в состояние зомби (zombie). Мы можем проверить это с помощью команды ps.
bsdi % ps
PID
тт
STAT
TIME
COMMAND
19130
pl
Ss
0
05
08
-ksh (ksh)
21130
pl
I
0
00
06
tcpservOl
21132
pl
z
0
00
00
(tcpservOl)
21167
pl
R+
o-oo
10
PS
Теперь дочерний процесс находится в состоянии Z (зомбн).
Процессы-зомби нужно своевременно удалять, а это требует работы с сигналами Unix. Поэтому в следующем разделе мы сделаем обзор управления сигналами, а затем продолжим рассмотрение нашего примера.
Обработка сигналов Posix
Сигнал — это уведомление процесса о том, что произошло некое событие. Иногда сигналы называют программными прерываниями (software interrupts). Подразумевается, что процесс не знает заранее о том, когда придет сигнал.
Сигналы могут посылаться в следующих направлениях:
^ одним процессом другому процессу (или самому себе);
择 ядром процессу.
Сигнал SIGCHLD, упомянутый в конце предыдущего раздела, ядро посылает родительскому процессу при завершении дочернего.
Для каждого сигнала существует определенное действие {action или disposition —характер). Действие, соответствующее сигналу, задается с помощью вызова функции sigaction (ее описание следует чуть ниже) и может быть выбрано тремя способами:
Мы можем предоставить функцию, которая вызывается при перехвате определенного сигнала. Эта функция называется обработчиком сигнала (signal handled), а действие называется перехватыванием сигнала (catching). Сигналы SIGKILL н SIGSTOP перехватить нельзя. Наша функция вызывается с одним целочисленным артументом, который является номером сигнала, н ничего не возвращает. Следовательно, прототип этой функции имеет вид:
void handler (int signo)
Для большинства сигналов вызов функции sigaction и задание функции, вызываемой при получении сигнала, — это все, что требуется для обработки сигнала. Но дальше вы увидите, что для перехватывания некоторых сигналов, в частности SIGIO, SIGPOLL н SIGURG, требуются дополнительные действия со сто
роны процесса.
Мы можем игнорировать сигнал, если действие задать как SIG_IGN. Сигналы SIGKILL н SIGSTOP не могут быть проигнорированы.
Мы можем установить действие для сигнала по умолчанию, задав его как SIG DFL. Действие сигнала по умолчанию обычно заключается в завершении процесса по получении сигнала, а некоторые сигналы генерируют копию области памяти процесса в его текущем каталоге (так называемый дамп — core dump). Есть несколько сигналов, для которых действием по умолчанию является игнорирование. Например, SIGCHLD н SIGURG (посылаются по получении внеполосных данных, см. главу 21)— это два сигнала, игнорируемых по умолчанию, с которыми мы встретимся в тексте.
Функция signal
Согласно Posix, чтобы определить действие для сигнала, нужно вызвать функцию sigaction. Однако это достаточно сложно, поскольку один аргумент этой функции — это структура, для которой необходимо выделение памяти и заполнение. Поэтому проще задать действие сигнала с помощью функции signal. Первый ее артумент — это имя сигнала, а второй — либо указатель на функцию, либо одна из констант SIG_IGN н SIG_DFL. Но функция signal существовала еще до появления Posix. 1, н ее различные реализации имеют разную семантику сигналов с целью обеспечения обратной совместимости. В то же время Posix четко диктует семантику при вызове функции sigaction. Это обеспечивает простой интерфейс с соблюдением семантики Posix. Мы включили эту функцию в нашу собственную библиотеку вместе функциями err&ХХ и функциями-обертками, которые мы используем для построения всех наших программ. Она представлена в листинге 5.5.
Листинг 5.5. Функция signal, вызывающая функцию Posix sigaction //lib/signal с
#i nd ude"unp h"
Sigfunc *
signal(int signo, Sigfunc *func)
{
struct sigaction actoact,
act sa_handler = func,
sigemptyset(&act sa_mask),
act sa_flags = 0.
if (signo == SIGALRM){
#ifdef SAJNTERRUPT
act sa_flags |= SA_INTERRUPT: /* SunOS 4 x */
#endif
} else {
#ifdef SA_RESTART
act sa_flags |= SA_RESTART. /* SVR4, 44BSD */
和nd if
}
if (sigaction(signo. &act. &oact) < 0)
return (SIG_ERR).
return (oact.sa handler),
}. ~
Упрощение прототипа функции при использовании typedef
-3 Обычный прототип для функции signal усложняется наличием вложенных скобок:
void (*signal(int signo void (*fty/?c)(int)))(int).
Чтобы упростить эту запись, мы определяем тип Sigfunc в нашем заголовочном файле unp.h следующим образом:
typedef void Sigfunc(int)
указывая тем самым, что обработчики сигналов — это функции с целочисленным аргументом, ничего не возвращающие (vol d). Тогда прототип функции выглядит следующим образом:
Sigfunc *signal(mt signo Sigfunc *func),
Указатель на функцию, являющуюся обработчиком сигнала, — это второй аргумент функции и, в то же время, возвращаемое функцией значение.
Установка обработчика
Элемент sd_hand】er структуры sigaction устанавливается равным аргументу func функции signal.
Установка маски сигнала для обработчика
Posix позволяет нам задавать набор сигналов, которые будут блокированы при вызове обработчика сигналов. Любой блокируемый сигнал не может быть доставлен (deliver) процессу. Мы устанавливаем элемент sa_mask равным пустому набору. Это означает, что во время работы обработчика дополнительные сигналы не блокируются. Posix гарантирует, что перехватываемый сигнал всегда блокирован, пока выполняется его обработчик.
Установка флага SA_RESTART
~17 Флаг SZ\_RESTART не является обязательным, н если он установлен, то системный вызов, прерываемый этим сигналом, будет автоматически снова выполнен ядром. (В продолжении нашего примера мы более подробно поговорим о прерванных системных вызовах.) Если перехватываемый сигнал не является сигналом SIGALRM, мы задаем флаг SA~RESTART, если он определен. (Причина, по которой сигнал SIGALRM выделяется в особый случай, состоит в том, что обычно цель его генерации — ввести временное ограничение в операцию ввода-вывода, как показано в листинге 13.2. В этом случае мы хотим, чтобы блокированный системный вызов был прерван сигналом.) Более ранние системы, особенно SunOS 4.x, автоматически перезапускают прерванный системный вызов по умолчанию н затем определяют дополнительный флаг SA^INTERRUPT. Если этот флаг задан, мы устанавливаем его при перехвате сигнала SIGALRM.
Вызов функции sigaction
8-20 Мы вызываем функцию sigaction, азатем возвращаем старое действие сигнала как результат функции signal.
В кииге мы везде используем (Ьункпию sianal из листинга 5 5.
Семантика сигналов Posix
Сведем воедино следующие моменты, относящиеся к обработке сигналов в системе, совместимой с Posix.
Однажды установленный обработчик сигналов остается установленным (в более ранних системах обработчик сигналов удалялся каждый раз по выполнении).
На время выполнения функции-обработчика сигнала доставляемый сигнал блокируется. Более того, любые дополнительные сигналы, заданные в наборе сигналов sa_mask, переданном функции sigaction при установке обработчика, также блокируются. В листинге 5.5 мы устанавливаем sa_mask равным пустому набору, что означает, что никакие сигналы, кроме перехватываемого, не блокируются.
雖 Если сигнал генерируется один или несколько раз, пока он блокирован, то обычно после разблокирования он доставляется только один раз, то есть по умолчанию сигналы Unix не устанавливаются 6 очередь. Пример мы рассмотрим в следующем разделе. Стандарт Posix реального времени 1003.1b определяет набор надежных сигналов, которые помещаются в очередь, но в этой книге мы их не используем.
绘 Существует возможность выборочного блокирования н разблокирования набора сигналов с помощью функции sigprocmask. Это позволяет нам защитить критическую область кода, не допуская перехватывания определенных сигналов во время ее выполнения.
Обработка сигналов SIGCHLD
Назначение состояния зомби — сохранить информацию о дочернем процессе, чтобы родительский процесс мог ее впоследствии получить. Эта информация включает идентификатор дочернего процесса, статус завершения н данные об использовании ресурсов (время процессора, память и т. д.). Если у завершающегося процесса есть дочерний процесс в зомбированном состоянии, идентификатору родительского процесса всех зомбированных дочерних процессов присваивается значение 1 (процесс imt), что позволяет унаследовать дочерние процессы н сбросить их (то есть процесс imt будет ждать (wait) их завершения, благодаря чему будут удалены зомби). Некоторые системы Unix в столбце COMMAND выводят для зомбированных процессов значение <defunct>.
Обработка зомбированных процессов
Очевидно, что нам не хотелось бы оставлять процессы в виде зомби. Они занимают место в ядре, и в конце концов у нас может не остаться никаких процессов, кроме зомбированных. Когда мы выполняем функцию fork для дочерних процессов, необходимо с помощью функции wait дождаться их завершения, чтобы они не превратились в зомби. Для этого мы устанавливаем обработчик сигналов для перехватывания сигнала SIGCHLD и внутри обработчика вызываем функцию wait. (Функции wait н waitpid мы опишем в разделе 5.10.)
Signal(SIGCHLD. sig_chld).
в листинге 5.1, после вызова функции li sten. (Необходимо сделать это за некоторое время до вызова функции fork для первого дочернего процесса, причем только один раз.) Затем мы определяем обработчик сигнала — функцию sigjchld, представленную в листинге 5.6.
Листинг 5.6. Версия обработчика сигнала SIGCHLD, вызывающая функцию wait (усовершенствованная версия находится в листинге 5.8)
//tcpcliserv/sigchldwait с
include "unp h"
void
sig_chld(mt signo)
{ ~
pid_t pid
intstat.
pid = wait(&stat),
prmtf("child %d terminatedXen' pid).•
return.
}
ВНИМАНИЕ
В обработчике сигналов не рекомендуется вызов стандартных функций ввода-вывода, таких как pnntf, по причинам, изложенным в разделе 11.14. В данном случае мы вызываем функцию prmtf как средство диагностики, чтобы увидеть, когда завершается дочерний процесс.
В системах System V и Unix 98 дочерний процесс не становится зомби, если процесс задает действие SIG_IGN для SIGCHLD. К сожалению, это верно только для System V и Unix 98. В Posix 1 прямо сказано, что такое поведение этим стандартом не предусмотрено. Переносимый способ обработки зомби состоит в том, чтобы перехватывать сигналы SIGCHLD и вызывать функцию wait или waitpid.
Если мы откомпилируем в Solaris 2.5 программу, представленную в листинге 5.1, вызывая функцию Signal с нашим обработчиком sig.chld, н будем использовать функцию signal из системной библиотеки (вместо нашей версии, показанной в листинге 5 5), то получим следующее:
solans % tcpserv02 &[2]16939solans Z tcpcliOl 127.0.0.1hi therehi there人Dchild 16942 terminated '*accept error Interrupted system callsolans % tcpserv02 &[2]16939solans Z tcpcliOl 127.0.0.1hi therehi there人Dchild 16942 terminated '*accept error Interrupted system callзапускаем сервер в фоновом режиме
solans % tcpserv02 &
[2]16939
solans Z tcpcliOl 127.0.0.1
hi there
hi there
人D
child 16942 terminated '*
accept error Interrupted system call
solans % tcpserv02 &
[2]16939
solans Z tcpcliOl 127.0.0.1
hi there
hi there
人D
child 16942 terminated '*
accept error Interrupted system call
затем клиент
набираем эту строну
и она отражается сервером вводим символ конца файла функция pm ntf из обработчика сигнала выводит эту строку но функция mam преждевременно прекращает выполнение
Последовательность шагов в этом примере такова:
Мы завершаем работу клиента, вводя символ EOF. TCP клиента посылает сегмент FIN серверу, н сервер отвечает сегментом АСК.
Получение сегмента FIN доставляет EOF ожидающей функции readl те дочернего процесса. Дочерний процесс завершается.
Родительский процесс блокирован в вызове функции accept, когда доставляется сигнал SIGCHLD. Функция sig_chld (наш обработчик сигнала) выполняется, функция wait получает PID дочернего процесса н статус завершения, после чего из обработчика сигнала вызывается функция prmtf. Обработчик сигнала возвращает управление.
Поскольку сигнал был перехвачен родительским процессом, в то время как родительский процесс был блокирован в системном вызове (функция 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 (через readlme) в листинге 5.2 никогда не возвратит управление, если клиент никогда не пошлет серверу строку для отражения. Другие примеры медленных системных вызовов — чтение и запись в случае программных каналов н терминальных устройств. Важным исключением является дисковый ввод-вывод, который обычно завершается возвращением управления вызвавшему процессу (в предположении, что не происходит фатальных аппаратных ошибок).
Основное применяемое здесь правило связано с тем, что когда процесс, блокированный в медленном системном вызове, перехватывает сигнал, а затем обработчик сигналов завершает работу, системный вызов может возвратить ошибку
EINTR. Некоторые ядра автоматически перезапускают некоторые прерванные системные вызовы. Для обеспечения переносимости программ, перехватывающих сигналы (большинство параллельных серверов перехватывает сигналы SIGCHLD), следует учесть, что медленный системный вызов может возвратить ошибку EINTR. Проблемы переносимости связаны с приведенньши выше ^могут^ и ^некоторые^ и тем фактом, что поддержка флага Posix SA_RESTART не является обязательной. Даже если реализация поддерживает флаг SA_RESTART, не все прерванные системные вызовы могут автоматически перезапуститься. Например, большинсгво реализаций, происходящих от Беркли, никогда автоматически не перезапускают функцию sei ect, а некоторые из этих реализаций никогда не перезапускают функции accept и recvfrom.
Чтобы обработать прерванный вызов функции accept, мы изменяем вызов функции accept, приведенной в листинге 5.1, в начале цикла for следующим образом:
for () {
clilen = sizeof(cliaddr)
if ( (connfd = acceptflistenfd. (SA *) &cliaddr &clilen)) < 0) { if (errno == EINTR)
continue, /* назад в forО */ else
erjsys("accept error")
} -
Обратите внимание, что мы вызываем функцию accept, а не функцию-обертку Accept, поскольку мы должны обработать неудачное выполнение функции самостоятельно.
В этой части кода мы сами перезапускаем прерванный системный вызов. Это допустимо для функции accept и таких функций, как read, write, seiect и open. Но есть функция, которую мы не можем перезапустить самостоятельно, — это функция connect. Если она возвращает ошибку EINTR, мы не можем снова вызвать ее, поскольку в этом случае немедленно возвратится еще одна ошибка. Когда функция connect прерывается перехваченным сигналом и не перезапускается автоматически, нужно вызвать функцию select, чтобы дождаться завершения соединения (см. раздел 15.3).
Функции wait и waitpid
В листинге 5.7 мы вызываем функцию wait для обработки завершенного дочернего процесса.
#include <sys/wait h>
pid_t wait(int *s£d£7oc),
pid_t waitpid(pid_t p7d int *stdtloc int options)
Обе функции возвращают ID процесса в случае успешного выполнения -1 в случае ошибки
Обе функции, и wait, и waitpid, возвращают два значения. Возвращаемое значение каждой из этих функций — это идентификатор завершенного дочернего процесса, а через указатель statl ос передается статус завершения дочернего про-
цесса (целое число). Для проверки статуса завершения можно вызвать три макроса, которые сообщают нам, что произошло с дочерним процессом: дочерний процесс завершен нормально, уничтожен сигналом пли только приостановлен программой управления заданиями (job-control). Дополнительные макросы позволяют получить состояние выхода дочернего процесса, а также значение сигнала, уничтожившего или остановившего процесс. В листинге 14.8 мы используем макроопределения WIFEXITED и WEXITSTATUS.
Если у процесса, вызывающего функцию wa: t, нет завершенных дочерних процессов, но есть один или более выполняющихся, функция wait блокируется до тех пор, пока первый из дочерних процессов не завершится.
Функция waitpid предоставляет более гибкие возможности выбора ожидаемого процесса и его блокирования. Прежде всего, в аргументе pid задается идентификатор процесса, который мы будем ожидать. Значение -1 говорит о том, что нужно дождаться завершения первого дочернего процесса. (Существуют и другие значения идентификаторов процесса, но здесь они нам не понадобятся.) Аргумент options позволяет задавать дополнительные параметры. Наиболее об- щеупот ребительным является параметр WNOHANG: он сообщает ядру, что не нужно выполнять блокирование, если нег завершенных дочерних процессов.
Различия между функциями wait и waitpid
Теперь мы проиллюстрируем разницу между функциями wait и waitpid, используемыми для сброса завершенных дочерних процессов. Для этого мы изменим код нашего клиента TCP так, как показано в листинге 5.7. Клиент устанавливает пять соединений с сервером, а затем использует первое из них (sockfd[0]) в вызове функции str_cli. Несколько соединений мы устанавливаем для того, чтобы породить от параллельного сервера множество дочерних процессов, как показано на рис. 5.2.
Клиент
Рис. 5.2. Клиент установивший пять соединений с одним и тем же параллельны^» сервером
Листинг 5.7. Клиент TCR устанавливающий пять соединений с сервером //tcpcl 1 serv/tcpcl ? 04 с
#mclude "unp h”
int
main(int argc. char **argv)
{?》
прооолжение 之
Листинг 5.7 (продолжение)
int 1. sockfd[5].
struct sockaddrjn servaddr,
if (argc !* 2)
err_quit(Musage. tcpcli <IPdddress>").
for (i - 0, i < 5. i++) {
sockfd[i] - Socket(AF_INET. SOCK_STREAM. 0);
DzeroC&servaddr, sizeof(servaddr)).
servaddr sin_family - AF_INET.
servaddr sin[port - htons(SERV_P0RT).
14Inet_pton(AFJNET. argv[l]. &servaddr sin_addr),
15Connect(sockfd[i], (SA *) &servaddr. sizeof(servaddr))
16}
17 str^cli(stdin. sockfd[0]). /* эта функция выполняет все необходимые действия для формирования запроса клиента*/
18 exit(O),
19 }
Когда клиент завершает работу, все открытые дескрипторы автоматически закрываются ядром (мы не вызываем функцию cl ose, а пользуемся только функцией exit) и все пять соединений завершаются приблизительно в одно и то же время. Это вызывает отправку пяти сегментов FIN, по одному на каждое соединение, что, в свою очередь, вызывает примерно одновременное завершение всех пяти дочерних процессов. Это приводит к доставке пяти сигналов SIGCHLD практически в один и тот же момент, что показано на рис. 5.3.
Рис. 5.3. Клиент завершает работу, закрывая все пять соединений и завершая все пять дочерних процессов
Доставка множества экземпляров одного и того же сигнала вызывает проблему, к рассмотрению которой мы и приступим.
Сначала мы запускаем сервер в фоновом режиме, а затем — новый клиент. Наш сервер, показанный в листинге 5.1, несколько модифицирован — теперь в нем вызывается функция signal для установки обработчика сигнала SIGCHLD, приведенного в листинге 5.6.
bsdi Z tcpserv03 &
[1]21282
bsdi % tcpc!104 206.62.226.35
hello мм набираем эту строку hello и она отражается сервером 人Dмы набираем символ конца файла
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 па узле сервера обработчик сигналов может выполниться три или даже четыре раза.
Корректным решением будег вызвать функцию waitpid вместо wait. В листинге 5.8 представлена версия нашей функции sig_chld, корректно обрабатывающая сигнал SIGCHLD. Эта версия работает, потому что мы вызываем функцию wsitpid в цикле, получая состояние любого из дочерних процессов, которые завершились» Необходимо задать параметр WNOHANG: это указывает функции waitpid, что не нужно блокироваться, если существуют выполняемые дочерние процессы, которые еще не завершились. В листинге 5.6 мы не могли вызвать функцию wait в цикле, поскольку нет возможности предотвратить блокирование функции wait при наличии выполняемых дочерних процессов, которые еще не завершились.
В листинге 5.9 показана окончательная версия нашего сервера. Он корректно обрабатывает возвращение ошибки EINTR из функции accept и устанавливает обработчик сигнала (листинг 5.8), который вызывает функцию waitpid для всех завершенных дочерних процессов.
Листинг 5.8. Окончательная (корректная) версия функции sig_chld, вызывающая
функцию waitpid
//tcpcl1serv/sigchldwaitpid.c 1 include "unp h“
2
3
4
5
6
void
sig chld(int sign。)
{'
pid_t p】d.
int stat.
7
8
9
10
while ( (pid = waitpid(-l. &stat. WNOHANG)) > 0) printfC'child %d terminated'erT. pid)
return.
}
Листинг 5.9. Окончательная (корректная) версия TCP-сервера, обрабатывающего
ошибку EINTR функции accept
//tcpcl1serv/tcpserv04 с 1 include "unp h"
2
3
4
5
6
7
8
9
mt
mam(mt argc. char **argv)
{
int listenfd. connfd.
pid_tchi 1dpid.
socklen^t clilen,丫
struct sockaddr_in cliaddr, servaddr:
void sig_chld(int).
10
11stenfd = Socket(AF_fNET. SOCK_STREAM. 0):
11
12
13
14
bzero(&servaddr. sizeof(servaddr)).
servaddr sm_family = AFJNET.
servaddr sm_addr s_addr = htonl (INADDR_ANY).
servaddr sw_port = htons(SERV_P0RT).
15
Bi nd(1】stenfd. (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 (..) {
cl ilen = sizeof(clidddr).
if ( (connfd = accept(listenfd. (SA *) &c1idddr. &clilen)) < 0) { if (errno == EINTR)
continue./* назад к for() */
else
err^sys("accept error"),
} 一
if ( (chi 1 dpid = ForkO) == 0) {/* дочерний процесс */
Close(listenfd),/* закрываем прослушмваемь曲 сокет */
str echo(connfd)./* обрабатываем запрос */
exit(0).
30}
31Close(connfd):/* родитель закрывает прмсоедмненнь麻 сокет */
32}
33 }
Целью этого раздела было продемонстрировать три сценария, которые могут встретиться в сетевом программировании,
При выполнении функции fork, порождающей дочерние процессы, следует перехватить сигнал SIGCHLD.
При перехватывании сигналов мы должны обрабатывать прерванные системные вызовы.
Обработчик сигналов SIGCHLD должен быть создан корректно с использованием функции waitpid, чтобы не допустить появления зомби.
Окончательная версия нашего сервера TCP (см. листинг 5.9) вместе с обработчиком сигналов SIGCHLD в листинге 5.8 обрабатывает все три сценария.
Прерывание соединения перед завершением функции accept
Существует другое условие, аналогичное прерванному системному вызову, пример которого был описан в предыдущем разделе. Оно может привести к возвращению функцией accept нефатальной ошибки, в случае чего следует заново вызвать функцию accept. Последовательность пакетов, показанная на рис. 5.4, встречается на загруженных серверах (эта последовательность типична для загруженных web-серверов).
Рис. 5.4. Получение сегмента RST для состояния соединения ESTABLISHED перед вызовом функции accept
Трехэтапное рукопожатие TCP завершается, устанавливается соединение, а затем TCP клиента посылает сегмент RST. На стороне сервера соединение ставится в очередь в ожидании вызова функции accept, который последует при получении сегмента RST процессом сервера. Спустя некоторое время процесс сервера вызывает функцию accept.
ПРИМЕЧАНИЕ
Этот сценарий очень просто имитировагь. Запустите сервер, который должен вызвать функции socket, bind и listen, а затем перед вызовом функции accept переведите сервер па короткое время в состояние ожидания Пока процесс сервера находится в состоянии ожидания, запустите клиент, который вызовет функции socket и connect. Как только функция connect завершится, установите параметр сокета SO LINGER, чтобы сгенерировать сегмент RST (который мы описываем в разделе 7.5 и демонстрируем в листинге 15.14), и завершите процессы.
К сожалению, принцип обработки прерванного соединения зависит от реализации. Реализации, происходящие от Беркли, обрабатывают прерванное соединение полностью внутри ядра прозрачно для процесса сервера. Большинство реализаций SVR4, однако, возвращают процессу ошибку, и эта ошибка зависит от реализации. При этом переменная errno принимает значение EPROPTO (ошибка протокола), между тем как в Posix.lg указано, что должна возвращаться ошибка ECONNABORTED (прерывание соединения). Posix.lg иначе определяет эту ошибку, так как ошибка EPROTO возвращается еще и в том случае, когда в подсистеме потоков происходят какие-либо фатальные события, имеющие отношение к протоколу. Возвращение той же ошибки для нефатального прерывания установленного соединения клиентом приводит к тому, что сервер пе знает, вызывать снова функцию accept или нет. В случае ошибки ECONNABORTED сервер может игнорировать ошибку и снова вызывать функцию accept.
ПРИМЕЧАНИЕ
В Solaris 2.6 реализованы требования Posix.lg.
В [105] описана обработка этой ошибки в Беркли-ядрах (Berkeiey-denvcd kernels), которые никогда не передают ее процессу Обработка RST с вызовом функции tcp close представлена на с. 964 [105], Эта функция вызывает функцию injcbdetach со с. 897 [105], которая, в свою очередь, вызывает функцию sofree со с. 719 [105] Функция sofree [105, с. 473] обнаруживает, что сокет все еще находится в очереди полностью установленных соединений прослушиваемого сокета Она удаляет этот сокет из очереди и освобождает сокет. Когда сервер, наконец, вызовет функцию accept, он не сможет узнать, что установленное соединение было удалено из очереди
Мы вернемся к подобным прерванным соединениям в разделе 15.6 и покажем, какие проблемы они могут порождать совместно с функцией select и прослушиваемым сокетом в нормальном режиме блокирования.
Завершение процесса сервера
Теперь мы запустим соединение клиент-сервер и уничтожим дочерний процесс сервера. Это симулирует сбой (crash) процесса сервера, благодаря чему мы сможем выяснить, что происходит с клиентом в подобных ситуациях. (Следует точно различать сбой процесса сервера, который мы рассмотрим здесь, и сбой на самом узле сервера, о котором речь пойдет в разделе 5.14.) События развиваются так: 1. Мы запускаем сервер и клиент на разных узлах и вводим на стороне клиента одну строку, чтобы проверить, все ли в порядке. Строка отражается дочерним процессом сервера.
Мы находим идентификатор дочернего процесса сервера и уничтожаем его с помощью программы kill. Одним из этапов завершения процесса является закрытие всех открытых дескрипторов в дочернем процессе. Это вызывает отправку сегмента FIN клиенту, и TCP клиента отвечает сегментом АСК. Это первая половина завершения соединения TCP.
Родительскому процессу сервера посылается сигнал SIGCHLD, и он корректно обрабатывается (см. листинг 5.9).
С клиентом ничего не происходит. TCP клиента получает от TCP сервера сегмент FIN и отвечает сегментом АСК, но проблема состоит в том, что клиентский процесс блокирован в вызове функции fgets в ожидании строки от терминала.
Запуск программы netstat на этом шаге из другого окна на стороне клиента показывает состояние клиентского сокета:
solans I netstat | grep 9877
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
solans 34673 bsdi 98778760 08760 0 CLOSE^WAIT
(Здесь мы впервые показываем вывод программы netstat в Solaris, поэтому мы добавили строку-заголовок. Формат несколько отличается от формата вывода BSD, но информация та же.) Мы также запускаем netstat из другого окна на стороне сервера:
bsdi % netstat | grep 9877
tcp 0 0 bsdi 9877 solans 34673 FIN_WAIT_2
Как видите, согласно рис. 2.4, осуществилась половина последовательности завершения соединения TCP.
Мы можем снова ввести строку на стороне клиента. Вот что происходит на стороне клиента (начиная с шага 1):.
solans % tcpcbOl 206.62.226.35 запускаем клиент
hello первая строка, которую мы ввели
hello она корректно отражается
теперь мы уничтожаем(к111) дочерний процесс
сервера на узле сервера
another line затем мн вводим следующую строку на сторона
клиента
str_c】i server tenmnated prematurely
Когда мы вводим следующую строку, функция str_cli вызывает функцию wri ten, иТСР клиента отправляет данные серверу. TCP это допускает, поскольку получение сегмента FIN протоколом TCP клиента только указывает, что процесс сервера закрыл свой конец соединения и больше не будет отправлять данные. Получение сегмента FIN не сообщает протоколу TCP клиента, что процесс сервера завершился (хотя в данном случае он завершился). Мы вернемся к этому вопросу в разделе 6.6, когда будем говорить о половинном закрытии TCP.
Когда TCP сервера получает данные от клиента, он отвечает, посылая сегмент RST, поскольку процесс, у которого был открытый сокет, завершился. Мы мо~ жем проверить, что этот сегмент RST отправлен, просмотрев пакеты с помощью поогпаммы tCDdumD.
Однако процесс клиента не увидит сегмента RST, поскольку он вызывает функцию readl 1 ne сразу же после вызова функции writen, и readline сразу же возвращает 0 (признак конца файла) по причине того, что на шаге 2 был получен сегмент FIN. Наш клиент не предполагает получать признак конца файла на этом этапе (см. листинг 5.3), поэтому он завершает работу, сообщая об ошибке Server terminated prematurely (Сервер завершил работу преждевременно).
Когда клиент завершает работу (вызывая функцию err_quit в листинге 5.4), все его открытые дескрипторы закрываются.
ПРИМЕЧАНИЕ
Шаги описанной последовательности действий также зависят от синхронизации времени. Когда мы запускаем сервер и клиент па разных узлах, отправка данных от клиента к серверу (следующая строка в примере) и получение клиентом RST сервера занимают несколько миллисекунд. Поэтому клиентский вызов функции readline возвращает нуль, так как сегмент FIN, полученный раньше, уже готов к тому, чтобы быть считанным. Но если мы запустим клиент и сервер па одном узле или если нам нужно сделать незнач ительную паузу перед вызовом функции readline на стороне клиента, то полученный от сервера сегмент RST имеет приоритет по отношению к сегменту FIN, полученному раньше. В результате этого функция readline возвратит ошибку, а значение переменной еггпо будет равно ECONNRESET (Connection reset by the peer — Соединение сброшено собеседником).
Проблема заключается в том, что клиент блокируется в вызове функции fgets, когда сегмент FIN приходит на сокет. Клиент в действительности работает с двумя дескрипторами — дескриптором сокета и дескриптором ввода пользователя, и поэтому он должен блокироваться при вводе из любого источника (сейчас в функции str_cl 1 он блокируется при вводе только из одного источника). Обеспечить подобное блокирование — это одно из назначений функций sei ect и pol 1, о которых рассказывается в главе 6. Когда в разделе 6.4 мы перепишем функцию str_cli, то как только мы уничтожим с помощью программы kill дочерний процесс сервера, клиенту будет отправлено уведомление о полученном сегменте FIN.
Сигнал SIGPIPE
Что происходит, если клиент игнорирует возвращение ошибки из функции readl i ne и отсылает следующие данные серверу? Это может произойти, если, например, клиенту нужно выполнить две операции по отправке данных серверу перед считыванием данных от сервера, причем первая операция отправки данных вызывает RST.
Применяется следующее правило: когда процесс производит запись в сокет, получивший сегмент RST, процессу посылается сигнал SIGPIPE. По умолчанию действием этого сигнала является завершение процесса, так что процесс должен перехватить сигнал, чтобы не произошло непроизвольного завершения.
Если процесс либо перехватывает сигнал и возвращается из обработчика сигнала, либо игнорирует сигнал, то операция записи возвращает ошибку EPIPE.
Чтобы увидеть, что происходит с сигналом SIGPIPE, изменим код нашего кли
ента так, как показано в листинге 5.10.
ПРИМЕЧАНИЕ
Часто задаваемым вопросом (FAQ) в Usenet является такой: как получить этот сигнал при первой, а не при второй операции записи? Это невозможно. Как следует из приведенных выше рассуждений, первая операция записи выявляет cci мепт RST, а вторая — сигнал. Если запись в сокет, получивший сегмент FIN, допускается, то запись в сокет, получивший сегмент RST, является ошибочной.
Листинг 5.10. Функция str_cli, дважды вызывающая функцию writen
//tcpcliserv/str_clill с
#inc】ude "unp h"
void
str c】i(FILE *fp mt sockfd)
{-
char sendlme[MAXLINE], recvlme[MAXLINE].
while (Fgets(sendlme MAXLINE fp) ,= NULL) {
Writen(sockfd,sendlme. 1),
8sle即⑴.
9Writen(sockfd,sendlme + 1,strlen(sendline)~ 1).
10if (Readline(sockfd recvlme.MAXLINE)==0)
11 err_quit( 'str_cli server terminated prematurely*').
12Fputs(recvlmestdout)
13}
14 }
7-9 Все изменения, которые мы внесли, — это повторный вызов функции writen: сначала в сокет записывается первый байт данных, за этим следует пауза в 1 секунду и далее идет запись остатка строки. Наша цель — выявить сегмент RST при первом вызове функции writen и генерировать сигнал SIGPIPE при втором вызове.
Если мы запустим клиент на нашем узле BSD/OS, мы получим:
bsdi % tcpclill 206.62.226.34
hi there мы вводим эту строку
hi there и она отражается сервером
здесь мы завершаем дочерний процесс сервера
byeзатем мы вводим зту строку
bsdi 當 echo $? что возвращает KornShelP
269 269 = 256 + 13
bsdi 戈 CBgrep SIGPIPE /usr/include/sys/sTgnal.h
#define SIGPIPE 13/* write on a pipe with no one to read it */
Мы запускаем клиент, вводим одну строку, видим, что строка отражена корректно, и затем завершаем дочерний процесс сервера на узле сервера. Затем мы вводим другую строку (bye), но ничего не отражается, и мы получаем только при- глашенне командной строки. Поскольку действием сигнала SIGPIPE по умолчанию является завершение процесса без записи дампа, программа КогпShell ничего не выводит. Это проблема, bciречающаяся в программах, завершаемых с помощью сигнала SIGPIPE: даже интерпретатор командной строки ничего не выводит, так что нет возможности узнать, что произошло.
Чтобы вывести значение, возвращаемое интерпретатором команд, нам придется выполнить команду 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Л6).
Мы вводим строку на стороне клиента, она записывается с помощью функции writer) (см. листинг 5.3) и отправляется протоколом TCP клиента как сегмент данных. Затем клиент блокируется в вызове функции read'llпе в ожидании отраженного ответа.
Если мы понаблюдаем за сетью с помощью программы tepdump, то увидим, что TCP клиента последовательно осуществляет повторные передачи сегмента
данных, пытаясь получить сегмент АСК от сервера. В разделе 25.11 [105] показан типичный образец повторных передач TCP: реализации, происходящие от Беркли, делают попытки передачи сегмента данных 12 раз, ожидая около 9 минут перед прекращением попыток, Коцца TCP клиента наконец прекращает попытки ретрансляции (считая, что узел сервера за это время не перезагружался или что он все еще недоступен, если на узле сервера сбоя не было, но он был недоступен по сети), клиентскому процессу возвращается ошибка. Поскольку клиент блокирован в вызове функции read" ne, он возвращает ошибку. Если на узле сервера произошел сбой, и на все сегменты данных клиента не было ответа, будет возвращена ошибка ETIMEDOUT. Но если некий промежуточный маршрутизатор определил, что узел сервера был недоступен, и ответил сообщением ICMP о недоступности получателя, клиент получит либо ошибку EHOSTUNREACH, либо ошибку ENETUNREACH.
Хотя наш клиент в конце концов обнаруживает, что собеседник выключен или недоступен, бывает, что нужно определить это прежде чем пройдут условленные девять минут. Чтобы предусмотреть эту ситуацию, нужно поместить тайм-аут в вызов функции readline, о чем рассказывается в разделе 13.2.
В описанном сценарии сбой на узле сервера можно обнаружить только послав данные на этот узел. Если мы хотим обнаружить сбой на узле сервера, не посылая активно данные, требуется другая технология. Мы рассмотрим параметр сокета SO_KEEPALl VE в разделе 7.5 и некоторые функции проверки пульса (heartbeatfunctions) клиент-серверного соединения в разделе 21.5.
Сбой и перезагрузка на узле сервера
В этом сценарии мы устанавливаем соединение между клиентом и сервером и затем считаем, что на узле сервера происходит сбой, после чего узел перезагружается. В предыдущем разделе узел сервера был выключен, когда мы отправляли ему данные. Здесь же перед отправкой данных серверу узел сервера перезагрузится. Простейший способ имитировать такую ситуацию — установить соединение, отсоединить сервер от сети, выключить узел сервера и перезагрузить его, а затем снова присоединить узел сервера к сети.
Как было сказано в предыдущем разделе, если клиент не посылает активно данные серверу, то он не узнает о произошедшем на узле сервера сбое. (При этом считается, что мы не используем параметр сокета SO_KEEPALIVE.)
Мы запускаем сервер, затем — клиент и вводим строку для проверки установленного соединения.
Узел сервера выходит из строя и перезагружается.
Мы вводим строку на стороне клиента, которая посылается как сегмент данных TCP на узел сервера.
Когда узел сервера перезагружается после сбоя, его TCP теряет информацию о существовавших до сбоя соединениях. Следовательно, TCP сервера отвечает на полученный от клиента сегмент данных, посылая RST.
Наш клиент блокирован в вызове функции readline, когда приходит сегмент RST, заставляющий функцию readline возвратить ошибку ECONNRESET.
Если для нашего клиента важно диагностировать выход из строя узла сервера даже когда клиент активно не посылает данные, то требуется другая технология (с использованием параметра сокета SO_KEEPALIVE или некоторых функций, проверяющих наличие связи в клиент-серверном соединении).
Выключение узла сервера
В двух предыдущих разделах рассматривался выход из строя узла сервера или недоступность узла сервера в сети. Теперь мы рассмотрим, что происходит, если узел сервера выключается оператором в то время, когда на этом узле выполняется наш серверный процесс.
Когда система Unix выключается, процесс i m t обычно посылает всем процессам сигнал SIGTERM (мы можем перехватить этот сигнал), ждет в течение некоторого фиксированного времени (часто от 5 до 20 секунд), а затем посылает сигнал SIGKILL (который мы перехватить не можем) всем еще выполняемым процессам. Это дает всем выполняемым процессам короткое время для завершения работы. Если мы не перехватили сигнал SIGTERM и завершили выполнение процессов, работу нашего сервера завершит сигнал SIGKILL. При завершении процесса закрываются все открытые дескрипторы, а затем мы проходим ту же последовательность шагов, что описывалась в разделе 5.12. Как отмечалось в разделе 5.12, в нашем клиенте следует использовать функцию seiect или poll, чтобы клиент определил завершение процесса сервера, как только оно произойдет.
Итоговый пример TCP
Прежде чем клиент и сервер TCP смогут взаимодействовать друг с другом, каждый из них должен определить пару сокетов для соединения: локальный IP-адрес, локальный порт, удаленный IP-адрес, удаленный порт. На рис. 5.5 мы схематически изображаем эти значения черными кружками. На этом рисунке ситуация представлена с точки зрения клиента. Удаленный IP-адрес и удаленный порт должны быть заданы клиентом при вызове функции connect. Два локальных значения обычно выбираются ядром тоже при вызове функции connect. У клиента есть выбор: он может задать только одно из локальных значений или оба, вызвав функцию bi nd перед вызовом функции connect, однако второй подход нетипичен.
Как мы отмечали в разделе 4.10, клиент может получить два локальных значения, выбранных ядром, вызвав функцию getsockname после установления соединения.
На рис. 5.6 показаны те же четыре значения, но с точки зрения сервера.
Локальный порт (заранее известный порт сервера) задается функцией bind. Обычно сервер также задает в этом вызове универсальный IP-адрес, хотя может и ограничиться получением соединений, предназначенных для одного определенного локального интерфейса, путем связывания с IP-адресом, записанным без символов подстановки (то есть не универсальным). Если сервер связывается с универсальным IP-адресом на узле с несколькими сетевыми интерфейсами, он может определить локальный IP-адрес (указываемый как адрес отправителя
socket () accept ()
Рис. 5.5. TCP-соединение клиент-сервер с точки зрения клиента
Рис. 5.6. TCP-соединение клиент-сервер с точки зрения сервера
в исходящих пакетах) при помощи вызова функции getsockname после установления соединения (см. раздел 4.10). Два значения удаленного адреса возвращаются серверу при вызове функции accept. Как мы отмечали в разделе 4,10, если сервером, вызывающим функцию accept, выполняется с помощью функции ехес другая программа, то эта программа может вызвать функцию getpeername, чтобы при необходимости определить IP-адрес и порт клиента.
Формат данных
В нашем примере сервер никогда не исследует запрос, который он получает от клиента. Сервер лишь читает все данные, включая символ перевода строки, и отправляет их обратно клиенту, отслеживая только разделитель строк. Это исключение, а не правило, так как обычно необходимо принимать во внимание формат данных, которыми обмениваются клиент и сервер.
Пример: передача текстовых строк между клиентом и сервером
Изменим наш сервер так, чтобы он, по-прежнему принимая текстовую строку от клиента, предполагал, что строка содержит два целых числа, разделенных пробелом, и возвращал сумму этих чисел. Функции main наших клиента и сервера остаются прежними, как и функция str_cli. Меняется только функция str_ech。, что мы показываем в листинге 5.11.
Листинг 5.11. Функция str_echo, суммирующая два числа
//tcpcl1serv/str_echo08 с
include *unp h"
void
str echo(int sockfd)
{-
longarglarg2.
ssize_tn.
char "line[MAXLINE]
for ( .){
if ( (n = Readline(sockfd. line. MAXLINE)) == 0)
return,/* соединение закрывается удаленным концом */
if (sscanfClme.•,^ld^ld•,. &argl &arg2) == 2)
snprmtfCline. sizeof(line) '^1 d\en* argl + arg2)
else
snpnntfdine, sizeof(lme) *'input error\en"),
n = strlen(lme)
Writen(sockfd. line.n).
}
}
1-14 Мы вызываем функцию sscanf, чтобы преобразовать два аргумента из текстовых строк в целые числа типа 1 ong, а затем функцию snpri ntf для преобразования ре <\\чьтата в текстовую строку.
Jth клиент и сервер работают корректно вне зависимости от порядка байтов на их узлах.
Пример: передача двоичных структур между клиентом и сервером
Теперь мы изменим код клиента и сервера, чтобы передавать через сокет не текстовые стпоки. а двоичные значения. Мы увидим, что клиент и ceDBeD Da отают
некорректно, когда они запущены на узлах с различным порядком байтов или на узлах, не согласовывающих размер целых чисел типа long (см, табл. 1.5).
Функции main наших клиента и сервера не изменяются. Мы определяем одну структуру для двух аргументов, другую структуру для результата и помещаем оба определения в наш заголовочный файл unp.h, представленный в листинге 5.12. В листинге 5.13 показана функция str_cl i.
Листинг 5.12. Заголовочный файл unp.h //tcpcliserv/sum h
struct args {
long argl.
long arg2.
}.
struct result {
long sum.
}.
Листинг 5.13. Функция str_cli, отправляющая два двоичных целых числа серверу
//tcpcl1serv/str_cl109 с
#include“unp h”
#incl ude"sum h"
void
str_cli(FILE *fp. int sockfd)
{ ~
charsendlme[MAXLINE],
structargs args.
structresult result.
while (Fgets(sendlme. MAXLINE, fp) " NULL) {
if (sscanf(sendline "灯d灯d", &args argl. &args arg2) i= 2) {
pnntff invalid input 犯".sendline).
continue
}
Writen(sockfd, &args. sizeof(args)).
if (Readn(sockfd. &result sizeofCresult)) == 0)
err_quit(Mstr_cli server terminated prematurely"):
printf(,,^ld\en,,> result sum).
}
}
10-14 Функция sscanf преобразует два аргумента из текстовых строк в двоичные, и мы вызываем функцию wnten для отправки структуры серверу.
15-17 Мы вызываем функцию readn для чтения ответа и выводим результат с помощью функции printf.
В листинге 5.14 показана наша функция str_echo.
Листинг 5,14. Функция str_echo, складывающая два двоичных целых числа
//tcpcliserv/str_echo09 с
#incl ude%np h"
#indude"sum h"
Листинг 5.14 {продолжение)
void
str echo(1 nt sockfd)
{一
ssize_t n
struct args args.
struct result result;
for ( ) {
if ( (n = Readn(sockfd. &args sizeof(args))) — 0)
return/* соединение закрыто удаленным концом */
result sum = args argl + args arg2
Wnten(sockfd &result sizeof(result))
}
}
14 Мы считываем аргументы при помощи вызова функции readn, вычисляем и запоминаем сумму и вызываем функцию wnten для отправки результирующей структуры обратно.
Если мы запустим клиент и сервер на двух машинах с аналогичной архитектурой, —допустим, solans и sunos5 (см. рис. 1 7), — все будет работать нормально. Вот взаимодействие с клиентом:
sunos5 Z tcpc?i09 206.62.226.33
11 22 мы вводим эти числа
33 а это ответ сервера
-11 -44
-55
Но если клиент и сервер работают на машинах с разными архитектурами — например, сервер в системе Sparc solans, в которой используется обратный порядок байтов (big-endian), а клиент в системе Intel bsdi с прямым порядком байтов (little-endian), — результат будет неверным:
bsdi % tcpcli09 206.62.226.33 1 2мы вводим эти числа
3и сервер дает правильный ответ
-22 -77 потом мы вводим эти числа
-16777314 и сервер дает неверный ответ
Проблема заключается в том, что два двоичных числа передаются клиентом через сокет в формате с прямым порядком байтов, а сервер интерпретирует их как целые числа, записанные с обратным порядком байтов. Мы видим, что это допустимо для положительных целых чисел, но для отрицательных такой подход не срабатывает (см. рис. 5.1). Действительно, в подобной ситуации могут возникнуть три проблемы:
Различные реализации хранят двоичные числа в различных форматах. Наиболее характерный пример — прямой и обратный порядок байтов, описанный в разделе 3.4.
Различные реализации могут хранить один и тот же тип данных языка С по- разному. Например, большинство 32-разрядных систем Unix используют 32 бита для типа 1 ong, но 64-разрядные системы обычно используют 64 бита для того же типа данных (см табл. 1.5). Нет никакой гарантии, что типы short, а 1 nt или 1 ong имеют какой-либо определенный размер.
Различные реализации по-разному упаковывают структуры в зависимости от числа битов, используемых для различных типов данных, и ограничений по выравниванию для данного компьютера. Следовательно, неразумно передавать через сокет двоичные структуры.
Есть два общих решения проблемы, связанной с различными форматами данных:
Передавайте все численные данные как текстовые строки. Это то, что мы делали в листинге 5.11. При этом предполагается, что у обоих узлов один и тот же набор символов,
Явно определяйте двоичные форматы поддерживаемых типов данных (число битов и порядок байтов) и передавайте все данные между клиентом и сервером в этом формате. Пакеты удаленного вызова процедур (Remote Procedure Call, RPC) обычно используют именно эту технологию. В RFC 1832 [92] описывается стандарт представления внешних данных (External Data Representation, XDR), используемый с пакетом Sun RPC.
19. Резюме
Первая версия наших эхо-клиента и эхо-сервера содержала около 150 строк (включая функции readl i ne и writen), но многие ее детали пришлось модифицировать. Первой проблемой, с которой мы столкнулись, было превращение дочерних процессов в зомби, и для обработки этой ситуации мы перехватывали сигнал SIGCHLD. Затем наш обработчик сигнала вызывал функцию waitpid, и мы показали, что должны вызывать именно эту функцию вместо более старой функции wait, поскольку сигналы Unix не помещаются в очередь. В результате мы рассмотрели некоторые подробности обработки сигналов Posix, а за дополнительной информацией по этой теме вы можете обратиться к главе 10 [93]
Следующая проблема, с которой мы столкнулись, состояла в том, что клиент не получал уведомления о завершении процесса сервера Мы видели, что TCP нашего клиента получал уведомление, но оно не доходило до клиентского процесса, поскольку он был блокирован в ожидании ввода пользователя. В главе 6 для обработки этого сценария мы будем использовать функцию sei ect или pol 1, чтобы ожидать готовности любого из множества дескрипторов вместо блокирования при обращении к одному дескриптору.
Мы также обнаружили, что если узел сервера выходит из строя, мы не можем определить это до тех пор, пока клиент не пошлет серверу какие-либо данные. Некоторые приложения должны узнавать об этом факте раньше, о чем мы поговорим далее, когда будем рассматривать в разделе 7.5 параметр сокета SO_KEEPALIVE, а в разделе 21.5 мы создадим набор функций проверки пульса (heartbeat functions) для клиент-серверного соединения.
В нашем простом примере происходил обмен текстовыми строками, и поскольку от сервера не требовалось просматривать отражаемые им строки, все работало нормально. Отправка численных данных между клиентом и сервером может привести к ряду новых проблем, что и было продемонстрировано.
Упражнения
Создайте сервер TCP на основе листингов 5.1 и 5.2 и клиент TCP на основе листингов 5.3 и 5.4. Запустите сервер, затем запустите клиент. Введите несколько строк, чтобы проверить, что клиент и сервер работают. Завершите работу клиента, введя символ конца файла, и заметьте время. Используйте программу netstat на узле клиента для проверки того, что клиентский конец соединения проходит состояние TIME_WAIT. Запускайте netstat примерно каждые 5 секунд, чтобы посмотреть, когда закончится состояние TIME_WAIT. Каково время MSL для этой реализации?
Что происходит с нашим соединением клиент-сервер, если мы запускаем клиент и подключаем к стандартному потоку ввода двоичный файл?
В чем разница между нашим соединением клиент-сервер и использованием клиента Telnet для взаимодействия с нашим эхо-сервером?
В нашем примере в разделе 5.12 мы проверили, что первые два сегмента завершения соединения отправляются (сегмент FIN от сервера, на который затем клиент отвечает сегментом АСК) при просмотре состояний сокета с помощью программы netstat. Происходит ли обмен двумя последними сегментами (FIN от клиента, на который затем сервер отвечает сегментом АСК)? Если да, то когда? Если нет, то почему?
Что произойдет с примером, рассмотренным в разделе 5.14, если между шагами 2 и 3 мы перезапустим сервер на узле сервера?
Чтобы проверить, что происходит с сигналом SIGPIPE в разделе 5.13, измените листинг 5.3 следующим образом. Напишите обработчик сигнала для SIGPIPE, который будет просто выводить сообщение и возвращать управление. Установите этот обработчик сигнала перед вызовом функции connect. Измените номер порта сервера на 13 (порт сервера времени и даты). Когда соединение установится, с помощью функции si еер войдите в состояние ожидания на 2 секунды, с помощью функции write запишите несколько байтов в сокет, проведите в состоянии ожидания (sleep) еще 2 секунды и с помощью функции wri te запишите еще несколько байтов. Запустите программу. Что происходит?
Что произойдет на рис. 5.5, если IP-адрес узла сервера, заданный клиентом при вызове функции connect, является IP-адресом, связанным с крайним правым канальным уровнем на стороне сервера, а не IP-адресом, связанным с крайним левым канальным уровнем?
В нашем примере эхо-сервера, осуществляющего сложение двух целых чисел (см. листинг 5.14), когда клиент и сервер принадлежат системам с различным порядком байтов, для небольших положительных чисел получается правильный ответ, но для небольших отрицательных чисел ответ неверен. Почему? (Подсказка: нарисуйте схему обмена значениями через сокет, аналогичную рис. 3.4.)
В нашем примере в листингах 5.13 и 5.14 можем ли мы решить проблему, связанную с различным порядком байтов на стороне клиента и на стороне сервера, если клиент преобразует два аргумента в сетевой порядок байтов, исполь-
зуя функцию htonl, а сервер затем вызывает функцию ntohl для каждого аргумента перед сложением и выполняет аналогичное преобразование результата?
Что произойдет в листингах 5.13 и 5.14, если в качестве узла клиента используется компьютер Sparc, где данные типа long занимают 32 бита, а в качестве узла сервера — Digital Alpha, где данные типа long занимают 64 бита? Изменится ли что-либо, если клиент и сервер поменяются местами?
И. На рис. 5.5 указано, что IP-адрес клиента выбирается IP на основе маршрутизации. Что это значит?
ГЛАВА 6
Мультиплексирование ввода- вывода: функции select и poll
Введение
В разделе 5.12 мы видели, что наш TCP-клиент обрабатывает два входных потока одновременно: стандартный поток ввода и сокет TCP. Проблема, с которой мы столкнулись, состояла в том, что пока клиент был блокирован в вызове функции fgets (чтение из стандартного потока ввода), процесс сервера уничтожался. TCP сервера корректно отправляет сегмент FIN протоколу TCP клиента, но поскольку процесс клиента блокирован при чтении из стандартного потока ввода, он не получит признак конца файла, пока не считает данные из сокета (возможно, значительно позже). Нам нужна возможность сообщить ядру, что мы хотим получить уведомления о том, что выполняется одно или несколько условий для ввода-вывода (например, присутствуют данные для считывания или дескриптор готов к записи новых данных). Эта возможность называется мулыпиплексированием (multiplexing) ввода-вывода и обеспечивается функциями seiect и poll. Мы рассмотрим также более новый вариант функции sei ect, входящей в стандарт Posixdg, называемый pselect.
Мультиплексирование ввода-вывода обычно используется сетевыми приложениями в следующих случаях:
Когда клиент обрабатывает множество дескрипторов (обычно интерактивный ввод и сетевой сокет), должно использоваться мультиплексирование ввода- вывода. Это сценарий, который мы только что рассмотрели выше.
Возможно, хотя это и редкий случай, что клиент одновременно обрабатывает множество сокетов. Tai<oii пример мы приведем в разделе 15.5 при использовании функции select в контексте клиента Web.
Если сервер TCP обрабатывает и прослушиваемый сокет, и присоединенные сокеты, обычно используется мультиплексирование ввода-вывода, как это показано на рис. 6.7.
Если сервер работает и с TCP, и с UDP, обычно также используется мультиплексирование ввода-вывода. Такой пример мы приводим в листинге 8.3.
Если сервер обрабатывает множество служб и, возможно, множество протоколов (например, демон i netd, который описан в разделе 12.5), обычно исполь-
• гея м\г чьтиплексирование ввода-вывода.
Область применения мультиплексирования ввода-вывода не ограничивается только сетевым программированием. Любому нетривиальному приложению часто приходится использовать эту технологию.
Модели ввода-вывода
Прежде чем начать описание функций sei ect и pol 1, мы должны вернутся н^зад и уяснить основные различия между пятью моделями ввода-вывода, досггупны* ми нам в Unix:
блокируемый ввод-вывод;
неблокируемый ввод-вывод;
мультиплексирование ввода-вывода (функции select И ро¥1);
ввод-вывод, управляемый сигналом (сигнал SIGIO);
асинхронный ввод-вывод (функции Posix. 1g aio_).
Возможно, вы захотите пропустить этот раздел при первом прочтении, а затем вернуться к нему по мере знакомства с различными моделями ввода-вывода, подробно рассматриваемыми в дальнейших главах.
Как вы увидите в примерах этого раздела, обычно различаются две фазы операции ввода:
Ожидание готовности данных.
Копирование данных от ядра процессу.
Первый шаг операции ввода иа сокете обычно включает ожидание прихода данных по сети. Когда пакет приходит, ои копируется в буфер внутри ядра. Второй шаг 一 копирование этих данных из буфера ядра в буфер приложения.
Модель блокируемого ввода-вывода
Наиболее распространенной моделью ввода-вывода является модель блокируемого ввода-вывода, которую мы использовали для всех примеров в тексте книги. По умолчанию все сокеты являются блокируемым и. Используя в наших примерах сокет дейтаграмм, мы получаем сценарий, показанный на рис. 6.1.
В этом примере вместо TCP мы используем UDP, поскольку в случае UDP признак готовности данных очень прост: получена вся дейтаграмма или нет. В случае TCP он становится сложнее, поскольку приходится учитывать дополнительные переменные, например минимальный объем данных в сокете (low water-mark).
В примерах этого раздела мы говорим о функции recvfrom как о системном вызове, поскольку делаем различие между нашим приложением и ядром. Вне зависимости от того, как реализована функция recvfrom (как системный вызов в ядре, происходящем от Беркли, или как функция, активизирующая системный вызов функции getmsg в ядре System V), она обычно выполняет переключение между работой в режиме приложения и работой в режиме ядра, за которым через определенный промежуток времени следует возвращение в режим приложения.
На рис. 6.1 процесс вызывает функцию recvfrom, и системный вызов не возвращает управление, пока дейтаграмма не придет и не будет скопирована в буфер приложения либо пока не произойдет ошибка. Наиболее типичная ошибка —
Приложение.recvfromПриложение.recvfromСистемный вызов >Системный вызов >Процессблокировандо завершенияфункцииrecvfromПроцессблокировандо завершенияфункцииrecvfromI Обработка дейтаграммыI Обработка дейтаграммыНормальноезавершениефункцииНормальноезавершениефункцииЯдроДейтаграмма не готова )к Ожидание ? данныхVДейтаграмма готова 丿Копирование дейтаграммьЛКопирование 卜 данных ,из ядра ▼I в буферКопирование завершено I приложенияЯдроДейтаграмма не готова )к Ожидание ? данныхVДейтаграмма готова 丿Копирование дейтаграммьЛКопирование 卜 данных ,из ядра ▼I в буферКопирование завершено I приложения
Приложение
.recvfrom
Приложение
.recvfrom
Системный вызов >
Системный вызов >
Процессблокировандо завершенияфункцииrecvfrom
Процессблокировандо завершенияфункцииrecvfrom
I Обработка дейтаграммы
I Обработка дейтаграммы
Нормальноезавершениефункции
Нормальноезавершениефункции
Ядро
Дейтаграмма не готова )
к Ожидание ? данных
V
Дейтаграмма готова 丿
Копирование дейтаграммьЛ
Копирование 卜 данных ,из ядра ▼I в буфер
Копирование завершено I приложения
Ядро
Дейтаграмма не готова )
к Ожидание ? данных
V
Дейтаграмма готова 丿
Копирование дейтаграммьЛ
Копирование 卜 данных ,из ядра ▼I в буфер
Копирование завершено I приложения
Рис. 6.1. Модель блокируемого ввода-вывода
это прерывание системного вызова сигналом, о чем рассказывалось в разделе 5.9. Процесс блокирован в течение всего времени с момента, когда он вызывает функцию recvfrom, до момента, когда эта функция завершается. Когда функция recvfrom выполняется нормально, наше приложение обрабатывает дейтаграмму.
Модель неблокируемого ввода-вывода
Когда мы определяем сокет как неблокируемый, мы тем самым сообщаем ядру следующее: «когда запрашиваемая нами операция ввода-вывода не может быть завершена без перевода процесса в состояние ожидания, следует не переводить процесс в состояние ожидания, а возвратить ошибку». Неблокируемый ввод-вывод мы описываем подробно в главе 15, а на рис. 6.2 мы лишь демонстрируем его свойства.
В первых трех случаях вызова функции recvfrom данных для возвращения нет, поэтому ядро немедленно возвращает ошибку EWOULDBLOCK. Когда мы в четвертый раз вызываем функцию recvfrom, дейтаграмма готова, поэтому она копируется в буфер приложения и функция recvfrom успешно завершается. Затем мы обрабатываем данные.
Такой процесс, когда приложение находится в цикле и вызывает функцию recvfrom на неблокируемом дескрипторе, назь1ваетс只 опросом (polling). Приложение последовательно опрашивает ядро, чтобы увидеть, что какая-то операция может быть выполнена. Часто это пустая трата времени процессора, но такая модель все же иногда используется, обычно в системах, выделяемых для одной функции.
Модель мультиплексирования ввода-вывода
В случае мультиплексирования ввода-вывода мы вызываем функцию select или pol 1, и блокирование происходит в одном из этих двух системных вызовов, а не в действительном системном вызове ввода-вывода. На рис. 6.3 обобщается модель мультиплексирования ввода-вывода.
Процесс блокируется в вызове функции sei ect, ожидая, когда дейтаграммный сокет будет готов для чтения. Когда функция select возвращает сообщение, что сокет готов для чтения, процесс вызывает функцию recvfrom, чтобы скопировать дейтаграмму в наш буфер приложения.
Приложение
Ядро
Системный вызов >
EWOULDBLOCK
Дейтаграмма не готова
Процесснесколько развызываетфункциюrecvfromдо ееуспешногозавершения(опрос)
Системный вызов
►
EWOULDBL0CK
Системный вызов >
EWOULDBLOCK
Системный вызов
►
Нормальноезавершение функции
Дейтаграмма не готова
Дейтаграмма готова
Копированиедейтаграммы
Копированиезавершено
\ Ожидание/ данных
Копированиеданных> из ядрав буферприложения
' Обработкадейтаграммы
Рис. 6.2. Модель неблокируемого ввода-вывода
Приложение
Процесс блокирован / select до завершения
функции select и ожидает, когда / один из (возможной нескольких сокетов I
станет готов для | чтения I
Системный вызов «Л
> Дейтаграмма не готова
к Ожидание
Функция select/ данных
сообщает, что.I
сокет готов для чтенияI
< Дейтаграмма готова J
Процессблокирован,пока данныекопируютсяв буферприложения
Системный вызов >
Нормальное завершение функции
Копированиедейтаграммы
Копированиезавершено
Копирование данных из ядра в буфер приложения
Обработкадейтаграммы
Рис. 6.3. Модель мультиплексирования ввода-вы вода
Сравнивая рис. 6.3 и 6.1, мы не найдем в модели мультиплексирования ввода- вывода каких-либо преимуществ, более того, она даже обладает незначительным недостатком, поскольку использование функции select требует двух системных
вызовов вместо одного. Но преимущество использования функции sei ect, которое мы увидим далее в этой главе, состоит в том, что мы сможем ожидать готовности не одного дескриптора, а нескольких.
Модель ввода-вывода, управляемого сигналом
Мы можем сообщить ядру, что необходимо уведомить процесс о готовности дескриптора с помощью сигнала SIGIO. Такая модель имеет название ввода-вывода, управляемого сигналом (signal-dnven I/O). Она представлена в обобщенном виде на рис. 6.4.
ПриложениеПриложениеПроцесспродолжаетвыполнятьсяПроцесспродолжаетвыполнятьсяУстановкаобработчикасигнала SIGIOУстановкаобработчикасигнала SIGIOСистемный вызовsigactionСистемный вызовsigactionЯдроЯдроВозвращениеВозвращение\ Ожидание 〉данных\ Ожидание 〉данных/ Обработчик сигнала/ Обработчик сигналаПередача сигнала / SIGIOПередача сигнала / SIGIOПроцесс блокирован, пока данные < копируются в буфер приложенияПроцесс блокирован, пока данные < копируются в буфер приложенияrecvfготrecvfгот>Системный вызов>Системный вызовДейтаграммаготоваКопированиедейтаграммыДейтаграммаготоваКопированиедейтаграммыОбработкадейтаграммыОбработкадейтаграммыНормальноезавершение функцииНормальноезавершение функции▼Копированиезавершено▼КопированиезавершеноКопирование\ данных/из ядра в буферприложенияКопирование\ данных/из ядра в буферприложения77
Приложение
Приложение
Процесспродолжаетвыполняться
Процесспродолжаетвыполняться
Установкаобработчикасигнала SIGIO
Установкаобработчикасигнала SIGIO
Системный вызовsigaction
Системный вызовsigaction
Ядро
Ядро
Возвращение
Возвращение
\ Ожидание 〉данных
\ Ожидание 〉данных
/ Обработчик сигнала
/ Обработчик сигнала
Передача сигнала / SIGIO
Передача сигнала / SIGIO
Процесс блокирован, пока данные < копируются в буфер приложения
Процесс блокирован, пока данные < копируются в буфер приложения
recvfгот
recvfгот
>
Системный вызов
>
Системный вызов
Дейтаграммаготова
Копированиедейтаграммы
Дейтаграммаготова
Копированиедейтаграммы
Обработкадейтаграммы
Обработкадейтаграммы
Нормальноезавершение функции
Нормальноезавершение функции
▼
Копированиезавершено
▼
Копированиезавершено
Копирование
\ данных
/из ядра в буфер
приложения
Копирование
\ данных
/из ядра в буфер
приложения
7
7
Рис. 6.4. Модель управляемого сигналом ввода-вывода
Сначала мы включаем на сокете управляемый сигналом ввод-вывод (об этом рассказывается в разделе 22.2) и устанавливаем обработчик сигнала при помощи системного вызова sigaction. Возвращение из этого системного вызова происходит незамедлительно, и наш процесс продолжается (он не блокирован). Когда дейтаграмма готова для чтения, для нашего процесса генерируется сигнал SIGIO. Мы можем либо прочитать дейтаграмму из обработчика сигнала с помощью вызова функции recvfrom и затем уведомить главный цикл о том, что данные готовы для обработки (см. раздел 22.3), либо уведомить основной цикл и позволить ему прочитать дейтаграмму.
Независимо от способа обработки сигнала эта модель имеет то преимущество, что во время ожидания дейтаграммы не происходит блокирования. Основной цикл может продолжать выполнение, ожидая уведомления от обработчика сигнала о том, что данные готовы для обработки либо дейтаграмма готова для чтения.
Модель асинхронного ввода-вывода
Асинхронный ввод-вывод был введен в редакции стандарта Posix. 1g 1993 года (расширения реального времени). Мы сообщаем ядру, что нужно начать операцию и VRPnnMIITb МЯГ С тем 1ГЛГПЙ КГЯ ППРПЙГГИ5Т (нкяшияя К"ЛПИПЛКЙМИР ЛЛГГГГЫУ ялпа
в наш буфер) завершится. Мы не обсуждаем эту модель в этой книге, поскольку она еще не получила достаточного распространения. Ее основное отличие от модели ввода-вывода, управляемого сигналом, заключается в том, что при использовании сигналов ядро сообщает нам, когда операция ввода-вывода может быть инициирована, а в случае асинхронного ввода-вывода — когда операция завершается. Пример этой модели приведен на рис. 6.5.в наш буфер) завершится. Мы не обсуждаем эту модель в этой книге, поскольку она еще не получила достаточного распространения. Ее основное отличие от модели ввода-вывода, управляемого сигналом, заключается в том, что при использовании сигналов ядро сообщает нам, когда операция ввода-вывода может быть инициирована, а в случае асинхронного ввода-вывода — когда операция завершается. Пример этой модели приведен на рис. 6.5.ПриложениеПриложениеЯдроЯдроаю readаю readСистемный вызс^ Дейтаграмма не готова\Системный вызс^ Дейтаграмма не готова\ВозвращениеВозвращениеДейтаграмма готоваДейтаграмма готоваОжидание > данныхОжидание > данныхПроцесспродолжаетвыполнятьсяПроцесспродолжаетвыполнятьсяКопированиедейтаграммыКопированиедейтаграммыОбработчик сигналаобрабатываетдейтаграммуОбработчик сигналаобрабатываетдейтаграммуПередача сигнала, / указанного в функции aio_readПередача сигнала, / указанного в функции aio_readКопирование завершено 丿Копирование завершено 丿Копированиеданныхиз ядра в буферприложенияКопированиеданныхиз ядра в буферприложения
в наш буфер) завершится. Мы не обсуждаем эту модель в этой книге, поскольку она еще не получила достаточного распространения. Ее основное отличие от модели ввода-вывода, управляемого сигналом, заключается в том, что при использовании сигналов ядро сообщает нам, когда операция ввода-вывода может быть инициирована, а в случае асинхронного ввода-вывода — когда операция завершается. Пример этой модели приведен на рис. 6.5.
в наш буфер) завершится. Мы не обсуждаем эту модель в этой книге, поскольку она еще не получила достаточного распространения. Ее основное отличие от модели ввода-вывода, управляемого сигналом, заключается в том, что при использовании сигналов ядро сообщает нам, когда операция ввода-вывода может быть инициирована, а в случае асинхронного ввода-вывода — когда операция завершается. Пример этой модели приведен на рис. 6.5.
Приложение
Приложение
Ядро
Ядро
аю read
аю read
Системный вызс^ Дейтаграмма не готова\
Системный вызс^ Дейтаграмма не готова\
Возвращение
Возвращение
Дейтаграмма готова
Дейтаграмма готова
Ожидание > данных
Ожидание > данных
Процесспродолжаетвыполняться
Процесспродолжаетвыполняться
Копированиедейтаграммы
Копированиедейтаграммы
Обработчик сигналаобрабатываетдейтаграмму
Обработчик сигналаобрабатываетдейтаграмму
Передача сигнала, / указанного в функции aio_read
Передача сигнала, / указанного в функции aio_read
Копирование завершено 丿
Копирование завершено 丿
Копированиеданныхиз ядра в буферприложения
Копированиеданныхиз ядра в буферприложения
Рис. 6.5. Модель асинхронного ввода-вьявода
Мы вызываем функцию dio.redd (функции асинхронного ввода-вывода Posix начинаются саю_ или 1ю_)и передаем ядру дескриптор, указатель на буфер, размер буфера (те же три аргумента, что и для функции read), смещение файла (аналогично функции Iseek), а также указываем, как уведомить нас, когда операция полностью завершится. Этот системный вызов завершается немедленно, и наш процесс не блокируется в ожидании завершения ввода-вывода. В этом примере предполагается, что мы указали ядру сгенерировать некий сигнал, когда операция завершится. Сигнал не генерируется до тех пор, пока данные не скопированы в наш буфер приложения, что отличает эту модель от модели ввода-вывода, управляемого сигналом.
ПРИМЕЧАНИЕ
На момент написания книги только некоторые системы поддерживают асинхронный ввод-вывод стандарта Posix. 1. Например, мы пс уверены, что системы поддерживают его для сокетов. Мы используем его только как пример для сравнения с моделью уп・ равляемого сигналом ввода-вывода.
Сравнение моделей ввода-вывода
На рис. 6.6 сравнивается пять различных моделей ввода-вывода. Здесь видно глав-
ЛТИИЧИР UPTRTnnY TTPriRRTV мг»пр»пптт а гтегчтэе 产 Яюол гтссспт」/、,тэтегчпа 小。《а。*, it my
одна и та же: процесс блокируется в вызове функции recvfrom, на то время, пока данные копируются: нз ядра в буфер вызывающего процесса. Однако асинхронный ввод-вывод в обеих фазах отличается от первых четырех моделей.
Модельблокируемоговвода-выведаМодельблокируемоговвода-выведаМодельнеблокируемоговвода-выводаМодельнеблокируемоговвода-выводаМодельмультиплексированияввода-выводаМодельмультиплексированияввода-выводаМодельуправляемогосигналомввода-выводаМодельуправляемогосигналомввода-выводаМодельасинхронноговведа-выводаМодельасинхронноговведа-выводаНачалоНачалоПроверка Проверка Проверка Проверка Проверка Проверка Проверка Проварка ПроверкаПроверка Проверка Проверка Проверка Проверка Проверка Проверка Проварка ПроверкаПроваркаПроваркаНачалоНачалоzso^israMzso^israM\ Ожидание/ данных\ Ожидание/ данныхHesodwiougeeHesodwiougeeЗавершениеЗавершениеЗавершениеЗавершениеУведомление началоУведомление началоДейтвграмма готоваДейтвграмма готоваНачалоНачалоЗавершениеЗавершениеЗавершениеЗавершениеУведомлениеУведомлениеДля этих моделей первая фаза различается, вторая фаза (блокирование процесса при вызове функции recvfrom) одинаковаДля этих моделей первая фаза различается, вторая фаза (блокирование процесса при вызове функции recvfrom) одинаковаРис. 6.6- Сравнение моделей ввода-выводаРис. 6.6- Сравнение моделей ввода-выводаВыполнение обеих фазВыполнение обеих фазzso^sosronzso^sosronхёо^хжоЕюопхёо^хжоЕюопxsoasssra^xsoasssra^
Модельблокируемоговвода-выведа
Модельблокируемоговвода-выведа
Модельнеблокируемоговвода-вывода
Модельнеблокируемоговвода-вывода
Модельмультиплексированияввода-вывода
Модельмультиплексированияввода-вывода
Модельуправляемогосигналомввода-вывода
Модельуправляемогосигналомввода-вывода
Модельасинхронноговведа-вывода
Модельасинхронноговведа-вывода
Начало
Начало
Проверка Проверка Проверка Проверка Проверка Проверка Проверка Проварка Проверка
Проверка Проверка Проверка Проверка Проверка Проверка Проверка Проварка Проверка
Проварка
Проварка
Начало
Начало
zso^israM
zso^israM
\ Ожидание
/ данных
\ Ожидание
/ данных
Hesodwiougee
Hesodwiougee
Завершение
Завершение
Завершение
Завершение
Уведомление начало
Уведомление начало
Дейтвграмма готова
Дейтвграмма готова
Начало
Начало
Завершение
Завершение
Завершение
Завершение
Уведомление
Уведомление
Для этих моделей первая фаза различается, вторая фаза (блокирование процесса при вызове функции recvfrom) одинакова
Для этих моделей первая фаза различается, вторая фаза (блокирование процесса при вызове функции recvfrom) одинакова
Рис. 6.6- Сравнение моделей ввода-вывода
Рис. 6.6- Сравнение моделей ввода-вывода
Выполнение обеих фаз
Выполнение обеих фаз
zso^sosron
zso^sosron
хёо^хжоЕюоп
хёо^хжоЕюоп
xsoasssra^
xsoasssra^
Сравнение синхронного и асинхронного ввода-вывода
Posix. 1 дает следующие определения этих терминов:
勰 Операция синхронного ввода-вывода блокирует запрашивающий процесс до тех пор, пока операция ввода-вывода не завершится.
恭 Операция асинхронного ввода-вывода не вызывает блокирования запрашивающего процесса.
Используя эти определения, можно сказать, что первые четыре модели ввода- вывода —блокируемая, неблокируемая, модель мультиплексирования ввода-вывода и модель управляемого сигналом ввода-вывода — являются синхронными, поскольку фактическая операция ввода-вывода (функция recvfrom) блокирует процесс. Только модель асинхронного ввода-вывода соответствует определению асинхронного ввода-вывода.
Функция select
Эта функция позволяет процессу сообщить ядру, что необходимо подождать, пока не произойдет одно из некоторого множества событий, и вывести процесс из состояния ожидания, только когда произойдет одно или несколько таких событий или когда пройдет заданное количество времени.
Например, мы можем вызвать функцию sei ect и сообщить ядру, что возвращать управление нужно только когда наступит любое из следующих событий: 瑟 любой дескриптор из набора {1,4,5} готов для чтения;
索 любой дескриптор из набора {2, 7} готов для записи;
敎 любой дескриптор из набора {1, 4} вызывает исключение, требующее обработки;
第 истекает 10,2 секунды.
Таким образом, мы сообщаем ядру, какие дескрипторы нас интересуют (готовые для нтенн只,готовые для записи или требующие обработки исключения) и как долго нужно ждать. Интересующие нас дескрипторы не ограничиваются сокетами: любой дескриптор можно проверить с помощью функции sei ect.
ПРИМЕЧАНИЕ
Беркли-реализации всегда допускали мультиплексирование ввода-вы вода с любыми дескрипторам и. Система SVR3 изначально ограничивала мультиплексирование ввода-вывода дескрипторами, которые являлись потоковыми устройствами (см. главу 33), но это ограничение было снято в SVR4.
include <sys/select.h>
#include <sys/time h>
int seiect(int maxfdpl. fd_set *readset. fd_set ^wn teset. fd_set ★exceptset. const struct timeval * timeout),
Возвращает положительное число - счетчик готовых дескрипторов 0 в случае тайм-аута. -1 в случае ошибки
Описание этой функции мы начнем с последнего аргумента, который сообщает ядру, сколько ждать, пока один из заданных дескрипторов не будет готов. Структура timeval задает число секунд и микросекунд;
struct timeval {
long tv_sec./* секунды */
long tv_usec./* микросекунды */
С помощью этого аргумента можно реализовать три сценария.
• Ждать вечно: завершать работу, только когда один из заданных дескрипторов готов для ввода-вывода. Для этого нужно определить аргумент структуры timeval как пустой указатель.
. Ждать в течение определенного времение: завершение будет происходить, когда один из заданных дескрипторов готов для ввода-вывода, но период ожидания ограничивается количеством секунд и микросекунд, заданным в структуре timeval, на которую указывает аргумент timeout.
・ Не ждать вообще: завершение происходит сразу же после проверки дескрипторов. Это называется опросом (polling). Аргумент timeout должен указывать
на структуру timeval, а значение таймера (число секунд и микросекунд, заданных этой структурой) должно быть нулевым.
Ожидание в первых двух случаях обычно прерывается, когда процесс перехватывает сигнал и возвращается из обработчика сигнала.
ПРИМЕЧАНИЕ
Ядра реализаций, происходящих от Беркли, никогда автоматически нс перезапускают функцию select [105, с. 527], в то время как ядра SVR4 перезапускают, если задан флаг SA__RESTART при установке обработчика сигнала. Это значит, что в целях переносимости мы должны быть готовы к тому, что функция select возвратит ошибку EINTR, если мы перехватываем сигна刀и.
Хотя структура timeval позволяет нам задавать значение с точностью до микросекунд, реальная точность, поддерживаемая ядром, часто значительно ниже. Например, многие ядра Unix округляют значение тайм-аута до серий по 10 микросекунд. Присутствует также и некоторая скрытая задержка, то есть между истечением времени таймера и моментом, когда ядро запустит данный процесс, проходит некоторое врем只.
Квалификатор const аргумента timeout означает, что данный аргумент не изменяется функцией select при ее возвращении. Например, если мы зададим предел времени, равный 10 секундам, и функция sei ect возвратит управление до истечения этого времени с одним или более готовых дескрипторов или ошибкой EINTR, то структура timeval не изменится, то есть при завершении функции значение тайм-аута не станет равно числу секунд, оставшихся от исходных 10. Чтобы узнать количество неизрасходованных секунд, следует определить системное время до вызова функции select, а когда она завершится, определить его еще раз и вычесть первое значение из второго.
ПРИМЕЧАНИЕ
В современных системах Linux структура timeval изменяема Следовательно, в целях переносимости будем считать, что структура timeval по возвращении становится иео- пределенпой, и будем инициализировать ее перед каждым вызовом функции select. В Posix. 1g определяется квалификатор const.
Три средних аргумента, readset, writeset и exceptset, определяют дескрипторы, которые ядро должно проверить на возможность чтения и записи и на наличие исключений (exceptions). В настоящее время поддерживается только два исключения:
На сокет приходят внеполосные данные. Более подробно мы опишем этот случай в главе 21.
Присутствие информации об управлении состоянием (control status information), которая должна быть считана с управляющего (master side) псевдотерминала, помещенного в режим пакетной обработки. Псевдотерминалы в данном томе не рассматриваются.
Проблема в том, как задать одно или несколько значений дескрипторов для каждого из трех аргументов. Функция select использует наборы дескрипторов, обычно это массив целых чисел, где каждый бит в каждом целом числе соответствует дескриптору. Например, при использовании 32-разрядных целых чисел
первый элемент массива (целое число) соответствует дескрипторам от 0 до 31, второй элемент — дескрипторам от 32 до 63 и т. д. Детали реализации не влияют на приложение и скрыты в типе данных df_set и следующих четырех макросах:
void FD_ZERO(fd_set *fdset) void FD^SETdnt fd. fd_set *fdset). void FD[CLR(int flfd fd_set *fdset) int FDJSSETdnt fd. fd'set *fdset).void FD_ZERO(fd_set *fdset) void FD^SETdnt fd. fd_set *fdset). void FD[CLR(int flfd fd_set *fdset) int FDJSSETdnt fd. fd'set *fdset)./* сбрасываем все биты в fdset *//* устанавливаем бит для fd в fdset *//* сбрасываем бит для fd в fdset *//* установлен ли бит для fd в fdset ? *//* сбрасываем все биты в fdset *//* устанавливаем бит для fd в fdset *//* сбрасываем бит для fd в fdset *//* установлен ли бит для fd в fdset ? */
void FD_ZERO(fd_set *fdset) void FD^SETdnt fd. fd_set *fdset). void FD[CLR(int flfd fd_set *fdset) int FDJSSETdnt fd. fd'set *fdset).
void FD_ZERO(fd_set *fdset) void FD^SETdnt fd. fd_set *fdset). void FD[CLR(int flfd fd_set *fdset) int FDJSSETdnt fd. fd'set *fdset).
/* сбрасываем все биты в fdset */
/* устанавливаем бит для fd в fdset */
/* сбрасываем бит для fd в fdset */
/* установлен ли бит для fd в fdset ? */
/* сбрасываем все биты в fdset */
/* устанавливаем бит для fd в fdset */
/* сбрасываем бит для fd в fdset */
/* установлен ли бит для fd в fdset ? */
Мы размещаем в памяти набор дескрипторов типа fd_set, с помощью этих макросов устанавливаем и проверяем биты в наборе, а также можем присвоить его (как значение) другому набору дескрипторов с помощью оператора присваивания языка С.
ПРИМЕЧАНИЕ
Описываемый нами массив целых чисел, использующий по одному биту для каждого дескриптора, — эго только один из возможных способов реализации функции select Тем не менее является обычной практикой ссылаться на отдельные дескрипторы в наборе дескрипторов как па биты — например, так: «установить би г для прослушиваемого дескриптора в наборе для чтения»
В разделе 6.10 мы увидим, чго функция poll использует совершенно лру! ос представление' массив структур переменной длины, по одной струкгурсдлл каждого дескриптора
Например, чтобы определить переменную типа fd_set и затем установить биты для дескрипторов 1,4 и 5, мы пишем:
fd_set rset.
FD_ZERO(&rset).FD二 SET(1 &rset).FD_SET(4 &rset).FD_SET(5 &rset)FD_ZERO(&rset).FD二 SET(1 &rset).FD_SET(4 &rset).FD_SET(5 &rset)все биты сброшены */fd 1 */fd 4 */fd 5 */все биты сброшены */fd 1 */fd 4 */fd 5 *//* инициализируем набор
FD_ZERO(&rset).
FD二 SET(1 &rset).
FD_SET(4 &rset).
FD_SET(5 &rset)
FD_ZERO(&rset).
FD二 SET(1 &rset).
FD_SET(4 &rset).
FD_SET(5 &rset)
все биты сброшены */
fd 1 */
fd 4 */
fd 5 */
все биты сброшены */
fd 1 */
fd 4 */
fd 5 */
/* устанавливаем бит для /* устанавливаем бит для /* устанавливаем бит для
Важно инициализировать набор, так как если набор будет создан в виде автоматической переменной и не проипициализирован, результат может оказаться непредсказуемым.
Любой из трех средних аргументов функции select — readset, writeset или exceptset — может быть задан как пустой указатель, если нас не интересует определяемое им условие. На самом деле, если все три указателя пустые, мы просто получаем таймер большей точности, чем обычная функция Unix sleep (которая дает точность около секунды). Функция poll обеспечивает аналогичную функциональность. На рис. С.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 заставляет нас вычислять наибольший интересующий нас дескриптор и затем сообщать ядру его значение.
Например, в предыдущем коде, который включает дескрипторы 1,4 и 5, значение аргумента maxfdpl равно 6. Причина, по которой это 6, а не 5, в том, что мы задаем количество дескрипторов, а не наибольшее значение, а нумерация дескрипторов начинается с нуля.
ПРИМЕЧАНИЕ
Зачем нужно было включать этот аргумент и вычислять его значение? Причина в том, что он повышает эфс})ективность работы ядра. Хотя каждый набор типа fd_set может содержать множество дескрипторов (обычно до 1024), реальное количество дескрипторов, используемое типичным процессом, значительно меньше. Эффективность возрастает за счет того, что не копируются ненужные части набора дескрипторов между ядром и процессом, и не требуется проверять биты, которые всегда являются нулевыми (см. раздел 16.13 [105]).
Функция select изменяет наборы дескрипторов, на которые указывают аргументы readset,wri teset и exceptset. Эти три аргумента являются аргументами типа «значение-результат». Когда мы вызываем функцию, мы задаем значения интересующих нас дескрипторов, и по ее завершении результат указывает, какие дескрипторы готовы. Проверить после завершения определенный дескриптор из структуры fd_set можно с помощью макроса FD_ISSET. Для дескриптора, не готового при завершении функции, соответствующий бит в наборе дескрипторов будет сброшен. Поэтому мы устанавливаем все интересующие нас биты во всех наборах дескрипторов каждый раз, когда вызываем функцию select.
ПРИМЕЧАНИЕ
Две наиболее общих ошибки программирования при использовании функции select — это забыть добавить единицу к наибольшему номеру дескриптора и забыть, что наборы дсскрнп горов имеют тип «значение-результат». Вторая ошибка приводит к тому, что функция select вызывается с нулевым битом в наборе дескрипторов, когда мы думаем, что он установлен в единицу. Автор лично потерял два часа, оглаживая пример для этой книги, так как забыл прибавить единицу к первому аргументу функции.
Возвращаемое этой функцией значение указывает общее число готовых дескрипторов во всех наборах дескрипторов. Если значение таймера истекает до того, как какой-нибудь из дескрипторов оказывается готов, возвращается нулевое значение. Возвращаемое значение —1 указывает на ошибку (которая может произойти если, например, выполнение функции прервано перехваченным сигналом).
ПРИМЕЧАНИЕ
В более ранних реализациях SVR4 функция select содержала ошибку если один и тот же бит находился в нескольких наборах дескрипторов — допустим, дескрипюр был готов и для чтения, и для записи, — он учитывался только один раз. В современных реализациях эта ошибка исправлена.
При каких условиях дескриптор становится готовым?..
Мы говорили об ожидании готовности дескриптора для ввода-вывода (чтения
ИЛИ записи^ И ЛИ ВОЗНИКНИВРНИЯ ИСКПТПЧЫТР ПЬНЛЙ ГМТХХЯПММ ТПА^ХГШИТАМ-
ки (внеполосные данные). В то время как готовность к чтению и записи очевидна для файловых дескрипторов, в случае дескрипторов сокетов следует более внимательно изучить те условия, при которых функция select сообщает, что сокет готов (рис. 16.52 [105]).
Сокет готов для чтения, если выполнено одно из следующих условий:
Число байтов данных в приемном буфере сокета больше или равно текущему значению минимального количества данных (low water-mark) для приемного буфера сокета. Операция считывания данных из сокета не блокируется и возвратит значение, большее нуля (то есть данные, готовые для чтения). Мы можем задать значение минимального количества данных (low-water mark) с помощью параметра сокета SO_RCVLOWAT. По умолчанию для сокетов TCP и UDP это значение равно 1.
На противоположном конце соединение закрывается (нами получен сегмент FIN). Операция считывания данных из сокета не блокируется и возвратит нуль (то есть признак конца файла).
Сокет является прослушиваемым, и число установленных соединений ненулевое. Функция accept на прослушиваемом сокете обычно не блокируется, хотя в разделе 15.6 мы описываем ситуацию, в которой функция accept может оказаться блокируемой.
Ошибка сокета, ожидающая обработки. Операция чтения на сокете не блокируется и возвратит ошибку (~1)со значением переменной errno, указывающим на конкретное условие ошибки. Эти ошибки, ожидающие обработки, можно также получить, вызвав функцию getsockopt с параметром SO_ERROR, после чего состояние ошибки будет сброшено.
Сокет готов для записи, если выполнено одно из следующих условий:
Количество байтов доступного пространства в буфере отправки сокета больше или равно текущему значению минимального количества данных для буфера отправки сокета и либо сокет является присоединенным, либо сокету не требуется соединения (например, сокет UDP). Это значит, что если мы отключим блокировку для сокета (см. главу 15), операция записи не заблокирует сокет и возвратит положительное значение (например, число байтов, принятых транспортным уровнем). Устанавливать минимальное количество данных мы можем с помощью параметра сокета SO_SNDLDWAT. По умолчанию это значение равно 2048 для сокетов TCP и UDP.
Получатель, которому отправляются данные, закрывает соединение. Операция записи в сокет сгенерирует сигнал SIGPIPE (см. раздел 5.12).
Ошибка сокета, ожидающая обработки. Операция записи в сокет не блокируется и возвратит ошибку (-1)со значением переменной errno, указывающей на конкретное условие ошибки. Эти ошибки, ожидающие обработки, 、 можно также получить и сбросить, вызвав функцию getsockopt с параметром сокета SO_ERROR・
Исключительная ситуация, требующая обработки, может возникнуть на сокете в том случае, если приняты внеполосные данные либо если отметка внеполосных данных в принимаемом потоке еще не достигнута. (Внеполосные данные описываются в главе 21.、
ПРИМЕЧАНИЕ
Наши определения «готов для чтения» и «готов для записи» взяты непосредственно из макроопределений ядра soreadable и sowntable (которые описываются на с. 530-531 [105]). Аналогично, наше определение «исключительной ситуации» взято из функции soo select, которая описана там же.
Обратите внимание, что когда происходит ошибка на сокете, функция select отмечает его готовым как для чтения, так и для записи.
Значения минимального количества даных (low-water mark) для приема и отправки позволяют приложению контролировать, сколько данных должно быть доступно для чтения или сколько места должно быть доступно для записи перед тем, как функция select сообщит, что сокет готов для чтения или записи. Например, если мы знаем, что наше приложение не может сделать ничего полезного, пока не будет получено как минимум 64 байта данных, мы можем установить значение минимального количества данных равным 64, чтобы функция select не вывела нас из состояния ожидания, если для чтения готово менее 64 байт.
Пока значение минимального количества данных для отправки в сокете UDP меньше, чем буфер отправки сокета (а такое отношение между ними всегда устанавливается по умолчанию), сокет UDP всегда готов для записи, поскольку соединения не требуется.
В табл. 6.1 суммируются описанные выше условия, при которых сокет становится готовым для вызова функции select.
Таблица 6.1. Условия, при которых функция select сообщает, что сокет готов для чтения или для записи либо необходима обработка исключительной ситуации
х»冷•i>'oj«««««'«^<x»»w<»c««wWi»w!««»«««jx««««wwew»»>»M»q,x4«< wv»»MW»««>MMW«v«4,.1M>.wv«w«>««ww»KW№<»aww1~«>«»»»W№«w»o<;"*m<4fti»ow1>eive »««ч4«■'■ «мм^^м^лсмвкзтк-мммммжю УсловиеСокет готов дляСокет готов дляИсключительная
чтениязаписиситуация
Данные для чтения•
Считывающая половина•
соединения закрыта Для прослушиваемого••
сокета готово новое соединение Пространство, доступное•
для записи Записывающая половина ••
соединения закрыта Ошибка, ожидающая обработки Внеполосные данные TCP•
Максимальное число дескрипторов для функции select
Ранее мы сказали, что большинство приложений не используют много дескрипторов. Например, редко можно найти приложение, использующее сотни дескрипторов. Но такие приложения существуют, и часто они используют функцию set ect
для мультиплексирования дескрипторов. Когда функция select была создана, операционные системы обычно имели ограничение на максимальное число дескрипторов для каждого процесса (этот предел в реализации 4.2BSD составлял 31), и функция select просто использовала тот же предел. Но современные версии Unix допускают неограниченное число дескрипторов для каждого процесса (часто оно ограничивается только количеством памяти и административными правилами), поэтому возникает вопрос: как же теперь работает функция select?
Многие реализации имеют объявления, аналогичные приведенному ниже, которое взято из заголовочного файла 4.4BSD <sys/types. h>:
/*
Значение FO_SETSIZE может быть определено пользователем. но запанное здесь по умолчанию является постаточным в большинстве случаев.
*/
#】fndef FD_SETSIZE
#define FD]SETSIZE 256
#endif
Исходя из этого комментария, можно подумать, что если перед подключением этого заголовочного файла присвоить FD_SETSIZE значение, превышающее 256, то увеличится размер наборов дескрипторов, используемых функцией select. К сожалению, обычно это не действует.
ПРИМЕЧАНИЕ
Чтобы понять, в чем дело, обратите внимание, что на рис. 16.53 [105] объявляются три набора дескрипторов внутри ядра, а в качестве верхнего предела используется определенное в ядре значение FD SETSIZE. Единственный способ увеличить размер наборов дескрипторов — это увеличить значение FD_SETSIZE и затем перекомпилировать ядро. Изменения значения без перекомпиляции ядра недостаточно.
Некоторые производители изменяют свои реализации функции select, с тем чтобы позволить процессу задавать значение FD.SETSIZE, превышающее значение по умолчанию. BSD/OS также изменила реализацию ядра, чтобы допустить большие наборы дескрипторов, кроме того, в ней добавлено четыре новых макроопределения FD_xxx для динамического размещения больших наборов дескрипторов в памяти и для работы с ними. Однако с точки зрения переносимости не стоит злоупотреблять использованием больших наборов дескрипторов.
Функция str_cli (продолжение)
Теперь мы можем переписать нашу функцию str_cli, представленную в разделе 5.5 (на этот раз используя функцию select), таким образом, чтобы мы получали уведомление, как только завершится процесс сервера. Проблема с предыдущей версией состояла в том, что процесс мог оказаться заблокированным в вызове функции fgets, когда что-то происходило на сокете. Наша новая версия этой функции вместо этого блокируется в вызове функции select, ожидая готовности для чтения либо стандартного потока ввода, либо сокета. На рис. 6.7 показаны различные условия, обрабатываемые с помощью вызова функции select.
Клиент
Данные или EOFА1 stdinВезов функции select
(конец файла)Для проверки готовности
сокета к чтению
Рис. 6.7. Условия, обрабатываемые функцией select в вызове функции str_cli
Сокет обрабатывает три условия:
Если протокол TCP собеседника отправляет данные, сокет становится готовым для чтения, и функция read возвращает положительное значение (то есть число байтов данных).
Если протокол TCP собеседника отправляет сегмент FIN (процесс завершается), сокет становится готовым для чтения, и функция read возвращает нуль (признак конца файла).
Если TCP собеседника отправляет RST (узел вышел из строя и перезагрузился), сокет становится готовым для чтения, и функция read возвращает-1,а переменная еггпо содержит код соответствующей ошибки.
В листинге 6.1* представлен исходный код этой версии функции.
Листинг 6.1. Реализация функции str_cli с использованием функции select (усовершенствованный вариант находится в листинге 6.2)
//select/strcliseiectOl с
include "unp h*
2 3 4 5 6 72 3 4 5 6 7void
2 3 4 5 6 7
2 3 4 5 6 7
str_cli(FILE *fp int sockfd)
{_
intmaxfdpi.
fd set rset,»
charsendline[MAXLINE], recvUnerM^LINE]:
FD_ZERO(&rset),
for (. ) {
FO SET(fi 1 eno( fp) &rset)
FD 二SET(sockfd &rset)
maxfdpl = max(fileno(fp) sockfd) + 1
Select(maxfdpl &rset. NULL NULL. NULL).
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http" www piter com/download
if (FD_ISSET(sockfd. &rset)) { /* сокет готов для чтения */
if (ReadlineCsockfd, recvline MAXLINE) == 0)
err_quit("str_cli server terminated prematurely")
Fputs^recvline stdout)
}
if (FD_ISSET(fileno(fp) Srset)) { /* входное устройство готово для чтение ^
20if (Fgets(sendlme MAXLINE fp) = NULL),
return, /* все сделано */
WritenCsockfd. sendline strlen(sendline))
}
}
}
Вызов функции select
8-13 Нам нужен только один набор дескрипторов — для проверки готовности сокета для чтения. Этот набор дескрипторов инициализируется макросом FD.ZERD, после чего с помощью макроса FD_SET устанавливаются два бита: бит, соответствующий указателю файла fp стандартного потока ввода-вывода, и бит, соответствующий дескриптору сокета sockfd. Функция f11 eno преобразует указатель файла стандартного потока ввода-вывода в соответствующий ему дескриптор. Функция select (а также poll) работает только с дескрипторами.
Функция select вызывается после определения максимального из двух дескрипторов. В этом вызове указатель на набор дескрипторов для записи и указатель на набор дескрипторов с исключениями являются пустыми. Последний аргумент (ограничение по времени) также является пустым указателем, поскольку мы хотим, чтобы процесс был блокирован, пока не будут готовы данные для чтения.
Обработка сокета, готового для чтения
14-18 Если по завершении функции select сокет готов для чтения, отраженная строка считывается функцией readline и выводится функцией fputs.
Обработка ввода, допускающего возможность чтения
19-23 Если стандартный поток ввода готов для чтения, строка считывается функцией fgets и записывается в сокет с помощью функции writen.
Обратите внимание, что используются те же четыре функции ввода-вывода, что и в листинге 5 5: fgets, writen, readline и fputs, — но порядок их следования внутри функции str_cl 1 изменился. Раньше выполнение функции str_cli определялось функцией fgets, а теперь ее место заняла select. С помощью всего нескольких дополнительных строк кода (сравните листинги 6 1 и 5 4) мы значительно увеличили устойчивость клиента.
Пакетный ввод
К сожалению, наша функция str_cl 1 все еще не вполне корректна. Сначала вернемся к ее исходной версии, приведенной в листинге 5.5. Эта функция работает в режиме остановки и ожидания (stop-and-wait mode), что удобно для интерактивного использования: функция отправляет строку серверу и затем ждет его
ответа. Время ожидания складывается из одного периода обращения (RTT) и времени обработки сервером (которое близко к нулю в случае простого эхо-серве- ра). Следовательно, мы можем предположить, сколько времени займет отражение данного числа строк, если мы знаем время обращения (RTT) между клиентом и сервером.
Измерить RTT позволяет утилита Ping. Если мы измерим с ее помощью время обращения к conmх сот с нашего узла 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.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Запрос 9Запрос 8Запрос 7Запрос 6Ответ 2Ответ 3Ответ 4Ответ 5
Запрос 9
Запрос 8
Запрос 7
Запрос 6
Ответ 2
Ответ 3
Ответ 4
Ответ 5
Запрос 9
Запрос 8
Запрос 7
Запрос 6
Ответ 2
Ответ 3
Ответ 4
Ответ 5
Рис. 6.9. Заполнение канала между клиентом и сервером: пакетный режим
Предполагается, что после отправки первого запроса мы немедленно посылаем другой запрос и т. д. Также предполагается, что мы можем отправлять запросы с той скоростью, с какой сеть способна их принимать, и обрабатывать ответы так быстро, как сеть их поставляет.
ПРИМЕЧАНИЕ
Существуют различные нюансы, имеющие отношение к передаче большого количества данных TCP (bulk data flow), которые мы здесь игнорируем. К ним относятся алгоритм медленного запуска (slow start algorithm), ограничивающий скорость, с которой данные отправляются на новое или незанятое соединение, и возвращаемые сегменты АСК. Все эти вопросы рассматриваются в главе 20 [94].
Чтобы увидеть, в чем заключается проблема с нашей функцией st「_c11, представленной в листинге 6.1, будем считать, что файл ввода содержит только девять
строк. Последняя строка отправляется в момент времени 8, как показано на рис. 6.9. Но мы не можем закрыть соединение после записи этого запроса, поскольку в канале еще есть другие запросы и ответы. Причина возникновения проблемы кроется в нашем способе обработки конца файла при вводе, когда процесс возвращается в функцию mai п, которая затем завершается. Но в пакетном режиме конец файла при вводе не означает, что мы закончили читать из сокета, — в нем могут оставаться запросы к серверу или ответы от сервера.
Нам нужен способ закрыть одну половину соединения TCP. Другими словами, мы хотим отправить серверу сегмент FIN, тем самым сообщая ему, что закончили отправку данных, но оставляем дескриптор сокета открытым для чтения. Это делается с помощью функции shutdown, которая описывается в следующем разделе.
Функция shutdown
Обычный способ завершить сетевое соединение — вызвать функцию close. Но у функции close есть два ограничения, которых лишена функция shutdown:
Функция close постепенно уменьшает счетчик ссылок дескриптора и закрывает сокет, только если счетчик доходит до нуля. Мы рассматривали это в разделе 4.8. Используя функцию shutdown, мы можем инициировать обычную последовательность завершения соединения TCP (четыре сегмента, начинающихся с FIN, на рис. 2.5) независимо от значения счетчика ссылок.
Функция close завершает оба направления передачи данных — и чтение, и запись. Поскольку соединение TCP является двусторонним, возможны снтуа-
writewriteshutdownwritewriteshutdownФункция read возвращает значение больше ОФункция read возвращает значение больше ОФункция read возвращает значение ОФункция read возвращает значение больше ОФункция read возвращает значение больше ОФункция read возвращает значение ОФункция read возвращает значение больше ОФункция read возвращает значение больше ОФункция read возвращает значение ОwritewritecloseФункция read возвращает значение больше ОФункция read возвращает значение больше ОФункция read возвращает значение ОwritewritecloseРис. 6.10. Вызов функции shutdown для закрытия половины соединения TCPРис. 6.10. Вызов функции shutdown для закрытия половины соединения TCPКлиент
write
write
shutdown
write
write
shutdown
Функция read возвращает значение больше О
Функция read возвращает значение больше О
Функция read возвращает значение О
Функция read возвращает значение больше О
Функция read возвращает значение больше О
Функция read возвращает значение О
Функция read возвращает значение больше О
Функция read возвращает значение больше О
Функция read возвращает значение О
write
write
close
Функция read возвращает значение больше О
Функция read возвращает значение больше О
Функция read возвращает значение О
write
write
close
Рис. 6.10. Вызов функции shutdown для закрытия половины соединения TCP
Рис. 6.10. Вызов функции shutdown для закрытия половины соединения TCP
ции, когда нам понадобится сообщить другому концу соединения, что мы закончили отправку, даже если на том конце соединения имеются данные для отправки нам. Это случай, рассмотренный в предыдущем разделе при описании работы нашей функции str_c1 т в пакетном режиме. На рис. 6.10 показаны типичные вызовы функций в этом сценарии.
#include <sys/socket h>
int shutdown (mt sockfd, int hcwto).
Возвращает 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_xxx введены впервые в Posix 1g Типичные значения аргумента howto, с которыми вьг встретитесь, — это 0 (закрытие читающей половины), 1 (закрытие записывающей половины) и 2 (закрытие обеих половин).
Функция str_cli (еще раз)
В листинге 6.2 представлена наша обновленная (и корректная) функция str_cl i.
В этой версии используются функции select и shutdown. Первая уведомляет нас
о том, когда сервер закрывает свой конец соединения, а вторая позволяет корректно обрабатывать пакетный ввод.
Листинг 6.2. Функция str_cfi, использующая функцию select, которая корректно обрабатывает конец файла
//seiect/strcliseiect02 c
include 'unp h'
void
str cli(FILE *fp int sockfd)
{-
int maxfdpl,stdmeof
fd_set rset,
char sendline[MAXLINE]recvline[MAXLINE]:
std7nuof = 0
FD_ZERO(&rset).
for (. ) {
if (stdmeof == 0)
FD_SET(fileno(fp). &rset),
FD_SET(sockfd &rset)
maxfdpl = max(fileno(fp) sockfd) + 1
Select(maxfdpl &rset NULL NULL. NULL)
if (FD_ISSET(sockfd &rset)) { /* сокет готов для чтения */
if (Readlme(sockfd. recvline. MAXLINE) == 0) {
if (stdineof == 1)
return, /* нормальное завершение */
else
err quitestr_cli server terminated prematurely").
}「 一
Fputs(「ecvline. stdout),
}
if (FD_ISSET(fi1eno(fp) &rset)) { /* устройство ввода готово для чтения */
if (FgetsCsendline MAXLINE fp) == NULL) {
stdmeof = 1,
Shutdown(sockfd, SHUT_WR)/* отправляем сегмент FIN */
FD_CLR(fileno(fp). &rset)
continue.
}
Wnten(sockfd.sendЪne.strlen(sendlme))
}
}
}
j-8 stdmeof — это новый флаг, инициализируемый нулем. Пока этот флаг равен нулю, мы будем проверять готовность стандартного потока ввода к чтению с помощью функции select.
5-24 Если мы считываем на сокете признак конца файла, когда нам уже встретился ранее признак конца файла в стандартном потоке ввода, это является нормальным завершением и функция возвращает управление. Но если конец файла в стандартном потоке ввода еще не встречался, это означает, что процесс сервера завершился преждевременно.
5-33 Когда нам встречается признак конца файла на стандартном устройстве ввода, наш новый флаг stdmeof устанавливается в единицу и мы вызываем функцию shutdown со вторым аргументом SHUT一WR для отправки сегмента FIN.
Если мы измерим время работы нашего клиента TCP, использующего функцию str_cl 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 показано состояние сервера до того как первый клиент установил соединение.
Сервер
dolLIUdNOocl>3;3s<DeBsrnx50d 匚dolLIUdNOocl>3;3s<DeBsrnx50d 匚
dolLIUdNOocl
>3;3s<DeBsrnx50d 匚
dolLIUdNOocl
>3;3s<DeBsrnx50d 匚
Рис. 6.11. Сервер TCP до того, как первый клиент установил соединение
У сервера имеется одиночный прослушиваемый дескриптор, показанный на рисунке точкой.
Сервер обслуживает только набор дескрипторов для чтения, который мы показываем на рис. 6.12. Предполагается, что сервер запускается в приоритетном (foreground) режиме, а дескрипторы 0,1 и2 соответствуют стандартным потокам ввода, вывода и ошибок. Следовательно, первым доступным для прослушиваемого сокета дескриптором является дескриптор3. Массив целых чисел client содержит дескрипторы присоединенного сокета для каждого клиента. Все элементы этого массива инициализированы значением -1.
client[][0][1][2]client[][0][1][2][FD_SETSIZE-1][FD_SETSIZE-1]fdO fd 1 fd2 fd3fdO fd 1 fd2 fd3Рис. 6.12. Структуры данных для сервера TCP с одним прослушиваемым сокетомРис. 6.12. Структуры данных для сервера TCP с одним прослушиваемым сокетом
client[]
[0]
[1]
[2]
client[]
[0]
[1]
[2]
[FD_SETSIZE-1]
[FD_SETSIZE-1]
fdO fd 1 fd2 fd3
fdO fd 1 fd2 fd3
Рис. 6.12. Структуры данных для сервера TCP с одним прослушиваемым сокетом
Рис. 6.12. Структуры данных для сервера TCP с одним прослушиваемым сокетом
Единственная ненулевая запись в наборе дескрипторов — это запись для прослушиваемого сокета, и поэтому первый аргумент функции select будет равен 4.
Когда первый клиент устанавливает соединение с нашим сервером, прослушиваемый дескриптор становится доступным для чтения и сервер вызывает функцию accept. Новый присоединенный дескриптор, возвращаемый функцией accept, будет иметь номер 4, если выполняются приведенные выше предположения. На рис. 6.13 показано соединение клиента с сервером.
Сервер
Клиент!Клиент!^obcs&osmqHHoH Hbooosd 匚dollisdxo①口^-xsoromsa^ooac^obcs&osmqHHoH Hbooosd 匚dollisdxo①口^-xsoromsa^ooac
Клиент!
Клиент!
^obcs&osmqHHoH Hbooosd 匚dollisdxo①口^-xsoromsa^ooac
^obcs&osmqHHoH Hbooosd 匚dollisdxo①口^-xsoromsa^ooac
Рис. 6.13. Сервер TCP после того, как первый клиент устанавливает соединение
Теперь наш сервер должен запомнить новый присоединенный сокет в своем массиве client, и присоединенный сокет должен быть добавлен в набор дескрипторов Изменившиеся структуры данных показаны на рис. 6.14.
client{]:
I0丨0丨0丨1 □ II0丨0丨0丨1 □ Imaxfd+l=5maxfd+l=5fdO fd 1 fd 2fd 3fd 4
I0丨0丨0丨1 □ I
I0丨0丨0丨1 □ I
maxfd+l=5
maxfd+l=5
[FD_SETSIZE-1] -1
Рис. 6.14. Структуры данных после того, как установлено соединение с первым клиентом
Через некоторое время второй клиент устанавливает соединение, н мы получаем сценарий, показанный на рис. 6 15.
Сервер
Клиент1Клиент1doH匚 Adyoeq江ZH工oHswgoosdu^obcs^^ootf-HqHHoHHboOOHdL!daLLisd*。①ВJSzsaemH m Asod 匚doH匚 Adyoeq江ZH工oHswgoosdu^obcs^^ootf-HqHHoHHboOOHdL!daLLisd*。①ВJSzsaemH m Asod 匚Клиент2Клиент2
Клиент1
Клиент1
doH匚 Adyoeq江ZH工oHswgoosdu^obcs^^ootf-HqHHoHHboOOHdL!daLLisd*。①ВJSzsaemH m Asod 匚
doH匚 Adyoeq江ZH工oHswgoosdu^obcs^^ootf-HqHHoHHboOOHdL!daLLisd*。①ВJSzsaemH m Asod 匚
Клиент2
Клиент2
Рис. 6.15. Сервер TCP после того, как установлено соединение со вторым клиентом
Новый присоединенный сокет (который имеет номер 5) должен быть размещен в памяти, в результате чего структуры данных меняются так, как показано на рис. 6.16.
client[]:client[]:[0][1][2][0][1][2]fdO fd1 fd 2 fd 3 fd 4 fd 5 rset: I 0 I 0 I 0 I 1 口 [kJfdO fd1 fd 2 fd 3 fd 4 fd 5 rset: I 0 I 0 I 0 I 1 口 [kJ>>[FD__SETSIZE-1][FD__SETSIZE-1]-1-14 Т 74 Т 7maxfd+l==6maxfd+l==6
client[]:
client[]:
[0]
[1]
[2]
[0]
[1]
[2]
fdO fd1 fd 2 fd 3 fd 4 fd 5 rset: I 0 I 0 I 0 I 1 口 [kJ
fdO fd1 fd 2 fd 3 fd 4 fd 5 rset: I 0 I 0 I 0 I 1 口 [kJ
>
>
[FD__SETSIZE-1]
[FD__SETSIZE-1]
-1
-1
4 Т 7
4 Т 7
maxfd+l==6
maxfd+l==6
Рис. 6.16. Структуры данных после того, как установлено соединение со вторым клиентом
[0](1][2][0](1][2]client[]:client[]:[FD_SETSIZE-1][FD_SETSIZE-1]Далее мы предположим, что первый клиент завершает свое соединение. TCP клиента отправляет сегмент FIN, превращая тем самым дескриптор номер 4 на стороне сервера в готовый для чтения. Когда наш сервер считывает этот присоединенный сокет, функция read! т пе возвращает нуль. Затем мы закрываем сокет, и соответственно изменяются наши структуры данных. Значение cl т ent[O] устанавливается в -1, а дескриптор 4 в наборе дескрипторов устанавливается в нуль. Это показано на рис. 6.17. Обратите внимание, что значение переменной maxfd не изменяется.
[0]
(1]
[2]
[0]
(1]
[2]
client[]:
client[]:
[FD_SETSIZE-1]
[FD_SETSIZE-1]
max£d+l=6max£d+l=6fdO fd 1 fd 2 fd 3 fd 4 fd 5rset: I 0 ] 0 I 0 口「丁]丁7
max£d+l=6
max£d+l=6
Рис. 6.17. Структуры данных после того, как первый клиент разрывает соединение
Итак, по мере того как приходят клиенты, мы записываем дескриптор их присоединенного сокета в первый свободный элемент массива client (то есть в первый элемент со значением -1). Следует также добавить присоединенный сокет в набор дескрипторов для чтения. Переменная тахт — это наибольший используемый в данный момент индекс в массиве cli ent, а переменная maxfd (плюс один)— это текущее значение первого аргумента функции sei ect. Единственным ограничением на количество обслуживаемых сервером клиентов является минимальное из двух значений: FD_SETSIZE и максимального числа дескрипторов, которое допускается для данного процесса ядром (о чем мы говорили в конце раздела 6.3).
В листинге 6.3 показана первая половина этой версии сервера.
Листинг 6.3. Сервер TCR использующий одиночный процесс и функцию select: инициализация
//tcpcl1serv/tcpservselectOl с
#include "unp h"、 八
продолжение 夕
234567890234567890Листинг 6.3 (продолжение) int main(int argc. char **argv) {
234567890
234567890
int i maxi, maxfd, 1istenfd connfd. sockfd.
mt nready client[FD_SETSIZE].
s$ize_t n
fd_set rset, all set char line[MAXLINE] socklen_t clilen
struct sockaddr in cliaddr. servaddr.
bstenfd= Socket(AF^INET. SDCK_STREAM.0):
bzero(&servaddr, sizeof(servaddr)).
servaddrsin_faimly = AFJNET.
servaddrs】n_addr s_addr = htonl(INADDR_ANY).
servaddrsin_port = htons(SERV_PDRT).
Bind(listenfd (SA *) &servaddr. sizeof(servaddr))
19maxfd = listenfd20maxi = -121for (1=0 1 < FD_SETSI2E22 client[i] = -1. 一23FD_ZERD(&dllset),24FD_SET(listenfd. &allset).19maxfd = listenfd20maxi = -121for (1=0 1 < FD_SETSI2E22 client[i] = -1. 一23FD_ZERD(&dllset),24FD_SET(listenfd. &allset)./* инициализация *//* индекс в массиве client[] */】++)/* -1 означает свободный элемент *//* инициализация *//* индекс в массиве client[] */】++)/* -1 означает свободный элемент */18 Listen(listenfd LISTENQ)
19maxfd = listenfd
20maxi = -1
21for (1=0 1 < FD_SETSI2E
22 client[i] = -1. 一
23FD_ZERD(&dllset),
24FD_SET(listenfd. &allset).
19maxfd = listenfd
20maxi = -1
21for (1=0 1 < FD_SETSI2E
22 client[i] = -1. 一
23FD_ZERD(&dllset),
24FD_SET(listenfd. &allset).
/* инициализация */
/* индекс в массиве client[] */
】++)
/* -1 означает свободный элемент */
/* инициализация */
/* индекс в массиве client[] */
】++)
/* -1 означает свободный элемент */
Создание прослушиваемого сокета и инициализация функции select
12-24 Этапы создания прослушиваемого сокета те же, что и раньше: вызов функций socket, bind и listen. Мы инициализируем структуры данных при том условии, что единственный дескриптор, который мы с помощью функции sei ect выберем, изначально является прослушиваемым сокетом.
Вторая половина функции mdi п показана в листинге 6.4.
Листинг 6.4. Сервер TCR использующий одиночный процесс и функцию select: цикл
//tcpcl1serv/tcpservselectOl с
for (..) {
rset = all set/* присваивание значения структуре */
nready = Seiect(maxfd + 1. &rset. NULL NULL. NULL),
if (FD_ISSET(Ъstenfd. &rset)) {/* соединение с новым клиентом */
clilen = sizeof(cliaddr),
connfd = AcceptClistenfd. (SA *) &cliaddr, &clilen).
for (i=0 1 < FD—SETSIZE. i++)
if (clientti] < 0) {
clientti] = connfd. /* сохраняем дескриптор */
break.
}
if (1 == FDJETSIZE)
err_quit("too many clients'1).
38 FD_SET(connfd. Gallset). /* добавление нового дескриптора к набору */
39if(connfd > maxfd)
40maxfd = connfd /* для функции select */
41if(1 > maxi)
42maxi = i./* максимальный индекс в массиве client[] */
43if(--nready <= 0)
44continue./* больше нет дескрипторов готовых для чтения */
45}
46 for (1 = 0, 1 <= maxi. i++) { /* проверяем все клиенты на наличие данных */
47 if ( (sockfd = client[i]) < 0)
48continue,
49 if (FD_ISSET(sockfd &rset)) {
50if ( (n = Read!we(sockfd, line. MAXLINE)) == 0) {
51/* соединение закрыто клиентом */
52Close(sockfd).
53FD_CLR(sockfd&a】lset)
54client[i] = -1.
55} else
56Wmten(sockfd.bne.n)
57if (--nready <= 0)
58break. /* больше нет дескрипторов готовых для чтения */
59}
60}
61}
62 }
Блокирование в функции select
26-27 Функция sel ect ждет, пока не будет установлено новое клиентское соединение или на существующем соединении не прибудут данные, сегмент FIN или сегмент RST.
Принятие новых соединений с помощью функции accept
28-45 Если прослушиваемый сокет готов для чтения, новое соединение установлено. Мы вызываем функцию accept, изменяя тем самым наши структуры данных. Для записи присоединенного сокета мы используем первый незадействованный элемент массива cli ent. Число готовых дескрипторов уменьшается, и если оно равно нулю, мы можем не выполнять следующий цикл for. Это позволяет нам использовать значение, возвращаемое функцией select, чтобы избежать проверки не готовых дескрипторов.
Проверка существующих соединений
46-60 Каждое существующее клиентское соединение проверяется на предмет того, содержится ли его дескриптор в наборе дескрипторов, возвращаемом функцией sel ect. Если да, то от клиента считывается строка и отражается обратно клиенту. Если клиент закрывает соединение, функция readline возвращает нуль и наши структуры данных соответствующим образом изменяются.
Мы не уменьшаем значение переменной тахт, но могли бы проверять эту возможность каждый раз, когда клиент закрывает свое соединение.
Этот сервер сложнее, чем сервер, показанный в листингах 5.1 и 5.2, но он позволяет избежать затрат на создание нового процесса для каждого клиента, что является хорошим примером использования функции sei ect. Тем не менее в разделе 15.6 мы опишем проблему, связанную с этим сервером, которая, однако, легко устраняется, если сделать прослушиваемый сокет неблокируемым, а затем проверить и проигнорировать несколько ошибок из функции accept.
Атака типа «отказ в обслуживании»
К сожалению, функционирование только что описанного сервера вызывает проблемы. Посмотрим, что произойдет, если некий клиент-злоумышленник соединится с сервером, отправит 1 байт данных (отличный от разделителя строк) и войдет в состояние ожидания. Сервер вызовет функцию readl т пе, которая прочитает одиночный байт данных от клиента и заблокируется в следующем вызове функции read, ожидая следующих данных от клиента. Сервер блокируется (вернее, «подвешивается») этим клиентом и не может предоставить обслуживание никаким другим клиентам (ни новым клиентским соединениям, ни данным существующих клиентов), пока упомянутый клиент-злоумышленник не отправит символ перевода строки или не завершит свой процесс.
Дело в том, что обрабатывая множество клиентов, сервер никогда не должен блокироваться в вызове функции, относящейся к одному клиенту. В противном случае можно «подвесить» сервер, что приведет к отказу в обслуживании для всех остальных клиентов. Это называется атакой типа «отказ в обслуживании» (DoS attack — Denial of Service). Такая атака воздействует на сервер, делая невозможным обслуживание нормальных клиентов. Обезопасить себя от подобных атак позволяют следующие решения: использовать неблокируемый ввод-вывод (см. главу 15), предоставлять каждому клиенту обслуживание отдельным потоком (например, для каждого клиента порождать процесс или поток) или установить таймаут для ввода-вывода (см. раздел 13.2).
Функция pselect
Функция pselect была введена в Posix. 1g.
include <sys/select h> 和nclude 〈signal h> include <time h>
mt pselect(int maxfdpl, fd_set *readset. fd_set *wnteset fd_set *exceptset, const struct timespec *timeout. const sigset_t *s】gmask).
Возвращает количество готовых дескрипторов. О в случае тайм-аута -1 в случае оимбки Функция psel ect имеет два отличия от обычной функции sei ect:
Функция psel ect использует структуру timespec, нововведение стандарта реального времени Posix. 1Ь, вместо структуры timeval.
struct timespec {
tmjt tv_sec. /* секунды */
long tv_nsec. /* наносекунды */
}. 一
Эти структуры отличаются вторыми элементами: элемент tv^nsec новой структуры задает наносекунды, в то время как элемент tv_usec прежней структуры задает микросекунды.
В функции psel ect добавляется шестой аргумент — указатель на маску сигналов. Это позволяет программе отключить доставку ряда сигналов, проверить некоторые глобальные переменные, установленные обработчиками этих отключенных сигналов, а затем вызвать функцию pselect, сообщив ей, что нужно переустановить маску сигналов.
В отношении второго пункта рассмотрим следующий пример (описанный на с. 308-309 [93]). Обработчик сигнала нашей программы для сигнала SIGINT просто устанавливает глобальную переменную т ntr_f 1 ад и возвращает управление. Если наш процесс блокирован в вызове функции select, возвращение из обработчика сигнала заставляет функцию завершить работу, присвоив errno значение EINTR. Но когда вызывается функция sei ect, код выглядит следующим образом: if (intr_flag)
handle_intr(), /* обработка этого сигнала */
if ( (nready = select( )) < 0) {
if (errno == EINTR) {
if (intr_flag) handle_intr().
} 一
}
Проблема заключается в том, что если в течение промежутка времени между проверкой переменной intr_flag и вызовом функции select приходит сигнал, он будет потерян в том случае, если функция select заблокирует процесс навсегда. С помощью функции pselect мы можем переписать этот пример так, чтобы он работал более надежно:
sigset_t newmask. oldmask zeromask.
sigemptyset(&zeromask),
sigemptysetC&newmask),
sigaddsetC&newmask. SIGINT),
sigprocmask(SIG_BLOCK. &newmask, &oldmask): /* блокирование сигШа SIGINT */ if (intr_flag)-
handle_intr(). /* обработка этого сигнала */ if ( (nready = pselect(, &zeromask)) < 0) {
if (errno == EINTR) {
if (intr_flag) handle_intr().
} 一
}
Перед проверкой переменной intjflag мы блокируем сигнал SIGINT, Когда вызывается функция psel ect, она заменяет маску сигналов процесса пустым набором (zeromask), а затем проверяет дескрипторы, возможно, переходя в состояние ожидания. Но когда функция pselect возвращает управление, маске сигна-
лов процесса присваивается то значение, которое предшествовало вызову функции pselect (то есть сигнал SIGINT блокируется).
Мы поговорим о функции psel ect более подробно и приведем ее пример в разделе 18.5. Функцию pselect мы используем в листинге 18.3, а в листинге 18.4 показываем простую, хотя и некорректную реализацию этой функции.
ПРИМЕЧАНИЕ
Есть одно незиачительное отличие между двумя функциями select. Первый элемент структуры timeval является целым числом типа long со знаком, в то время как первый элемент структуры timspec имеет тип time_t. Число типа long со знаком в первой функции также должно было относиться к типу time_t, но мы не меняли его тип, чтобы не разрушать существующего кода. Однако в новой фуикции это можно было бы сделать.
Функция poll
Функция poll появилась впервые в SVR3, и изначально ее применение ограничивалось потоковыми устройствами (streams devices) (см. главу 33). В SVR4 это ограничение было снято, что позволило функции poll работать с любыми дескрипторами. Функция pol 1 предоставляет функциональность, аналогичную функции select, но позволяет получать дополнительную информацию при работе с потоковыми устройствами.
#】nclude <poll h>
mt polПstruct pollfd *fdarray. unsigned long nfds. mt timeout).
Возвращает- количество готовых дескрипторов, 0 в случае тайм-аута, -1 в случае ошибки
Первый аргумент — это указатель на первый элемент массива структур. Каждый элемент массива — это структура pol 1 fd, задающая условия, проверяемые для данного дескриптора fd.
struct pol 1 fd {
int fd. /* дескриптор, который нужно пооверить */
short events,/* события на дескрипторе. которые нас интересуют */
short revents. /* события, промзо!иеп1име на дескрипторе fd */
}■
Проверяемые условия задаются элементом events, н состояние этого дескриптора функция возвращает в соответствующем элементе revents. (Наличие двух переменных для каждого дескриптора, одна из которых — значение, а вторая — результат, дает возможность обойтись без аргументов типа «значение-результат». Вспомните, что три средних аргумента функции select имеют тип «значение-результат».) Каждый из двух элементов состоит из одного или более битов, задающих определенное условие. В табл. 6.2 перечислены константы, используемые для задания флага events и для проверки флага revents.
Мы разделили эту таблицу на три части; первые четыре константы относятся ко вводу, следующие три — к выводу, а последние три — к ошибкам. Обратите внимание, что последние три константы не могут устанавливаться в элементе events, но всегда возвращаются в revents, когда выполняется соответствующее условие.
Существует три класса данных, различаемых функцией poll: обычные, приоритетные и данные с высоким приоритетом. Эти термины берут начало в реализациях. основанных на потоках Гпис. ЗТ51* *' 4
Таблица 6.2. Различные значения флагов events и revents для функции poll
КонстантаНа входе (events)
POLLIN•
POLLRDNORM •
POLLRDBAND •
POLLPRI•
На выходе (revents) Описание
Можно считывать обычные или приоритетные данные
Можио считывать обычные данные
Можно считывать
«приоритетные данные
“ Можно считывать данные
с высоким приоритетом
POLLOUT•
POLLWRNORM •
POLLWRBAND •
.、-Можно записывать обычные
данные
Можно записывать обычные данные
Можно записывать
приоритетные данные
POLLERR
POLLHUP POLLNVAL
Произошла ошибка
Произошел разрыв соединения Дескриптор не соответствует открытому файлу
ПРИМЕЧАНИЕ
Константа POLLIN может быть задана путем логического сложения констант POL- LRDNORM и POLLRDBAND. Константа POLLIN существовала еще в реализациях SVR3, которые предшествовали полосам приоритета в SVR4, то есть эта константа существует в целях обратной совместимости. Аналогично, константа POLLOUT эквивалентна POLLWRNORM, и первая из них предшествовала второй.
Для сокетов TCP и UDP при описанных условиях функция poll возвращает указанный флаг revent. К сожалению, в определении функции poll стандарта Posix.lg имеется множество слабых мест (дополнительных возможностей возвратить то же условие):
哀 Все регулярные данные TCP н все данные UDP считаются обычными.
/: Внеполосные данные TCP (см. главу 21) считаются приоритетными.
杀 Когда считывающая половина соединения TCP закрывается (например, если получен сегмент FIN), это также считается равнозначным обычным данным, и последующая операция чтения возвратит нуль.
А Наличие ошибки для соединения TCP может расцениваться либо как обычные данные, либо как ошибка (POLLERR). В любом случае последующая функция read возвращает -1,что сопровождается установкой переменной errno в соответствующее значение. Так обрабатываются такие условия, как получение RST или тайм-аута.
Информация о доступности нового соединения на прослушиваемом сокете может считаться либо обычными, либо приоритетными данными. В больший-
Число элементов в массиве структур задается аргументом nfds.
ПРИМЕЧАНИЕ
Исторически этот аргумент имел тип long без знака, что является некоторым излишеством. Достаточно будет типа int без знака. В Unix 98 для этого аргумента определяется новый тип — ngfds t.
Аргумент timeout определяет, как долго функция находится в ожидании перед завершением. Положительным значением задается количество миллисекунд — время ожидания. В табл. 6.3 показаны возможные значения аргумента timeout.
Таблица 6.3. Значения аргумента timeout для функции poll
Значение аргумента timeout
Описание
INFTIM
Ждать вечно
0
Возвращать управление немедленно, без блокирования
>0
Ждать в течение указанного числа миллисекунд
Константа INFTIM определена как отрицательное значение. Если таймер в данной системе не обеспечивает точность порядка миллисекунд, значение округляется в большую сторону до ближайшего поддерживаемого значения.
ПРИМЕЧАНИЕ
Posix.lg требует, чтобы константа INFTIM была определена в заголовочном файле <poll.h>, но многие системы все еще определяют ее в заголовочном файле <sys/ stropts.h>.
Как и в случае функции select, любой тайм-аут, установленный для функции poll, ограничивается снизу разрешающей способностью часов в данной реализации (обычно 10 миллисекунд).
Функция poll возвращает -1, если произошла ошибка, 0 — если нет готовых дескрипторов до истечения времени таймера; иначе возвращается число дескрипторов с ненулевым элементом revents.
Вспомните наши рассуждения в конце раздела 6.3 относительно константы FD_SETSIZE и максимального числа дескрипторов в наборе в сравнении с максимальным числом дескрипторов для процесса. У нас не возникает подобных проблем с функцией poll, поскольку вызывающий процесс отвечает за размещение массива структур pollfd в памяти и за последующее сообщение ядру числа элементов в массиве. Не существует типа данных фиксированного размера, аналогичного f d_set, о котором знает ядро.
ПРИМЕЧАНИЕ
Posix.lg требует наличия и функции select, и функции poll. Но если сравнивать их с точки зрения переносимости, то функцию select в настоящее время поддерживает больше систем, чем функцию poll. Posix.lg определяет также функцию pselect — усовершенствованную версию функции select, которая обеспечивает возможность блокирования сигналов и предоставляет лучшую разрешающую способность по времени, а для функции poll ничего подобного в Posix.lg нет.
Эхо-сервер TCP (еще раз)
Теперь мы изменим наш эхо-сервер TCP из раздела 6.8, используя вместо функции sei ect функцию poll. В предыдущей версии сервера, работая с функцией sei ect, мы должны были выделять массив client вместе с набором дескрипторов rset (см. рис. 6.12). С помощью функции poll мы разместим в памяти массив структур poll fd. В нем же мы будем хранить и информацию о клиенте, не создавая для нее другой массив. Элемент fd этого массива мы обрабатываем тем же способом, которым обрабатывали массив client (см. рис. 6.12): значение -1 говорит о том, что элемент не используется, а любое другое значение является значением дескриптора. Вспомните из предыдущего раздела, что любой элемент в массиве структур poll fd, передаваемый функции poll с отрицательным значением элемента fd, просто игнорируется.
В листинге 6.5 показана первая часть кода нашего сервера.
Листинг 6.5. Первая часть сервера TCR использующего функцию poll //tcpcliserv/tcpservpol 101 с
include "unp h"
#include<limits h>/* для OPEN_MAX */
int
main(int argc. char **argv)
{
inti maxi.listenfd, connfd. sockfd.
intnready
ssize t n.
charline[MAXLINE].
socklen_t clilen.
struct pol1fd client[OPEN_MAX].
struct sockaddr_in cliaddr. servaddr.
listenfd « Socket(AF_INET. SOCK_STREAM, 0);
bzero(&servaddr. sizeof(servaddr)),
servaddr sin_family - AF_INET.
servaddr sin_addr s_addr - htonl(INADDR^ANY):
17servaddr szn_port - htons(SERV_PORT).
18 BindClistenfd. (SA *) &servaddr, sizeof(servaddr)).
9 0 12 3 41 2 2 2 2 29 0 12 3 41 2 2 2 2 2Listendistenfd. LISTENQ).
9 0 12 3 4
1 2 2 2 2 2
9 0 12 3 4
1 2 2 2 2 2
client[0] fd = listenfd.
client[0] events = POLLRDNORM.
for (1 - 1. 1 < OPEN—MAX. i++)
client[i].fd = -1./* -1 означает, что элемент свободен */
maxi =0./* максимальный индекс массива client[] */
Размещение массива структур pollfd в памяти
И Мы объявляем элементы OPEN_MAX в нашем массиве структур ро〕〕fd. Определение максимального числа дескрипторов, которые могут быть одновременно открыты для процесса, выполняется непросто. Мы снова столкнемся с этой про-
блемой в листинге 12.1. Один из способов ее решения 一 вызвать функцию sysconf Posix. 1 с аргументом _SC_OPEN_MAX [93, с. 42-44], а затем динамически выделять в памяти место для массива соответствующего размера. Но одним из возвращаемых функцией sysconf значений может быть indeterminate (неопределенность), говорящее о том, что нам нужно самим задавать значение. Здесь мы используем только константу OPEN_MAX стандарта Posix. 1.
Инициализация
20-24 Мы используем первый элемент в массиве cl lent для прослушиваемого сокета и присваиваем дескрипторам для оставшихся элементов -1. Мы также задаем в качестве аргумента функции poll событие POLLRDNORM, чтобы получить уведомление от этой функции в том случае, когда новое соединение будет готово к приему. Переменная maxi содержит максимальный индекс массива client, используемый в настоящий момент.
Вторая часть нашей функции приведена в листинге 6.6.
Листинг 6.6. Вторая часть сервера TCR использующего функцию poll //tcpcl1serv/tcpservpol101 с 25 for (..) {
26 nready = Poll(client maxi + 1 INFTIM)
27 if (client[0] revents & POLLRDNORM) { /* новое соединение с клиентом */
28 clilen = sizeof(cliaddr).
29connfd = AcceptClistenfd (SA *) &cliaddr. &clilen).
30for C = 1. 1 < OPEN_MAX. i++)
31if (client[i] fd <一0) {
32client[i]fd = connfd. /*сохраняем дескриптор */
33break.
34}
35if (1 — OPEN_MAX)
36err_qu〕t("t。。many clients").
37clientfi] events =POLLRDNORM
38if (i > maxi)
39 maxi =】./* максимальный индекс в массиве client[] */
40 if (--nready <= 0)
41continue. /* больше нет дескрипторов. готовых для чтения */
42}
43 for (1 = 1. 1 <= maxi i++) { /* проверяем все клиенты на наличие данных */
44 if ( (sockfd = client[i] fd) < 0)
45 continue.
46 if (cl〕ent[i] revents & (POLLRDNORM | POLLERR)) {
47 if ( (n = read!me(sockfd line. MAXLINE)) < 0) {
48if (errno =* ECONNRESET) {
49/* соединение переустановлено клиентом */
50Close(sockfd).
51client[i] fd = -1
52} else
53err_sys(' read lineerror1'),
54} else if (n === 0) f
56Close(sockfd).
57client[i] fd =-1,
58} else
59Writen(sockfd.linen),
60if (--nready <= 0)
61break,/* больше нет дескрипторов. готовых для чтения */
62}
63}
64}
65 }
Вызов функции poll, проверка нового соединения
26-42 Мы вызываем функцию poll для ожидания нового соединения либо данных на существующем соединении. Когда новое соединение принято, мы находим первый свободный элемент в массиве cl i ent — это первый элемент с отрицательным дескриптором. Обратите внимание, что мы начинаем поиск с индекса 1, поскольку элементе! i ent[0] используется для прослушиваемого сокета. Когда свободный элемент найден, мы сохраняем дескриптор и устанавливаем событие PDLLRDNORM.
Проверка данных на существующем соединении
43-63 Два события, которые нас интересуют, — это POLLRDNORM и POLLERR. Второй флаг в элементе event мы не устанавливали, поскольку этот флаг возвращается всегда, если соответствующее условие выполнено. Причина, по которой мы проверяем событие POLLERR, в том, что некоторые реализации возвращают это событие, когда приходит сегмент RST, другие же в такой ситуации возвращают событие POLLRDNORM. В любом случае мы вызываем функцию readl i ле, и если произошла ошибка, эта функция возвратит ее. Когда существующее соединение завершается клиентом, мы просто присваиваем элементу fd значение -1.■
6.12. Резюме
В Unix существует пять различных моделей ввода-вывода:
厶 блокируемый ввод-вывод;
餌 неблокируемый ввод-вывод;
畿 мультиплексирование ввода-вывода;
綫 управляемый сигналом ввод-вывод; „«
零 асинхронный ввод-вывод.
По умолчанию используется блокируемый ввод-вывод, и этот вариант встречается наиболее часто. Неблокируемый ввод-вывод и управляемый сигналом ввод-вывод мы рассмотрим в последующих главах. В этой главе мы рассмотрели мультиплексирование ввода-вывода. Асинхронный ввод-вывод определяется в стандарте Posix.l, но поддерживающих его реализаций не так много.
Наиболее часто используемой функцией для мультиплексирования ввода- вывода является функция select. Мы сообщаем этой функции, какие дескрипторы нас интересуют (для чтения, записи или условия ошибки), а также передаем
ченное на единицу). Большинство вызовов функции select определяют количество дескрипторов, готовых для чтения, и, как мы отметили, единственное условие исключения при работе с сокетами — это прибытие внеполосных данных (см. главу 21). Поскольку функция select позволяет ограничить время блокирования функции, мы используем зто свойство в листинге 13.3 для ограничения по времени операции ввода.
Используя эхо-клиент в пакетном режиме с помощью функции select, мы выяснили, что даже если обнаружен конец ввода, данные все еще могут находиться в канале на пути к серверу или от сервера. Обработка этого сценария требует применения функции shutdown, которая позволяет воспользоваться таким свойством TCP, как возможность половинного закрытия соединения (halfclose feature).
Posix.lg определяет новую функцию psel ect, повышающую точность таймера с микросекунд до наносекунд, которой передается новый аргумент — указатель на набор сигналов. Это позволяет избежать ситуации гонок (race condition) при перехвате сигналов, о которой мы поговорим более подробно в разделе 18.5.
Функция poll из System V предоставляет функциональность, аналогичную функции sei ect. Кроме того, она обеспечивает дополнительную информацию при работе с потоковыми устройствами. Posix 1g требует наличия и функции select, и функции poll, но первая используется чаще.
Упражнения
Мы говорили, что набор дескрипторов можно присвоить другому набору дескрипторов, используя оператор присваивания языка С. Как это сделать, если набор дескрипторов является массивом целых чисел? (Подсказка: посмотрите на свой системный заголовочный файл <sys/select h> или <sys/types h>.)
Описывая в разделе 6.3 условия, при которых функция select сообщает, что дескриптор готов для записи, мы указали, что сокет должен быть неблокируемым, для того чтобы операция записи возвратила положительное значение. Почему?
Что произойдет в листинге 6.1, если мы поставим слово el se перед i f в строке 19?
В листинге 6.3 добавьте необходимый код, чтобы позволить серверу использовать максимальное число дескрипторов, допустимое ядром. (Подсказка: рассмотрите функцию setrl innt.)
Посмотрите, что происходит, если в качестве второго аргумента функции shutdown передается SHUT_RD. Возьмите за основу код клиента TCP, представленный в листинге 5.3, и выполните следующие изменения: вместо номера порта SERV_PORT задайте порт 19 (служба chargen, см. табл. 2.1), а также замените вызов функции str_cli вызовом функции pause. Запустите программу, задав IP-адрес локального узла, на котором выполняется сервер chargen. Просмотрите пакеты с помощью такой программы, как, например, tcpdump (см. раздел В.5). Что происходит?
Почему приложение должно вызывать функцию shutdown с ар!ументом SHUT_RDWR, вместо того чтобы просто вызвать функцию cl ose?
Что происходит в листинге 6.4, когда клиент отправляет RST для завершения соединения?
Перепишите код, показанный в листинге 6.5, чтобы вызывать функцию sysconf для определения максимального числа дескрипторов и размещения соответствующего массива cl lent в памяти..
ГЛАВА 7
Параметры сокетов
Введение
Существуют различные способы получения и установки параметров сокетов: функции getsockopt и setsockopt;
, функция fcntl;
функция wctl.
Эту главу мы начнем с описания функций getsockopt и setsockopt. Далее мы приведем пример, в котором выводятся заданные по умолчанию значения параметров, а затем дадим подробное описание всех параметров сокетов. Мы разделили описание параметров на следующие категории: общие, IPv4, IPv6 и TCP. При первом прочтении главы можно пропустить подробное описание параметров и при необходимости прочесть отдельные разделы, на которые даны ссылки. Отдельные параметры подробно описываются в дальнейших главах, например параметры многоадресной передачи IPv4 и IPv6 мы обсуждаем в разделе 19.5.
Мы также рассмотрим функцию fcntl, поскольку она реализует предусмотренные стандартом Posix возможности отключить для сокета блокировку ввода- вывода, включить управление сигналами, а также установить владельца сокета. Функцию wctl мы опишем в главе 16.
Функции getsockopt и setsockopt
Эти две функции применяются только к сокетам.
#mclude <sys/socket h>
int getsockopt(int sockfd mt level, int optname. void *optval, socklen_t *optlen)
int setsockoptdnt sockfd, mt level. int optname const void *optval. socklen_t optlen)
Переменная sockfd должна ссылаться на открытый дескриптор сокета. Переменная 1 evel определяет, каким кодом должен интерпретироваться параметр: общими программами обработки сокетов или зависящими от протокола программами (например, IPv4, IPv6 или TCP).
optval — это указатель на переменную, из которой извлекается новое значение параметра с помощью функции setsockopt или в которой сохраняется текущее значение параметра с помощью функции getsockopt. Размер этой переменной задается последним аргументом. Для функции setsockopt тип этого аргумента — «значение», а для функции getsockopt — «значение-результат».
В табл. 7.1 приведены параметры, которые могут запрашиваться функцией getsockopt или устанавливаться функцией setsockopt. В колонке «Тип данных» приводится тип данных того, на что указывает указатель optval для каждого параметра. Две фигурные скобки мы используем для того, чтобы обозначить структуру, —например, linger {} обозначает struct linger.
Существует два основных типа параметров: двоичные параметры, включающие или отключающие определенное свойство (флаги), и параметры, получающие и возвращающие специфичные значения, которые мы можем либо задавать, либо проверять (значения). В колонке «①лаг» определяется, относится ли параметр к флагам. Для флагов при вызове функции getsockopt *optval является целым числом. Возвращаемое значение *optval нулевое, если параметр отключен, и ненулевое, если параметр включен. Аналогично, функция setsockopt требует ненулевого значения *optval для включения параметра, и нулевого значения — для его выключения. Если в колонке «①лаг» не содержится символа «•», то параметр используется для передачи значения заданного типа между пользовательским процессом и системой.
В последующих разделах этой главы приводятся дополнительные подробности о параметрах сокетов.
Таблица 7.1. Параметры сокетов для функций getsockopt и setsockopt
leveloptnameget set ОписаниеФлаг Тип данных
SO_BROADCAST
Позволяет посылать широковещательные дейтаграммы Разрешает отладку
int
int
SO_DEBUG
SO_DONTROUTE
SO—ERROR
•
Обходит таблицу маршрутизаци и Получает ошибку, ожидающую обработки, и возвращает значение параметра в исходное состояние
•
int
int
SO_KEEP ALIVE
•
•
Периодически проверяет, находится ли соединение в рабочем состоянии
•
int
SO—LINGER
•
•
Задерживает закрытие сокета, если имеются данные для отправки
hnger{}
SO_OOBINLINE
Оставляет полученные внеполосные данные вместе с обычными данными (inline)
•
int
SO_RCVBUF
•
•
Размер приемного буфера
int
SO_SNDBUF
•
•
Размер буфера отправки
int
SO_RCVLOWAT
•
•
Минимальное количество данных для приемного буфера сокета
int
SOL SOCKET
Таблица 7.1 (продолжение)
level
optname
get
set
Описание
Флаг Тип данных
SO—SNDLOWAT
•
•
Минимальное коли-
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
•
Возвращает IP-адрес получателя
•
int
IP_RECVIF
•
Возвращает индекс интерфейса, на кото-
•
int
ром принимается дейтаграмма UDP
IP_TOS
•
Тип сервиса
int
и приоритет
IP_TTL
•
Время жизни
int
IP_MULTICAST_IF
•
Задает интерфейс для исходящих дейтаграмм
in_addr{}
IP_MULTICAST_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, которое передается процессу
leveloptnameget set ОписаниеФлаг Тип данных
IPPROTO_IPV6 IPV6_ADDRFORM •• Меняет формат адресаint
сокета
IPV6_CHECKSUM •• Отступ поля контроль-int
ной суммы ДЛЯ СИМВОЛЕ-
ных (неструктурирован-
ных) сокетов
IPV6_DSTOPTS•• Получение параметров •mt
получателя
IPV6_HOPLIMIT•• Получение предельно- •int
го количества транзит-
ных узлов для однона-
правленной передачи
(unicast hop limit)
IPV6_HOPOPTS•• Получение парамет- •int
ров транзитных
узлов (hop-by-hop
options)
IPV6_NEXTHOP•• Задает следующий•sockaddrfl
транзитный адрес
IPV6_PKTINFO•• Получает информа- •int
цию о пакете
IPV6_PKTOPTIONS •• Задает параметрыСм текст
пакета
IPV6_RTHDR•• Получает маршрут •int
IPV6_UNICAS_•• Предел количестваint
THOPSтранзитных узлов,
задаваемый по
умолчанию
IPV6_MULTICAST_ •• Задает интерфейс дляin6_addr{}
IFисходящих
дейтаграмм
IPV6_MULTICAST_ •• Задает предельноеu_int
HOPSколичество транзит-
ных узлов для исхо-
дящих широковеща-
тельных сообщений
IPV6_MULT1CAST_ ••Разрешает или отме- •u_int
LOOPняет отправку копии
дейтаграммы на тот
узел, откуда она была
послана (loopback)
IPV6_ADD_• Включение в группуipv6_mreq{}
MEMBERSHIPмногоадресной
передачи
IPV6 DROP_• Отключение отipv6__mreq{}
MEMBERSHIPгруппы многоадресной
передачи
IPPROTO_TCP ТСР_КЕЕРALIVE•• Время простоя в секун-int
дах до проверки
TCP_MAXRT•• Максимальное времяint
для повторной передачи
TCP
продолжение 夕
Таблица 7.1 (продолжение)
<ant^mKttwvMtwm«^
level
>^>м«с«<^(^х(«л<«<<ык<л^к«<л^{^хл^т>>^ optname
get
set
Описание
Флаг Тип данных
TCP__MAXSEG
•
•
Максимальный размер сегмента TCP
int
TCP一NODELAY
•
•
Отключает алгоритм Нагла
•int
TCP_STDURG
•
Интерпрет ация срочного указателя
•int
Проверка наличия параметра и получение значения по умолчанию
Напишем программу, которая проверяет, поддерживается ли большинство параметров, представленных в табл. 7.1, и если да, то выводит их значения, заданные по умолчанию. В листинге 7.11 содержатся объявления нашей программы.
Листинг 7.1. Объявления для нашей программы, проверяющей параметры сокетов
//sockopt/checkopts с
include "unp h"
#include <netinet/tcp h> /* 。叩епеленмя констант ТСР_ххх */
3
union val {
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
intl_Vdl.
longl_val.
charc_val[10],
struct 11 nger11 ngejval ;
struct timevaltimeva\_val;
} val.
static char *sock_str_flag(union val *, int); static char *sock_str_int(union val *・ int): static char *sock_str_lmger(union val *. int): static char *sock_str_timeval(union val *. int):
struct sock_opts {
char *opt_str.
intoptjevel:
intopt_name.
char *(*opt_val_str)(union val *. mt): } sock_opts[] = {
"SO BROADCAST'1. SOL SOCKET, SO BROADCAST.
sock_str一fl ag.
21
"SOJEBUG".
SOL SOCKET.
30一DEBUG.
sock_str_fl ag.
22
”SO_DONTROUTE”
. SOL_SOCKET,
SO 二DDNTROUTE.
sock_str_flag.
23
”SO[ERROR".
SOL_SOCKET.
SO 二ERROR.
sock_str_int.
24
,,SO_KEEPALIVEM
. SOLJOCKET,
SO~KEEPALIVE.
sock_str_f1ag.
25
"SALINGER” ・
SOL_SOCKET.
SOJINGER.
sock_str_linger
26
"SO_OOBINLINE“
. SOL_SOCKET.
SO"OOBINLINE.
sock_str_flag.
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http.// www.Diter com/download
27
•'SO_RCVBUF”.
SOL_SOCKET,
SO_RCVBUF.
sock_str一】nt.
28
"SO二SNDBUF” ・
SOL_SOCKET.
SO—SNDBUF.
sock_str_int.
29
"SO 二RCVLOWAT”.
SOL二 SOCKET.
SO^RCVLOWAT.
sock_str_int.
30
'•SOjNOLOWAT".
SOL_SOCKET.
SO_SNDLOWAT.
sock_str_int.
31
••soZrcvtimeo*' f
SOL_SOCKET・
SO 二RCVTIMEO
sock_str_timeval
32
"SUSNDTIMEO",
sql[socket.
SO_SNDTIMEO.
sock_str_t】meva1
33
"S—REUSEADDR".
SOL_SOCKET.
SO^REUSEADDR.
sock_str_flag.
34
#】fdef ~SO_REUSEPORT
35
"SOJEUSEPORT".
SOL_SOCKET,
SO_REUSEPORT,
sock_str_flag.
36
#else
37
"SO—REUSEPORT.
0,
0.
NULL.
38
#endif
39
“SOJTYPE”.
SOL_SOCKET,
SO_TYPE・
sock_str_int.
40
"SO 二 USELOQPBACK".
SOL 二SOCKEL
SO~USELOOPBACK,
sock_str_flag.
41
TP—TOS".
IPPROTO_IP,
IPJOS.
sock_str_i nt.
42
"IP^TTL".
IPPROTO~IP.
IP TTL.
sock_str_mt.
43
"TCP_MAXSEG",
IPPROTO_TCP.TCP_MAXSEG.
sock_str_int.
44
"TCP—NODELAY”.
IPPROTO~TCP.TCP二NODELAY ・
sock_str_flag.
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 показана наша функция mai п.
Листинг 7.2. Функция main для проверки параметров сокетов //sockopt/checkopts с 47 int
ma】n(int argc. char **argv)
{
продолжение 少продолжение 少mt fd. len.
продолжение 少
продолжение 少
Листинг 7.2 (продолжение)
struct sock_opts *ptr
2 34567890123456785 55555556666668662 34567890123456785 5555555666666866fd = Socket(AFJNET SOCK_STREAM, 0)
2 3456789012345678
5 5555555666666866
2 3456789012345678
5 5555555666666866
for (ptr = sock_opts ptr->opt__str i= NULL; ptr++) { printf("^s ” ptr->opt_str), if (ptr->opt_val_str " NULL)
printf("(undefined)\en")
el se {
len = sizeof(val)
if (getsockopt(fd ptr->optjevel, ptr->opt_name.
&val. &len) g -1) {
err_ret("getsockopt error").»
} else {
pnntf("default 工 ^s\en". (*ptr->opt val str) (&val, len));
}--
}
}
exit(0).
}
Создание сокета TCP, перебор всех параметров
52-56 Мы создаем сокет TCP, а затем перебираем все элементы нашего массива. Если указатель opt_val_str пустой, то параметр не определен реализацией (что, как мы показали, возможно для SO_REUSEPORT).
Вызов функции getsockopt
57-61 Мы вызываем функцию getsockopt, но не завершаем ее выполнение, если возвращается ошибка. Многие реализации определеяют имена некоторых параметров сокетов, даже если не поддерживают эти параметры. Неподдерживаемые параметры выдают ошибку ENOPROTOOPT.
Вывод значения параметра по умолчанию
62-63 Если функция getsockopt успешно завершается, мы вызываем нашу функцию для преобразования значения параметра в строку и выводим эту строку.
В листинге 7.1 мы показали четыре прототипа функций, по одному для каждого типа возвращаемого значения параметра В листинге 7.3 показана одна из этих функций, sock_str_f1ag, которая выводит значение параметра, являющегося флагом. Другие три функции аналогичны этой.
Листинг 7.3. Функция sock_str_flag: преобразование флага в строку //sockopt/checkopts с 69 static char strres[128]
70 static char *
71 sock str_flag(union val *ptr mt len)
72 {- 一
73 if (len *= sizeof(int))
74 snprintfCstrres. sizeof(strres) "size (Zd) not sizeof(lnt)". len):
75 else
767778767778snpnntf(strres, sizeof(strres).
76
77
78
76
77
78
•^s". (ptr->i_val " 0) ? "off* "on"), return(strres).
73-78 Вспомните, что последний аргумент функции getsockopt — это аргумент типа «значение-результат». Первое, что мы проверяем, — это то, что размер значения, возвращаемого функцией getsockopt, совпадает с предполагаемым. В зависимости от того, является ли значение флага нулевым или нет, возвращается строка off или on.
Выполнение этой программы под AIX 4.2 дает следующий вывод:
aix checkopts
SOJROADCAST 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_1mger 工 0
SOZOOBINLINE default 二 off 一
SO"RCVBUF default 士 16384
SO 二 SNDBUF default = 16384
SOlRCVLOWAT default » 1
SOJNDLOWAT default = 4096
SO_RCVTIMEO default = 0 sec 0 usee
SO^SNDTIMEO default » 0 sec 0 usee
SO 二REUSEADDR default - off
SO 二REUSEPORT (undefined)
SO 二 TYPE default = 1
SOIUSELOOPBACK default = off
IP JOS default 工 0
IP_TTL default = 60
TCP_MAXSEG default 工 512
TCP—NODELAY default 工 off
Значение 1, возвращаемое для параметра SO_TYPE, для этой реализации соответствует SOCK_STREAM.
Состояния сокетов
Для некоторых параметров сокетов время их установки или получения зависит некоторым образом от состояния сокета Ниже мы обсудим эту зависимость для тех параметров, к которым это относится.
Следующие параметры сокетов наследуются присоединенным сокетом TCP от прослушиваемого сокета [105, с. 462-463]: SO_DEBUG, SO_DONTROUTE, SO_KEEPALIVE, SO_LINGER, SO_OOBINLINE, SO_RCVBUF и SO_SNDBUF. Это'важнодля TCP, поскадысу присоединенный сокет не возвращается серверу функцией accept, пока трехэтапное рукопожатие не завершится на уровне TCP Если мы хотим убедиться при завершении трехэтапного рукопожатия, что один из этих параметров установлен для присоединенного сокета, нам следует установить этот параметр для прослушиваемого сокета.
Общие параметры сокетов
Мы начнем с обсуждения общих параметров сокетов. Эти параметры не зависят от протокола (то есть они управляются не зависящим от протокола кодом внутри ядра, а не отдельным модулем протокола, такого как 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.
Этот параметр часто используется демонами маршрутизации (routed и gated) для того, чтобы миновать таблицу маршрутизации (в случае, если таблица маршрутизации неверна) и заставить пакет отправиться на определенный интерфейс.
Параметр сокета SO_ERROR
Когда на сокете происходит ошибка, модуль протокола в ядре, происходящем от Беркли, присваивает переменной so_error для этого сокета одно из стандартных значений Unix Еххх. Это так называемая ошибка, требующая обработки (pending eiror) для данного сокета. Процесс может быть немедленно оповещен об ошибке одним из двух способов:
Если процесс блокируется в вызове функции select (см. раздел 6.3), ожидая готовности данного сокета к чтению или записи, функция sei ect возвращает управление и уведомляет процесс о соответствующем состоянии готовности.
Если процесс использует управляемый сигналом ввод-вывод (см. главу 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 ненулевое, то при вызове процессом функции write возвращается -1 с еггпо, равной значению переменной so_error [105, с. 495], а значение so_error сбрасывается в 0.
ПРИМЕЧАНИЕ
В коде, показанном па с. 495 [105], есть ошибка: so error не сбрасывается в 0. Она была выявлена в реализации BSD/OS. Всегда, когда для сокета возвращается ошибка, требующая обработки, so error должна быть сброшена в 0.
Здесь вы впервые встречаетесь с параметром сокета, который можно получить, по нельзя установить.
Параметр сокета SO_KEEPALIVE
Когда параметр SO KEEPALIVE установлен для сокета TCP и в течение 2 часов не происходит обмена данными по сокету в любом направлении, TCP автоматически посылает собеседнику проверочное сообщение (keepalive probe). Это сообщение —сегмент TCP, на который собеседник должен ответить. Далее события могут развиваться по одному из трех сценариев.
Собеседник отвечает, присылая ожидаемый сегмент АСК. Приложение не получает уведомления (поскольку все в порядке). TCP снова отправит одно проверочное сообщение еще через 2 часа отсутствия активности в этом соединении.
Собеседник отвечает, присылая сегмент RST, который сообщает локальному TCP, что узел собеседника вышел из строя и перезагрузился. Ошибка сокета, требующая обработки, устанавливается равной ECONNRESET, и сокет закрывается.
На проверочное сообщение не приходит ответ от собеседника. Код TCP, происходящий от Беркли, отправляет восемь дополнительных проверочных со* общений с интервалом в 75 секунд, пытаясь выявить ошибку. TCP прекратит попытки, если ответа не последует в течение 11 минут и 15 секунд после отправки первого сообщения. Если на все проверочные сообщения TCP не приходит ответа, то ошибка сокета, требующая обработки, устанавливается в ETIMEDOUT, и сокет закрывается. Если же сокет получает ошибку 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 минут полностью перекрывают период отправки проверочных сообщений от нашего узла, равный 11 минутам и 15 секундам.
Этот параметр обычно используется серверами, хотя его могут использовать и клиенты. Серверы используют его, поскольку большую часть своего времени они проводят в блокированном состоянии, ожидая ввода по соединению TCP, то есть в ожидании запроса клиента. Но если узел клиента выходит из строя, процесс сервера никогда не узнает об этом, и сервер будет продолжать ждать ввода данных, которые никогда не придут. Это называется наполовину открытым соединением {half-open connection). Данный параметр позволяет обнаружить наполовину открытые соединения и завершить их.
ПРИМЕЧАНИЕ
Болыпинство серверов Rlogin и Telnet устанавливают э гот параметр, чтобы завершить соединение в том случае, если интерактив!iый клиеи г дает отбой но телефонной линии или, например, выключает терминал без завершения сеанса.
Некоторые серверы, особенно серверы FTP, нредоставляют приложению тайм-аут, часто до нескольких минут. Эго выполняется самим приложением, обычно при вызове функции read, когда считыва(л ся следующая команда клиента. Этот тайм-аут не влияет па данный параметр соке га.
В табл. 7.2 суммируются различные методы, применяемые для обнаружения того, что происходит на другом конце соединения TCP. Когда мы говорим «использование функции select для проверки готовности к чтению», мы имеем в виду вызов функции select для проверки, готов ли сокет для чтения.
Таблица 7.2. Методы определения различных условий TCP
Сценарий
Процесс собеседника выходит из строя
Узел собеседника выходит из строя
Узел собеседника недоступен
Наш TCP актив-
TCP собеседника посылает
По истечении времени
По истечении времени
ио посылает
сегмент FIN, что мы можем
ожидания TCP возвра-
ожидания TCP возвра-
данные
сразу же обнаружить,
тается ошибка
щастся ошибка
f
используя функцию select для проверки ютовности к чтению Ее пн TCP посылает вюроп CCI мепт, TCP собеседника посылает в ответ сегмеш RST Если TCP посылап еще один сегмент, наш TCP посылает сигнал SIGPIPE
ETIMEDOUT
ETIMEDOUT
Наш TCP актив-
TCP собеседника посылает
Мы больше ие получаем
Мы больше пе пол)№ем
но принимает данные
сегмент FIN, который мы прочитаем как признак конца файла (возможно, преждевременный)
никаких данных
ннкакпх данных i
Соединение не-
TCP собеседника посылает
По истечении 2 часов
По истечении 2 часов
акт ивно, посы-
сегмент FIN, который мы
О1 сутс гвия ак швности
отсутствия активности
лается пробны и
можем сразу же обпару-
отсылается 9 сообщении
отсылается 9 сообщений
пакет
жить, используя функцию select для проверки готовности к чтению
для проверки наличия связи с собеседником, азатем возвращается ошибка ETIMEDOUT
для проверки наличия связи с собеседником, а затем возвращается ошибка ETIMEDOUT
Соединение неактивно, ие посылается проверочное сообщение
TCP собеседника посылает сегмент FIN, который мы можем сразу же обнаружить, используя функцию select для проверки готовности к чтению
Ничего не происходит
Ничего не происходит
Параметр сокета SO_LINGER
Этот параметр определяег, как работает функция close, если протокол ориентирован на установление соединения (например, TCP, ио не UDP). По умолчанию
функция close возвращает управление немедленно, но если в отправляющем буфере сокета остаются какие-либо данные, система попытается доставить данные собеседнику.
Параметр сокета SO_LINGER позволяет нам изменять поведение, используемое по умолчанию. Для этого необходимо, чтобы между пользовательским процессом и ядром передавалась следующая структура, определяемая в заголовочном файле <sys/socket h>:
struct linger {
int l_onoff /* 0=off. ненулевое зкачение=оп */
int 1_1inger. /* время ожидания. в Posix 1g измеряется в секундах */
Вызов функции setsockopt приводит к одному из трех следующих сценариев в зависимости от значений двух элементов структуры linger.
Если 1 _onoff имеет нулевое значение, параметр выключается. Значение 1 _11 nger игнорируется и применяется ранее рассмотренный заданный по умолчанию сценарий TCP: функция close немедленно завершается.
Если значение l_onoff ненулевое, а 】_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 и снова запусти п> прослушивающий сервер, даже если соединения все еще используются с известным портом сервера. Этого не нужно делать, поскольку это может привести к искажению данных, как показано в RFC 1337 [10]. Вместо этого перед вызовом функции bind на стороне сервера всегда нужно использовать параметр сокета SO_REUSEADDR, как показано ниже. Состояние TIME WAIT — наш друг, так как опо предназначено для того, чтобы помочь нам дождаться, когда истечет время жизни в сети старых дублированных сегментов. Вместо того чтобы пытаться избежать этого состояния, следует понять его назначение (см. раздел 2.6).
Если оба значения — l_onoff и ]_1 inger — ненулевые, то при закрытии сокета ядро будет ждать (linger) [105, с. 472], то есть если в буфере отправки сокета еще имеются какие-либо данные, процесс входит в состояние ожидания до тех пор, пока либо все данные не будут отправлены и подтверждены другим концом TCP, либо ие истечет время ожидания. Если сокет был установлен как неблокируемый (см. главу 15), он не будет ждать завершения выполнения функции close, даже если время задержки ненулевое.
При использовании этого свойства параметра SO_LINGER приложению важно проверить значение, возвращаемое функцией close. Если время ожидания истечет до того, как оставшиеся данные будут отправлены и подтверждены, функция close возвратит ошибку EWOULDBLOCK, и все данные, оставшиеся в буфере отправки сокета, будут сброшены.
ПРИМЕЧАНИЕ
К сожалению, интерпретация ненулевого элемента l_linger в третьем случае зависит от реализации. В 4.4BSD единицей измерения является одна сотая секунды, a Posix задает в качестве единиц измерения секунды. Другой проблемой, существующей у реализаций, происходящих от Беркли, является то, что элемент l linger (тина mt) копируется в переменную ядра (so linger), которая представляет собой 16-разрядиое целое число, что ограничивает время задержки до 327,67 секунды.
Теперь нам нужно точно определить, когда завершаете月 функция close на сокете в различных сценариях, которые мы рассмотрели. Предполагается, что клиент записывает данные в сокет и вызывает функцию close. На рис. 7.1 показана ситуация по умолчанию.
Рис. 7.1. Действие функции close, заданное по умолчанию: немедленное завершение
Мы предполагаем, что когда приходят данные клиента, сервер временно занят. Поэтому данные добавляются в приемный буфер сокета его протоколом TCP. Аналогично следующий сегмент (сегмент FIN клиента) также добавляется к приемному буферу сокета (каким бы образом реализация ни сохраняла сегмент FIN). Но по умолчанию клиентская функция с】ose сразу же завершается. Как мы показываем в этом сценарии, клиентская функция close может завершиться перед тем, как сервер прочитает оставшиеся данные в приемном буфере его сокета. Если узел сервера выйдет из строя перед тем, как приложение-сервер считает оставшиеся данные, клиентское приложение никогда об этом не узнает.
Клиент может установить параметр сокета SO_LINGER, задав некоторое положительное время задержки. Когда это происходит, клиентская функция close не
завершается до тех пор, пока все данные клиента и его сегмент FIN не будут подтверждены протоколом TCP сервера. Мы показываем это на рис. 7.2. Но у нас остается та же проблема, что и на рис. 7.1: если на узле сервера происходит сбой до того как приложение-сервер считает оставшиеся данные, клиентское приложение никогда не узнает об этом.
Основным принципом такого взаимодействия является то, что успешное завершение функции close с установленным параметром сокета SO_LINGER говорит нам лишь о том, что данные, которые мы отправили (и наш сегмент FIN) подтверждены протоколом TCP собеседника. Но это не говорит нам, прочитало ли данные приложение собеседника. Если мы не установим параметр сокета SO_LINGER, мы не будем знать, подтвердил ли другой конец TCP отравленные ему данные.
Рис. 7.2. Функция close с установленным параметром сокета SO_LINGER и IJinger; имеющим положительное значение
Чтобы узнать, что сервер прочитал данные клиента, клиент может вызвать функцию shutdown (со вторым аргументом SHUT.WR) вместо функции cl ose и ждать, когда собеседник закроет с помощью функции cl ose свой конец соединения. Этот сценарий показан на рис. 7.3.
Сравнивая этот рисунок с рис. 7.1 и 7.2, мы видим, что когда мы закрываем наш конец соединения, то в зависимости от вызванной функции (close или shutdown) и от того, установлен или нет параметр сокета SO LINGER, завершение может произойти в один из трех различных моментов времени:
Функция close завершается немедленно, без всякого ожидания (сценарий, заданный по умолчанию, см. рис. 7.1).
Функция cl ose задерживается до тех пор, пока не будет получен сегмент АСК, подтверждающий получение сервером сегмента FIN от клиента (рис. 7.2).
Функция shutdown, за которой следует функция read, ждет, когда мы получим сегмент FIN собеседника (в данном случае сервера) (рис. 7.3).
Другой способ узнать, что приложение-собеседник прочитало наши данные,— использ вать подтверждение на уровне приложения, или АСК приложения.
Рис. 7_3. Использование функции shutdown для проверки того, что собеседник получил наши данные
Рис_ 7.4. АСК поиложения
Например, клиент отправляет данные серверу и затем вызывает функцию read для одного байта данных:
char ack
Write(sockfd data nbytes) /* данные от клиента к серверу */
n = Read(sockfd &ack 1)/* ожидание подтверждения на уровне 叩мложенмя */
Сервер читает данные от клиента и затем отправляет ему 1-байтовый сегмент — подтверждение на уровне приложения:
nbytes = Read(sockfd buff sizeof(buff)) /* данные от клиента */
/* сервер проверяет. правильное ли количество данных он получил от клиента */ Write(sockfd '' 1)/* сегмент АСК сервера возвращается клиенту */
Таким образом, мы имеем гарантию, что на момент завершения функции read на стороне клиента процесс сервера прочитал данные, которые мы отправили. (При этом предполагается, что либо сервер знает, сколько данных отправляет клиент, либо существует некоторый заданный приложением маркер конца записи, который мы здесь не показываем.) В данном случае сегмент АСК на уровне приложения представляет собой нулевой байт, но содержимое этого байта можно использовать для передачи от сервера к клиенту сообщений о других условиях. На рис. 7 4 показан возможный обмен пакетами.
В табл. 7.3 описаны два возможных вызова функции shutdown и три возможных вызова функции close, а также их влияние на сокет TCP.
Таблица 7.3. Итоговая таблица сценариев функции shutdown и параметров сокета SOJJNGER
<M0««4«M50MMWM4««&S«««<Wli««M««<>MM««^M4W««e««W«*Z«0««4W«««»M6Wl»4W4«»S^*S4WWt««««»W«K«WWWWWWe^^
Функция
Описание
shutdown, SHUT—RD
Через сокет больше нельзя припи\гать данные, процесс может по-прежнему отправлять данные через этот сокет, приемный буфер сокета сбрасывается, все данмне, получаемые в дальнсишем, игнорируются г(|Х)токолом TCP (см упражнение 6 5), не влияет на буфер отправки сокета
shutdown, '
S1IUT_WR
Через сокет больше нельзя отравлять данные, процесс может по-прежнему получать данные через этот сокет, содержимое буфера отправки сокета отсылается на другой конец соединения, затем выполняется обычная последовательность действии по завершению соед^гнсния TCP (FIN), не влияет на приемный буфер сокета
dose, l onoff я 0 (по умолчанию)
Через сокет больше нельзя отправлят ь и получать данные, содержимое буфера отправки сокета отсылается на другой конец соединения Если счетчик ссылок дескрип гора становится ну刀cbliv, то следом за отправкой данных из буфера отправки сокета выполняется нормшгьпая последовательное! ь завершеиия соединения TCP (FIN), данные из приемною буфера сокета сбрасываются
close, l onoff = 1
Unger r 0
Через сокет больше нельзя отпраилять и получать данные Если счетчик ссылок дескриптора становится нулевым, то на другой конец соединения г досылается сегмент RST, сосд1»»ение переходит в состояние в CLOSED (минуя состояние TIME_WAIT), данные из буфера отиравк” и приемною буфера сокета сбрасываются
close, l_onoff = 1
l_linger 1= 0
Через сокет больше ггсльзя отправлять и получать даи»ые, содержимое буфера отправки сокета отсылается на другой конец соединения Если счетчик ссылок дескриптора становится нулевым, то следом за отправкои данных из буфера отнравкн сокета выполняется нормальная последователыюсть завершс1гия соединения TCP (FIN), данные из приемною буфера сокета сбрасываются, и если время задержки истекает, прежде чем оставишеся в буфере данные будут посла] 1ы и будет подтвержден их мрнсм, функция close возвратит ошибку EWOULDBLOCK
Параметр сокета SO_OOBINLINE
Когда установлен этот параметр, внеполосные данные помещаются в очередь нормального ввода (то есть вместе с обычными данными (inline)). Когда это происходит, флаг MSG_OOB не может быть использован для Ч1ения полученных внеполосных данных. Более подробно внеполосные данные мы рассмотрим в главе 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 (максимальный размер сегмента) для соединения Если мы имеем дело с направленной передачей данных, такой как передача файла в одном направлении, то говоря «размеры буферов сокета》, мы подразумеваем буфер отправки сокета на отправляющем узле или приемный буфер сокета на принимающем узле. В случае
двусторонней передачи данных мы имеем в виду оба размера буферов на обоих узлах. С типичным размером буфера 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
I Подтверждение 1
I Подтверждение 2
Подтверждение 3
(Подтверждение 4
ч>
Сервер
Клиент
Рис. 7.5. Соединение TCP (канал), вмещающее восемь сегментов
Мы показываем четыре сегмента данных вверху и четыре сегмента АСК внизу. Даже если в канале только четыре сегмента данных, у клиента должен быть буфер отправки, вмещающий минимум восемь сегментов, потому что TCP клиента должен хранить копию каждого сегмента, пока не получен сегмент АСК от сервера.
ПРИМЕЧАНИЕ
Здесь мы игнорируем некоторые подробности. Прежде всего, алгоритм медленного запуска TCP ограничивает скорость, с которой сегменты начинают отправляться по соединению, которое до этого было неактивным. Далее, TCP часто подтверждает каждый второй сегмент, а пе каждый сегмент, как мы это показываем. Все эти подробности описаны в главах 20 и 24 [94].
Нам необходимо понять принцип функционирования двустороннего канала и узнать, что такое его вместимость и как она влияет на размеры буферов сокетов на обоих концах соединения. Вместимость капала характеризуется произведением пропускной способности на задержку (bandwidth-delay product). Мы будем вычислять ее, умножая пропускную способность канала (в битах в секунду) на период обращения (RTT, round-trip time) (в секундах) и преобразуя результат из битов в байты. RTT легко измеряется с помощью утилиты Ping. Пропускная способность —это значение, соответствующее наиболее медленной связи между двумя конечными точками; предполагается, что это значение каким-то образом определено. Например, линия Т1 (1 536 000 бит/с) с RTT 60 миллисекунд дает
произведение пропускной способности на задержку, равное 11 520 байт. Если размеры буфера сокета меньше указанного, канал не будет заполнен и производительность окажется ниже предполагаемой. Большие буферы сокетов требуются, когда повышается пропускная способность (например, для линии ТЗ, где она равна 45 Мбит/с) или когда увеличивается RTT (например, спутниковые каналы связи с RTT около 500 миллисекунд). Когда произведение пропускной способности на задержку превосходит максимальный нормальный размер окна TCP (65 535 байт), обоим концам соединения требуются также параметры TCP для канала с повышенной вместимостью (long fat pipe), о которых мы упоминали в разделе 2.5.
ПРИМЕЧАНИЕ
В большинстве реализаций размеры буферов отправки и приема ограничиваются некоторым предельным значением, В более ранних реализациях, происходящих от Беркли, верхний предел был около 52 000 байт, но в новых реализациях предел но умолчанию равен 256 000 байт или больше, и обычно админис ipaiop имеет возможность увеличивать его. К сожалению, не существует простого способа, с помощью которого приложение могло бы узнать этот предел. Posix.l определяет функцию fpathconf, поддерживаемую большинством реализаций, a Posix.lg определяет новую константу _PC_SOCK_ MAXBUF, являющуюся максимальным размером буферов сокета, которую можно использовать в качестве второго аргумента этой функции.
Параметры сокета SO_RCVLOWAT и SO_SNDLOWAT
Каждый сокет характеризуется также минимальным количеством данных (low- water mark) для буферов приема и отправки. Эти значения используются функцией select, как мы показали в разделе 6.3. Указанные параметры сокета позволяют нам изменять эти два значения.
Минимальное количество данных — это количество данных, которые должны находиться в приемном буфере сокета, чтобы функция select возвратила ответ «Сокет готов для чтения». По умолчанию это значение равно 1 для сокетов TCP и UDP. Минимальный объем для буфера отправки — это количество свободного пространства, которое должно быть в буфере отправки сокета, чтобы функция sei ect возвратила «Сокет готов для записи». Для сокетов TCP по умолчанию оно обычно равно 2048. С UDP это значение используется так, как мы показали в разделе 6.3, но поскольку число байтов доступного пространства в буфере отправки для сокета UDP никогда не изменяется (так как UDP не хранит копии дейтаграмм, отправленных приложением), сокет UDP всегда готов для записи, пока размер буфера отправки сокета UDP больше минимального объема. Вспомните рис. 2.12: UDP не имеет буфера отправки, у него есть только размер буфера отправки.
ПРИМЕЧАНИЕ
Posix.lg не требует поддержки этих двух парамез ров сокетов.
Параметры сокета SO_RCVTIMEO и SO_SNDTIMEO
Эти два параметра сокета позволяют нам устанавливать тайм-аут при получении и отправке через сокет. Обратите внимание, что аргумент двух функций sockopt —
это указатель на структуру 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 служит для четырех целей.
Параметр SO REUSEADDR позволяет прослушивающему серверу запуститься и с помощью функции bi nd связаться со своим заранее известным портом, даже если существуют ранее установленные соединения, использующие этот порт в качестве своего локального порта. Эта ситуация обычно возникает следующим образом:
Запускается прослушивающий сервер.
От клиента приходит запрос на соединение, и для обработки этого клиента генерируется дочерний процесс.
Прослушивающий сервер завершает работу, но дочерний процесс продолжает обслуживание клиента на существующем соединении.
Прослушивающий сервер перезапускается.
По умолчанию, когда прослушивающий сервер перезапускается при помощи вызова функций socket, bind и li sten, вызов функции bi nd оказывается неудачным, потому что прослушивающий сервер пытается связаться с портом, который является частью существующего соединения (обрабатываемого ранее созданным дочерним процессом). Но если сервер устанавливает параметр сокета SO REUSEADDR между вызовами функций socket и bind, последняя выполнится успешно. Все серверы TCP должны задавать этот параметр сокета, чтобы позволить перезапускать сервер в этой ситуации.
ПРИМЕЧАНИЕ
Этот сценарий порождает одни из наиболее часто задаваемых вопросов в Usenet.
Параметр SO_REUSEADDR позволяет множеству экземпляров одного и того же сервера запускаться на одном и том же порте, так как все экземпляры связы-
ваются с различными локальными 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 duplicate binding), то есть мы не можем запустить один сервер, связывающийся с адресом 198.69Л0.2 и портом 80, и другой сервер, также связывающийся с адресом 198.69.10.2 и портом 80, даже если для второго сервера мы установим параметр SO_REUSEADDR.
Параметр SD_REUSEADDR позволяет одиночному процессу связывать один и тот же порт со множеством сокетов, так как при каждом связывании задается уникальный IP-адрес. Это обычное явление для серверов UDP, так как им необходимо знать IP-адрес получателя запросов клиента в системах, не поддерживающих параметр сокета IP_RECVSTADDR. Мы разработаем примере использованием этой технолоши в разделе 19.11. Эта технология обычно не применяется с серверами TCP, поскольку сервер TCP всегда может определить IP-адрес получателя при помощи вызова функции getsockname, после того как соединение установлено.
Параметр SO_REUSEADDR допускает полностью дублированное связывание', связывание с помощью функции bind с IP-адресом и портом, когда тот же IP- адрес и тот же порт уже связаны с другим сокетом. Обычно это свойство
доступно только в системах с поддержкой многоадресной передачи без поддержки параметра сокета SO REUSEPORT (который мы опишем чуть ниже) и только для сокетов UDP (многоадресная передача не работает с TCP)
Это свойство применяется при многоадресной передаче для многократного выполнения одного и того же приложения на одном и том же узле Когда приходит дейтаграмма UDP для одного из многократно связанных сокетов, действует следующее правило если дейтаграмма предназначена либо для широковещательного адреса, либо для адреса многоадресной передачи, то одна копия дейтаграммы доставляется каждому сокету Но если дейтаграмма предназначена для адреса направленной передачи, то дейтаграмма доставляется только на один сокет Какой сокет получит дейтаграмму, если в случае направленной передачи существует множество сокетов, соответствующих дейтаграмме,— зависит от реализации На с 777-779 [105] об этом свойстве рассказывается более подробно О широковещательной и многоадресной передачах мы поговорим соответственно в главах 18 и 19
В упражнениях 7 5 н 7 6 показаны примеры использования этого параметра сокета
В 4 4BSD при добавлении поддержки многоадресной передачи был введен параметр сокета SO REUSEPORT Вместо того чтобы перегружать параметр SO_REUSEADDR семантикой многоадресной передачи, допускающей полностью дублированное связывание, был введен новый параметр сокета, обладающий следующей семантикой
Этот параметр допускает полностью дублированное связывание, но только если каждый сокет, который хочет связаться с тем же IP-адресом и портом, задает этот параметр сокета
Параметр SO_REUSEADDR считается эквивалентным параметру SO_REUSEPORT, если связываемый IP-адрес является адресом многоадресной передачи [105, с 731] Проблема с этим параметром сокета заключается в том, что не все системы его поддерживают В системах без поддержки этого параметра, но с поддержкой мпо гоадресной передачи его функции выполняется параметр SO_REUSEADDR, допускающий полностью дублированное связывание, когда оно имеет смысл (то есть когда имеется сервер UDP, который может быть запущен много раз на одном и том же узле в одно и то же время, предполагающий получать либо широковещательные дейтаграммы, либо дейтаграммы многоадресной передачи)
Обобщить обсуждение этих параметров сокета можно с помошью следующих рекомендаций
Устанавливайте параметр SO_REUSEADDR перед вызовом функции bind на всех серверах TCP
При создании приложения многоадресной передачи, которое может быть запущено несколько раз на одном и том же узле в одно и то же время, устанавливайте параметр SO_REUSEADDR и связывайтесь с адресом многоадресной передачи, используемым в качестве локально! о IP-адреса
Более подробно об этих параметрах сокета рассказывается в главе 22 [105]
Существует потенциальная проблема безопасности, связанная с использованием параметра SD_REUSEADDR Если существует соке1, связанный, скажем, с универсальным адресом и портом 5555, то задав параметр SO__REUSEADDR, мы можем связать этот порт с другим IP-адресом, например с основным (primary) 1Р-адре- сом узла Любые приходящие дейтаграммы, предназначенные для порта 5555 и IP- адреса, который мы связали с нашим сокетом, доставляются на наш сокет, а не на другой сокет, связанный с универсальным адресом Это могут быть сегменты SYN TCP или дейтаграммы UDP (В упражнении ИЗ показано это свойство для UDP ) Для большинства известных служб, таких как HTTP, FTP и Telnet, это не составляет проблемы, поскольку все эти серверы связываются с зарезервированным портом Следовательно, любой процесс, запущенный позже и пьпающийся связаться с более специфичным экземпляром этого порта (то есть пытающийся завладеть портом), требует прав привилегированного пользователя Однако NFS (Network File System — сетевая файловая система) может вызвать проблемы, поскольку ее стандартный порт (2049) не зарезервирован
ПРИМЕЧАНИЕ
Одна из сопутствующих проблем API сокетов в том, что установка пары сокетов выполняется с помощью двух вызовов функции (bind н connect) вместо одного В [100] предлагается одиночная функция, разрешающая эту проблему int bind_connect_listen(int sockfd
const struct sockaddr *ldddr int Idddrlen const struct sockaddr *faddr int faddrlen int listen)
Аргуметнт laddr задает локальный IP-адрес и локальный порт, apiyMeirr faddr — удаленный IP-адрес и удаленный порт, аргумент listen задает клиент (0) или сервер (значение ненулевое, то же, что л аргумент backlog функции listen) В гаком случае функция bind могла бы быть библиотечной функцией, вызывающей эту функцию с пустым указателем fad dr и пулевым faddrlen, а функция connect — библиотечной функцией, вызывающей эту функцию с пустым указателем laddr и нулевым laddr 1еп Существует несколько приложении, особенно FTP, копюрым необходимо задавать и локальную пару, и удаленную пару, которые могут вызывать bind_connect_listen непосредствснно При наличии подобной функции отпадает необходимость в параметре SO REUSEADDR, в отличие от серверов UDP, которым явно необходимо допускать полностью дублированное связывание с одним и тем же IP-адресом и портом Другое преимущество этой новой функции в том, что сервер TCP может О1рапичить себя обслуживанием запросов на соединения, приходящих от одного определенного IP адреса и порта Это определяется в RFC 793 [83], но невозможно с существующими API сокетов
Аналогичное предложение о функции set_addresses было сделано в 1993 году рабочей группой Posix 1003 12 Кейта Склоуэра (Keith Sklower) Однако предложение было отклонено
Параметр сокета SO_TYPE
Этот параметр возвращает тип сокета Возвращаемое целое число — это значение, такое как SOCK_STREAM или SDCK_DGRAM Этот параметр обычно используется процессом, наследующим сокет при запуске
Параметр сокета SO^USELOOPBACK
Этот параметр применяется только к сокетам в маршрутизирующем домене (AF_ ROUTE). По умолчанию он включен на этих сокетах (единственный из параметров S0_xxx, по умолчанию включенный). Когда этот параметр включается, сокет получает копию всего, что отправляется на сокет.
ПРИМЕЧАНИЕ
Другой способ отключить получение этих циклических копий — вызвать функцию shutdown со вторым аргументом SHUT RD.
Параметры сокетов IPv4
Эти параметры сокетов обрабатываются IPv4, и для них аргумент level равен IPPROTO_IP. Обсуждение пяти параметров сокетов многоадресной передачи мы отложим до раздела 19.5.
ПРИМЕЧАНИЕ
Все параметры сокетов, которые мы описываем в этом разделе, определяются стандартом Posix. 1g, за исключением параметра IP RECVIF.
Параметр сокета IP_HRDINCL
Если этот параметр задан для символьного сокета IP (см. главу 25), нам следует создать наш собственный заголовок IP для всех дейтаграмм, которые мы отправляем через символьный сокет. Обычно ядро создает заголовок IP для дейтаграмм, отправляемых через символьный сокет, но существует ряд приложений (особенно Traceroute), создающих свой собственный заголовок IP, заменяющий значения, которые IP поместил бы в определенные поля заголовка.
Когда установлен этот параметр, мы создаем полный заголовок IP со следующими исключениями:
紆 IP всегда вычисляет и хранит контрольную сумму заголовка IP.
君 Если мы устанавливаем поле идентификации IP в 0, ядро устанавливает это поле.
史 Если IP-адрес отправителя (source address) — INADDR_ANY, IP устанавливает его равным основному IP-адресу исходящего интерфейса.
看' Как устанавливать параметры IP, зависит от реализации. Некоторые реализации принимают любые параметры IP, установленные с использованием параметра сокета IP.OPTIONS, и добавляют их к создаваемому нами заголовку, в то время как другие требуют, чтобы наш заголовок также содержал все необходимые параметры IP.
Пример использования этого параметра показан в разделе 26.6. На с. 1056-1057 [105] предоставлена дополнительная подробная информация об этом параметре.
Параметр сокета IP_OPTIONS
Установка этого параметра позволяет нам задавать параметры IP в заголовке IPv4. Это требует точного знания формата параметров IP в заголовке IP.
Мы рассмотрим этот параметр в контексте маршрутизации от отправителя IPv4 в разделе 24.3.
Параметр сокета I P_RECVDSTADDR
Этот параметр сокета заставляет функцию recvmsg возвращать 1Р*адрес получателя в получаемой дейтаграмме UDP в качестве вспомогательных данных. Пример использования этого параметра мы приводим в разделе 20.2.
Параметр сокета IP_RECVIF
Этот параметр сокета заставляет функцию recvmsg возвращать индекс интерфейса, на котором принимается дейтаграмма UDP, в качестве вспомогательных данных. Пример использования этого параметра мы приводим в разделе 20.2.
ПРИМЕЧАНИЕ
Это новый параметр, разработанный Биллом Феннером (Bill Fenner) для FreeBSD и Net BSD (DARTNet) [28]. DARTNet — это экспсриментал ьная исследова гсльская сеть, используемая для тестирования новых протоколов и приложений. Данный параметр сокета предполагалось использовать в 4.4BSD, но он так и не вошел в эту реализацию. Автор взял реализацию FreeBSD и добавил ее в BSD/OS 3.0.
Параметр сокета IP_TOS
Этот параметр позволяет нам устанавливать поле тип службы (тип сервиса) (TOS, type-of-service) (рис. АЛ) в заголовке IP для сокета TCP или UDP. Если мы вызываем для этого сокета функцию getsockopt, возвращается текущее значение, которое будет помещено в поле TOS заголовка IP (по умолчанию значение нулевое). Не существует способа извлечь это значение из полученной дейтаграммы IP.
Мы можем присвоить TOS одну из констант, показанных в табл, 7.4, которые определяются в заголовочном файле <neti net/ip h>.
Таблица 7.4. Константы типов сервиса IPv4
<^V/WWXfW^WM!A^К ^WWSMMWWWWVW^^MW
Константа
Описание
IPTOS一LOWDELAY IPTOS—THROUGHPUT IPTOS—RELIABLITY IPTOS_LOWCOST
Минимизирует задержку
Доводит производительность до максимума
Доводит надежность до максимума
Минимизирует стоимость
Документ RFC 1349 [2] содержит подробное описание поля TOS и способов установки этого поля для стандартных приложений Интернета. Например, Telnet и Rlogin должны задавать IPTOS_LOWDELAY, в то время как при передаче данных FTP следует задавать IPTOSJHROUGHPUT.
Параметр сокета IP_TTL
С помощью этого параметра мы можем устанавливать и получать заданное по умолчанию значение TTL (time-to-live field — поле времени жизни, рис. АЛ), которое система будет использовать для данного сокета. В системе 4.4BSD, напри-
мер, значение TTL по умолчанию для сокетов TCP и UDP равно 64 (оно определяется в RFC 1700), а для символьных сокетов — 255. Как и в случае поля TOS, вызов функции getsockopt возвращает значение поля по умолчанию, которое система будет использовать в исходящих дейтаграммах, и не существует способа определить это значение по полученной дейтаграмме. Мы устанавливаем этот параметр сокета в нашей программе Traceroute в листинге 25.14.
Параметр сокета ICMPv6
Этот параметр сокета обрабатывается ICMPv6 и имеет аргумент level, равный IPPR0T0_ICMPV6.
Параметр сокета ICMP6_FILTER
Этот параметр позволяет нам получать и устанавливать структуру icmp6_filter, которая определяет, какие из 256 возможных типов сообщений ICMPv6 передаются для обработки на символьный сокет. Мы обсудим этот параметр в разделе 25.4.
Параметры сокетов IPv6
Эти параметры сокетов обрабатываются IPv6 и имеют аргумент 1 evel, равный 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 (значение по умолчанию), ядро не будет вычислять и хранить контрольную сумму для исходящих пакетов на этом символьном сокете и не будет проверять контрольную сумму для получаемых пакетов.
ПРИМЕЧАНИЕ
Все протоколы, использующие IPv6, должны имегь контрольную сумму в своих собственных заголовках. Эти контрольные суммы включают iюевдозаголовок (RFC 1883 [25]), куда входит IPvG-адрес отправителя как часть контрольной суммы (чго отличает IPv6 от всех остальных протоколов, которые обычно реализую!ся с использованием символьного сокета с IPv4). Ядро нс заставляет приложение использовать символьный сокет для выбора адреса отправителя, а делает это самостоятелыю и затем вычисляет и сохраняет контрольную сумму, включающую 11севдозаголовок IPv6.
Параметр сокета IPv6_DSTOPTS
Установка этого параметра означает, что любые полученные IPv6-napaMCTpbi получателя должны быть возвращены в качестве вспомогательных данных функцией reevmsg. По умолчанию параметр отключен. Мы опишем функции, используемые для создания и обработки этих параметров, в разделе 24.5.
Параметр сокета IPv6_HOPLIMIT
Установка этого параметра определяет, что полученное поле с предельным количеством транзитных узлов1 (hop limit field) должно быть возвращено в качестве вспомогательных данных функцией reevmsg. По умолчанию параметр отключен. Мы опишем функции, используемые для создания и обработки этого параметра, в разделе 20.8.
ПРИМЕЧАНИЕ
В IPv4 нс существует способа определить значение получаемого поля времени жизни.
Параметр сокета IPv6_HOPOPTS
Установка этого параметра означает, что любые полученные параметры транзитных узлов (hop-by-hop options) IPv6 должны быть возвращены в качестве вспомогательных данных функцией reevmsg. По умолчанию параметр отключен. Мы опишем функции, используемые для создания и обработки этого параметра, в разделе 24.5.
IPv6_NEXTHOP
Это не параметр сокета, а тип объекта вспомогательных данных, который можно задать в функции sendmsg. Объект задает адрес следующего транзитного узла для дейта)'раммы в качестве структуры адреса сокета. Более подробно об этом свойстве мы поговорим в разделе 20.8.
Параметр сокета IPv6_PKTINFO
Установка этого параметра означает, что два фрагмента информации о полученной дейтаграмме IPv6 — 1Ру6-адрес получателя и индекс принимающего интер-
Используются также термины «поле количества переходов», «поле ограничения пересылок» и др.— Примеч перео.
фейса — должны быть возвращены в качестве вспомогательных данных функцией recvmsg. Мы опишем этот параметр в разделе 20.8.
Параметр сокета IPv6_PKTOPTIONS
Большинство параметров сокетов IPv6 рассчитаны па сокет UDP, где информация передается между ядром и приложением через вспомогательные данные в функциях recvmsg и sendmsg. Сокет TCP получает и хранит эти значения с помощью параметра IPV6_PKT0PTI0NS.
Буфер, на который указывают функции getsockopt и setsockopt, содержит ту же информацию, которая передавалась бы как вспомогательные данные в функциях recvmsg или sendmsg. Этот параметр сокета мы обсудим в разделе 24.7.
Параметр сокета IPv6_RTHDR
Установка этого параметра означает, что получаемый заголовок маршрутизации IPv6 должен быть возвращен в качестве вспомогательных данных функцией recvmsg. По умолчанию этот параметр отключен. Мы опишем функции, которые используются для создания и обработки этих параметров, в разделе 24.6.
Параметр сокета I Pv6_U NIС AST_H О PS
Этот параметр аналогичен параметру сокета IPv4 IP_TTL. Он определяет предельное количество транзитных узлов, заданное по умолчанию для исходящих дейтаграмм, отправляемых через этот сокет. При получении значения этого параметра сокета возвращается предельное количество транзитных узлов, которые адро будет использовать для сокета. Чтобы определить действительное значение предельного количества транзитных узлов из полученной дейтаграммы IPv6, тре- 5уется использовать параметр сокета IPV6_H0PLIMIT. Мы устанавливаем этот параметр сокета в нашей программе Traceroute в листинге 25.14.
Параметры сокетов TCP
Существует пять параметров сокетов TCP, однако три из них введены в Posix. 1g 4 не получили широкого применения. Мы присваиваем аргументу 1 evel значе- 1ие IPPROTO_TCP.
Параметр сокета TCP_KEEPALIVE
Этот параметр появился только в Posix. 1g. Он задает время простоя в секундах [ля соединения, по истечении которого TCP начнет отправлять сообщения для гроверки работоспособности собеседника (keepalive probes). Значение по умол- ганию должно быть как минимум 7200 секунд, то есть 2 часа. Этот параметр дей- твует, только если включен параметр сокета SO_KEEPALIVE.
Параметр сокета TCP_MAXRT
)тот параметр появился только в Posix. 1g. Он задает количество времени в се- ундах, отсчитываемое с момента начала повторной передачи данных TCP, по [стечении которого соединение разрывается. Есди этот парамтер нулевой, ис-
пользуется значение, заданное системой по умолчанию. Значение -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 всегда может отправить меньше данных, чем было анонсировано собеседником.
Параметр сокета TCP〜NODELAY
Если этот параметр установлен, он отключает алгорштш Нагла (Nagle algorithm) (см. раздел 19.4 [94] и с. 858-859 [105]). По умолчанию этот алгоритм включен.
Назначение алгоритма Нагла — сократить число небольших пакетов в глобальной сети. Согласно этому алгоритму, если у данного соединения имеются неподтвержденные (outstanding) данные (то есть данные, которые отправил наш TCP и подтверждения которых он ждет), то небольшие пакеты не будут отправляться через соединение до тех пор, пока существующие данные не будут подтверждены. Под «небольшим» пакетом понимается любой пакет, меньший MSS. TCP будет по возможности всегда отправлять пакеты нормального размера. Таким образом, назначение алгоритма Нагла — не допустить, чтобы у соединения было множество небольших пакетов, ожидающих подтверждения.
Два типичных генератора небольших пакетов — клиенты Rlogin и Telnet, поскольку обычно они посылают каждое нажатие клавиши в отдельном пакете.
В быстрой локальной сети мы обычно не замечаем действия алгоритма Нагла с этими клиентами, потому что время, требуемое для подтверждения небольшого пакета, составляет несколько миллисекунд — намного меньше, чем промежуток между вводом двух последовательных символов Но в глобальной сети, где для подтверждения небольшого пакета может потребоваться секунда, мы можем заметить задержку в отражении символов, и эта задержка часто увеличивается при включении алгоритма Нагла
Рассмотрим следующий пример Мы вводим строку из шести символов hello* либо клиенту Rlogin, либо клиенту Telnet, промежуток между вводом символов составляет точно 250 миллисекунд Время обращения к серверу (RTT) составляет 600 миллисекунд, и сервер немедленно отправляет обратно отражение символа Мы считаем, что сегмент АСК, подтверждающий получение клиентского символа, отправляется обратно клиенту с отражением символа, а сегменты АСК, которые клиент отправляет для подтверждения приема отраженного сервером символа, мы игнорируем (Мы поговорим о задержанных сегментах АСК чуть ниже ) Считая, что алгоритм Нагла отключен, получаем 12 пакетов, изображенных на рис 7 6 h->0
е —► 250 —
1-> 500-^<Z^ZZZLZ !-► 750-^<^12Г^^ о -► 1 ооо -)
'-► 1250
1500 - <—•-"■
1750 - +"———
2000 —*
Рис. 7.6. Шесть символов, отраженных сервером при отключенном алгоритме Нагла
Каждый символ отправляется в индивидуальном пакете сегменты данных слева направо, а сегменты АСК справа налево
Но если алгоритм Нагла включен (по умолчанию), у нас имеется 8 пакетов, показанных на рис 7 7 Первый символ посылается как пакет, но следующие два символа не отправляются, поскольку у соединения есть небольшой пакет, ожидающий подтверждения Эти пакеты отправляются, когда прошло 600 миллисекунд, то есть когда прибывает сегмент АСК, подтверждающий прием первого пакета, вместе с отражением первого символа Пока второй пакет не будет подтвержден сегментом АСК в момент времени 1200, не будет отправлено ни одного небольшого пакета
Рис. 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 байт,
TCP сервера задержит сегмент АСК, подтверждающий получение 4 байт данных (то есть не будет данных от сервера клиенту, в которые можно вложить сегмент АСК). Есть три способа решить проблему с таким клиентом.
Использовать функцию writev вместо двух вызовов функции write. Один вызов функции writev приводит к отправке только одного сегмента TCP в нашем примере. Это предпочтительное решение.
Скопировать 4 байта данных и 396 байт данных в один буфер и вызвать один раз функцию wn te для этого 6у@)ера.
Установить параметр сокета TCP_NODELAY и продолжать вызывать функцию write дважды. Это наименее желательное решение.
Упражнения 7.8 и 7.9 продолжают этот пример.
Параметр сокета TCP_STDURG
Этот параметр появился в Posix.lg и влияет на интерпретацию срочного указателя (с которым мы встретимся при обсуждении внеполосных данных в главе 21). Есть две интерпретации срочного указателя TCP [94, с. 292-293]. По умолчанию срочный указатель указывает на байт данных» следующий за байтом, отправленным с флагом MSG_OOB. Так большинство приложений взаимодействует сегодня. Но если этот параметр сокета является ненулевым, то срочный указатель будет указывать на байт данных, отправленный с флаг ом MSG OOB.
ПРИМЕЧАНИЕ
Этот параметр сокета никогда не требуется устанавливать, и остается неясным, почему -Ki Posix.lg определяет его.
Функция fcntl
Сокращение fcntl означает «управление файлами» (file control). Эта функция выполняет различные операции управления дескрипторами. Перед описанием этой функции и ее влияния на сокет нам нужно составить некоторое более общее представление о ее возможностях. В табл. 7.5 приводятся различные операции, выполняемые функциями fcntl и ioctl и маршрутизирующими сокетами.
Таблица 7.5. Операции функций fcntl и ioctl и маршрутизирующих сокетов
Операция
fcntlioctlТиаршрутизиру' Posix.lg
ющий сокет
Установка сокета для неблокирусмого ввода-вывода
Установка сокета для ввода-вывода, управляемого сигналом Установка владельца сокета
Получение владельца сокета
F_SETFL,FIONBIOfcntl
O_NONBLOCK
F—SETFL,FIOASYNCfcntl
O_ASYNC
F_SETOWNSIOCSPGRP илиfcntl
FIOSETOWN
F_GETOWNSIOCGPGRP илиfcntl
FJOGETOWN
чктытпмтмк»ск*ш^тюят(мк%>^(то^^^
Операцияfcntl
ioctl
Маршрутизирующий сокет
Posix.lg
Получение текущего количества байтов в приемном буфере сокета
FIONREAD
Проверка, находится ли процесс на 0TMe1ке внеполосных данных
SIOCATMARK
sockatmark
Получение списка интерфейсов
SIOCGIFCONF
Sysctl
Операции интерфейсов
SIOC[GS]IFa-ix
Кэш-онсрации ARP
SIOCrARP
RTM_3
Операции таблицы маршрутизаци и
SIOCwtRT
RTM_.xxx
Первые шесть операций могут применяться к сокетам любым процессом, в то время как большинство из последних четырех (интерфейс, ARP и таблица марш* рутизации) выполняются администрирующими программами, такими как т fconfig и route. О различных операциях функции ioctl мы поговорим подробнее в главе 16, а о маршрутизирующих сокетах — в главе 17.
Существует множество способов выполнения первых четырех операций, но, как указано в последней колонке, стандарт Posix.lg определяет, что функция fcntl является предпочтительным способом. Отметим также, что Posix.lg предлагает функцию sockatmark (см. раздел 21.3) как наиболее предпочтительный способ тестирования на предмет пребывания процесса на отметке внеполосных данных. Оставшиеся операции с пустой последней колонкой пе стандартизованы Posix.
ПРИМЕЧАНИЕ
Отмез им также, что первые две операции, усчапавлпвающие сокет для иеблокируемо- го ввода-вывода и для ввода-вывода, управляемого сигналом, традиционно применялись с использованием команд FNDELAY и FASYNC функции fcntl. Posix определяет константы О ххх.
Функция fcntl предоставляет следующие возможности, относящиеся к сетевому программированию:
Неблокируемый ввод-вывод. Мы можем установить флаг состояния файла O_NONBLOCK, используя команду F_SETFL для установки неблокируемого сокета. Неблокируемый ввод-вывод мы описываем в главе 15.
Управляемый сигналом ввод-вывод. Мы можем установить флаг состояния файла O_ASYNC, используя команду F SETFL, которая вызывает генерацию сигнала SIGI0, когда состояние сокета изменяется. Мы рассмотрим это в главе 22.
ПРИМЕЧАНИЕ
Этот флаг введен в Posix.lg.
Команда F SETOWN позволяет нам указать, что владелец сокета (идентификатор процесса или идентификатор группы процессов) должен получать сигналы SIGIO и SIGURG. Первый сигнал генерируется, когда для сокета включается управляемый сигналом ввод-вывод (см. главу 22), второй — когда для сокета при-
ходят новые внеполосные (out~of-band data) данные (см. главу 21). Команда F GETOWN возвращает текущего владельца сокета.
ПРИМЕЧАНИЕ
Термин «в刀аделен сокета» введен в Posix. 1g. Исторически реализации, происходящие от Беркли, называли его «идентификатор группы процессов сокета», потому что переменная, хранящая этот идентификатор, — это элемент so_pgid структуры socket [105, с. 438].
#include <fcntl h>
int fcntl(Tnt fd. mt cmd. /* int arg */ ).
Возвращает в случае успешного выполнения результат зависит от аргумента cmd. -1 в случае ошибки
Каждый дескриптор (включая сокет) имеет набор флагов, задающих статус файла, которые можно получить с помощью команды F~GETFL и установить с помощью команды F_SETFL. На сокет влияют два следующих флага:
O—NONBLOCK неблокируемый ввод-вывод
0_ASYNCввод-вывод, у叩авляемь曲 сигналом
Позже мы опишем оба эти флага подробнее. Отметим, что типичный код, который устанавливает неблокируемый ввод-вывод с использованием функции fcntl, выглядит следующим образом:.
int flags.
/* Делаем сокет неблокируемым */
if ( (flags = fcntl(fd. F_GETFL. 0)) < 0)
err_sys("F_GETFL error”).
flags |= 0_N0痛LOCK.
if (fcntl(fd. F_SETFL. flags) < 0)
err_sys("FSETFL error"),
Учтите, что вам может встретиться код, который просто устанавливает желаемый флаг:
/* Неправильны% способ сделать сокет неблокируемым */
if (fcntl(fd. F_SETFL. O_NONBLOCK) < 0)
err_sys("F_§ETFL error"),
Хотя при этом и устанавливается флаг отключения блокировки, также снимаются все остальные флаги состояния файла. Единственный корректный способ установить один из этих флагов состояния файла — получить текущие флаги, с помощью операции логического ИЛИ добавить новый флаг, а затем установить флаги.
Следующий код сбрасывает флаг отключения блокировки в предположении, что переменная fl ags была задана с помощью вызова функции fcntl, показанного ранее:
flags &= ~O_NONBLOCK.
if (fcntKfd. F_SETFL. flags) < 0)
err_sys("F_SETFL error").
Два сигнала, SIGIO и SIGURG, отличаются от других сигналов тем, что они генерируются для сокета только если сокету был присвоен владелец с помощью ко-
манды F_SETOWN. Целое значение аргумента arg для команды F_SETOWN может быть либо положительным, задающим идентификатор процесса, получающего сигнал, либо отрицательным, абсолютное значение которого — это идентификатор группы процессов, получающей сигнал. Команда F_GETOWN возвращает владельца сокета, так как возвращаемое значение функции fcntl — либо идентификатор процесса (положительное возвращаемое значение), либо идентификатор группы процессов (отрицательное значение, отличное от -1). Разница между заданием процесса и группы процессов, получающих сигнал, в том, что в первом случае сигнал будет получен только одиночным процессом, в то время как во втором случае его получают все процессы в группе.
ПРИМЕЧАНИЕ
В SVR4 допускается задавать владельца сокета как идентификатор процесса, но не как идентификатор группы процессов.
Когда создается новый сокет с помощью функции socket, у него нет владельца. Но когда новый сокет создается из прослушиваемого сокета, владелец сокета наследуется присоединенным сокетом от прослушиваемого (как и многие параметры сокетов [105, с. 462-463].
Резюме
Параметры сокетов лежат в широком диапазоне от очень общих (SO_ERROR) до очень специфичных (параметры заголовка IP). Наиболее общеупотребительные параметры сокетов, которые нам могут встретиться, — это SO_KEEPALIVE, SO_RCVBUF, SQ_SNDBUF и SO_REUSEADDR. Последний должен всегда задаваться для сервера TCP до того, как сервер вызовет функцию bi nd (см. листинг 11.4). Параметр S(^BROADCAST и десять параметров сокетов многоадресной передачи предназначены только для приложений, передающих соответственно широковещательные или многоадресные сообщения.
Параметр сокета S JKEEPALIVE устанавливается многими серверами TCP и автоматически закрывает наполовину открытое соединение. Замечательное свойство этого параметра в том, что он обрабатывается на уровне TCP, не требуя на уровне приложения наличия таймера, измеряющего период отсутствия активности. Однако недостаток этого параметра в том, что он не видит разницы между выходом собеседника из строя и временной потерей соединения с ним.
Параметр сокета SOJJNGER расширяет наши возможности в отношении контроля над функцией close — мы можем отложить ее завершение на некоторое время. Кроме того, этот параметр позволяет нам отправить сегмент RST вместо обычной последовательности из четырех пакетов, завершающих соединение TCP. Следует соблюдать осторожность при отправке сегментов RST, поскольку в этом случае не наступает состояние TCP TIME_WAIT. Существуют случаи, когда этот параметр сокета не обеспечивает необходимой нам информации, и тогда требуется сегмент АСК уровня приложения.
У каждого сокета TCP имеются буфер отправки и буфер приема, а у*каждого сокета UDP есть буфер приема. Параметры сокета SO_SNDBUF и SO_RCVBUF позволяют нам изменять размеры этих буферов. Основное применение эти функции
находят при передаче большого количества данных по каналам с повышенной вместимостью (long fat pipe), которые представляют собой соединения TCP либо с широкой полосой пропускания, либо с большой задержкой, часто с использованием расширений из RFC 1323. Сокеты UDP, наоборот, могут стремиться увеличить размер приемного буфера, чтобы позволить ядру установить в очередь больше дейтаграмм, если приложение занято.
Упражнения
Напишите программу, которая выводит заданные по умолчанию размеры буферов отправки и приема TCP и UDP, и запустите ее в системе, к которой у вас имеется доступ.
Измените листинг 1.1 следующим образом. Перед вызовом функции connect вызовите функцию getsockopt, чтобы получить размер приемного буфера сокета и MSS. Выведите оба значения. После успешного завершения функции извлеките значения тех же двух параметров сокета и выведите их. Изменились ли значения? Почему? Запустите программу, соединяющуюся с сервером в вашей локальной сети, и программу, соединяющуюся с сервером в удаленной сети. Изменяется ли MSS? Почему? Запустите также программу на разных узлах, к которым у вас есть доступ.
Запустите наш сервер TCP, приведенный в листингах 5.1 и 5.2, и наш клиент ' из листингов 5.3 и 5.4. Измените функцию main клиента, чтобы установить параметр сокета SO_LINGER перед вызовом функции exi t, задав l_onoff равным 1, а 1 linger — равным 0. Запустите сервер, а затем запустите клиент. Введите строку или две на стороне клиента для проверки работоспособности, а затем 'завершите работу клиента, введя символ конца файла. Что происходит? Пос' ле завершения работы клиента запустите программу netstat на узле клиента
и посмотрите, проходит ли сокет через состояние TIME_WAIT.
Будем считать, что два клиента TCP запускаются одновременно. Оба устанавливают параметр сокета SO_REUSEADDR и затем с помощью функции bind связываются с одним и тем же локальным IP-адресом и одним и тем же локальным портом (допустим, 1500). Но один из клиентов соединяется с помощью функции connect с адресом 198.69.10.2, порт 7000, а второй — с адресом 198.69.10.2 (тот же IP-адрес сбеседника), порт 8000. Опишите возникающую ситуацию гонок (race condition).
Получите исходный код для примеров в этой книге (см. предисловие) и откомпилируйте программу sock (см. раздел В.З). Сначала классифицируйте свой узел как узел, не поддерживающий многоадресную передачу, затем — как поддерживающий многоадресную передачу, но не поддерживающий параметр
< SO_REUSEPORT, и наконец как узел, поддерживающий многоадресную передачу с предоставлением параметра SO_REUSEPORT. Постарайтесь запустить несколько экземпляров программы sock в качсст ве сервера TCP (параметр -s командной строки) на одном и том же порте, связывая универсальный адрес, один из адресов интерфейсов вашего узла и адрес закольцовки (loopback address). Нужно ли вам задавать параметр SO_REUSEADDR (параметр -А командной стро-
ки)? Используйте программу netstat для просмотра прослушиваемых со~ кетов.
Продолжайте предыдущий пример, но запустите сервер UDP (параметр -и командной строки) и попытайтесь запустить два экземпляра, связанные с одними и теми же локальным IP-адресом и портом. Если ваша реализация поддерживает параметр SO REUSEPDRT, попытайтесь использовать ее (параметр -Т командной строки).
Многие версии утилиты Ping имеют флаг -d, задающий параметр сокета SO_DEBUG. В чем его назначение?
Продолжая пример в конце нашего обсуждения параметра сокета TCP NODELAY, предположим, что клиент выполняет две операции записи с помощью функции write: первую для 4 байт данных и вторую для 396 байт. Также будем считать, что время задержки АСК — 100 миллисекунд, период RTT между клиентом и сервером равен 100 миллисекундам, а время обработки сервером каждого клиентского запроса — 50 миллисекунд. Нарисуйте временную диаграмму, показывающую взаимодействие алгоритма Нагла с задержанными сегментами АСК.
Снова выполните предыдущее упражнение, считая, что установлен параметр сокета TCP一NOD ELAY.
Снова выполните упражнение 8, считая, что процесс вызывает функцию wri tev один раз для обоих буферов — и для 4-байтового, и для 396~байтового.
Прочтите RFC 1122 [9], чтобы определить рекомендуемый интервал для задержанных сегментов АСК.
В какой из версий наш сервер тратит больше времени — в листинге 5.1 или 5 2? Что происходит, если сервер устанавливает параметр сокета SO_KEEPALIVE, через соединение не происходит обмена данными, узел клиента выходит из строя и не перезагружается?
В какой из версий наш клиент тратит больше времени — в листинге 5.3 или 5.4? Что происходит, если клиент устанавливает параметр сокета SO KEEPALIVE, через соединение не происходит обмена данными и узел сервера выходит из строя и нс перезагружается?
В какой из версий наш клиент тратит больше времени — в листинге 5.3 или 6.2? Что происходит, если клиент устанавливает параметр сокета SO_KEEPALIVE, через соединение не происходит обмена данными и узел сервера выходит из строя и не перезагружается?
Будем считать, что и клиент, и сервер устанавливают параметр сокета SOJCEEPALIVE. Между собеседниками поддерживается соединение, но через это соединение не происходит обмена данными между приложениями. Когда проходят условленные 2 часа и требуется проверить наличие связи, сколькими сегментами TCP обмениваются собеседники?
Почти все реализации определяют константу SO_ACCEPTON в заголовочном файле <sys/socket h>, но мы нс описывали этот параметр. Прочтите [59], чтобы понять, зачем этот параметр существует.
ГЛАВА 8
Основные сведения о сокетах UDP
Введение
Существуют определенные фундаментальные различия между приложениями, использующими TCP и UDP. Они возникают из-за различий в двух транспортных уровнях: UDP является ненадежным протоколом дейтаграмм, не ориентированным на установление соединения, что значительно отличает его от ориентнро~ ванного на установление соединения надежного потока байтов, предоставляемого TCP. Тем не менее есть случаи, когда имеет смысл использовать UDP вместо TCP. Подобные случаи мы рассматриваем в разделе 20 4. Некоторые популярные приложения построены с использованием UDP, например DNS (Domain Name System — система доменных имен), NFS (сетевая файловая система — Neb work File System) и SNMP (Simple Network Management Protocol — простой протокол управления сетью).
Сервер UDP
Рис. 8,1. Функции сокета для модели клиент-севрер UDP
На рис. 8.1 показаны вызовы функций для типичной схемы клиент-сервер UDP. Клиент не устанавливает соединения с сервером. Вместо этого клиент лишь отправляет серверу дейтаграмму, используя функцию sendto (она описывается в следующем разделе), которой нужно задать адрес получателя (сервера) в качестве аргумента. Аналогично сервер не устанавливает соединения с клиентом. Вместо этого сервер лишь вызывает функцию recvfrom, которая ждет, когда придут данные от какого-либо клиента. Функция recvfrom возвращает адрес клиента (для данного протокола) вместе с дейтаграммой, и таким образом сервер может отправить ответ именно тому клиенту, который прислал дейтаграмму.
Рисунок 8.1 иллюстрирует временную диаграмму типичного сценария, имеющего место при обмене UDP-дейтаграммами между клиентом и сервером. Мы можем сравнить этот пример с типичным обменом по протоколу TCP, изображенным на рис. 4.1.
В этой главе мы опишем новые функции, применяемые с сокетами UDP,— recvfrom и sendto, и переделаем нашу модель клиент-сервер для применения UDP. Кроме того, мы рассмотрим использование функции connect с сокетом UDP и концепцию асинхронных ошибок.
Функции recvfrom и sendto
Эти две функции аналогичны стандартным функциям read и write, но трес^ют трех дополнительных аргументов.
#шс1 ude <sys/socket h>
ssize_t recvfrom(int sockfd void ★buff sizjt nbytes int flags struct sockaddr *frcm. socklen.t *addrlen)
ssize t sendto(mt sockfd const void *buff, size_t nbytes int flags const struct sockaddr *to sock]en_t dddrlen)
Обе функции возвращают количество записанных или прочитанных байтов в случае успешного выполнения -1 в случае ошибки
Первые три аргумента, sockfd, buff и nbytes, идентичны первым трем аргументам функций read и write: дескриптор» указатель на буфер, из которого производится чтение или в который происходит запись, и число байтов для чтения или записи.
Мы расскажем об аргументе flags в главе 13, где мы рассматриваем функции recv, send, recvmsg и sendmsg, поскольку сейчас в нашем простом примере они не нужны. Пока мы всегда будем устанавливать аргумент fl ags в пуль.
Аргумент to для функции sendto — это структура адреса сокета, содержащая адрес протокола (например, IP-адрес и номер порта) места, куда отправляются данные. Размер этой структуры адреса сокета задается аргументом addrlen. Функция recvfrom заполняет структуру адреса сокета, на которую указывает аргумент from, записывая в нее адрес протокола места, откуда отправлена дейтаграмма. Число байтов, хранящихся в структуре адреса сокета, также возвращается вызывающему процессу в целом числе, на которое указывает аргумент addrlen. Обратите внимание, что последний аргумент функции sendto является целочисленным значением, в то время как последний аргумент функции recvfrom — это указатель на целое значение (аргумент типа «значение-пезультат» 1
Последние два аргумента функции 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.
Эхо-сервер UDP: функция main
Теперь мы переделаем нашу простую модель клиент-сервер из главы 5, используя UDP. Диаграмма вызовов функций в программах наших клиента и сервера UDP показана на рис. 8.1. На рис. 8.2 представлены используемые функции. В листинге 8.11 показана функция сервера main.
stdin stdoutstdin stdoutРис. 8.2. Простая модель клиент-сервер, использующая UDPРис. 8.2. Простая модель клиент-сервер, использующая UDP
stdin stdout
stdin stdout
Рис. 8.2. Простая модель клиент-сервер, использующая UDP
Рис. 8.2. Простая модель клиент-сервер, использующая UDP
Все исходные кочы программ, опубликованные в эгой книге, вы може1е найти по адресу 11£限方 w \vu piter com/dou nload.
Листинг 8.1. Эхо-сервер UDP
//udpcliserv/udpservOl c
#iDelude "unp h"
int
maindnt argc, char **argv)
{
int sockfd.
struct sockaddr_in servaddr. cliaddr;
sockfd = Socket(AF_INET. SOCK_DGRAM. 0);
bzero(&servaddr. sizeof(servaddr)).
servaddr sin_fami]y = AF_INET.
servaddr sin_addr.s_addr = htonl(INADDR_ANY).
11servaddr. si n_port = htor)s(SERV_PORT):
12Bi nd(sockfd. (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 для обработки клиентского запроса сервером.
Эхо-сервер: функция dg_echo
В листинге 8.2 показана функция dg_echo.
Листинг 8.2. Функция dg_echo: отражение строк на сокете дейтаграмм
//lib/dg_echo с
#1 ncTude "unp h"
void
dg_echo(int sockfd. SA *pclieddr. socklen_t clilen)
{ ~~
mtn.
socklen_t len.
charmesg[MAXLINE],
for (..) {
len = clilen,
n = Recvfrom(sockfd. mesg. MAXLINE. 0. pciiaddr. &len).
SendtoCsockfd, mesg. n. 0. pciiaddr. len).
}
}
Чтение дейтаграммы, отражение отправителю
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.3. Обобщение модели TCP клиент-сервер с двумя клиентами
Рис, 8.3. Обобщение модели TCP клиент-сервер с двумя клиентами
Рис, 8.3. Обобщение модели TCP клиент-сервер с двумя клиентами
Здесь имеется два присоединенных сокета, и каждый из присоединенных сокетов на узле сервера имеет свой собственный буфер приема.
На рис. 8.4 показан случай, котща два клиента отправляют дейтаграммы серверу UDP.
Существует только один процесс сервера, и у него имеется один сокет, на который сервер получает все приходящие дейтаграммы и с которого отправляет все ответы. У этого сокета имеется буфер приема, в который помещаются все приходящие дейтаграммы.
Рис. 8.4* Обобщение модели UDP клиент-сервер с двумя клиентами
Функция mam в листинге 8.1 является зависящей от протокола (она создает сокет протокола AF.INET, а затем выделяет и инициализирует структуру адреса сокета IPv4), но функция dg_echo от протокола не зависит. Причина, по которой функция dg_ech。не зависит от протокола, заключается в том, что вызывающий процесс (в нашем случае функция mam) должен разместить в памяти структуру адреса сокета корректного размера, и указатель на эту структуру вместе с ее размером передаются в качестве аргументов функции dg_echo. Функция dg_echo никогда не углубляется в эту специфичную для протокола структуру: она просто передает указатель на структуру функций recvfrom и sendto. Функция recvfrom заполняет эту структуру, вписывая в нее IP-адрес и номер порта клиента, и поскольку тот же указатель (pc】 laddr) затем передается функции sendto в качестве адреса получателя, таким образом дейтаграмма отражается обратно клиенту, отправившему дейтаграмму.
Эхо-клиент UDP: функция main
Функция main клиента UDP показана в листинге 8.3.
Листинг 8.3. Эхо-клиент UDP //udpcl1serv/udpcli01 с
#include "unp h"
int
mam(int argc. char **argv)
{
intsockfd.
struct sockaddr_in servaddr.
if (argc 『2)
err_quit('usage udpcli <IPaddress>");
bzero(&servaddr sizeof(servaddr)).
servaddr sm_faimly = AFJNET.
Иservaddr sin_port = htons(SERV_PORT)
Inet_pton(AF_INET. argv[l] &servaddr sin_edd「),
sockfd = Socket(AF_INET. SOCK_DGRAM 0).
Листинг 8.3. (продолжение)
dg_cli(stdin, sockfd, (SA *) &servaddr sizeof(servaddr)),
exit(O).
}
Заполнение структуры адреса сокета адресом сервера
12 Структура адреса сокета IPv4 заполняется IP-адресом и номером порта сервера. Эта структура будет передана функции dg_c】1. Она определяет, куда отправлять дейтаграммы.
1-14 Создается сокет UDP и вызывается функция dg_cl i.
Эхо-клиент UDP: функция dg_cli
В листинге 8.4 показана функция dg_cli, которая выполняет большую часть работы на стороне клиента.
Листинг 8.4. Функция dg_cli: цикл обработки клиента
〃lib/dg_cli с
#includa "unp h"
void
dg_c“(FILE *fp, int sockfd const SA *pservaddr socklen t servlen)
{-一
int n,
char sendlme[MAXLINE], recvline[MAXLINE + 1].
while (Fgets(sendline, MAXLINE, fp) i= NULL) {
SendtoCsockfd sendlme, strlen(sendlme). 0. pservaddr, servlen):
n = Recvfrom(sockfd. recvline. MAXLINE, 0 NULL. NULL).
recvline[n] = 0. /* завершающий нуль */
Fputs(recvline. stdout).
}
}
В цикле обработки на стороне клиента имеется четыре шага: чтение строки из стандартного потока ввода при помощи функции fgets, отправка строки серверу с помощью функции sendto, чтение отраженного ответа сервера с помощью функции recvfrom и помещение отраженной строки в стандартный поток вывода с помощью функции fputs.
Наш клиент не запрашивал у ядра присваивания динамически назначаемого порта своему сокету (тогда как для клиента TCP это имело место при вызове функции connect). В случае сокета UDP при первом вызове функции sendto ядро выбирает динамически назначаемый порт, если с этим сокетом еще не был связан никакой локальный порт. Как и в случае TCP, клиент может вызвать функцию bi nd явно, но это делается редко.
Обратите внимание, что при вызове функции recvfrom в качестве пятого и шестого аргументов задаются пустые указатели. Таким образом мы сообщаем ядру,
что мы не заинтересованы в том, чтобы знать, кто отправил ответ. Существует риск, что любой процесс, находящийся как на том же узле, так и на любом другом, может отправить на IP-адрес и порт клиента дейтаграмму, которая будет прочитана клиентом, предполагающим, что это ответ сервера. Эту ситуацию мы рассмотрим в разделе 8.8.
Как и в случае функции сервера dg_echo, функция клиента dg_cl 1 является не зависящей от протокола, но функция mam клиента зависит от протокола. Функция mai п размещает в памяти и инициализирует структуру адреса сокета, относящегося к определенному типу протокола, а затем передает функции da cl i указатель на структуру вместе с ее размером.
Потерянные дейтаграммы
Наш пример клиента и сервера UDP ненадежен. Если дейтаграмма клиента потеряна (допустим, она проигнорирована неким маршрутизатором между клиентом и сервером), клиент навсегда заблокируется в своем вызове функции recvfrom внутри функции dg_cl 1, ожидая ответа сервера, который никогда не придет. Аналогично если дейтаграмма клиента приходит к серверу, но ответ сервера потерян, клиент навсегда заблокируется в своем вызове функции recvfrom. Единственный способ предотвратить эту ситуацию — поместить тайм-аут в клиентский вызов функции recvfrom. Мы рассмотрим это в разделе 13.2.
Простое помещение тайм-аута в вызов функции recvfrom — еще не полное решение. Например, если заданное время ожидания истекло, а ответ не получен, мы не можем сказать точно, в чем дело — или наша дейтаграмма не дошла до сервера, или же ответ сервера не пришел обратно. Если бы запрос клиента содержал требование типа «перевести определенное количество денег со счета А на счет В» (в отличие от случая с нашим простым эхо-сервером), то тогда между потерей запроса и ответа существовала бы большая разница. Более подробно о добавлении надежности в модель клиент-сервер UDP мы расскажем в разделе 20.5.
Проверка полученного ответа
В конце раздела 8.6 мы упомянули, что любой процесс, который знает номер динамически назначаемого порта клиента, может отправлять дейтаграммы нашему клиенту, и они будут перемешаны с нормальными ответами сервера. Все, что мы можем сделать, — это изменить вызов функции recvfrom, представленный в листинге 8.4, так, чтобы она возвращала IP-адрес и порт отправителя ответа, и игнорировать любые дейтаграммы, приходящие не от того сервера, которому мы отправляем дейтаграмму. Однако здесь есть несколько ловушек, как мы дальше увидим.
Сначала мы изменяем функцию клиента mam (см. листинг 8.3), написанную для стандартного эхо-сервера (см. табл. 2.1). Мы просто заменяем присваивание servaddr sin_port = htons(SERV_PORT).
присваиванием
servaddr sin_port = htons(7).
Теперь мы можем использовать с нашим клиентом любой узел, на котором работает стандартный эхо-сервер.
Затем мы переписываем функцию dg_cli, с тем чтобы она размещала в памяти другую структуру адреса сокета для хранения структуры, возвращаемой функцией recvfrom. Мы показываем ее в листинге 8.5.
Листинг 8.5. Версия функции dg_cli, проверяющая возвращаемый адрес сокета //udpcliserv/dgcliaddr с
include”unp h"
void
dg cli(FILE *fp. int sockfd. const SA *pservaddr socklen_t servlen)
{一"
intn.
charsendline[MAXLINE]. recvline[MAXLINE + 1].
socklen_t len,
struct sockaddr *preply_addr,
preply_addr = Malloc(servlen).
while (Fgets(sendline. MAXLINE, fp) ;= NULL) {
Sendto(sockfd sendlme. strlen(sendline). 0. pservaddr, servlen):
len = servlen,
n = Recvfrom(sockfd. recvline. MAXLINE. 0 preply_addr &len)
if (len i= servlen || memcmp(pservaddr. preply_addr len)丨=0) {
printfC*reply from %s (ignored)\en"
Sock_ntop(preply_addr. len))
continue
}
recvline[n] = 0./*завершающийнуль*/
Fputs(recvline stdout).
}
}
Размещение другой структуры адреса сокета в памяти
9 Мы размещаем в памяти другую структуру адреса сокета при помощи функции mall ос. Обратите внимание, что функция dg_cl i все еще является не зависящей от протокола. Поскольку нам не важно, с каким типом структуры адреса сокета мы имеем дело, мы используем в вызове функции malloc только ее размер.
Сравнение возвращаемых адресов
2-13 В вызове функции recvfrom мы сообщаем ядру, что нужно возвратить адрес отправителя дейтаграммы. Сначала мы сравниваем длину, возвращаемую функцией recvfrom в аргументе типа «значение-результат», азатем сравниваем сами структуры адреса сокета при помощи функции memcmp.
Новая версия нашего клиента работает замечательно, если сервер находится на узле с одним-единственным IP-адресом. Но эта программа может не сработать, если сервер имеет несколько сетевых интерфейсов (multihomed server). Запускаем эту программу на нашем узле bsdi, у которого имеется два интерфейса и два IP-адреса:
solans % host bsdi
bsdi kohala com has address 206 62 226 35 bsdi kohala com has address 206 62 226 66 solans % 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-адрес, возвращаемый функцией reevfrom (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-адреса, возвращаемого функцией reevfrom. Другое решение — сделать так, чтобы сервер UDP создал по одному сокету для каждого IP-адреса, сконфигурированного на узле, связал с помощью функции bind этот IP-адрес с сокетом, вызвал функцию select для каждого из всех этих сокетов (ожидая, когда какой- либо из них станет готов для чтения), а затем ответил с сокета, готового для чтения. Поскольку сокет, используемый для ответа, связан с IP-адресом, который являлся адресом получателя клиентского запроса (иначе дейтаграмма не была бы доставлена на сокет), мы можем быть уверены, что адреса отправителя ответа и получателя запроса совпадают. Мы показываем эти примеры в разделах 19.11 и 20.6.
ПРИМЕЧАНИЕ
В системе Solaris с несколькими сетевыми интерфейсами IP-адрес отправителя ответа сервера — это IP-адрес получателя клиентского запроса. Сценарий, описанный в данном разделе, относится к реализациям, происходящим от Беркли, которые выбирают IP-адрес отправителя, основываясь на исходящем интерфейсе.
Запуск клиента без запуска сервера
Следующий сценарий, который мы рассмотрим, — это запуск клиента без запуска сервера. Если мы сделаем так и введем одну строку на стороне клиента, ничего не будет происходить. Клиент навсегда блокируется в своем вызове функции recvfrom, ожидая ответа сервера, который никогда не придет. Но в данном примере это не имеет значения, поскольку сейчас мы стремимся глубже понять протоколы и выяснить, что происходит с нашим сетевым приложением.
Сначала мы запускаем программу tcpdump на узле bsdi, а затем запускаем клиент на том же узле, задав в качестве узла сервера sol an s. Затем мы вводим одну строку, но эта строка не отражается сервером.
bsch % udpcliOl 206.62.226.33 hello, world мы вводим эту строку но ничего не получаем в ответ
В листинге 8.6 показан вывод программы tcpdump.
Листинг 8.6. Вывод программы tcpdump, когда процесс сервера не запускается на узле сервера
01 0 0а гр who-has solans tell bsdi
020002526(00025)а гр reply solans is-at 8 0 20 78еЗ еЗ
030002932(00004)bsdi 1105 > solans 9877 udp 13
040006932(00040)solans > bsdi icmp sol an s udpport 9877unreachable
В первую очередь, мы замечаем, что запрос и ответ 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. Клиент входит в цикл, вы-
зывающий функцию recvfrom для чтения ответов. Две дейтаграммы доставляются корректно (то есть сервер был запущен на двух из трех узлов), но на третьем узле не был запущен сервер, и третий узел отвечает сообщением ICMP о недоступности порта. Это сообщение об ошибке ICMP содержит IP-заголовок и UDP- заголовок дейтаграммы, вызвавшей ошибку. (Сообщения об ошибках ICMPv4 и IC Pv6 всегда содержат заголовок IP и весь заголовок UDP или часть заголовка TCP, чтобы дать возможность получателю сообщения определить, какой сокет вызвал ошибку. Это показано на рис. 25.5 и 25.6.) Клиент, отправивший три дейтаграммы, должен знать получателя дейтаграммы, вызвавшей ошибку, чтобы точно определить, какая из трех дейтаграмм вызвала ошибку. Но как ядро может сообщить эту информацию процессу? Единственное, что может возвратить функция recvfrom, — это значение переменной errno. Но функция recvfrom не может вернуть в ошибке IP-адрес и номер порта получателя UDP-дейтаграммы. Следовательно, было принято решение, что эти асинхронные ошибки возвращаются процессу, только если процесс присоединил сокет UDP лишь к одному определенному собеседнику.
ПРИМЕЧАНИЕ
Linux возвращает большинство ошибок ICMP о недоступности порта даже для непри- соедииенного сокета, поскольку параметр сокета SO_DSBCOMPAT не включен. Возвращаются все ошибки о недоступности получателя, показанные в табл. А.З, отличные от ошибок с кодами 0,1,4, 5, И и 12.
Интерфейс XTI предоставляет способ вернуть процессу дополнительную информацию: это возможно благодаря функции t_rcvuderr (см. раздел 31.4). К сожалению, многие реализации XTI не возвращают эту информацию.
Мы вернемся к проблеме асинхронных ошибок с сокетами UDP в разделе 25 7 и покажем простой способ получения этих ошибок на неприсоединепиом сокете при помощи нашего собственного демона.
Итоговый пример клиент-сервера UDP
На рис. 8.5 крупными черными точками показаны четыре значения, которые должны быть заданы или выбраны, когда клиент отправляет дейтаграмму UDP.
Клиент должен задать IP-адрес сервера и номер порта для вызова функции sendto. Обычно клиентский IP-адрес и номер порта автоматически выбираются ядром, хотя мы отмечали, что клиент может вызвать функцию bind. Мы также отмечали, что если эти два значения выбираются для клиента ядром, то динамически назначаемый порт клиента выбирается один раз — при первом вызове функции sendto — и более никогда не изменяется. Однако IP-адрес клиента может меняться для каждой дейтаграммы UDP, которую отправляет клиент, если предположить, что клиент не связывает с сокетом определенный IP-адрес при помощи функции bind. Причину объясняет рис. 8.5: если узел клиента имеет несколько сетевых интерфейсов, клиент может переключаться между ними (на рис. 8.5 один адрес относится к канальному уровню, изображенному слева, другой — к изобоаженному справа). В худшем ваоианте этого спенапия IP-алпес клиента.
socket ()sendto ()
КлиентКлиентДинамическиназначаемый портклиента, выбранный UDPДинамическиназначаемый портклиента, выбранный UDPСерверЗадается номер заранее известного порта клиентаUDPСерверЗадается номер заранее известного порта клиентаUDPКаналпередачиданныхКаналпередачиданныхUDPUDPIP-адрес клиента,выбранный IP (на основемаршрутизации)IP-адрес клиента,выбранный IP (на основемаршрутизации)КаналпередачиданныхКаналпередачиданныхКаналпередачиданныхКаналпередачиданныхIPIPКаналпередачиданныхКаналпередачиданных
Клиент
Клиент
Динамическиназначаемый портклиента, выбранный UDP
Динамическиназначаемый портклиента, выбранный UDP
Сервер
Задается номер заранее известного порта клиента
UDP
Сервер
Задается номер заранее известного порта клиента
UDP
Каналпередачиданных
Каналпередачиданных
UDP
UDP
IP-адрес клиента,выбранный IP (на основемаршрутизации)
IP-адрес клиента,выбранный IP (на основемаршрутизации)
Каналпередачиданных
Каналпередачиданных
Каналпередачиданных
Каналпередачиданных
IP
IP
Каналпередачиданных
Каналпередачиданных
Рис. 8.5. Обобщение модели клиент-сервер UDP с точки зрения клиента
выбираемый ядром на основе исходящего канального уровня, будет меняться для каждой дейтаграммы.
Что произойдет, если клиент с помощью функции bind свяжет IP-адрес со своим сокетом, но ядро решит, что исходящая дейтаграмма должна быть отправлена с ка- кого-ю другого канального уровня? В этом случае дейтаграмма IP будет содержать IP-адрес отправителя, отличный от IP-адреса исходящего канального уровня (см. упражнение 8.6).
На рис. 8.6 представлены те же четыре значения, но с точки зрения сервера.
recvfrom()recvfrom()socket () bind ()socket () bind ()Рис. 8.6. Обобщение модели клиент-сервер UDP сточки зрения сервераРис. 8.6. Обобщение модели клиент-сервер UDP сточки зрения сервера
recvfrom()
recvfrom()
socket () bind ()
socket () bind ()
Рис. 8.6. Обобщение модели клиент-сервер UDP сточки зрения сервера
Рис. 8.6. Обобщение модели клиент-сервер UDP сточки зрения сервера
У сервера TCP всегда есть простой доступ ко всем четырем фрагментам информации для присоединенного сокета, и эти четыре значения остаются постоянными в течение всего времени жизни соединения. Однако в случае соединения UDP IP-адрес получателя можно получить только с помощью установки параметра сокета IP_RECVDSTADDR для IPv4 или IPV6_PKTINF0 для IPv6 и последующего
вызова функции recvmsg вместо функции recvfrom. Поскольку протокол UDP не ориентирован на установление соединения, IP-адрес получателя может меняться для каждой дейтаграммы, отправляемой серверу. Сервер UDP может также получать дейтаграммы, предназначенные для одного из широковещательных адресов узла или для адреса многоадресной передачи, что мы обсуждаем в главах 18 и 19. Мы покажем, как определить адрес получателя дейтаграммы UDP, в разделе 20.2, после того как опишем функцию recvmsg.
Таблица 8.1. Информация, доступная серверу из приходящей дейтаграммы IP
Из IP-дейтаграммы клиента
ТСР-сервер
UDP-сервер
IP-адрес отправителя
accept
recvfrom
Номер порта отправителя
accept
recvfrom
IP-адрес получателя
gctsockname
recvmsg
Номер порта получателя
getsockname
getsockname
Функция connect для UDP
В конце раздела 8.9 мы упомянули, что асинхронные ошибки не возвращаются на сокете UDP, если сокет не был присоединен. На самом деле для сокета UDP мы можем вызвать функцию connect (см. раздел 4.3). Но это не приведет ни к чему похожему на соединение TCP: здесь не существует трехэтапного рукопожатия. Ядро просто записывает IP-адрес и номер порта собеседника, которые содержатся в структуре адреса сокета, передаваемой функции connect, и немедленно возвращает управление вызывающему процессу.
ПРИМЕЧАНИЕ
Перегрузка функции connect этой новой возможностью для сокетов UDP может внести путаницу. Если используется соглашение о том, что sock пате — это адрес локального протокола, a peername — адрес удаленного протокола, то лучше бы эта функция называлась setpeername. Аналогично, функции bind больше подошло бы название setsockname.
Обладая этой возможностью, необходимо понимать разницу между двумя видами сокетов UDP.
* Неприсоединенный (unconnected) сокет UDP, то есть сокет UDP, создаваемый по умолчанию.
'' Присоединенный {connected) сокет UDP, результат вызова функции connect для сокета UDP.
Присоединенному сокету UDP свойственны три отличия от неприсоединен- ного сокета, который создается по умолчанию.
Мы больше не можем задавать IP-адрес получателя и порт для операции вывода. То есть мы используем вместо функции sendto функцию write или send. Все, что записывается в присоединенный сокет UDP, автоматически отправляется на адрес протокола (например, IP-адрес и порт), заданный функцией connect.、
ПРИМЕЧАНИЕ
Аналогично TCP мы можем вызвать функцию sendto для присоединенного сокета UDP, но не можем задать адрес получателя. Пятый аргумент функции sendto (указатель на структуру адреса сокета) должен быть пустым указателем, а шестой аргумент (размер структуры адреса сокета) должен быть нулевым. В стандарте Posix.lg определено, что когда пятый аргумент является пустым указателем, шестой аргумент игнорируется.
Вместо функции recvfrom мы используем функцию read или recv. Единственные дейтаграммы, возвращаемые ядром для операции ввода через присоединенный сокет UDP, — это дейтаграммы, приходящие с адреса, заданного в функции connect. Дейтаграммы, предназначенные для адреса локального протокола присоединенного сокета UDP (например, IP-адрес и порт), но приходящие с адреса протокола, отличного от того, к которому сокет был присоединен с помощью функции connect, не передаются присоединенному сокету. Это ограничивает присоединенный сокет UDP, позволяя ему обмениваться дейтаграммами с одним и только одним собеседником.
ПРИМЕЧАНИЕ
Точнее, обмен дейтаграммами происходит только с одним IP-адрссом, а не с одним собеседником, поскольку это может быть IP-адрес многоадресной передачи, представляющий, таким образом, группу собеседников.
Асинхронные ошибки возвращаются процессу только при операциях с присоединенным сокетом UDP. В результате, как мы уже говорили, неприсоеди- ненный сокет UDP не получает никаких асинхронных ошибок.
В табл. 8.2 сводятся воедино свойства, перечисленные в первом пункте, применительно к 4.4BSD.
Таблица 8.2. Сокеты TCP и UDP: может ли быть задан адрес протокола получателя
Тип сокетаwrite или send
sendto, без указания получателя
sendto, с указани* ем получателя
Сокет TCPДа
Да
EISCONN
Сокет UDP, присоединенны й Да
Да
EISCONN
Сокет UDP, неприсоединенный EDESTADDRREQ
EDESTADDRREQ
Да
ПРИМЕЧАНИЕ
Posix.lg определяет, что операция вывода, не задающая адрес получателя на пеприсо- единенном сокете UDP, должна возвращать ошибку ENOTCONN, а не EDESTADDR- REQ
Solans 2.5 допускает функцию sendto, которая задает адрес получателя для присоединенного сокета UDP. Posix.lg определяет, что в такой ситуации должна возвращаться ошибка EISCONN.
На рис. 8.7 обобщается информация о присоединенном сокете UDP.
Приложение вызывает функцию connect, задавая IP-адрес и номер порта собеседника. Затем оно использует функции read и write для обмена данными с собеседником.
и/или номера порта
Рис. 8.7. Присоединенный сокет UDP
Дейтаграммы, приходящие с любого другого IP-адреса или порта (который мы обозначаем как «???》на рис. 8.7), не передаются на присоединенный сокет, поскольку либо IP-адрес, либо UDP-порт отправителя не совпадает с адресом протокола, с которым сокет соединяется с помощью функции connect. Эти дейтаграммы могут быть доставлены на какой-то другой сокет UDP на узле. Если нет другого совпадающего сокета для приходящей дейтаграммы, UDP проигнорирует ее и сгенерирует сообщение об ошибке ICMP о недоступности порта.
Обобщая вышесказанное, мы можем утверждать, что клиент или сервер UDP может вызвать функцию connect, только если этот процесс использует сокет UDP для связи лишь с одним собеседником. Обычно именно клиент UDP вызывает функцию connect, но существуют приложения, в которых сервер UDP связывается с одним клиентом на длительное время (например, TFTP), и в этом случае и клиент, и сервер вызывают функцию connect.
DNS предоставляет другой пример, как показано на рис. 8.8.
Клиент DNS
Сервер DNS
Клиент DNS
Сервер DNS
-
< ■
Вызов функции connect проходит успешно (клиент сконфигурирован для использования одного сервера)
Вызов функции connect проходит успешно (клиент сконфигурирован для использования одного сервера)
Невозможно вызвать функцию connect
Невозможно вызвать функцию connect
Рис. 8.8. Пример клиентов и серверов DNS и функции connect
Клиент DNS может быть сконфигурирован для использования одного или более серверов, обычно с помощью перечисления IP-адресов серверов в файле /etc/resolv.conf. Если в этом файле указан только один сервер (на рисунке этот клиент изображен в крайнем слева прямоугольнике), клиент может вызвать функцию connect, но если перечислено множество серверов (второй справа прямоугольник на рисунке), клиент не может вызвать функцию connect. Обычно сервер DNS обрабатывает также любые клиентские запросы, следовательно, серверы не могут вызывать функцию connect. • ,
Многократный вызов функции connect для сокета UDP
Процесс с присоединенным сокетом UDP может снова вызвать функцию connect для этого сокета, чтобы
задать новый IP-адрес и порт;
отсоединить сокет.
Первый случай, задание нового собеседника для присоединенного сокета UDP, отличается от использования функции connect с сокетом TCP: для сокета TCP функция connect может быть вызвана только один раз.
Чтобы отсоединить сокет UDP, мы вызываем функцию connect, но прсваиваем элементу семейства структуры адреса сокета (si n_fami 1удля IPv4 или sin6_fami 1у для IPv6) значение AF_UNSPEC. Это может привести к ошибке EAFNOSUPPORT [105, с. 736], но это нормально. Именно процесс вызова функции connect на уже присоединенном сокете UDP позволяет отсоединить сокет [105, с. 787-788].
ПРИМЕЧАНИЕ
В руководстве BSD по поводу функции connect традиционно говорилось. «Сокеты I дейтаграмм могут разрывать связь, соединяясь с недействительными адресами, таки
ми как пустые адреса». К сожалению, ни в одном руководстве не сказано, что представляет собой «пустой адрес», и не упоминается, что в результате происходит ошибка (что нормально). Стандарт Posix. 1g явно указывает, что семейство адресов должно быть установлено в AF_UNSPEC, но затем сообщает, что этот вызов функции connect может возвратить, а может и не возвратить ошибку EAFNOSUPPORT.
Производительность
Когда приложение вызывает функцию sendto на неприсоединенном сокете UDP, ядра реализаций, происходящих от Беркли, временно соединяются с сокетом, отправляют дейтаграмму и затем отсоединяются от сокета [105, с. 762-763]. Таким образом, вызов функции sendto для последовательной отправки двух дейтаграмм на неприсоединенном сокете включает следующие шесть шагов, выполняемых ядром:
孫 присоединение сокета;
爨 вывод первой дейтаграммы;
龍 отсоединение сокета;
髪 присоединение сокета;
纓 вывод второй дейтаграммы;
?" отсоединение сокета.
ПРИМЕЧАНИЕ
Другой момент, который нужно учитывать, — количество поисков в таблице маршрутизации. Первое временное соединение производит поиск в таблице маршрутизации IP-адреса получателя и сохраняет (кэширует) эту информацию. Второе временное соединение отмечает, что адрес получателя совпадает с кэшированным адресом из таблицы маршрутизации (мы считаем, что обеим функциям sendto задан один и тот же получатель), и ему не нужно снова проводить поиск в таблице маршрутизации [105, с. 737-738].
Когда приложение знает, что оно будет отправлять множество дейтаграмм одному и тому же собеседнику, эффективнее будет использовать сокет явно. Вызов функции connect, за которым следуют два вызова функции write, теперь будет включать следующие шаги, выполняемые ядром:
'присоединение сокета;
' > вывод первой дейтаграммы;
< вывод второй дейтаграммы.
В этом случае ядро копирует структуру адреса сокета, содержащую IP-адрес получателя и порт, только один раз, а при двойном вызове функции sendto копирование выполняется дважды. В [76] отмечается, что на временное присоединение отсоединенного сокета UDP приходится примерно треть стоимости каждой передачи UDP.
Функция dg.cli (продолжение)
Вернемся к функции dg cl?, показанной в листинге 8.4, и перепишем ее, с тем чтобы она вызывала функцию connect. В листинге 8.7 показана новая функция.
Листинг 8・7・ Функция dg_ch, вызывающая функцию connect
//udpcl1serv/dgcl1connect с
1 ^include "unp h"
2 3 4 5 62 3 4 5 6void
2 3 4 5 6
2 3 4 5 6
dg_cli(FILE *fp. int sockfd const SA *pservadclr. socklen_t servlen) {"-'
1ntn.
charsendline[MAXLINE], recvlinefMAXLINE + 1];
Connect(sockfd (SA *) pservaddr, servlen).
while (Fgets(sendhne MAXLINE, fp) i= NULL) {
Write(sockfd sendline. strlen(sendline))
n = Read(sockfd. recvline MAXLINE)
recvline[n] = 0./* завершающий нуль */
Fputs(recvline. stdout)
}
}
Изменения по сравнению с предыдущей версией — это добавление вызова функции connect и замена вызовов функций sendto и recvfrom вызовами функций write и read. Функция dg_cl 】остается не зависящей от протокола, поскольку она не вникает в структуру адреса сокета, передаваемую функции connect. Наша функция main клиента, показанная в листинге 8.3, остается той же.
Если мы запустим программу на узле bsdi, задав IP-адрес узла solans (который не запускает наш сервер на порте 9877), мы получим следующий вывод: bsdi % udpcli04 206.62.226.33 hello, world read error Connection refused
Первое, что мы замечаем — мы не получаем ошибку, когда запускаем процесс клиента. Ошибка происходит только после того, как мы отправляем серверу первую дейтаграмму. Именно отправка этой дейтаграммы вызывает ошибку ICMP от узла сервера. Но когда клиент TCP вызывает функцию connect, задавая узел сервера, на котором не запущен процесс сервера, функция connect возвращает ошибку, поскольку вызов функции connect вызывает отправку первого пакета трехэтапного рукопожатия TCP, и именно этот пакет вызывает получение сегмента RST от TCP сервера (см. раздел 4.3).
В листинге 8.8 показан вывод программы tcpdump.
Листинг 8.8. Вывод программы tcpdump при запуске функции dg_ch bsdi ] tcpdump
01 0 0bsdi 1318 > solans 9877 udp 13
02 0 000628 ( 0 0006) solans > bsdi icmp solans udp port 9877 unreachable
В табл. A.3 мы также видим, что возникшую ошибку ICMP ядро сопоставляет ошибке ECONNREFUSED, которая соответствует выводу строки сообщения Connection refused (В соединении отказано) функцией err一sys.
ПРИМЕЧАНИЕ
К сожалению, не все ядра возвращают сообщения ICMP присоединенному сокету UDP, как мы показали в этом разделе. Обычно ядра реализаций, происходящих от Беркли, возвращают эту ошибку, а ядра System V — не возвращают Например, если мы запустим тот же клиент на узле Solans 24 и с помощью функции connect соединимся с узлом, на котором не запушен наш сервер, то с помошью программы tcpdump мы сможем убедиться, что ошибка ICMP о недоступности порта возвращается узлом сервера, но вызванная клиентом функция read никогда не завершается Эта ситуация была исправлена в Solans 2.5. UnixWare не возвращает ошибку, в то время как А1Х» Digital Unix, HP-UX и Linux возвращают.
Отсутствие управления потоком в UDP
Теперь мы проверим, как влияет на работу UDP отсутствие какого-либо управления потоком. Сначала мы изменим нашу функцию dg_cl 1 так, чтобы она отправляла фиксированное число дейтаграмм. Она больше не читает из стандартного потока ввода. В листинге 8.9 показана новая версия функции. Эта функция отправляет серверу 2000 1400-байтовых дейтаграмм UDP.
Листинг 8.9. Функция dg_cli, отсылающая фиксированное число дейтаграмм серверу —
//udpcl1serv/dgcli1oopl c
#mc1ude "unp h"
#defme NDG2000/* количество дейтаграмм для отправки */
#defme DGLEN 1400/* длина каждой дейтаграммы */
void
dg_cli(FILE *fp int sockfd const SA *pservaddr socklen_t servlen)
{一一
int1,
charsend1ine[DGLEN].
for (I = 0. i < NDG i++) {
Sendto(sockfd sendline DGLEN. 0. pservaddr. servlen);
}
}
Затем мы изменяем сервер так, чтобы он получал дейтаграммы и считал число полученных дейтаграмм. Сервер больше не отражает дейтаграммы обратно клиенту. В листинге 8.10 показана новая функция dg_echo. Когда мы завершаем процесс сервера с помощью нашего ключа прерывания (SIGINT), он выводит число полученных дейтаграмм и завершается.
Листинг 8.10. Функция dg_echo, считающая полученные дейтаграммы //udpcl1serv/dgecholoopl с
include ”unp h"
static void recvfrom_int(int)
static mt count
void
dg_echo(mt sockfd SA *pc1iaddr. socklen t clilen)
{ ~—
socklen_t len.
char mesg[MAXLINE].
Signal(SIGINT recvfromjnt);
for ( ) {
len = clilen.
Recvfrom(sockfdmesg MAXLINE, 0. pcltaddr. &1en);
count++,
}'
IS }…
static void
recvfrom_int(int signo)
{—
рнntf('\enreceived %d datagrams\en", count):
exit(0),
}
Теперь мы запускаем сервер на узле bsch, в медленной системе 80386. Клиент мы запускаем в значительно более быстрой системе Sparc Station 4. Кроме того, мы запускаем программу netstat —s на узле сервера и до, и после запуска клиента и сервера, поскольку выводимая статистика покажет, сколько дейтаграмм мы потеряли. В листинге 8.11 показан вывод сервера.
Листинг 8.11. Вывод на узле сервера bsdi % netstat -s | tail udp 80300 datagrams received
0 with incomplete header
0 with bad data 1ength field
;0 with bad checksum
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 % udpserv063dHycкаем наш сервер
клиент посылает дейтаграммы
人?
после окончания работы клиента вводим наш символ прерывания
bsdi
% netstat -s 1 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 (38821970), что при добавлении к выводу счетчика дейтаграмм, полученных приложением (82), дает 1994 дейтаграммы, полученные узлом. К сожалению, счетчик дейтаграмм, отброшенных из-за заполненного буфера, в программе netstat распространяется на всю систему. Не существует способа определить, на какие приложения (например, какие порты UDP) это влияет.
ПРИМЕЧАНИЕ
Обратите внимание, что 97% всех полученных дейтаграмм UDP (77 725 из 80 300) на этом конкретном узле являются дейтаграммами широковещательной или многоадресной передачи, которые затем игнорируются, поскольку нет приложения с сокетом, свя- 、занным с портом получателя. Мы вернемся к этому феномену, когда будем говорить о широковещательной передаче в главе 18.
Число дейтаграмм, полученных сервером в этом примере, недетерминирова- но. Оно зависит от многих факторов, таких как нагрузка сети, загруженность узла клиента и узла сервера. Если мы запустим этот пример еще пять раз, счетчик полученных дейтаграмм может принять значения 37,108,30,108 и 114.
Если мы запустим тот же клиент и тот же сервер, но на этот раз клиент на медленной системе 80386, а сервер на быстрой системе Sparc Station 4, никакие лейтагпаммы не потепяются
solans % udpserv06
人? после окончдния работы клиентд вводим ндш символ прерывания received 2000 datagrams
Если мы запустим программу netstat -s под Solaris, формат вывода будет отличаться от классического вывода для реализаций Беркли, показанного в листинге 8.11. Формат Solaris воспроизводит счетчики SNMP (Simple Network Management Protocol — простой протокола управления сетью, см. главу 25 [94]). Счетчик udpInDatagrams программы netstat указывает число дейтаграмм UDP, доставленных пользовательским процессам, — 139 до начала работы нашего клиента и 2139 после, то есть доставлены все 2000 дейтаграмм. Счетчик udpInOverf ] 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, увеличивающая размер приемного буфера сокета
//udpcl1serv/dgecholоор2 с 1 include "unp h"
static void recvfrom_int(int):
static int count,-
4 5 6 7 8 94 5 6 7 8 9void
4 5 6 7 8 9
4 5 6 7 8 9
dg_echo(int sockfd SA *pcliaddr. socklen_t clilen) { '-
int n.
socklen^t len
charmesg[MAXLINE],'
Signal(SIGINT recvfrom int).
n = 240 * 1024
Setsockopt(sockfd. SOL_SOCKET. SO RCVBUF. &n. sizeof(n));
Листинг 8.12 (продолжение)
15Recvfrom(sockfd mesg MAXLINE, 0. pcliaddr, &len).
16count++,
17}
18 }
9 0 12 3 412 2 2 2 29 0 12 3 412 2 2 2 2static void
9 0 12 3 4
12 2 2 2 2
9 0 12 3 4
12 2 2 2 2
「ecvf「om_】nt(int signo)
{ 一
p门ntf("\enreceived %d datagrams\en" count) exit(O)
ПРИМЕЧАНИЕ
Почему мы устанавливаем размер буфера приема сокета равным 240x1024 байт в листинге 8.12? Максимальный размер приемного буфера сокета в BSD/OS 2 1 по умолчанию равен 262 144 байт (256x1024), но из-за способа размещения буфера в памяти (описанного в главе 2 [105]) он в действительности ограничен до 246 723 байт Многие более < ранние системы, основанные на 4.3BSD, ограничивали размер буфера приема сокета г примерно до 52 000 байт.
Определение исходящего интерфейса для UDP
С помощью присоединенного сокета UDP можно также задавать исходящий интерфейс, который будет использован для отправки дейтаграмм к определенному получателю. Это объясняется побочным эффектом функции connect, примененной к сокету UDP: ядро выбирает локальный IP-адрес (считая, что процесс еще не вызвал функцию bind для явного его задания). Локальный адрес выбирается в процессе поиска адреса получателя в таблице маршрутизации, причем берется основной IP-адрес интерфейса, с которого, согласно таблице, будут отправляться дейтаграммы.
В листинге 8.13 показана простая программа UDP, которая с помощью функции connect соединяется с заданным IP-адресом и затем вызывает функцию getsockname, выводя локальный IP-адрес и порт.
Листинг 8.13. Программа UDR использующая функцию connect для определения исходящего интерфейса
//udpcl1serv/udpcli09 с
#include Munp h"
int
main(mt argc char **argv)
{
mt sockfd.
sock1en_t len.
struct sockaddjin cliaddr. servaddr,
if (argc •= 2)
err quit(“usage udpcl1 <IPaddress>M)
sockfd = Socket(AF_INET SOCK_DGRAM 0).
bzero(&servaddr sizeof(servaddr))
servaddr sin_family = AF_INET
servaddr sin_port = htons(SERV_P0RT)
Inet_pton(AF_INET argv[l] &servdddr s】n_addr)
Connect(sockfd, (SA *) &servaddr sizeof(servaddr)).
len = sizeof(cliaddr)
Getsockname(sockfd (SA *) &c1iddd「 &len)
prmtf("local address %s\en' Sock_ntop((SA *) &cliaddr. len)):
exit(0) 20 }
Если мы запустим программу на узле bsdi с несколькими сетевыми интерфейсами, мы получим следующий вывод:
bsdi % udpcl109 206.62.226.42 local address 206 62 226 35 1331
bsdi % udpcl109 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 и UnixWare, но работает в AIX, Digital Unix, Linux и Solaris 2.6.
Эхо-сервер TCP и UDP, использующий функцию select
Теперь мы объединим наш параллельный эхо-сервер TCP из главы 5 и наш последовательный эхо-сервер UDP из данной главы в один сервер, использующий функцию select для мультиплексирования сокетов TCP и UDP. В листинге 8 14 представлена первая часть этого сервера
Листинг 8.14. Первая часть эхо-сервера, обрабатывающего сокеты TCP и UDP при помощи функции select
//udpcl1serv/udpservsel ectOl с
1
include "unp h"
2
3
4
5
6
7
8
9
10
11
12
13
1nt
mdin(int argc. char **argv)
{
mtlistenfd, connfd. udpfd. nready. maxfdpl;
charmesg[MAXLINE],
pid_t chi1 dpid.
fd_set rser.
ssize_t n.
socklen_t len,
const mt on = 1:
struct sockaddr_in cliaddr. servaddr,
void sigchld(int).
14
15
/* создание 叩。слуимваемо「。 сокета TCP */ listenfd = Socket(AF_INET, SOCK_STREAM. 0)j
16
17
18
19
bzero(&servaddr. sizeof(servaddr)).
servaddr sin_famil>y = AF_INET.
se「vadd「.sin_add「 s_addr = htonl(INADDR_ANY), servaddr sin_port 3s htons(SERV_PORT).
20
21
Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR. &on. sizeof(on)). Bind(listenfd. (SA *) &servaddr. sizeof(servaddr)).
22
Listen(listenfd. LISTENO).
23
24
/* создание сокета UDP */
udpfd = Socket(AF_INET, SOCK_DGRAM. 0):
25
26
27
28
bzero(&servaddr. sizeof(servaddr)):
servaddr sin_faim1y = AF_INET.
servaddr sin_addr s_addr = htonl(INADDR_ANY).
servaddr sm_port = htons(SERV_PORT).
29
Bi nd(udpfd, (SA *) &servaddr sizeof(servaddr)).
Создание прослушиваемого сокета TCP
Т-22 Создается прослушиваемый сокет TCP, который связывается с заранее известным портом сервера. Мы устанавливаем параметр сокета SO_REUSEADDR в случае если на этом порте существуют соединения.
Создание сокета UDP
"29 Также создается сокет UDP и связывается с тем же портом. Даже если один и тот же порт используется для сокетов TCP и UDP, нет необходимости устанавливать параметр сокета SO_REUSEADDR перед этим вызовом функции bind, поскольку порты TCP не зависят от портов UDP.
В листинге 8.15 показана вторая часть нашего сервера.
Листинг 8.15. Вторая половина эхо-сервера, обрабатывающего TCP и UDP при помощи функции select
udpcl1serv/udpservselectOl.с
30 Signal(SIGCHLD, sig_chld). /* требуется вызвать waitpidO */
31 FD_ZERO(&rset),
32 maxfdpl = maxdistenfd. udpfd) + 1.
33 for (.,) {
34FD_SET(listenfd. &rset).
35FD_SET(udpfd &rset).
36 if ( (nready = select(maxfdpl, &「set. NULL. NULL. NULL#?
37if (errno == EINTR)
38continue. /* назад в forО */
39else
40err_sys(,fselect error").
41} 一
42 if (FDJSSETdistenfd. &「set)) {
43 len = sizeof(cliaddr),
44 connfd = Acceptdistenfd. (SA *) &cliadd「. &1 en):
45 if ( (chi 1 dpid = ForkO) == 0) {/* дочерний процесс */
46Closedistenfd)./* закрывается проеду山мваемь血сокет */
47str echo(connfd):/* обработка запроса */
48exit(0).
49}
50Close(connfd)./* родитель закрывает прмсоедмненнь血 сокет */
51}
52if (FD_ISSET(udpfd. &rset)) {
53 len = sizeof(cliaddr).
54n = Recvfrom(udpfd mesg. MAXLINE, 0. (SA *) &cliaddr. &len).
55Sendto(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_ch】d может прервать вызов функции select, мы обрабатываем ошибку EINTR.
Обработка нового клиентского соединения
42-51 С помощью функции accept мы принимаем новое клиентское соединение, а когда прослушиваемый сокет TCP готов для чтения, с помощью функции fork порож-
даем дочерний процесс и вызываем нашу функцию str_echo в дочернем процессе. Это та же последовательность шагов, которую мы выполняли в главе 5.
Обработка приходящей дейтаграммы
52-57 Если сокет UDP готов для чтения, дейтаграмма пришла. Мы читаем ее с по* мощью функции recvfrom и отправляем обратно клиенту с помощью функции sendto.
Резюме
Преобразовать наши эхо-клиент и эхо-сервер так, чтобы использовать UDP вместо TCP, оказалось несложно. Но при этом мы лишились множества возможностей, предоставляемых протоколом TCP: определение потерянных пакетов и повторная передача, проверка, приходят ли пакеты от корректного собеседника, и т. д. Мы возвратимся к этой теме в разделе 20.5 и увидим, как можно улучшить надежность приложения UDP.
Сокеты UDP могут генерировать асинхронные ошибки, то есть ошибки, о которых сообщается спустя некоторое время, после того как пакет был отправлен. Сокеты TCP всегда сообщают приложению об этих ошибках, но в случае UDP для получения этих ошибок сокет должен быть присоединенным.
В UDP отсутствует возможность управления потоком, и это очень легко продемонстрировать. Обычно это не создает проблем, поскольку многие приложения UDP построены с использованием модели «запрос-ответ» и не предназначен ны для передачи большого количества данных.
Есть еще ряд моментов, которые нужно учитывать при написании приложений UDP, но мы рассмотрим их в главе 20 после описания функций интерфейсов, широковещательной и многоадресной передачи.
Упражнения
Допустим, у нас имеется два приложения: одно использует TCP, а другое — UDP. В приемном буфере сокета TCP находится 4096 байт данных, а в приемном буфере для сокета UDP находятся две дейтаграммы по 2048 байт. Приложение TCP вызывает функцию read с третьим аргументом 4096, а приложение UDP вызывает функцию recvfrom с третьим аргументом 4096. Есть ли между этими вызовами какая-нибудь разница?
Что произойдет в листинге 8.2, если мы заменим последний аргумент функции sendto (который мы обозначили 1 еп) аргументом clilеп?
Откомпилируйте и запустите сервер UDP из листингов 8.1 и 8.4, а затем — клиент из листингов 8.3 и 8.4 Убедитесь в том, что клиент и сервер работают вместе.
Запустите программу pi ng в одном окне, задав параметр -1 60 (отправка одного пакета каждые 60 секунд; некоторые системы используют ключ I вместо i), параметр -v (вывод всех полученных сообщений об ошибках ICMP) и задав
_ адрес закольцовки на себя (обычно 127.0.0.1). Мы будем использовать эту про-
грамму, чтобы увидеть ошибку ICMP недоступности порта, возвращаемую узлом сервера. Затем запустите наш клиент из предыдущего упражнения в другом окне, задав IP-адрес некоторого узла, на котором не запущен сервер. Что происходит?
Рассматривая рис. 8.3, мы сказали, что каждый присоединенный сокет TCP имеет свой собственный буфер приема, А если речь идет о прослушиваемом сокете, как вы думаете, есть ли у него свой собственный буфер приема?
Используйте программу sock (см. раздел В.З) и такое средство, как, например, tcpdump (см. раздел В.5), чтобы проверить утверждение раздела 8.10: если клиент с помощью функции bi nd связывает IP-адрес со своим сокетом, но отправляет дейтаграмму, исходящую от другого интерфейса, то результирующая дейтаграмма содержит IP-адрес, который был связан с сокетом, даже если он не соответствует исходящему интерфейсу.
Откомпилируйте программы из раздела 8.13 и запустите клиент и сервер на различных узлах. Помещайте pnntf в клиент каждый раз, когда дейтаграмма записывается в сокет. Изменяет ли это процент полученных пакетов? Почему? Помещайте pnntf в сервер каждый раз, когда дейтаграмма читается из сокета. Изменяет ли это процент полученных пакетов? Почему?
Какова наибольшая длина, которую мы можем передать функции sendto для сокета UDP/IPv4, то есть каково наибольшее количество данных, которые могут поместиться в дейтаграмму UDP/IPv4? Что изменяется в случае UDP/IPv6? Измените листинг 8.4, с тем чтобы отправить одну дейтаграмму UDP максимального размера, считать ее обратно и вывести число байтов, возвращаемых функцией recvfrom.
ГЛАВА9
Элементарные преобразования имен и адресов
Введение
Во всех примерах этой книги мы использовали численные адреса узлов (например, 206 6.226 33) и численные номера портов для идентификации серверов (например» порт 13 для стандартного сервера времени и даты и порт 9877 для нашего эхо-сервера). Однако по ряду соображений предпочтительнее использовать имена вместо чисел: во-первых, имена проще запоминаются, во-вторых, если численный адрес поменяется, имя можно сохранить, и в-третьих, с переходом на IPv6 численные адреса станут значительно длиннее, что увеличит вероятность ошибки при вводе адреса вручную. В этой главе описываются функции, выполняющие преобразование имен и адресов, gethostbyname и gethostbyaddr для преобразования имен узлов и IP-адресов, а также getservbyname и getservbyport для преобразования имен служб и номеров портов.
Функции имен узлов недавно были усовершенствованы для работы с IPv6 в дополнение к IPv4, и мы также опишем эти изменения Это будет началом нашего перехода к независимости от протокола, который мы продолжим в главе 11. В ней применяются функции, обсуждаемые в этой главе, а также разрабатываются различные функции, которые помогут сделать наши приложения не зависящими от протокола.
Система доменных имен
Система доменных имен (Domain Name System, DNS) используется прежде всего для сопоставления имен узлов и IP-адресов. Имя узла может быть либо простым (simple пате), таким как solans или bsdi, либо полным доменным именем (fully qualified domain name, FQDN) — например, sol ari s kohala com.
ПРИМЕЧАНИЕ
В техническом отношении FQDN может также называться абсолютным именем и должно оканчиваться точкой, но пользователи часто игнорируют точку в конце.
В этом разделе мы рассмотрим только основы DNS, необходимые нам для се- тат>стс ттпсгпо1игк〃ипспанъта ПтлтятРПМ WМТРПРГVTOTTIUPrЯ боПРР ПОППпбнЫМ ИЗЛО"
жением вопроса, могут обратиться к главе 14 [94] ик [1]. Дополнения, требуемые для IPv6, изложены в RFC 1886 [99].
Записи ресурсов
Записи в DNS называются записями ресурсов (resource recodrs, R&). Нас ннтере- сует только несколько типов RR.
А. Запись типа А сопоставляет имя узла 32-битовому адресу IPv4. Вот, например, четыре записи DNS для узла sol an s в домене kohal а сот, первая из которых —это запись типа А:
solans IN А 206 62 226 33
IN ААДА 5flb dfOO сеЗе е200 0020 0800 2078 еЗеЗ
IN MX 5 solsns kohala com IN MX 10 mai1 host kohala com
灘 AAAA. Запись типа AAAA, называемая «четыре А» (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) и добавляется к чрб 1nt.
Например, две записи PTR для нашего узла solans будут выглядеть так:
33 226 62 206 in-addr арга
3e3e870200800200002ee3ec00fdblf5ip6mt
疚 MX. Запись типа MX (Mail Exchange Record) определяет, что узел выступает в роли «маршрутизирующего почтового сервера» для заданного узла. В приведенном выше примере для узла solans предоставлено две записи типа MX. Первая имеет предпочтительное значение 5, вторая — 10. Когда существует множество записей типа MX, они используются в порядке предпочтения, начиная с наименьшего значения.
ПРИМЕЧАНИЕ—
Мы не используем в примерах книги записей типа MX, но упоминаем о них, потому что они широко используются в реальной жизни
、CNAME. Аббревиатура CNAME означает «каноническое имя» (canonical name). Обычно такие записи используются для присвоения имен общим службам, таким как ftp и www. При использовании имен служб вместо действительного имени узла перемещение службы на другой узел становится прозрачным (то есть незаметным для пользователя). Например, для нашего узла каноническими именами могут быть следующие записи:
ftpINCNAMEbsdikohala com
wwwINCNAMEbsdikohala com
mail hostINCNAMEbsdikohala com
Сейчас прошло еще слишком мало времени с момента появления протокола IPv6, чтобы сказать, каких соглашений будут придерживаться администраторы для узлов, поддерживающих и IPv4, и IPv6. В нашем примере мы задали узлу sol аки запись типа А, и запись типа АААА. Некоторые администраторы помещают все записи типа АААА в специальный поддомен, часто называемый ipv6. Например, имя узла, связанное с записью типа АААА, в этом случае будет иметь вид 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-61】
IN
AAAA
fe80 0800 5afc 2b36
Эта дает нам дополнительный контроль над протоколом, выбранным некоторыми приложениями, как мы увидим в следующей главе.
Распознаватели и серверы имен
Организации работают с одним или большим количеством серверов имен (name servers), при этом часто используется программа BIND (Berkeley Internet Name Domain). Приложения, такие как клиенты и серверы, которые мы создаем в этой книге, соединяются с сервером DNS при помощи вызова функций из библиотеки, называемой распознавателем (resolver). Обычные функции распознавателя — gethostbyname и gethostbyaddr, и обе они описаны в этой главе. Первая находит адрес узла по его имени, а вторая — наоборот.
На рис 9.1 показано типичное расположение приложений, распознавателей и серверов имен. Код распознавателя содержится в системной библиотеке и встраивается в приложение, когда оно создается. Код приложения вызывает код распознавателя, используя обычные вызовы функций, — как правило, gethostbyname иgethostbyaddr.
Код распознавателя читает свои файлы конфигурации, зависящие от системы, чтобы определить расположение серверов имен организации. (Мы говорим «серверы имен», употребляя множественное число, потому что большинство организаций работают с множеством серверов имен, хотя мы и показываем на рисунке только один локальный сервер.) Файл /etc/resol v conf обычно содержит IP-
адреса локальных серверов имен.
Приложение
Рис. 9.1. Типичное расположение приложений, распознавателей и серверов имен
Распознаватель посылает запрос локальному серверу имен, используя UDP. Если локальный сервер имен не знает ответа, он обычно запрашивает другие серверы имен через Интернет, также используя UDP.
Альтернативы DNS
Возможно получить информацию об имени и адресе без использования DNS. Типичной альтернативой служат статические файлы со списком узлов (обычно файл /etc/hosts, как мы указываем в табл. 9.2) или информационная система сети (Network Information System, NIS). К сожалению, способ конфигурирования узла для использования различных типов службы имен зависит от реализации. Solans 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/i rs conf. Если сервер имен должен применяться для поиска имен узлов, все эти системы используют для задания IP-адресов серверов имен файл /etc/resol v conf. К счастью, эти различия обычно скрыты от программиста приложений, поэтому мы просто вызываем функции распознавателя, такие как gethostbyname и gethostbyaddr.
Функция gethostbyname
Узлы компьютерных сетей мы обычно идентифицируем по их именам, удобным для человеческого восприятия. Но во всех примерах книги специально использовались IP-адреса вместо имен, поэтому мы точно знаем, что входит в структуры адресов сокетов для таких функций, как connect и sendto, и что возвращается функциями accept и recvfrom. Но большинство приложений имеют дело с именами, а не с адресами. Это особенно актуально при переходе на IPv6, поскольку адреса IPv6 (шестнадцатеричные строки) значительно длиннее адресов IPv4, запи-
санных в точечно-десятичном представлении. (Например» запись типа АААА и запись типа PRT для 1 рб. 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: /* указатель на массив указателей на псевдонимы */
inthadd г type, /* тип адреса узла AF_INET или AF_INET6 */
inth_length:/* длина адреса 4 или 16 */
char **h_addr_list. /* указатель на массив указателей с адресами IPv4 или IPv6 */
#define h_addr h_addr_list[O] /* первый адрес в списке */
В терминах DNS функция gethostbyname выполняет запрос на запись типа А или запись типа АААА. Эта функция может возвратить либо адрес IPv4, либо адрес IPv6. В табл. 9.1 мы приводим все условия, в которых данная функция возвращает эти два типа адресов.
ПРИМЕЧАНИЕ , Определение элемента h_addr необходимо только для обратной совместимости, и в новом коде не следует использовать элемент h addr. В 4.2BSD отсутствовал элементз h_addr_hst, но был указатель char *h_addr, указывавший только на один 1Р-адрес.ПРИМЕЧАНИЕ , Определение элемента h_addr необходимо только для обратной совместимости, и в новом коде не следует использовать элемент h addr. В 4.2BSD отсутствовал элементз h_addr_hst, но был указатель char *h_addr, указывавший только на один 1Р-адрес.Рис. 9.2. Структура hostent и ее одержимоеРис. 9.2. Структура hostent и ее одержимое
ПРИМЕЧАНИЕ
, Определение элемента h_addr необходимо только для обратной совместимости, и в новом коде не следует использовать элемент h addr. В 4.2BSD отсутствовал элемент
з h_addr_hst, но был указатель char *h_addr, указывавший только на один 1Р-адрес.
ПРИМЕЧАНИЕ
, Определение элемента h_addr необходимо только для обратной совместимости, и в новом коде не следует использовать элемент h addr. В 4.2BSD отсутствовал элемент
з h_addr_hst, но был указатель char *h_addr, указывавший только на один 1Р-адрес.
Рис. 9.2. Структура hostent и ее одержимое
Рис. 9.2. Структура hostent и ее одержимое
На рис. 9.2 представлено устройство структуры hostent и содержащаяся в ней информация, в предположении, что искомое имя узла имеет два альтернативных имени и три адреса IPv4. Все имена узла представляют собой строки языка С.
Возвращаемое имя h_name называется каноническим именем узла. Например, в показанных в предыдущем разделе записях CNAME каноническое имя узла ftp.kohala.com будет иметь вид bsdi.kohala.com. Также если мы вызываем функцию gethostbyname с узла sol а门s с неполным именем, например sol aris. то в качестве канонического имени возвращается полное доменное имя (FQDN) solan s. kohala.com.
Когда возвращаются адреса IPv6, элементу h_addrtype структуры hostent присваивается значение AF_INET6, а элементу h length —16. На рис. 9.3 показаны эти изменения, а затененными изображены поля, изменившиеся по сравнению с рис. 9.2.
Альтернативное имя номер 1 \0
in6_addr{}in6_addr{}н►I h_length=16н►I h_length=16Рис. 9.3. Изменения информации, возвращаемой в структуре hostent с адресом IPv6Рис. 9.3. Изменения информации, возвращаемой в структуре hostent с адресом IPv6Альтернативное имя номер 2 \0
in6_addr{}
in6_addr{}
н►
I h_length=16
н►
I h_length=16
Рис. 9.3. Изменения информации, возвращаемой в структуре hostent с адресом IPv6
Рис. 9.3. Изменения информации, возвращаемой в структуре hostent с адресом IPv6
ПРИМЕЧАНИЕ
Недавние версии функции gethostbyname, начиная с реализации BIND 4.9.2, допускают, что аргумент hostname может быть записан в виде строки десятичных чисел, разделенных точками, то есть вызов в форме hptr = gethostbyname(,t206.62.226.33*'); будет работать. Этот код был добавлен, поскольку клиент Rlogin принимает только имя узла, вызывая функцию gethostbyname, и не принимает точечно-десятичную запись [104].
Функция get host byname отличается от других функций сокетов, описанных нами, тем, что она не задает значение переменной errno, когда происходит ошибка. Вместо этого она присваивает глобальной целочисленной переменной h_errno одну из следующих констант, определяемых с помощью включения заголовка <netdb.h>:
HOST_NOT_FOUND
TRY_AGAIN
NO_RECOVERY
N0_DATA (идентично NO_ADDRESS)
Ошибка NO_DATA означает, что заданное имя действительно, но у него нет либо записи типа А, либо записи типа АААА. Примером может служить имя узла, имеющее только запись типа MX.
Текущие реализации BIND предоставляют функцию hsterror, которая в качестве единственного аргумента получает значение h_errno и возвращает указатель const char* на описание ошибки. Некоторые примеры строк, возвращаемых этой функцией, мы увидим в следующем примере.
Пример
В листинге 9.11 показана простая программа, вызывающая функцию gethostbyname для любого числа аргументов командной строки и выводящая всю возвращаемую информацию.
Листинг 9.1. Вызов функции и вывод возвращаемой информации //names/hostent с
#include "unp h"
int
main(mt argc. char **argv)
{
char *ptr **ppt「.
charstr[INET6_ADDRSTRLEN]:
struct hostent *hptr.
while (--argc > 0) {
ptr = *++argv.
if ( (hptr = gethostbyname(ptr)) = NULL) {
err_msg( "get host byname errorfor host 記:Xs".
ptr, hstrerror(h_errno)),
continue.
}
printf("offici al hostname 案s\en". hptr->h_name).
for (pptr = hptr->h_aliases *pptr 』NULL. pptr++)
printfC\eta7ias ^s\en", *pptr).
switch (hptr->h_addrtype){,
case AF_INET 一
#ifdef AFJNET6
case AFINET6
#end】f —
pptr = hptr->h_addr_list,
for ( *pptr i= NULL. pptr++)
printf(' \etaddress %s\en“.
lnet_ntop(hptr->h_addrtype. *pptr str sizeof(str))),
break
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// u w vv piter com/d ownload.♦
default
err_ret(Munknown address type"),
break,
}
}
exit(0).
}
8-14 Функция gethostbyname вызывается для каждого аргумента командной строки. 15-17 Выводится официальное имя узла, за которым идет список альтернативных имен.
20-22 Чтобы эта программа поддерживала и адреса IPv4, и адреса IPv6, мы допускаем, что возвращаемый адрес будет относиться либо к типу AF_INET, либо к типу AF_INET6. Но мы не допускаем последнего, если адрес не определен (то есть если узел не поддерживает IPv6).
23-26 Переменная pptr указывает на массив указателей на индивидуальные адреса. Для каждого адреса мы вызываем функцию i net_ntop и выводим возвращаемую строку. Обратите внимание, что met_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 gemm.tuc.noao.edu official hostname gemim tuc noao edu address140252111
address140252354
address140252454
address140252854
Далее идет имя, представленное в разделе 9.2 как имя с записью типа CNAME: solans % hostent www'
official hostname bsdi kohala com alias www kohala com address 206 62 226 35
Как мы и предполагали, официальное имя узла отличается от нашего аргумента командной строки.
Чтобы увидеть строки ошибок, возвращаемые функцией hstrerror, мы сначала задаем несуществующее имя узла, а затем имя, имеющее только запись типа MX:
solans 为 hostent nosuchname
gethostbyname error for host nosuchname Unknown host
solans % hostent uunet.uu.net
gethostbyname error for host uunet uu net No address associated with name
Параметр распознавателя RES_USEJNET6
Более новые реализации BIND (4.9.9 и выше) предоставляют параметр распознавателя, называемый RES_USE_INET6, который мы можем установить тремя различными способами. Мы можем использовать этот параметр для сообщения распознавателю о том, чтобы функция gethostbyname возвращала адрес IPv6 вместо адреса IPv4.
Приложение может установить этот параметр самостоятельно, вызвав сначала функцию распознавателя res init и затем включив параметр.
include <resolv h>
res_mit()
_res options |= RES_USE_INET6
Это нужно сделать перед первым вызовом функции gethostbyname или gethost- byaddr Параметр действителен только для установившего его приложения.
Если переменная окружения RES OPTIONS содержит строку 】net6, параметр включается. Область влияния параметра зависит от области видимости переменной окружения. Если мы установим ее в нашем файле prof 11 е, например (считая, что это KornShell), с атрибутом export, следующим образом.
export RES_OPTIONS=inet6
то тогда она повлияет на каждую программу, koi орую мы запустим из нашей оболочки входа в систему. Но если мы просто установим переменную в командной строке (как мы показываем чугь ниже), то она повлияет только на Э1у команду.
Файл конфигурации распознавателя (обычно файл /etc/resol v conf) может содержать строку
options inet6
Однако нужно отдавать себе отчет в том, что установка этого параметра в файле конфигурации распознавателя влияет на все приложения на узле, которые вызывают функции распознавателя. Следовательно, эту технологию не нужно использовать, пока все приложения на узле не будут способны обрабатывать адреса IPv6, возвращаемые в структуре hostent.
Первый метод устанавливает параметр для каждого приложения, второй — для каждого пользователя, третий — для каждой системы.
Теперь запустим пример программы, показанной в листинге 9 1, присвоив переменной окружения RES_OPTIONS значение met6:
solans % RES_OPTIONS=inet6 hostent solans имя с записью типа AAAA
official hostname solans kohala com
address 5flb dfOO ce3e e200 20 800 2078 еЗеЗ
Solaris % RES_0PTI0NS=】net6 hostent bsdi имя без записи AAAA official hostname bsdi kohala com
address ffff 206 62 226 35
address ffff 206 62 226 66
Когда мы выполняем нашу программу первый раз, она возвращает 1Ру6-адрес узла (вспомните его запись типа АААА в разделе 9.2). Когда программа выполняется второй раз, мы задаем имя узла, не имеющего записи типа АААА. Но тем не менее возвращаются адреса IPv6, точнее, 1Ру4-адреса, преобразованные к виду IPv6 (см. раздел А.5).
Более подробно о поддержке IPv6 в распознавателе мы поговорим в двух следующих разделах.
Функция gethostbyname2 и поддержка IPv6
Когда в BIND 4.9 4 была добавлена поддержка IPv6, появилась функция gethost- byname2 с двумя аргументами, позволяющими нам задать семейство адресов.
#include <netdb h>
struct hostent *gethostbyname2(const char ^hostna/ve 1nt fami ly)
Возвращает непустой указатель в случае успешного выполнения в случае ошибки возвращает NULL и задает значение переменной h_errno
Возвращаемое значение то же, что и у функции gethostbyname, — указатель на структуру hostent, и сама эта структура устроена так же. Логика функции зависит от аргумента family и параметра распознавателя RES_USE_INET6 (который мы упомянули в конце предыдущего раздела).
Прежде чем рассматривать в подробностях эту функцию, следует обратиться к табл 9.1, которая характеризует действия функций gethostbyname и gethostbyname2 в зависимости от параметра RE5_USE_INET6, Значения, которые могут изменяться, выделены полужирным шрифтом-
个 параметр RES_USE_INET6 может быть включен (on) или выключен (off);
4 f вторым аргументом функции gethostbyname2 может быть AF_INET или AF_INET6; з распознаватель ищет записи типа А или записи типа АААА;
笨 возвращаемые адреса могут иметь длину 4 или 16.
Таблица 9.1. Функции gethostbyname и gethostbyname2 с параметром RES_USE_INET6
*****<>>x«M<W»««t*<W5«we«*«*o«^KWeM««w««»l>«W5«>»SW>(^x»WWM«Mi»w«<^^
Параметр RES_USE_INET6 Параметр RES_USE_INET6
выключенвключен
gethostbyname(Aost)
Поиск записей типа А Если они Поиск записей типа АААА Если находятся, то возвращаютсяони находятся, то возвращаются
адреса IPv4 (h_length = 4)адреса IPv6 (hjength = 16)
В противном случае — ошибка В противном случае — поиск Это обеспечивает обратнуюзаписей типа А Если они
совместимость со всеми суще- находятся, то возвращаются адреса ствующнми приложениями,IPv4, преобразовапные к виду IPv6
в которых используются(h_lcngth = 16) В противном
адреса IPv4случае — ошибка
продолжение 6
Таблица 9.1 {продолжение)
Параметр RES_USEJNET6 выключен
Параметр RESJJSEJNET6 включен
gethostbynan)e2(/?O5Z,
Поиск записей гина А. Если они
Поиск записей типа А Если они
AFJNET)
находятся, то возвращаю】ся адреса IPv4 (h_length =4). В противном случае — ошибка
находя ня, то возвращаются адреса IPv4, преобразованные к виду IPv6 (h_length = 16). В противном случае — ошибка
gethostbyname(力 3,
Поиск записей типа АААА. Если
Поиск записей типа АААА. Если
AFJNET6)
они находятся, то возвращаются адреса IPv6 (h_lenglh ^ 16).
В противном случае — ошибка
они находятся, то возвращаются адреса IPv6 (h_length = 16) В противном случае — ошибка
Действие функции gethostbyname2 заключается в следующем:
「Если аргумент farm 1у имеет значение AF_INET, делается запрос на записи типа А. При неудачном выполнении функция возвращает пустой указатель. При успешном выполнении тип и размер возвращаемого адреса зависят от нового параметра распознавателя RES_USE_INET6: если параметр не установлен (по умолчанию), возвращаются адреса IPv4 п возвращаемый элемент h_l ength структуры hostent будет равен 4. Если же параметр установлен, то возвращаются адреса IPv4, преобразованные к виду IPv6, и элемент h_l ength структуры hostent будет равен 16.
: Если аргумент f апл 1у имеет значение AF INET6, делается запрос на записи типа АААА. При успешном выполнении функции возвращаются адреса IPv6, а элемент h_l ength структуры hostent равен 16, иначе функция возвращает пустой указатель.
Эта функция может использоваться, если приложение хочет осуществить поиск одного определенного типа адресов — IPv4 или IPv6. Но для приложений более типичным является вызов функции gethostbyname, и более новые версии этой функции могут возвращать как адреса IPv4, так и адреса IPv6.
Одним из способов изучения действия функции gethostbyname и параметра RES_USE_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 £ NETDB^INTERNAL;~
return (NULL).
}
if (_res options & RES__USE_INET6) {
hp = gethostbyname2(name. AF_INET6):
if (hp)
return (hp):
return (gethostbyname2(name. AF_INET)).
} '
Если распознаватель еще не инициализирован (флаг RES_INIT не установлен), вызывается функция res_init. Эта функция инициализации проверяет и обрабатывает переменную окружения RES_OPTIONS. Если эта переменная содержит строку 1 net6 или если файл конфигурации распознавателя содержит строку opti ons i net6, функция res_imt устанавливает параметр RES_USE_INET6. Функция res_i mt обычно вызывается автоматически функцией gethostbyname (как показано здесь), когда она впервые вызывается приложением, или функцией gethostbyaddr. Альтернативным вариантом является, как мы показали выше, вызов функции res_mit приложением, после чего явно устанавливается флаг RES_USE_INET6.
Если параметр RES_USE_INET6 “е установлен, выполняется последняя строка нашей функции и вызывается функция gethostbyname2 с аргументом AF_INET, задающим семейство адресов. В табл. 9.1 показано, что при этом вызове ищутся только записи типа А. Это обеспечивает обратную совместимость со всеми существующими приложениями.
Если параметр RES_USE_INET6 включен, вызывается функция gethostbyname2 с аргументом семейства адресов AF.INET6 для поиска записей типа АААА (см. табл. 9.1). Если вызов успешен, функция gethostbyname завершается. Если вызов неудачен, вызывается функция gethostbyname? с семейством адресов AF_INET для поиска записей типа А. Если этот вызов успешен, то (что неочевидно из листинга 9.2) эти 4-байтовые значения автоматически отображаются в 16-байтовый адрес IPv4, преобразованный к виду IPv6.
Таким образом, вызывая функцию gethostbyname при включенном параметре RES_USE_INET6, приложение тем самым сообщает распознавателю: «Мне нужны только адреса IPv6. Следует искать сначала записи типа АААА, а если ни одной не найдется, то записи типа А, п если они найдутся, возвратить эти адреса как адреса IPv4, преобразованные к виду IPv6».
Функция gethostbyaddr
Функция gethostbyaddr получает в качестве аргумента двоичный IP-адрес и пытается Haiira имя узла, соответствующее этому адресу. Ее действие обратно действию функции gethostbyname.
#include <netdb h>
struct hostent *gethostbyaddr(const char *addr. size_t len. mt farm Zy);
Возвращает непустой указатель в случае успешного выполнения, -1 в случае ошибки
Эта функция возвращает указатель па ту же структуру hostent, которую мы описывали при рассмотрении функции gethostbyname. Обычно в этой структуре нас интересует поле h_name, каноническое имя узла.
Аргумент addr не относится к типу char* но в действительности это указатель на структуру m_addr или in6_addr, содержащую адрес IPv4 или IPv6, len — это длина структуры: 4 для адресов IPv4 или 16 для адресов IPv6. Аргумент family будет иметь либо значение AFJNET, либо AFJNET6.
В терминах DNS функция gethostbyaddr запрашивает у сервера имен запись типа PTR в домене in-addr.apra для адреса IPv4 или запись типа PTR в домене ip6 mt для адреса IPv6.
Функция gethostbyaddr и поддержка IPv6
У функции gethostbyaddr всегда был аргумент семейства адресов, поэтому когда в BIND появилась поддержка IPv6, не возникло необходимости вводить другую функцию (аналогичную gethostbyname2). Но когда аргументом является адрес IPv6, существует ряд особенностей. Поэтому необходимо выполнить три следующих проверки в указанном порядке:
Если аргумент farm 1у — AF_INET6, len — 16 и адрес является адресом IPv4, преобразованным к виду IPv6, то младшие 32 бита адреса (часть IPv4) ищутся в домене m-addr арга.
Если аргумент family — AF_INET6, len — 16 и адрес является адресом IPv6, совместимым с IPv6, то младшие 32 бита адреса (часть IPv4) ищутся в домене in-addr арга.
Если мы искали адрес IPv4 (аргумент farm 1 у был равен AF_INET или имел место один из перечисленных выше случаев) и усгановлен параметр распознавателя RES_USE_INET6, то возвращаемый адрес (копия аргумента addr) преобразуется из IPv4 к виду IPv6: h_addrtype — эго AF INET6. а аргумент len равен 16.
Третья проверка обычно имеет небольшое значение, поскольку немногие приложения проверяют IP-адрес, возвращаемый функцией gethostbyaddr, так как это только копия аргумента. Обычно приложения вызывают эту функцию для проверки элемента h_name возвращаемой структуры hostent (а также, возможно, альтернативных имен).
Функция uname
Функция uname возвращает имя текущего узла. Эта функция не является частью библиотеки распознавателя, но мы рассматриваем ее здесь, поскольку она часто используется вместе с функцией gethostbyname для определения IP-адресов локальных узлов.
#include <sys/utsname h>
int uname(struct utsname *name)
Возвращает неотрицательное значение в случае успешного выполнения. -1 в случае ошибки
Эта функция заполняет структуру utsname, адрес которой передается вызывающим:
#define _UTS_NAMESIZE 16
struct char char char char char}struct char char char char char}utsname {sysname[_UTS_NAMESIZE] nodename[_UTS_NODESIZE] release[_UTS_NAMESIZE]. version匸UTS[NAMESIZE]. machine匸UTS[NAMESIZE]utsname {sysname[_UTS_NAMESIZE] nodename[_UTS_NODESIZE] release[_UTS_NAMESIZE]. version匸UTS[NAMESIZE]. machine匸UTS[NAMESIZE]#define _UTS_NDDESIZE 256
struct char char char char char
}
struct char char char char char
}
utsname {
sysname[_UTS_NAMESIZE] nodename[_UTS_NODESIZE] release[_UTS_NAMESIZE]. version匸UTS[NAMESIZE]. machine匸UTS[NAMESIZE]
utsname {
sysname[_UTS_NAMESIZE] nodename[_UTS_NODESIZE] release[_UTS_NAMESIZE]. version匸UTS[NAMESIZE]. machine匸UTS[NAMESIZE]
/* имя данной операционной системы */
/* имя данного узла */
/* наименование версии DC*/
/* наименование модификации версии ОС*/
/* тип аппаратного устройства */
К сожалению, в Posix. 1 лишь определены имена пяти элементов показанной структуры и сказано, что каждый массив является массивом символов, который
оканчивается пустым байтом. Ничего не говорится ни о размере каждого массива, ни об их содержимом. Размеры, представленные нами, взяты из 4.4BSD. Другие операционные системы используют другие размеры.
Наиболее важное упущение с точки зрения сетевого программирования касается определения размера и содержимого массива nodename. Некоторые системы хранят в этом массиве только имя узла (например, gemim), в то время как другие хранят FQDN (например, gemmi tuc noao edu). В некоторых операционных системах, таких как Solaris 2.x, массив может содержать либо одно, либо другое —в зависимости от того, как администратором нас гроена операционная система.
Пример: определение IP-адресов локального узла
Чтобы определить IP-адреса локального узла, мы вызываем функцию uname для получения имени узла, а затем — функцию gethostbyname для получения всех его IP-адресов. Функция my^addrs, показанная в листинге 9.3, выполняет перечисленные задачи.
Листинг 9.3. Функция, возвращающая все IP-адреса узла
//lib/m.y_addrs с、
#1 ncTude'unp h”,
include<sys/utsname h>
char **
my_addrs(int *addrtype)
{一
struct hostent *hptr.
struct utsname myname.
if (uname(&iT|yname) < 0)
return (NULL).
if ( (hptr 工 gethostbyname(myname nodename)) == NULL)
return (NULL)
*addrtype = hptr->h_addrtype
return (hptr->h_addr list)
}
Возвращаемое значение функции — это элемент h_addM 】st структуры hostent, массив указателей на IP-адреса, Мы также возвращаем семейство адресов через аргумент-указатель.
Другой способ определить IP-адреса локального узла — воспользоваться командой SIOCGIFCONF функции wctl. Мы рассмотрим этот случай в главе 16.
Функция gethostname
Функция gethostname также возвращает имя текущего узла.
#шс1 ude <um std h>
int gethostname(char *name size_t namelen)
Возвращает 0 в случае успешного выполнения -1 в случае ошибки
name — указатель на то место, где хранится имя узла, a name] еп — размер этого массива. Если в массиве есть место, то в конце стоит символ конца строки (нуль). Максимальный размер имени узла задается константой MAXHOSTNAMELEN, которая определяется во включаемом заголовочном файле <sys/param h>.
ПРИМЕЧАНИЕ
Исторически функция uname была определена в System V, а функция gethostname — в Беркли. Стандарт Posix.l задает uname, но Unix 98 гребует наличия обеих функций.
Функции 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_protostruct servent { char *s_name char **s_aliases. int s_port. char *s_protoФункция возвращает указатель на следующую структуру:
struct servent { char *s_name char **s_aliases. int s_port. char *s_proto
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.
sptr 工 getservbyname("domain". "udp'). sptr = getservbyname("ftp" 'tcp') sptr 工 getserybynameCftp". NULL).sptr 工 getservbyname("ftp". "udp"),sptr 工 getservbyname("domain". "udp'). sptr = getservbyname("ftp" 'tcp') sptr 工 getserybynameCftp". NULL).sptr 工 getservbyname("ftp". "udp"),/* DNS с использованием UDP *//* FTP с использованием TCP *//* FTP с использованием TCP *//* этот вызов приведет к ошибке *//* DNS с использованием UDP *//* FTP с использованием TCP *//* FTP с использованием TCP *//* этот вызов приведет к ошибке */
sptr 工 getservbyname("domain". "udp'). sptr = getservbyname("ftp" 'tcp') sptr 工 getserybynameCftp". NULL).
sptr 工 getservbyname("ftp". "udp"),
sptr 工 getservbyname("domain". "udp'). sptr = getservbyname("ftp" 'tcp') sptr 工 getserybynameCftp". NULL).
sptr 工 getservbyname("ftp". "udp"),
/* DNS с использованием UDP */
/* FTP с использованием TCP */
/* FTP с использованием TCP */
/* этот вызов приведет к ошибке */
/* DNS с использованием UDP */
/* FTP с использованием TCP */
/* FTP с использованием TCP */
/* этот вызов приведет к ошибке */
Поскольку протоколом FTP поддерживается только TCP, второй и третий вызовы одинаковы, а четвертый вызов приводит к ошибке. Вот типичные строки из файла /etc/services:
solans Z ftp-data ftp domain domain tftp
grep -e ftp -e domain /etc/services\fP
20/tcp
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(21). "tcp") sptr 工 getservbyport(htons(21) NULL), sptr = getservbyport(htons(21) "udp").sptr ^ getservbyport(htons(53) "udp”). sptr 工 getservbyport(htons(21). "tcp") sptr 工 getservbyport(htons(21) NULL), sptr = getservbyport(htons(21) "udp")./* DNS с использованием UDP *//* FTP с использованием TCP *//* FTP с использованием TCP *//* этот вызов прмвепет к ошибке *//* DNS с использованием UDP *//* FTP с использованием TCP *//* FTP с использованием TCP *//* этот вызов прмвепет к ошибке */
sptr ^ getservbyport(htons(53) "udp”). sptr 工 getservbyport(htons(21). "tcp") sptr 工 getservbyport(htons(21) NULL), sptr = getservbyport(htons(21) "udp").
sptr ^ getservbyport(htons(53) "udp”). sptr 工 getservbyport(htons(21). "tcp") sptr 工 getservbyport(htons(21) NULL), sptr = getservbyport(htons(21) "udp").
/* DNS с использованием UDP */
/* FTP с использованием TCP */
/* FTP с использованием TCP */
/* этот вызов прмвепет к ошибке */
/* DNS с использованием UDP */
/* FTP с использованием TCP */
/* FTP с использованием TCP */
/* этот вызов прмвепет к ошибке */
Последний вызов оказывается неудачным, поскольку нет службы, использующей порт 21 с протоколом UDP.
Помните, что некоторые номера портов используются с TCP для одной службы, а с UDP для совершенно другой службы, например:
solans 宵 grep 514 /etc/services
shel1514/tcp
syslog514,udp
Здесь показано, что порт 514 используется командой rsh в TCP и демоном syslog в UDP. Это свойство имеют порты 512-514.
Пример: использование функций gethostbyname и getservbyname
Теперь мы можем изменить код нашего TCP-клиента времени и даты, показанный в листинге 1.1, так, чтобы использовать функции gethostbyname и getservbyname и принимать два аргумента командной строки: имя узла и имя службы. Наша программа показана в листинге 9.4、Эта программа также демонстрирует желательное поведение при установлении соединения со всеми IP-адресами сервера на узле, имеющем несколько сетевых интерфейсов: попытки продолжаются до тех пор, пока соединение не будет успешно установлено или пока не будут перебраны все адреса.
Листинг 9.4. Наш клиент времени и даты, использующий функции gethostbyname и getservbyname"
//names/daytimetcpclil c
#include°unp h"
int
main(int argc char **argv)
{
intsockfd, n
charrecvline[MAXLINE + 1]
struct sockaddr_in servaddr
structin_addr**pptr.
structhostent*hp,
structservent*sp.
if (argc 』3)
err_quit( 'usage daytimetcpclil <hostname> <service>"):
if ( (hp = gethostbyname(argv[l])) == NULL)
14 err_quit( 'hostname error for ^s 記'argv[l] hstrerror(h_errno)):
15 if ( (sp = getservbyname(argv[2]» ,ftcp1)) == NULL)
16 err_quit(1 getservbyname error for 纪'argv[2]),
pptr = (struct in_addr **) hp*>h_addr Jist
for ( *pptr i= NULL. pptr++) {
sockfd = Socket(AF_INET SOCK_STREAM. 0).
bzeroC&servaddr. sizeof(servaddr))
servaddr sin_fannly = AF_INET
22servaddr sin_port = sp->s_port
23n)emcpy(&servaddr sin_addr *pptr sizeof(struct in_dddr)):
24printf("trying %s\en".
25 Sock_ntop((SA *) &servdddr. sizeof(servaddr))).
26 if (connect(sockfd (SA *) &servaddr, sizeof(servaddr)) — 0)
27 break/* успешное выполнение */
28err_ret(Hconnect error*').
29close(sockfd),
30}
31 if (*pptr == NULL)
32 err_quit( unable to connectf,)
33 while ( (n 工 Read(sockfd recvlme MAXLINE)) > 0) {
34recvline[n] = 0/* завершающий нуль */
35Fputs(recvline. stdout),
36}
37 exit(O)
3B }
Вызов функций gethostbyname и getservbyname
13-16 Первый аргумент командной строки — это имя узла, передаваемое в качестве аргумента функции gethostbyname, а второй — имя службы, передаваемое в качестве аргумента функции getservbyname. Наш код подразумевает исполь-
зование протокола TCP, что мы указываем во втором аргументе функции getservbyname.
Перебор всех адресов
18-25 Теперь мы пишем вызовы функций socket и connect в цикле, который выполняется для каждого адреса сервера, пока попытка вызова функции connect не окажется успешной или пока не закончится список серверов. После вызова функции socket мы заполняем структуру адреса сокета Интернета IP-адресом и номером порта сервера. Хотя в целях увеличения производительности мы могли бы вынести из цикла вызов функции bzero и последующие два присваивания и поместить их непосредственно в коде функции main, наш код легче читать в таком виде, как он представлен сейчас. Установление соединения с сервером редко вызывает затруднения у сетевого клиента.
Вызов функции connect
26-30 Вызывается функция connect, и если вызов оказывается успешным, функция break завершает цикл. Если установить соединение не удается, мы выводим ошибку и закрываем сокет. Вспомните, что дескриптор, для которого вызов функции connect оказался неудачным, нужно закрыть и больше не использовать. ,
Завершение программы
31-32 Если цикл завершается, потому что ни один вызов функции connect не удалб, программа завершается.
Чтение ответа сервера
33-37 В противном случае мы читаем ответ сервера н завершаем программу, когда сервер закрывает соединение.
Если мы запустим эту программу, указав один из наших узлов, на котором работает сервер времени и даты, мы получим вполне предполагаемый вывод- solans % 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
Другая информация о сетях
В этой главе мы сфокусировали внимание на именах узлов, IP-адресах, именах и номерах портов служб. Если же обобщить полученную информацию, мы увидим» что существует четыре иша данных (имеющих отношение к сетям), которые могут понадобиться приложению: узлы, сети, протоколы и службы. В большинстве случаев происходит поиск данных, относящихся к узлам (функции gethostbyname и gethostbyaddr), реже — к службам (функции getservbyname и getservbyaddr) и еще реже — к сетям и протоколам.
Все четыре типа данных могу г храниться в файле, и для каждого из четырех типов определены три функции:
Функция getAXXent, читающая следующую запись в файле, при необходимости открывающая файл.
Функция setAXXent, которая открывает файл (если он еще не открыт) и переходит к началу файла.
Функция endXXXent, закрывающая файл.
Для каждого из четырех типов данных определяется его собственная структура (соответственно, структуры hostent, netent, protoent и servent), что требует включения заголовка <netdb h>.
В дополнение к трем функциям — get, set и end — которые допускают последовательную обработку файла, для каждого из четырех типов данных предоставляются функции ключевого поиска, или поиска %。ключу (keyed lookup). Эта функции последовательно проходят файл (вызывая функцию getXYXent для чтения каждой строки файла), но вместо того чтобы возвращать каждую строку вызывающему процессу, эти функции ищут элемент, совпадающий с аргументом. Имена функций поиска по ключу имеют вид getAXYbyFFK Например, две функции ключевого поиска для информации об узле — это функции gethostbyname (ищет элемент, совпадающий с именем узла) и gethostbyaddr (ищет элемент, совпадающий с IP-адресом). Таблица 9.2 обобщает эту информацию.
Таблица 9.2. Четыре типа данных, относящихся к сетям
vassssxeVK^fist етм5»,йяяяг«йга®«ча>
Тип данных
Файл
Структура
Функции поиска по ключу
Узлы
/etc/hosts
hostcnt
gethostbyaddr, gethostbyname
Сеги
/etc/networks
netent
getnetbyaddr, getnetbynanie
Протоколы
/etc/protocols
protoent
gctprotobynanie, getprotobynu m ber
Службы
/etc/services
servent
getservbyname, gctservbyport
Как это применяется, если используется DNS? Прежде всего, с помощью DNS возможен доступ только к информации об узле и о сети. Информация о протоколе и службах всегда считывается из соответствующего файла. Ранее в этой главе мы отмечали (см. подраздел «Альтернативы DNS»), что в разных реализациях отличаются способы, с помощью которых администратор определяет, что именно использовать для получения информации об узле и сети — DNS или файл.
Далее, если DNS используется для получения информации об узле и о сети, имеют смысл только функции поиска по ключу. Используя, например, функцию
gethostent, не стоит надеяться, что она выполнит последовательный перебор всех записей DNS! Если вызывается функция gethostent, она считывает только информацию об узлах и пе использует DNS.
ПРИМЕЧАНИЕ ——
Хотя информацию о сети можно сделать доступной с помощью DNS, очень немногие пользуются этим. 2 la с. 347-348 [1 ] рассказывается об этой возможности. Однако обычно адм ин истраторы создают и обслуживают файл /etc/nctworks, используемый вместо DNS. Программа netstat с параметром -i использует этот файл, если он есть, и выводит имя каждой сети.
Резюме
Набор функций, вызываемых приложением для преобразования имени узла в IP- адрес и обратно, называется распознавателем. Две функции, gethostbyname и gethostbyaddr, являются типичными точками входа. С переходом на IPv6 структура hostent, заполняемая с помощью этих функций, остается той же, но часть информации внутри структуры изменяется. Новая функция gethostbyname2 и новый параметр распознавателя RES_USE_INET6 также необходимы для поддержки IPv6.
Общеупотребительной функцией, манипулирующей именами служб и номерами портов, является функция getservbyname, которая получает в качестве аргумента имя службы и возвращает структуру, содержащую номер порта. Это сопоставление обычно содержится в текстовом файле. Существуют дополнительные функции для сопоставления имен протоколов номерам протоколов и имен сетей —номерам сетей, но они редко используются.
Альтернативой DNS, которую мы не упомянули, является непосредственный вызов функций распознавателя вместо использования функций gethostbyname и get host byadd г. Таким способом пользуется, например, программа sendmai 1, предназначенная для поиска записи типа MX, чего не может сделать функция gethostbyXXX. У функций распознавателя имена начинаются с res_. Примером такой функции является функция res_init, которую мы описали в разделе 9.4. Описание этих функций и пример вызывающей их программы находятся в разделе 14 книги [1]. При вводе в командной строке man resolver должны отобразиться страницы руководства для этих функций.
Тему преобразований имен и адресов мы продолжим в главе И, когда мы познакомимся с не зависящим от протокола интерфейсом для функций gethostbyname и gethostbyaddr — с функциями getaddrinfo и getnameinfo. Эти две функции были разработаны для работы с IPv4 и IPv6, но сначала мы рассмотрим ряд аспектов совместимости протоколов IPv4 и IPv6 в следующей главе.
Упражнения
Измените программу, представленную в листинге 9.1, так, чтобы для каждого возвращаемого адреса вызывалась функция gethostbyaddr, а затем выведите возвращаемое имя h_name. Сначала запустите программу, задав имя узла толь-
ко с одним IP-адресом, а затем — с несколькими IP-адресами. Что происходит?
Устраните проблему, показанную в предыдущем упражнении.
Измените листинг 9.3, чтобы в нем вместо функции uname вызывалась функция gethostname. Напишите функцию main для вызова my_addrs и затем выведите 1Р-адрес.
Что может произойти в листинге 9.4, если мы изменим третий аргумент функции memcpy (при заполнении структуры адреса сокета) и он станет равен hp-> h_length? {Подсказка: Подумайте, что произойдет, если мы установим параметр RES_OPTIONS=i net6 при выполнении программы и зададим имя узла, имеющего адрес IPv6.)
Запустите программу, показанную в листинге 9.4, задав имя службы chargen.
Запустите программу, показанную в листинге 9.4, задав IP-адрес в точечнодесятичной записи в качестве имени узла. Допускает ли это ваш распознаватель? Измените листинг 9Д чтобы разрешить IP-адрес в виде строки десятичных чисел с точками в качестве имени узла и строку с десятичным номером порта в качестве имени службы. В каком порядке должно выполняться тестирование IP-адреса для строки в точечно-десятичной записи и для имени?
Измените программу в листинге 9.4 так, чтобы можно было работать либо с IPv4, либо с IPv6.
Измените программу в листинге 9.4 так, чтобы сделать запрос DNS, и сравните возвращаемый IP-адрес со всеми IP-адресами узла получателя, то есть вызовите функцию gethostbyaddr, используя IP-адрес, возвращаемый функцией recvfrom, а затем вызовите gethostbyname для поиска всех IP-адресов для узла.
ЧАСТЬ 3
ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ СОКЕТОВ
ГЛАВА 10
Совместимость IPv4 и IPv6
Введение
В течение ближайших лет, возможно, произойдет постепенный переход Интернета с 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
Клиент IPv6
Почти все существующие клиенты и серверы Обсуждается в разделе 103
Обсуждайся в разделе 102
Простые vfOfliKfjHKauHii большинства существующих клиентов (например, клиент из листинга 1 1 модифицируется к виду, ирсдставлемиому в листинге 1 2)
Мы не будем подробно рассматривать два сценария, когда клиент и сервер используют один и тот же протокол. Более интересны случаи, когда клиент и сервер используют разные протоколы.
Клиент IPv4, сервер IPv6
Общим свойством узла с двойным стеком является то, что серверы IPv6 могут выполнять обработку как клиентов IPv4, так и клиентов IPv6. Это достигается за счет преобразования адресов IPv4 к виду IPv6 (рис. А.8). На рис. 10.1 приводится подобный пример.
86dd86dd88888888Рис. 10.1« Сервер IPv6 на узле с двойным стеком, обслуживающий клиенты IPv4 и IPv6Рис. 10.1« Сервер IPv6 на узле с двойным стеком, обслуживающий клиенты IPv4 и IPv6
86dd
86dd
8888
8888
Рис. 10.1« Сервер IPv6 на узле с двойным стеком, обслуживающий клиенты IPv4 и IPv6
Рис. 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 содержит
порт получателя 8888 (в приложении А рассказывается более подробно о форматах и содержании этих заголовков). IP-адрес получателя в заголовке IPv4, который мы не показываем, — это 206.62.226.42.
Сегмент TCP от клиента IPv6 выглядит в сети как заголовок Ethernet, за которым следует заголовок IPv6, заголовок TCP и данные TCP. Заголовок Ethernet содержит поле типа 0x86dd, которое идентифицирует кадр как кадр IPv6. Заголовок TCP имеет тот же формат, что и заголовок TCP в пакете IPv4, и содержит порт получателя 8888. IP-адрес получателя в заголовке IPv6, который мы не показываем, будет таким: 5f lb:df00 • сеЗе: e200:20:800: 2Ь37 : 6426.
Принимающий канальный уровень просматривает поле типа Ethernet и передает каждый кадр соответствующему модулю IP. Модуль IPv4 (возможно, вместе с модулем TCP) определяет, что сокетом получателя является сокет IPv6, и 1Ру4-адрес отправителя в заголовке IPv4 заменяется на эквивалентный ему адрес IPv4, преобразованный к виду IPv6. Этот преобразованный адрес возвращается сокету IPv6 как 1Ру6-адрес клиента, когда функция accept сервера соединяется с клиентом IPv4. Все оставшиеся дейтаграммы для этого соединения являются дейтаграммами IPv4.
Когда функция сервера accept соединяется с клиентом IPv6, клиентский адрес IPv6 остается таким же, каким был адрес отправителя в заголовке IPv6. Все оставшиеся дейтаграммы для этого соединения являются дейтаграммами IPv6.
Теперь мы можем свести воедино шага, позволяющие ТСР-клиенту IPv4 соединяться с сервером IPv6.
Сервер IPv6 запускается, создает прослушиваемый сокет IPv6, и мы считаем, что с помощью функции bi nd он связывает с сокетом универсальный адрес.
Клиент IPv4 вызывает функцию gethostbyname и находит запись типа А для сервера (см. табл. 9.1). У узла сервера будут записи и типа А, и типа АААА, поскольку он поддерживает оба протокола, но клиент IPv4 запрашивает только запись типа А.
Клиент вызывает функцию connect, и клиентский узел отправляет серверу сегмент SYN IPv4.
Узел сервера получает сегмент SYN IPv4, направленный прослушиваемому сокету IPv6, устанавливает флаг, указывающий, что это соединение использует адреса IPv4, преобразованные к виду IPv6, и отвечает сегментом IPv4 SYN/АСК. Когда соединение установлено, адрес, возвращаемый серверу функцией accept, является адресом IPv4, преобразованным к виду IPv6.
Все взаимодействие между клиентом и сервером происходит с использованием дейтаграмм IPv4.
Пока сервер точно не определит, является ли данный IPv6-anpec адресом IPv4, преобразованным к виду IPv6 (с использованием макроопределения IN6_IS_ ADDR_V4MApPED, описанного в разделе 10.4), сервер не знает, что взаимодействует с клиентом IPv4. Двойной стек протоколов решает эту проблему. Аналогично, клиент IPv4 не знает, что он взаимодействует с сервером IPv6.
Главное в данном сценарии то, что узел сервера с двойным стеком имеет и адрес IPv4, и адрес IPv6. Этот сценарий будет работать, пока используются адреса IPv4.-
Сценарий работы 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Сокеты IPv6Рис. 10.2. Обработка полученных дейтаграмм IPv4 или IPv6 в зависимости от типа принимающего сокетаРис. 10.2. Обработка полученных дейтаграмм IPv4 или IPv6 в зависимости от типа принимающего сокетаAF_INET soEk_stream sockaddr inAF_INET soEk_stream sockaddr inAF_INET SOCK_STREAM sockaddr_inAF_INET SOCK_STREAM sockaddr_in力 Если дейтаграмма IPv4 приходит на сокет IPv4, ничего особенного не происходит. На рисунке изображены две стрелки, помеченные《IPv4», одна для TCP, другая для UDP. Между клиентом и сервером происходит обмен дейтаграммами IPv4.
Сокеты IPv6
Сокеты IPv6
Рис. 10.2. Обработка полученных дейтаграмм IPv4 или IPv6 в зависимости от типа принимающего сокета
Рис. 10.2. Обработка полученных дейтаграмм IPv4 или IPv6 в зависимости от типа принимающего сокета
AF_INET soEk_stream sockaddr in
AF_INET soEk_stream sockaddr in
AF_INET SOCK_STREAM sockaddr_in
AF_INET SOCK_STREAM sockaddr_in
蔡 Если дейтаграмма IPv6 приходит на сокет IPv6, ничего особенного не происходит. На рисунке изображены две стрелки, помеченные《IPv6», одна для TCP, другая для UDP. Между клиентом и сервером происходит обмен дейтаграммами IPv6.f,
- Когда дейтаграмма IPv4 приходит на сокет IPv6, ядро возвращает соответствующий адрес IPv4, преобразованный к виду IPv6, в качестве адреса, возвращаемого функцией accept (TCP) или recvfrom (UDP). На рисунке это показано двумя штриховыми стрелками. Такое сопоставление возможно, поскольку адрес IPv4 можно всегда представить как адрес IPv6. Между клиентом и сервером происходит обмен дейтаграммами IPv4.
Обратное неверно: поскольку, вообще говоря, адрес IPv6 нельзя представить как адрес IPv4, на рисунке отсутствуют стрелки от протокола IPv6 к двум сокетам IPv4.
Большинство узлов с двойным стеком должны использовать следующие правила обращения с прослушиваемыми сокетами:
Прослушиваемый сокет IPv4 может принимать соединения только от клиентов IPv4.
Если у сервера есть прослушиваемый сокет IPv6, связанный с универсальным адресом, этот сокет может принимать исходящие соединения как от клиентов IPv4, так и от клиентов IPv6. Для соединения с клиентом IPv4 локальный адрес сервера для соединения будет соответствующим адресом IPv4, преобразованным к виду IPv6.
Если у сервера есть прослушиваемый сокет IPv6, связанный с адресом IPv6, не являющимся адресом IPv4, преобразованным к виду IPv6, этот сокет может принимать исходящие соединения только от клиентов IPv6.
Клиент IPv6, сервер IPv4
Теперь мы поменяем протоколы, используемые клиентом и сервером в примере из предыдущего раздела. Сначала рассмотрим ТСР-клиент IPv6, запущенный на узле с двойным стеком протоколов.
Сервер IPv4 запускается на узле, поддерживающем только IPv4, и создает прослушиваемый сокет IPv4.
Запускается клиент IPv6 и вызывает функцию gethostbyname, запрашивая только адреса IPv6 (включает параметр RES_USE_INET6). Поскольку у сервера, поддерживающего только IPv4, есть л/гшь записи типа А, мы видим, согласно табл. 9.1, что клиенту возвращается адрес IPv4, преобразованный к виду IPv6.
Клиент IPv6 вызывает функцию connect с адресом IPv4, преобразованным к виду IPv6, в структуре адреса сокета IPv6. Ядро обнаруживает преобразованный адрес и автоматически посылает серверу сегмент SYN IPv4.
Сервер отвечает сегментом SYN/ACK IPv4, и устанавливается соединение, по которому происходит обмен дейтаграммами IPv4.
Этот сценарий мы схематически изображаем на рис. 10.3.
Если ТСР-клиент IPv4 вызывает функцию connect, задавая адрес IPv4, или если UDP-клнент IPv4 вызывает функцию sendto, задавая адрес IPv4, ничего особенного не происходит. На рисунке это изображено двумя стрелками, помеченными «IPv4».
Сокеты IPv4Сокеты IPv4AF—1NET SOCK_STRE7^M sockaddr_inAF—1NET SOCK_STRE7^M sockaddr_inСокеты IPv6Сокеты IPv6Адрес, вой^ращенный функцией connect или sendtoАдрес, вой^ращенный функцией connect или sendtoAF_INEI SOCK_STREAM sockaddr in6AF_INEI SOCK_STREAM sockaddr in6, AF—1NETSOCK_STRE7^M sockaddr_in6, AF—1NETSOCK_STRE7^M sockaddr_in6
Сокеты IPv4
Сокеты IPv4
AF—1NET SOCK_STRE7^M sockaddr_in
AF—1NET SOCK_STRE7^M sockaddr_in
Сокеты IPv6
Сокеты IPv6
Адрес, вой^ращенный функцией connect или sendto
Адрес, вой^ращенный функцией connect или sendto
AF_INEI SOCK_STREAM sockaddr in6
AF_INEI SOCK_STREAM sockaddr in6
, AF—1NET
SOCK_STRE7^M sockaddr_in6
, AF—1NET
SOCK_STRE7^M sockaddr_in6
Если ТСР'КЛиент IPv6 вызывает функцию connect, задавая адрес IPv6, или если UDP-клиент IPv6 вызывает функцию sendto, задавая адрес IPv6, ничего особенного не происходит. На рисунке это показано двумя стрелками, помеченными «IPv6».
AF—INET S0CK_STRE7^M sockaddr_in
Дейтаграмма 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 in. Следовательно, на рисунке нет стрелок от сокетов IPv4 к протоколу IPv6.
В предыдущем разделе (дейтаграмма IPv4, приходящая для сокета сервера IPv6) преобразование полученного адреса IPv4i< виду IPv6 выполняется ядром, и результат прозрачно (то есть незаметно для приложения) возвращается приложению функцией accept или recvfrom. В этом разделе (если необходимо отправить дейтаграмму IPv4 на сокете IPv6) преобразование адреса IPv4 к виду IPv6 выполняется распознава гелем в соответствии с правилами, представленными в табл. 9.1, и затем преобразованный адрес прозрачно передается приложению функцией connect или sendto. 瓜 ,… 」 • * -
Резюме: совместимость IPv4 и IPv6
Таблица 10.2, содержащая сочетания клиентов и серверов, подводит итог обсуждению, проведенному в данном и предыдущем разделах.
Таблица 10.2. Обобщение совместимости клиентов и серверов IPv4 и IPv6
tmWAtMmMMtfW^mMmwtWKVMimvmiiitwiKmMwmmtmmwimtMMKwvmcmMVivtm^^
Сервер IPv4, Сервер IPv4, Сервер IPv4, узел только узел только узел с двой- IPv4 (только А) IPv6 (только ным стеком
АААА) (А и АААА)
Сервер IPv6, узел с двойным стеком (А и АААА)
Клиент IPv4, узелIPv4HerIPv4
только IPv4
Клиент IPv6, узелНетIPv6Нет
только IPv6
Клиент IPv4, узелIPv4Нет麼 IPv4
с двойным стеком
Клиент IPv6, узелIPv4“IPv6Нет*
с двойным стеком
IPv4
IPv6
IPv4
IPv6
Каждая ячейка этой таблицы содержит поля «IPv4» или «IPv6» с указанием используемого протокола, если данное сочетание работает, либо «нет», если комбинация недопустима. Ячейка в последней строке третьей колонки отмечена звездочкой, поскольку совместимость зависит от адреса, выбранного клиентом. При выборе записи типа АААА отправка дейтаграммы IPv6 будет невозможна. Но выбор записи типа А, которая возвращается клиенту как адрес IPv4, преобразованный к виду IPv6, приведет к отправке дейтаграммы IPv4.
Хотя четверть из представленных в таблице сочетаний недопустима, в обозримом будущем большинство реализаций IPv6 будут использоваться на узлах с двойным стеком протоколов и будут поддерживать не только IPv6. Если мы удалим из таблицы вторую строку и вторую колонку, все записи «Нет» исчезнут и единственной проблемой останется запись, помеченная звездочкой.
Макроопределения проверки адреса IPv6
Существует небольшой класс приложений IPv6, которые должны знать, с каким собеседником они взаимодействуют (IPv4 или IPv6). Эти приложения должны знать, является ли адрес собеседника адресом IPv4, преобразованным к виду IPv6. Определены двенадцать макросов, проверяющих некоторые свойства адреса IPv6. #include <netinet/in h>
int IN6_IS_ADDR_UNSPECIFIED(const struct in6_addr *dptr), int IN6_IS_ADDR_LOOPBAC К(con st struct in6_addr *dptr); int IN6_IS_ADDR_MULTICAST(const struct in6_addr *dptr); mt IN6_IS_ADDR_LINKLOCAL(const struct in6 二 addr *dptr). int IN6_IS_ADDR_SITELOCAL(const struct in6_addr *叩卄) int IN6_IS_ADDR_V4MAPPED(const struct in6_addr *aptr) mt IN6_IS_ADDR_V4C0MPAT(const struct in6_addr *dptr)
int IN6_IS_ADDR_MC_NODELOCAL(const struct in6_addr *aptr)
int IN6_IS_ADDR_MC_LINKLOCAL(const struct in6_addr *aptr) mt IN6_IS_ADDR_MC_SITELOCAL (const struct in6_addr *dptr), 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 или reevfrom.
Как пример приложения, которому нужен этот макрос, можно привести 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 приводятся примеры использования обеих команд.
Параметр сокета IPv6_ADDRFORM
Параметр сокета IPv6_ADDRF0RM может менять один тип сокета на другой. При этом действуют следующие ограничения:
Сокет IPv4 можно всегда поменять на сокет IPv6. Любые адреса IPv4, уже связанные с сокетом, преобразуются к виду IPv4.
Сокет IPv6 можно поменять на сокет IPv4, только если любые адреса, уже связанные с сокетом, являются адресами IPv4, преобразованными к виду IPv6. Причиной для изменения формата адреса сокета является то, что в Unix дескрипторы можно легко передавать между процессами. Самый типичный способ сделать это — использовать функцию fork, но мы увидим в разделе 14.7, как дескриптор можно передать между связанными процессами, а в разделе 25.7 — между несвязанными.
В качестве примера рассмотрим процесс, создающий прослушиваемый сокет IPv4 и затем принимающий соединение от клиента IPv4. Этот сервер вызывает функции fork и ехес, запуская новую программу для обработки запроса клиента. Предположим, что в этом приложении действует соглашение, по которому присоединенный сокет передается новой программе как стандартные потоки ввода, вывода и ошибок (аналогично тому, что делает функция т netd, см. раздел 12.5). Мы можем использовать псевдокод, показанный в листинге 10 1. Единственным отличием от нашего параллельного сервера, описанного в разделе 4.8, будет дублирование присоединенного сокета на согласованных дескрипторах с последующим вызовом функции ехес.
Но программа, выполняемая с помощью функции ехес, предполагает использование сокета IPv6. Для преобразования формата адреса сокета мы можем использовать параметр сокета IPV6 ADDRF0RM, как показано в листинге 10.21.
Листинг 10.1. Сервер, принимающий входящее соединение и выполняющий новую программу
intlistenfd, connfd,
socklen_tcli1en,
struct sockaddrin serv, cli,
/* структуры IPv4 */
1istenfd = Socket(AF_INET, SOCK_STREAM. 0), /* сокет IPv4 */
/* вписываем в serv{} номер з叩анее известного порта */
4 Bind(listenfd &serv, sizeof(serv)):
.Listendistenfd. LISTENQ),
for ( . . ) {
clilen = sizeof(cli).
connfd = Accept(listenfd. &cli, &clilen)
if (ForkO == 0) {
Close(listenfd), /* дочерний процесс */ Dup2(connfd. STDIN_FILENO), Dup2(connfd, STDOUT_FILENO), Dup2(connfd, STDERR_FILENO).
Close(connfd).
Exec( )./* начинается новая программа */
}
Close(connfd). /* родитель */
Листинг 10.2. Преобразование сокета IPv4 в сокет IPv6
int
socklen_t
struct sockaddrin6 struct hostent
af. clilen, cli, *ptr.
/* IPv6 */
af = AF」NET6.,
Setsockopt(STDIN_FILENO. IPPR0TDJPV6. IPV6JWRFORM. &af. sizeof(af)):
clilen = sizeof(cli) GetpeernameCO &cli &clilen).
ptr = gethostbyaddr(&cli sin6_addr, 16. AF_INET6),
Вызов функции setsockopt изменяет формат адреса сокета от IPv4 к IPv6, а вызов функции getpeername возвращает адрес IPv4, преобразованный к виду IPv6, если сокет являлся сокетом IPv4. Но если эта программа выполняется с сокетом IPv6 на стандартном потоке ввода, при вызове функции setsockopt ничего не произойдет, так как адрес уже будет представлен в формате IPv6.
Этот параметр сокета можно использовать, например, в том случае, когда программа, принимающая входящее соединение IPv4, предоставляется откуда-то извне (например, у нас нет исходного кода и мы не можем изменить программу
Все исходные коды программ, опубликованные в этой книге, вы можете найтн по адресу http:// w v\ w.piter com/dovvnload
так, чтобы она использовала IPv6, или, еще лучше, чтобы она стала не зависящей от протокола), но наша программа, выполняемая с помощью функции ехес, обрабатывает IPv6.
Если функция getsockopt вызывается для получения значения параметра IPV6_ ADDRFORM, то в зависимости от формата адреса сокета будет возвращаться либо AFJNET, либо AF_INET6. Вторым аргументом функции getsockopt или setsockopt может быть либо IPPROTO_IP, либо IPPR0T0_IPV6.
Переносимость исходного кода
Большинство существующих сетевых приложений написаны для IPv4. Структуры sockaddr_in размещаются в памяти и заполняются, а функция socket задает AF INET в качестве первого аргумента. При переходе от листинга 1.1 к листингу 1.2 мы видели, что эти приложения IPv4 можно преобразовать в приложения IPv6 без особых усилий. Многие показанные нами изменения можно выполнить автоматически, используя некоторые сценарии редактирования. Программы, более зависящие от IPv4, использующие такие свойства, как многоадресная передача, параметры IP или символьные (неструктурированные) сокеты, потребуют больших усилий при преобразовании.
Если мы преобразуем приложение для работы с IPv6 и распространим его исходный код, нам придется думать о том, поддерживает ли принимающая система протокол IPv6. Типичный способ решения этой проблемы — применять в коде #i fdef, используя по возможности IPv6 (поскольку мы видели в этой главе, что клиент IPv6 может взаимодействовать с серверами IPv4 и наоборот). Проблема такого подхода в том, что код очень быстро становится «замусорен» директивами #i fdef, и его сложнее отслеживать и обслуживать.
Наилучшим подходом будет рассмотрение перехода на IPv6 как возможности сделать программу не зависящей от протокола. Первым шагом здесь будет удаление вызовов функций get hostbyname и gethostbyaddr и использование функций getadckinfo и getnameinfо, которые мы опишем в следующей главе. Эго позволит нам обращаться со структурами адресов сокетов как с непрозрачными объектами, ссылаться на которые можно с помощью указателя и размера, что как раз и выполняют основные функции сокетов: bind, connect, reevfrom и т. д. Наши функции sock_XXX из раздела 3.8 помогут работать с ними независимо от IPv4 и IPv6. Очевидно, эти функции содержат #i fdef для работы с IPv4 и IPv6, но если мы скроем эту зависимость от протокола в нескольких библиотечных функциях, наш код станет проще. В разделе 19.6 мы разработаем ряд функций mcast_XXX, которые помогут сделать приложения многоадресной передачи не зависящими от версии протокола IP.
Другой момент, который нужно учесть, — что произойдет, если мы откомпилируем иаш исходный код в системе, поддерживающей и IPv4, и IPv6, затем распространим либо исполняемый код, либо объектные файлы (но не исходный код), и кто-то запустит наше приложение в системе, пе поддерживающей IPv6. Есть вероятность, что сервер локальных имен поддерживает записи типа АААА и возвращает как записи типа АААА, так и записи типа А некоему собеседнику, с котором пытается соединиться наше приложение. Если наше приложение, работающее с IPv6, вызовет функцию socket для создания сокета IPv6, она не будет
работать, если узел не поддерживает IPv6. Мы решаем этот вопрос с помощью функций, описанных в следующей главе, игнорируя ошибку функции socket и пытаясь использовать следующий адрес в списке, возвращаемом сервером имен. Если предположить, что у собеседника имеется запись типа А и что сервер имен возвращает запись типа А в дополнение к любой записи типа АААА, то сокет IPv4 успешно создастся. Этот тип функциональности имеется в библиотечной функции, но не в исходном коде каждого приложения.
Резюме
Сервер IPv6 на узле с двойным стеком протоколов может предоставлять сервис как клиентам IPv4, так и клиентам IPv6. Клиент IPv4 посылает серверу дейтаграммы IPv4, но стек протоколов сервера преобразует адрес клиента к виду IPv6, поскольку сервер IPv6 работает со структурами адресов сокетов IPv6.
Аналогично, клиент IPv6 на узле с двойным стеком протоколов может взаимодействовать с сервером IPv4. Распознаватель клиента возвращает адреса IPv4, преобразованные к виду IPv6, для всех записей сервера типа А, и вызов функции connect для одного из этих адресов приводит к тому, что двойной стек посылает сегмент SYN IPv4. Только отдельным специальным клиентам и серверам необходимо знать протокол, используемый собеседником (например, FTP), и чтобы определить, что собеседник использует IPv4, можно использовать макрос IN6_IS_ ADDR_V4MApPED. Параметр сокета IPV6_ADDRF0RM может использоваться программой, которая предполагает наличие одного типа сокета (обычно сокета IPv6).
Упражнения
Запустите FTP-клиент IPv6 на узле с двойным стеком протоколов. Соединитесь с FTP-сервером IPv4, запустите команду debug, а затем команду di г. Далее выполните те же операции, но для сервера IPv6, и сравните команды PORT, являющиеся результатом выполнения команд di г.
Напишите программу, требующую ввода одного аргумента командной строки, который является адресом IPv4 в точечно-десятичной записи. Создайте ТСР-сокет IPv4 и свяжите этот адрес и некоторый порт, например 8888, с сокетом при помощи функции bind. Вызовите функцию listen, а затем pause. Напишите аналогичную программу, которая в качестве аргумента командной строки принимает шестнадцатеричную строку IPv6 и создает прослушиваемый ТСР-сокет IPv6. Запустите программу IPv4, задав в качестве аргумента универсальный адрес. Затем перейдите в другое окно и запустите программу IPv6, задав в качестве аргумента универсальный адрес IPv6. Можете ли вы запустить программу IPv6, если программа IPv4 уже связана с этим портом? Появляется ли разница при использовании параметра сокета SO_REUSEADDR? Что будет, если вы сначала запустите программу IPv6, а затем попытаетесь запустить программу IPv4?
ГЛАВА 11
Дополни тел ьные преобразования имен и адресов
Введение
Функции gethostbyname и gethostbyaddr, описанные в главе 9, являются зависящими от протокола. При использовании первой необходимо знать, в какой элемент структуры адреса сокета переместить результат (например, элемент sin_addr для IPv4 или элемент sin6_add「для IPv6), а при вызове последней — знать, какой из элементов содержит двоичный адрес. Эта глава начинается с описания новой функции Posix.lg getaddrinfo, которая обеспечивает независимость наших приложений от протокола. Функцию getnameinfo, являющуюся ее дополнением, мы рассмотрим в этой главе.
Далее мы будем использовать эту функцию, а также разработаем шесть наших собственных функций, обрабатывающих типичные сценарии для клиентов и серверов TCP и UDP. В оставшейся части книги вместо непосредственного вызова функции getaddnnfo мы будем использовать эти функции.
Функции gethostbyname и gethostbyaddr — это также замечательные примеры функций, не допускающих повторное вхождение (nonreentrant). Мы покажем, почему это так, и опишем некоторые функции, позволяющие обойти эту проблему. Вхождение — это проблема, к которой мы вернемся в главе 23, но мы можем описать и объяснить эту проблему уже сейчас, так как для этого нет необходимости понимать детали работы с потоками.
Глава заканчивается примером полной реализации нашей функции getaddri nfo, что позволит больше узнать об этой функции — как она работает, что возвращает и как взаимодействует с IPv4 и IPv6.
Функция getaddrinfo
Функция getaddrinfo скрывает все зависимости от протокола в библиотечной функции. Приложение работает только со структурами адресов сокетов, заполняемых с помощью функции getaddrinfo. Эта функция определяется в Posix.lg. findude <netdb h>
mt getaddrmfo(const char *hostname, const char ★ service.
const struct addrinfo "hints, struct addrmfo ^result).
Возвращает 0 в случае успешного выполнения ненулевое значение в случае ошибки (см табл И 2)
ПРИМЕЧАНИЕ
Определение этой функции в Posix.lg происходит от более раннего предложения Кейта С клоу эра (Keith Sklower) для функции, называемой getconnmfo. Эта функция стала результатом обсуждений с Эриком Олменом (Eric Allman), Вилльямом Дастом (William Durst), Майклом Карелсом (Michael Karels) и Стивеном Вайсом (Steven Wise), а также более ранней реализации, написанной Эриком Олменом. Замечание о том, что указания имени узла и имени службы достаточно для соединения с этой службой независимо от деталей протокола, было сделано Маршалом Роузом (Marshall Rose) в проекте Х/Ореп.
Через указатель resul t функция возвращает указатель на связный список структур addrmfo, который задается путем включения заголовочного файла <netdb h>:
struct addrinfo {
mt ai_flags,/* AI_PASSIVE. AI_CANONNAME */
int ai_family,/* AF_xxx */
int ai_socktype./* S0CK_xxx */
int ai_protocol,/* 0 или IPPR0T0_xxx для IPv4 и IPv6 */
size_t ai_addrlen,/* длина ai_addr */
char *ai_canonname;/* указатель на каноническое имя узла */
struct sockaddr *ai_addr, /* указатель на структуру адреса сокета */
struct addrinfo *di_next. /* указатель на следуюи(ую структуру в связном списке */
}
Переменная hostname — это либо имя узла, либо строка адреса (точечно-деся- тичиая запись для IPv4 или шестнадцатеричная строка для IPv6). Переменная service — это либо имя службы, либо строка, содержащая десятичный номер порта. (Вспомните наше решение упражнения 9.6, где мы допускаем строку с адресом для узла или строку с номером порта для службы.)
Структура hints — это либо пустой указатель, либо указатель на структуру addri nfo, заполненную рекомендациями вызывающего процесса о типах информации, которую он хочет получить. Например, если заданная служба предоставляется и для TCP, и для UDP (например, служба domain, которая ссылается на сервер DNS), вызывающий процесс может присвоить элементу ai_socktype структуры hi nts значение SOCK_DGRAM. Тогда возвращение информации будет иметь место только для дейтаграммных сокетов.
Вызывающим процессом могут быть изменены значения следующих элементов структуры hints:
、ai_f 1 ags (AI^PASSIVE, Al/ANONNAME);
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у.
Если функция завершается успешно (0), то в переменную, на которую указывает аргумент result, записывается указатель на связанный через указатель a i_next список структур addrmfo. Имеется два способа возвращения множественных структур.
Если существует множество адресов, связанных с именем узла, то одна структура возвращается для каждого адреса, который может использоваться с запрашиваемым семейством адресов (значение ai_fami 1у, если задано).
Если служба предоставляется для множества типов сокетов, то одна структура может быть возвращена для каждого типа сокета в зависимости от a i _socktype. Например, если структура hi nts пуста и если в службе domai п осуществляется поиск узла с двумя IP-адресами, возвращаются четыре структуры addnnfo:
、 одна для первого IP-адреса и типа сокета SOCK_STREAM;
, одна для первого IP-адреса и типа сокета SOCK_DGRAM;
а одна для второго IP-адреса и типа сокета SOCK_STREAM;
' одна для второго IP-адреса и типа сокета SOCK_DGRAM.
Мы показываем схематическое изображение этого примера па рис. 11.1. Не существует никакого гарантированного порядка структур при возвращении множества элементов. Например, мы не можем считать, что службы TCP возвращаются перед службами UDP.
ПРИМЕЧАНИЕ
Хотя это и не гарантируется, реализация должна возвращать IP-адреса в том же порядке, в котором они возвращаются DNS. Например, многие серверы DNS сортируют возвращаемые адреса так, что если узел, отправляющий запрос, и сервер имен находятся в одной сети, то адреса в этой совместно используемой сети возвращаются первыми. Также более новые версии BIND позволяют рас гюзнавател ю задавагь порядок соргн- ровки адресов в файле /etc/rcsoiv.conf.
Информация, возвращаемая структурами addnnfo, готова для вызова функции socket и последующего вызова функций connect или sendto (для клиента) и bi nd (для сервера). Аргументы функции socket — это элементы ai_fannly, ai_socktype и а 1 _protocol. Второй и третий аргументы функций connect и bind — это элементы ai_addr (указатель иа структуру адреса сокета соответствующего типа, заполняемую функцией getaddnnfo) и ai_add「len (длина этой структуры адреса сокета).
Если в структуре hi nts установлен флаг AI_CANONNAME, элемент ai_canonname первой возвращаемой структуры указывает на каноническое имя узла. В терминах DNS это обычно полное доменное имя (FQDN).
На рис. 11.1 представлена возвращаемая информация для случая, если мы выполняем следующее: struct addrinfo hints. *res.
bzero(&hints. sizeof(hints))
hints di_flags = AI_CANONNAME.
hints ai_family = AF_INET,
getaddrinfo(,,bsdr,. "domain", &hints. &res).
На этом рисунке все, кроме переменной res, относится к динамически выделяемой памяти (например, с помощью функции mall ос). Предполагается, что
addrmfof}addrmfof}resresРис. 11-1. Пример информации, возвращаемой функцией getaddrinfoРис. 11-1. Пример информации, возвращаемой функцией getaddrinfo
addrmfof}
addrmfof}
res
res
Рис. 11-1. Пример информации, возвращаемой функцией getaddrinfo
Рис. 11-1. Пример информации, возвращаемой функцией getaddrinfo
каноническое имя узла bsdi — bsdi .kohala com, и что этот узел имеет два адреса IPv4 в DNS (см. рис. 1.7).
Порт 53 предназначен для службы domain, и нужно учитывать, что этот номер порта будет представлен в структурах адресов сокетов в сетевом порядке байтов. Возвращаемые значения a i protocol нулевые, поскольку комбинация элементов ai^fami 1у и ai_socktype полностью задает протокол для TCP и UDP. Вполне допустимо, если функция getaddri nfo возвратит переменную ai_protocol со значением IPPROTO—TCP для двух структур SOCK_STREAM и со значением IPPROTO_UDP для двух структур SOCK_DGRAM.
В табл. 11.1 показано число структур addrmfo для каждого возвращаемого адреса, определяемое на основе заданного имени службы (которое может быть представлено десятичным номером порта) и рекомендации ai_socktype.
Таблица 11.1. Число структур addrmfo, возвращаемых для каждого IP-адреса
Элемент
Служба обозначена
Служба обозначена Служба обозначена Служба
ai_sock-
именем и предо-
именем и предо-
именем и предо-
обозначена
type
ставляется только
ставляется только
ставляется TCP
именем порта
TCP
UDP
и UDP
0
1
1
2
2
SOCK
1
Ошибка
1
1
STREAM
SOCK
Ошибка
1
1
1
DGRAM
Более одной структуры addnnfo возвращается для каждого IP-адреса только в том случае, когда поле ai_socktype структуры hints пусто и либо служба поддерживается TCP и UDP (как указано в файле /etc/services), либо задан номер порта для этой службы.
Если бы мы рассматривали все 64 возможных варианта сочетаний входных данных для функции getaddnnfo (имеется шесть входных переменных), многие сочетания оказались бы недопустимыми, а некоторые не имели бы смысла. Вместо этого рассмотрим наиболее типичные случаи.
" Задание имени узла и службы. Это традиционный случай для клиента TCP и UDP. По завершении клиент TCP перебирает в цикле все возвращаемые IP- адреса, вызывая функции socket и connect для каждого из них, пока не установится соединение или пока не будут перебраны все адреса. Мы показываем такой пример с нашей функцией tcp_connect в листинге 11.2.
Для клиента UDP структура адреса сокета, заполняемая с помощью функции getaddnnfo, будет использоваться в вызове функции sendto или connect. Если клиент сообщит, что первый адрес не работает (ошибка на присоединенном сокете UDP или тайм-аут на неприсоединенном сокете), будет придпринята попытка обратиться к другому адресу.
Если клиент знает, что он обрабатывает только один тип сокета (например, клиентами Telnet и FTP обрабатываются только сокеты TCP, а клиентами TFTP — только сокеты UDP), то элементу ai_socktype структуры hints должно быть задано соответственно либо значение SOCK_STREAM, либо значение SOCK_ DGRAM.-一
Типичный сервер задает службу, но не имя у зла, и задает флат AI_PASSIVE в структуре hints. Возвращаемая структура адреса сокета должна содержать 1Р-ад- рес, равный INADDR_ANY (для IPv4) или IN6ADDR_ANY_INIT (для IPv6). Сервер TCP затем вызывает функции socket, bind и listen. Если сервер хочет разместить в памяти с помощью функции mall ос другую структуру адреса сокета, чтобы получить адрес клиента из функции accept, то возвращаемое значение ai_addrl еп задает требуемый длд этого размер. Сервер UDP вызовет функции socket, bind и затем recvfrom. Если сервер хочет разместить в памяти с помощью функции mal 1 ос другую структуру адреса сокета, чтобы получить адрес клиента из функции recvfrom, возвращаемое значение ai__addrlen также задает нужный размер. Как и в случае типичного клиентского кода, если сервер знает, что он обрабатывает только один тип сокета, то элемент ат socktype структуры hi nts должен быть задан либо как SOCK_STREAM, либо как SOCK_DGRAM. Это позволяет избежать возвращения множества структур, с (возможно) неверным значением элемента ai_socktype.
< До сих пор мы демонстрировали серверы TCP, создающие один прослушиваемый сокет, и серверы UDP, создающие один сокет дейтаграмм. Это тот вариант, который подразумевался в предыдущем абзаце. Альтернативным устройством является сервер, который обрабатывает множество сокетов с помощью функции select. В этом сценарии сервер должен последовательно перебрать все структуры из списка, возвращаемого функцией 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 мы опишем обратную функ= цию getnamemfo, которая преобразует структуры адресов сокетов в имена узлов и имена служб. В разделе 11.16 мы покажем реализацию функций getaddrinfo, getnameinfo и freeaddrinfo.
Функция gai_strerror
Ненулевые значения ошибок, возвращаемых из функции get add г inf о, имеют названия и значения, показанные в табл. 11.2. Функция gainstrerror получает одно из этих значений в качестве аргумента и возвращает указатель на соответствующую строку с описанием ошибки.
include <netdb h>
char *gai_strerror(mt error).
Возвращает указатель на строку с описанием ошибки
EAI—ADDRFAMILY EAI—AGAIN EAI—BADFLAGS EAI—FAIL EAI_FAMILY EAI—MEMORY EAI—NODATA EAI—NONAME EAI—SERVICE
Семейство адресов не поддерживается для данного имени узла
Временный сбой при попытке разрешения имен
Недопустимое значение ai_flags
Неисправимая ошибка при разрешении имен
Семейство ai_family ие поддерживается
Ошибка при выделении памяти
С именем узла не ассоциирован адрес
Имя узла или имя службы неизвестны или равны NULL
Запрошенная служба ие поддерживается для данного типа сокета ai_socktype
EAI_SOCKTYPE
EA1—SYSTEM
Тип сокета ai_socktype не поддерживается
Другая системная ошибка, возвращаемая в переменной еггпо
Таблица 11.2. Ненулевые возвращаемые значения (константы) ошибок функции getaddnnfo
Константа
Описание
Функция freeaddrinfo
Вся память, занимаемая структурами addrinfo, структурами ai_addr и строкой ai_canonname, которые возвращаются функцией getaddn nfo, динамически выделяется функцией та 11 ос. Эта память освобождается при вызове функции freeaddri nfo. #mclude <netdb h> void freeaddrinfo(struct addrinfo *a?)
Переменная ai должна указывать на первую из структур addrinfo, возвращаемых функцией getaddn nfo. Освобождается вся область памяти, занятая структурами из связного списка, вместе с динамически выделенной областью памяти, содержащей данные, на которые указывают эти структуры (например, структуры адресов сокетов и канонические имена узлов).
Предположим, что мы вызываем функцию getaddnnfo, проходим последовательно по всему связному списку структур addri nfo и находим нужную структуру. Если далее мы попытаемся сохранить нужную нам информацию простым копированием структуры addrinfo, а затем вызовем функцию freeaddrinfo, мы получим скрытую ошибку. Причина в том, что структура addnnfo сама указывает на динамически выделенный участок памяти (для структуры адреса сокета и, возможно, для канонического имени). Но эта область памяти, на которую указывает сохраненная нами структура, при вызове функции freeaddrinfo освобождается и может использоваться для хранения какой-либо иной информации.
ПРИМЕЧАНИЕ
Создание копии только самой структуры addrinfb, а не структур, на которые она, в свою очередь, указывает, называется поверхностным копированием (shallow сору). Копирование структуры addrinfb и всех структур, на которые она указывает, называется детальным копированием (deep сору).
Функция getaddrinfo: IPv6 и доменный сокет Unix
Хотя стандарт Posix.lg определяет функцию getaddnnfo, в нем ничего не говорится об IPv6. Взаимодействие между этой функцией, распознавателем (особенно параметром RES_USE_INET6, вспомните табл. 9.1) и IPv6 нетривиально. Следует отметить следующие моменты, прежде чем свести воедино эти виды взаимодействия в табл. 11.3.
V Входные данные функции getaddrmfo могут относиться к двум различным типам, которые выбираются в зависимости от того, какой тип структуры адреса сокета вызывающий процесс хочет получить обратно и какой тип записей нужно искать в DNS.
: Семейством адресов, указанным вызывающим процессом в структуре hints, задается тип структуры адреса сокета, который вызывающий процесс предполагает получить. Если вызывающий процесс задает AF.INET, функция не должна возвращать структур sockaddr_w6, а если вызывающий процесс задает AF_ INET6, функция не должна возвращать структур sockdddr_in.
驚 В Posix.lg сказано, что при задании AF_UNSPEC должны возвращаться адреса, которые могут использоваться слюбым семейством протоколов, допускающим применение имени узла и имени службы. Это подразумевает, что если у узла имеются как записи типа АААА, так и записи типа А, то записи типа АААА возвращаются как структуры sock add г_1 пб, а записи типа А — как структуры sockaddr_in. Нет смысла возвращать еще и записи типа А как адреса IPv4, преобразованные к виду IPv6, в структурах sockdddr_in6, потому что при этом не возвращается никакой дополнительной информации — эти адреса уже возвращены в структурах sockaddr_in.
讀 Это утверждение Posix.lg также подразумевает, что если флаг AI_PASSIVE задан без имени узла, то должен быть возвращен универсальный адрес IPv6 (IN6ADDR_ANY_INIТ или 0::0) в структуре sockaddr_in6 вместе с универсальным адресом IPv4 (INADDR_ANY или 0.0.0.0) в структуре sock add r_in. Также нет смысла возвращать сначала универсальный адрес IPv6, поскольку мы видели в разделе 10.2, что на узле с двойным стеком сокет сервера IPv6 может обрабатывать и клиенты IPv4, и клиенты IPv6.
您 Параметр распознавателя RES_USE_INET6 вместе с какой-либо из вызываемых функций (gethostbyname или gethostbyname?) задает тип записей, поиск которых ведется в DNS (тип А или тип АААА), и тип возвращаемых адресов (IPv4, IPv6 или IPv4, преобразованные к виду IPv6). Мы обобщили это в табл. 9.1.
炎 Имя узла может также быть либо шестнадцатеричной строкой IPv6, либо строкой в точечно-десятичной записи IPv4. Допустимость этой строки зависит от
семейства адресов, заданного вызывающим процессом. Шестнадцатеричная строка IPv6 неприемлема, если задано AF_INET, а строка в точечно-десятичной записи IPv4 неприемлема, если задано AF_INET6. Но если задано семейство AFJJNSPE& то допустимы оба варианта, и при этом возвращается соответствующий тип структуры адреса сокета.
ПРИМЕЧАНИЕ
Можно возразить, что если в качестве семейства протоколов задано AF_INET6, строка в точечно-десятичной записи должна возвращаться как адрес IPv4, преобразованный к виду IPv6 в структуре sockaddr_in6. Но другим способом получения этого результата является установка префикса строки с десятичной точкой O::ffff:.
В табл. 11.3 показано, как будут обрабатываться адреса IPv4 и IPv6 функцией getaddnnfo. Колонка «Результат» отражает то, что мы хотим возвратить вызывающему процессу, если входные переменные таковы, как показано в первых трех колонках. Колонка «Действия» — то, каким образом мы получаем этот результат. Код, выполняющий эти действия, мы показываем в нашей реализации функции getaddnnfo в разделе 11.16.
Таблица 11.3. Функция getaddrinfo: ее действия и результаты
Имя узла,
Семейство
Строка с
Результат
Действия
указанное
адресов,
именем
вызываю-
указанное
узла со-
щим про-
вызываю-
держит
цессом
щим про-
цессом
Ненулевая
AFJJNSPEC
Имя узла
Все записи АААА возвра-
Выполняется два поиска
строка с име-
щаюгся как структуры
в DNS (примечание 7):
нем узла; ак-
sockaddr_in6{} и
gethostbyname2(AF_INET6)
тивное или
все записи А возврата-
с выключенным параметром
пассивное
ются как структуры
RES USE INET6 и
открытие
sockaddr__m{)
gethostbyname2(AF_INET)
с выключенным параметром
RES_USE_INET6
Шестнадца-
Одна структура
inet_pton(AF_INET6)
теричиая
sockaddr_in6{}
строка
Строка в то-
Одна структура
met_pton(AF_INET) *
чечио-деся-
sockaddr_in{}
тичной
записи
AFJNET6
Имя узла
Все записи АААА возвра-
gethostbyname() с включен-
щаются как структуры
ным параметром
sockaddr_m6{} либо
RES_USE_1NET6
все записи А возвраща-
(примечание 2)
ются как структуры
sockaddr_in6{} с адресами
IPv4, преобразованными
к виду IPv6
Шестнадца-
Одна структура
inet_pton(AF_INET6)
теричная
sockaddr一in6{}
строка
продолжение 少
Таблица 11.3 (продолжение)
MWMWMWWWWWWWMWW»»»
Имя узла,
Семейство
Строка с
Результат
Действия
указанное
адресов,
именем
вызываю-
указанное
узла со-
щим про-
вызываю»
держит
цессом
щим про-
цессом
Строка в то-
Ошибка.
чечно-деся-
EAI ADDRFAMILY
тичной
записи
AFJNET
Имя узла
Все записи А возвраща-
gethostbyname() с выклю-
ючся как струкгуры
ченным параметром
sockaddr_in{}
RES_USE_INET6
Шестнадца-
Ошибка
теричпая
EAI_ADDRFAMILY
строка
Строка в то-
Одиа структура
met pton(AF INET)
чечно-деся-
sockaddr_m{}
тичной
записи
Пустая стро-
AF_UNSPEC
Неявнн 访
Одна структура
inet_pton(AF_INET6)
ка с именем
адрес 0 0
sockaddr_in6{} и одна
met pton(AF INET)
узла» пассив-
Неявный
структура sockaddr_mf}
•
ное открытие
адрес 0000
AFJNET6
Неявный
Одна структура
met pton(AF 1NET6)
адрес 0* 0
sockaddr_in6{}
AFJNET
Неявный
Одна структура
met pton(AF INET)
адрес 0000
sockaddr_in{}
Пустая стро-
AF_UNSPEC
Неявный ад-
Одна структура
inet_pton(AF_INET6)
ка с именем
рес 0 1 Не-
sockaddr ш6{} и одна
met pton(AF INET)
узла, актив-
явный адрес
струкчура sockaddr_in{}
ное открытие
127 00 1
AFJNET6
Неявный
Одна структура
met pton(AF INET6)
адрес 0 1
sockdddr_in6{}
AF_INET
Неявный ад-
Одна струк! ура
met plon(AF INET)
рес 127 0 0 1
sockaddr_m{}
Примечание 1, Когда выполняется два поиска DNS, любой из них может оказаться неудачным (то есть не найдется ни одной записи нужного типа для имени узла), но как минимум один поиск закончится успешно. Но если успешными окажутся оба поиска (у узла имеется и запись типа А, и запись типа АААА), возвращаются оба типа структур адреса сокета.
Примечание 2. Данный поиск DNS должен выполниться успешно, иначе возвращается ошибка Но поскольку включен параметр RES_USE_INET6, функция get- hostbyname сначала ищет записи типа АААА, и если не находит их, то ищет функции типа А (см. листинг 9.2).
Установка и сброс параметра распознавателя RES_USE_INET6 со сценариями, представленными в примечаниях к таблице, нужны для осуществления требуемого поиска DNS, правила которого приводятся в табл 9.1.
Мы отметили, что табл. ИЗ только указывает, каким образом функция getaddr- 1 nfo обрабатывает IPv4 и IPv6, то есть позволяет определить число адресов, возвра-
щаемых вызывающему процессу. Действительное число структур addrmfo, возвращаемых вызывающему процессу, зависит также от заданного типа сокета и имени службы, как ранее было показано в табл. 11.1.
В Posix.lg не сказано ничего особенного о функции getaddrinfo и доменных сокетах Unix (их мы подробно рассмотрим в главе 14). Тем не менее добавление поддержки доменных сокетов Unix в нашей реализации функции getaddn nfo и проверка приложений с протоколами IPv4, IPv6 и доменными сокетами — это хорошая проверка на независимость от протоколов.
В нашей реализации принимается следующее предположение: если аргументом имени узла для функции getaddn nfo является либо /local, либо /ит х, а аргументом имени службы — полное имя (начинавгея с косой черты), то возвращаются структуры доменного сокета Unix. Действительные имена узлов DNS не могут содержать косой черты, и ни одно из имен существующих служб IANA не начинается с косой черты (см. упражнение 11.5). Возвращаемые структуры адресов сокетов содержат полное имя, готовое к вызову функции bind или connect. Если вызывающий процесс задает флаг AI^CANONNAME, имя узла (см. раздел 9.7) возвращается как каноническое имя.
Функция getaddrinfo: примеры
Теперь мы покажем некоторые примеры работы функции getaddrinfo, используя тестовую программу, которая позволяет нам вводить все параметры: имя узла, имя службы, семейство адресов, тип сокета и флаги AI.CANONNAME и AI_PASSIVE. (Мы не показываем эту тестовую программу, поскольку она содержит около 350 строк малоинтересного кода. Ее можно получить тем же способом, что и прочие исходные коды для этой книги, см. предисловие.) Тестовая программа выдает информацию о переменном числе возвращаемых структур addri nfo, показывая аргументы вызова функции socket и адрес в каждой структуре адреса сокета.
Сначала показываем тот же пример, что и на рис. 11.1:
solans Z testga -f met -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 SOCKJGRAM 0)
address 206 62 226 35 53
socket(AF_INET SOCK_STREAM 0)
address 206 62 226 66 53
socketCAFJNET SOCK_DGRAM 0)
address 206 62 226 66 53
Параметр -f met задает семейство адресов, -с указывает, что нужно возвратить каноническое имя, -h bsdi задает имя узла, -s domain задает имя службы.
Типичный сценарий клиента — задать семейство адресов, тип сокета (параметр -t), имя узла и имя службы. Следующий пример показывает это для узла с несколькими сетевыми интерфейсами с шестью адресами IPv4: solans % testga -f met -t stream -h gateway.tuc.noao.edu -s daytime
socketCAFJNET SOCK_STREAM 0)
address 140 252 101 4 13
socket(AF_INET SOCK_STREAM 0)
address 140 252 102 1 13
socket(AF_INET SOCK_STREAM 0)
address 140 252 104 1 13
socket(AF_INET SOCK_STREAM 0)
address 140 252 3 6 13
socket(AF_INET 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 SOCK_STREAM 0)
address 5flb dfOO ce3e e200 20 800 2037 6426 21
socket(AF_INET 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(AF_INET6 SOCK_DGRAM 0) address 8888
socket(AF_INET SOCK_STREAM 0) address 0 0 0 0 8888
socket(AF_INET SOCK_DGRAM 0) address 0 0 0 0 8888
Возвращаются четыре структуры Поскольку мы запустили эту программу на узле, поддерживающем и IPv4, и IPv6, не задав семейства адресов, функция getaddrmfo возвращает универсальный адрес IPv6 и универсальный адрес IPv4 Поскольку мы задали номер порта без типа сокета, функция getaddrmfo возвращает по одной структуре для каждого адреса в случае TCP и по одной структуре для каждого адреса в случае UDP Две структуры IPv6 возвращаются перед структурами IPv4, поскольку, как мы видели в главе 10, клиент или сервер IPv6 на узле с двойным стеком может взаимодействовать с собеседниками по IPv6 и по IPv4
Чтобы проиллюстрировать использование доменных сокетов 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 SOCK_DGRAM 0)
address /tmp/test 1^
Поскольку мы не задаем тип сокета, возвращаются две стижтурЫ: первая для потокового сокета, вторая — для сокета дейтаграмм
Функция host_serv
Наш первый интерфейс функции getaddn nfo не требует от вызывающего процесса размещать в памяти структуру рекомендаций и заполнять ее Вместо этого аргументами нашей функции host_serv будут интересующие нас поля — семейство адресов и тип сокета #include unp h
struct addrinfo *host_serv(const char *hostname const char *service mt farm ly int socktype)
Возвращает в случае успешного выполнения указатель на структуру addrinfo NULL в случае ошибки
В листинге И I1 показан исходный код этой функции
Листинг 11.1* Функция host_serv //lib/host_serv с
include unp h
struct addrinfo *
host serv(const char *host const char *serv int family Int socktypei
{-
int n
struct edd门nf。 hints *res
bze「o(&hints sizeof(struct addrinfo))
hintsai_flsgs = AI_CANONNAME/* всегда возвращает каноническоеимя */
hintsai_family = family/* AF_UNSPEC AF_INET AF_INET6 и т Д */
hintsa 7_sock type = sock type/* 0 SOCKJTREAM 50СК_ЮАМ итд */
if ( (n = getaddrinfo(host serv &hints &res)) ,= 0)
return (NULL)
return (res)/* возвращает указатель на первый зпемент в связном
списке */
}
7-13 Функция инициализирует структуру рекомендаций (hi nts), вызывает функцию getaddrinfo и возвращает пустой указатель, если происходит ошибка
Все исходные коды программ, опубликованные в этой книге вы можете найти по адресу http 〃 www piter com/download
Мы вызываем эту функцию в листинге 15.11, когда нам нужно использовать get add г 1 nfo для получения информации об узле и о службе и при этом мы хотим установить соединение самостоятельно.
11.8. Функция tcp_connect
Теперь мы напишем две функции, использующие функцию getaddn nfo для обработки большинства сценариев клиентов и серверов TCP, которые мы создаем. Первая из этих функций, tcp_connect, выполняет обычные шаги клиента: создание сокета TCP и соединение с сервером.
#mclude ' unp h
mt tcp_connect(const char ★hostname const char ★service)
Возвращает в случае успешного соединения - дескриптор присоединенного сокета. в случае ошибки не возвращается ничего
В листинге 11.2 показан исходный код.
Листинг 11.2. Функция tcp_connect* выполнение обычных шагов клиента
//11b/tcp一connect с
include unp h'
mt
tcp_connect(const char *host const char *serv)
{-
mt sockfd n
struct addrinfo hints *res*ressave
bzero(&hmts sizeof(structaddrinfo))
hints a1_fannly = AF_UNSPEC
hints ai_socktype = SOCK_STREAM.
if ( (n = getaddrinfo(host serv &hint$ &res)) •= 0)
err_quit('tcp_connect error for %s 毋 Xs
host serv gai_strerror(n))
ressave = res
do {
sockfd = socket(res->ai_fami ly res->ai_socktype res->ai_protocol)
if (sockfd < 0)
continue/* игнорируем этот адрес */
if (connect(sockfd res->ai_addr res->ai_addrlen) == 0)
break/* успех */
Close(sockfd)/* игнорируем этот адрес */
} while ( (res = res->ai_next) ,= NULL)
if (res == NULL) /* значение errno устанавливается при последней попытке connectО */
err_sys('tcp_connect error for Zs. A', host serv).
freeaddrinfo(ressave)
return (sockfd)
}
Вызов функции getaddrinfo
7-13 Функция getaddn nfo вызывается один раз, когда мы задаем семейство адресов AFJJNSPEC и тип сокета SOCK_STREAM.
Перебор всех структур addrinfo до успешного выполнения или до окончания списка
14-25 Затем пробуется каждый IP-адрес: вызываются функции socket и connect. Если выполнение функции socket неудачно, это не фа!альная ошибка, так как такое может случиться, если был возвращен адрес IPv6, а ядро узла не поддерживает IPv6 Если выполнение функции connect успешно, выполняется функция break для выхода из цикла. В противном случае, после того как перепробованы все адреса» цикл также завершается. Функция freeaddrinfo освобождает всю динамически выделенную память.
Эта функция (как и другие наши функции, предоставляющие более простой интерфейс для функции getaddrinfo в следующих разделах) завершается, если либо оказывается неудачным вызов функции getaddrinfo, либо вызов функции connect не выполняется успешно Возвращение из нашей функции возможно лишь в случае успешного выполнения Было бы сложно возвратить код ошибки (одну из констант EAI_лхг), не добавляя еще одного аргумента Это значит, что наша функция-обертка тривиальна.
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/daytimetcpcl i с
1 #i nd ude unp h
2 mt
3 mamCint argc char **argv)
4 {
5 mt sockfd n
6 char recvlmeLMAXLINE + 1]:
7 socklen t lenrc
-<~^一 - -продолжение 之
Листинг 11.3 (продолжение)
struct sockaddr *sa,
if (argc 「 3)
err_quit("usage daytimetcpcli <hostname/IPaddress> <service/port#>,t).
sockfd = Tcp_connect(argv[1] a「gv[2])
sa = Malloc(MAXSOCKADDR)
len = MAXSOCKADDR
GetpeemameCsockfd sa. &len)
printf("connected to Zs\en“. Sock_ntop_host(sa, len)),
while ( (n = Read(sockfd, recvlme MAXLINE)) > 0) {
recvline[n] = 0./* завершающий нуль */
Fputs(recvlme. stdout).
}
exit(O).
}
Аргументы командной строки
9-10 Теперь нам требуется второй аргумент командной строки для задания либо имени службы, либо номера порта, что позволит нашей программе соединяться с другими портами.
Соединение с сервером
11 Теперь весь код сокета для этого клиента выполняется функцией tcp_connect.
Вывод ответа сервера
12-15 Мы вызываем функцию getpeername, чтобы получить адрес протокола сервера и вывести его. Мы делаем это для проверки протокола, используемого в примерах, которые скоро покажем.
Обратите внимание, что функция tcp_connect не возвращает размера структуры адреса сокета, который использовался для функции connect. Мы могли добавить еще один аргумент-указатель, чтобы получить это значение, но при создании этой функции мы стремились добиться меньшего числа аргументов, чем у функции getaddnnfo. Поэтому мы определяем константу MAXSOCKADDR в нашем заголовке unp h так, чтобы ее размер равнялся размеру наибольшей структуры адреса сокета. Обычно это размер структуры адреса доменного сокета Unix (см. раздел 14.2), немного более 100 байт. Мы выделяем в памяти пространство для структуры указанного размера и заполняем ее с помощью функции getpeername.
ПРИМЕЧАНИЕ
Для размещения этой структуры мы вызываем функцию malloc, вместо того чтобы размещать ее следующим образом:
char sockaddг[MAXSOCKADDR]
Это делается для выравнивания расположения структуры в памяти. Функция mall ос всегда возвращает указатель со строжайшим выравниванием, гребуемым сисгемой, в то время как для массива char может быть выделена область памяти, граница которой проходит по нечетному байту, что может вызвать проблемы для полей IP-адреса или номера порта в структуре адреса сокета. Другой способ решения потенциальной проблемы выравнивания, состоящий в использовании объединения, был показан в листинге 4.4.
Эта версия нашего клиента работает и с IPv4, и с IPv6, тогда как версия, представленная в листинге 1.1, работала только с IPv4, а версия из листинга 1.2 — только с IPv6. Сравните нашу новую версию с представленной в листинге Д.8, которую мы написали, чтобы использовать функции gethostbyname и getservbyname для поддержки и IPv4, и IPv6.
Сначала мы задаем имя узла, поддерживающего только IPv4:
solans Z daytimetcpcli bsdi daytime connected to 206 62 226 35
Fri May 30 12 33 32 1997'
Затем мы задаем имя узла, поддерживающего и IPv4, и IPv6:
solans % daytimetcpcli aix daytime
connected to 5flb dfOO сеЗе e200 20 800 5afc 2b36
Fri May 30 12 43 43 1997
Используется адрес IPv6, поскольку у узла имеется и запись типа АААА, и запись типа А. Кроме того, функция tcp_connect устанавливает семейство адресов AF_UNSPEC, поэтому, как было отмечено в табл. 11.3, сначала идет поиск записей типа АААА, и только если этот поиск неудачен, выполняется поиск записей типа А.
В следующем примере мы указываем на необходимость использования именно адреса IPv4, задавая имя узла с суффиксом -4, что, как мы отмечали в разделе 9.2, в соответствии с принятым нами соглашением означает имя узла, который поддерживает только записи типа А:
solans Z daytimetcpcli aix-4 daytime connected to 206 62 226 43
Fri May 30 12 43 48 1997
Функция tcpjisten
Наша следующая функция, tcp_listen, выполняет обычные шаги сервера TCP: создание сокета TCP, связывание его с заранее известным портом с помощью функции bind и разрешение приема входящих запросов через соединение. В листинге 11.4 представлен исходный код.
#include “unp h"
int tcpjisten(const char *hostname. const char ^service socklen_t *lenptr).
В случае успешного выполнения возвращает дескриптор присоединенного сокета в случае ошибки не возвращает ничего
Листинг 11.4. Функция tcpjisten: выполнение обычных шагов сервера TCP //lib/tcp_listen с
^include 'unp h“
2 3 4 5 6 72 3 4 5 6 7int
2 3 4 5 6 7
2 3 4 5 6 7
tcpjisten(const char *host, const char *serv socklen_t *addrlenp) {'
int listenfd. n.
const int on = 1,
struct addrinfo hints, *res *ressave,
bzero(&hints. sizeof(struct addnnfo)).
hints ai_flags = AI_PASSIVE.
hints ai_family = AF_UNSPEC.
hints ai socktype = SOCK STREAM,
Листинг 11.4 (продолжение)
if ( (п = getdddnnfo(host. serv. &hints. &res)) ,= 0)
err_quit("tcp_listen error for Zs. Zs Zs".
host, serv. gai_strerror(n)).
ressave = res.
16 do {
17 listenfd = socket(res->ai_family, res->ai_socktype. res->ai_protocol).
18 if (listenfd < 0)
19 continue./* ошибка, пробуем следующий адрес */
20Setsockopt(1istenfd. SOL_SOCKET. SO_REUSEADDR. &on. sizeof(on));
21 if (bind(listenfd. res->ai_addr. res->ai_addrlen) == 0)
22 break./* успех */
23Closedistenfd)./* ошибка при вызове функции bind, закрываем сокет и
пробуем следующий адрес*/
24} while ( (res = res->ai_next) != NULL).
25if (res == NULL)/* значение errno устанавливается при последнем вызове
функции socket О или bmd() */
26err_sys(wtcp_l1sten error for Ms. %s". host. serv).
27 Listendistenfd. LISTENQ),
28 if (addrlenp)
29 *addrlenp = res->ai_addrlen, /* возвращает размер адреса протокола */
30freeaddrinfo(ressave).
31 return (listenfd).
32 }
Вызов функции getaddrinfo
8-15 Мы инициализируем структуру addn nfo с учетом следующих рекомендаций (элементов структуры hints): AI_PASSIVE, поскольку это функция для сервера, AFJJNSPEC для семейства адресов и SOCK_STREAM. Вспомните табл. 11.3: если имя узла не задано (что вполне нормально для сервера, который хочет связать с дескриптором универсальный адрес), то наличие значений AI_PASSIVE и AF_UNSPEC вызовет возвращение двух структур адреса сокета: первой для IPv6 и второй для IPv4 (в предположении, что это узел с двойным стеком).
Создание сокета и связывание с адресом
16-24 Вызываются функции socket и bi nd. Если любой из вызовов окажется неудачным, мы просто игнорируем данную структуру addrmfo и переходим к следующей. Как было сказано в разделе 7.5, для сервера TCP мы всегда устанавливаем параметр сокета SO REUSEADDR.
Проверка на наличие ошибки
25-26 Если все вызовы функций socket и bind окажутся неудачными, мы сообщаем об ошибке и завершаем выполнение. Как и в случае с нашей функцией tcp_connect из предыдущего раздела, мы не пытаемся возвратить ошибку из этой функции.
27 Сокет превращается в прослушиваемый сокет с помощью функции listen.
Возвращение размера структуры адреса
28-31 Если аргумент addrlenp является непустым указателем, мы возвращаем размер адресов протокола через этот указатель. Это позволяет вызывающему процессу выделять память для структуры адреса сокета, чтобы получить адрес протокола клиента из функции accept (см. также упражнение 11.1).
Пример: сервер времени и даты
В листинге 11.5 показан наш сервер времени и даты из листинга 4.2, переписанный с использованием функции tep 11 sten.
Листинг 11.5. Сервер времени и даты, переписанным с использованием функции tcpjisten//names/daytimetcpsrvl сЛистинг 11.5. Сервер времени и даты, переписанным с использованием функции tcpjisten//names/daytimetcpsrvl с11234567890234567890#iDelude#i nclude int main(mt#iDelude#i nclude int main(mt“unp.h"<time h>“unp.h"<time h>агде. char **argv)агде. char **argv)intintlistenfd. connfd.listenfd. connfd.socklen t addrlen. len.charbuff[MAXLINE].time_t ticks.struct sockaddr *cliaddr.socklen t addrlen. len.charbuff[MAXLINE].time_t ticks.struct sockaddr *cliaddr.
Листинг 11.5. Сервер времени и даты, переписанным с использованием функции tcpjisten
//names/daytimetcpsrvl с
Листинг 11.5. Сервер времени и даты, переписанным с использованием функции tcpjisten
//names/daytimetcpsrvl с
1
1
234567890
234567890
#iDelude
#i nclude int main(mt
#iDelude
#i nclude int main(mt
“unp.h"
<time h>
“unp.h"
<time h>
агде. char **argv)
агде. char **argv)
int
int
listenfd. connfd.
listenfd. connfd.
socklen t addrlen. len.
charbuff[MAXLINE].
time_t ticks.
struct sockaddr *cliaddr.
socklen t addrlen. len.
charbuff[MAXLINE].
time_t ticks.
struct sockaddr *cliaddr.
if (агде 2)
err_quit(,'usage daytimetcpsrvl <service or port#>"):
listenfd = Tcp_listen(NULL. argv[l], &addrlen).
cliaddr = Malloc(addrlen).
for (..) {
len = addrlen.
connfd = AcceptClistenfd. cliaddr. &len).
pm ntf("connection from 犯\en", Sock_ntop(cliaddr. len)):
ticks = time(NULL):
20snprintf(buff. sizeof(buff)."%24s\er\en,'. ctime(&ticks)).
21Wnte( connfd. buff,st Hen (buff)):
22Close(connfd).
}
}
Ввод имени службы или номера порта в качестве аргумента командной строки
11-12 Нам нужно использовать аргумент командной строки, чтобы задать либо имя службы, либо номер порта. Это упрощает проверку нашего сервера, поскольку связывание с портом 13 для сервера времени и даты требует прав привилегированного пользователя.
Создание прослушиваемого сокета
13-14 Функция tcp_l 1 sten создает прослушиваемый сокет, и функция mall ос размещает в памяти буфер для хранения адреса клиента,•,
Цикл сервера
5-23 Функция accept ждет соединения с клиентом. Мы выводим адрес клиента, вызывая функцию sock_ntop. В случае IPv4 или IPv6 эта функция выводит IP-адрес и номер порта. Мы могли бы использовать функцию getnamemfo (описанную в разделе И 13), чтобы попытаться получить имя узла клиента, но это подразумевает запрос PTR в DNS, что может занять некоторое время, особенно если запрос PTR окажется неудачным В разделе 14 8 [95] упоминается, что на занятом сервере Web почти у 25% всех клиентов, соединяющихся с этим сервером, в DNS нет записей типа PTR. Поскольку мы не хотим, чтобы наш сервер (особенно последовательный сервер) в течение нескольких секунд ждал запрос PTR, мы просто выводим IP-адрес и порт.
Пример: сервер времени и даты с указанием протокола
В листинге 115 есть небольшая проблема: первый аргумент функции tcp_l i Sten — пустой указатель, объединенный с семейством адресов AF_UNSPEC, который задает функция tcp_listen, — может заставить функцию getaddrmfo возвратить структуру адреса сокета с семейством адресов, отличным от желаемого Например, первой на узле с двойным стеком будет возвращена структура адреса сокета для 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 inet[pton(AF[lNET inet_pton(AF二INET6 inet_pton(AFJNET6inet_pton(AF_INET inet[pton(AF[lNET inet_pton(AF二INET6 inet_pton(AFJNET6и0 О О О О О”0 0 0 0"0 О'и0 О О О О О”0 0 0 0"0 О'&foo) &foo) &foo) &foo)&foo) &foo) &foo) &foo)/* успешно */ /* неудачно*/ /* неудачно */ /* успешно *//* успешно */ /* неудачно*/ /* неудачно */ /* успешно */
inet_pton(AF_INET inet[pton(AF[lNET inet_pton(AF二INET6 inet_pton(AFJNET6
inet_pton(AF_INET inet[pton(AF[lNET inet_pton(AF二INET6 inet_pton(AFJNET6
и0 О О О О О
”0 0 0 0"
0 О'
и0 О О О О О
”0 0 0 0"
0 О'
&foo) &foo) &foo) &foo)
&foo) &foo) &foo) &foo)
/* успешно */ /* неудачно*/ /* неудачно */ /* успешно */
/* успешно */ /* неудачно*/ /* неудачно */ /* успешно */
Следовательно, если мы изменим наши серверы таким образом, чтобы они получали дополнительный аргумент, то при вводе
Z server
по умолчанию мы получим IPv6 на узле с двойным стеком, но при вводе Z server 0 0 0 0
явно задается IPv4, а при вводе
Z server 0 0
явно задается IPv6.
В листинге 11.6 показана окончательная версия нашего сервера времени и даты.
Листинг 11.6. Не зависящий от протокола сервер времени и даты, использующий функцию tcpjisten
names/day11metcps rv2 c
^include'unp h"
"include<time h>
mt
mamCint argc char **argv)
{
mt listenfd connfd
socklen_t addrlen. len.
struct sockaddr *cliaddr
char buff[MAXLINE]
time_t ticks.
if (argc == 2)
11stenfd = Tcpjisten(NULL argv[l] &addrlen)
else if (argc == 3)
listenfd = Tcp_listen(argv[l] argv[2] &addrlen)
else—
err_quit("usage daytimetcpsrv2 [ <host> ] <service or port>").
cl laddr = Ma Hoc (addrlen)
for ( ) {
len = addrlen
connfd = AcceptClistenfd cliaddr.&len)
pnntf( 'connection from ^s\en* Sock_ntop(cliaddr len))
ticks = time(NULL)
23snpnntf(buff sizeof(buff) 礎 24s\er\en'cti me (Sticks))
24Write(connfd buff strlen(buff))
25Close(connfd)
}
}
Обработка аргументов командной строки
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:
solans Z daytimetcpsrv2 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,
но они используют 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 Ztmp/rendezvous
где имя /tmp/rendezvous — это произвольное имя, выбираемое нами для связывания на узле сервера, с которым соединяется клиент. Затем мы запускаем клиент на том же узле, задав /local в качестве имени узла и /tmp/rendezvous в качестве имени службы:
solans Z daytimetcpcli /local /tmp/rendezvous connected to /tmp/rendezvous
Fn May 30 16 31 37 1997
11.10. Функция udp_client
Наши функции, предоставляющие более простой интерфейс для функции getaddrinfo, в случае UDP изменяются: в этом разделе мы представляем клиентскую функцию, создающую неприсоединенный сокет UDP, а в следующем — другую функцию, создающую присоединенный сокет UDP.
#include "unp h"
int udp_cllent(const char *hostname, const char *service, void ★★saptr socklen_t *7enp). Возвращает дескриптор неприсоединенного сокета в случае успешного выполнения, в случае ошибки не возвращает ничего
Эта функция создает неприсоединенный сокет UDP, возвращая три элемента. Во-первых, возвращаемое значение функции — это дескриптор сокета. Во-вторых, saptr — это адрес указателя (объявляемого вызывающим процессом) на структуру адреса сокета (которая динамически размещается в памяти функцией udp_cl 1 ent), и в этой структуре функция хранит IP-адрес получателя и номер порта для будущих вызовов функции sendto. Размер этой структуры адреса сокета возвращается как значение переменной, на которую указывает 1 епр. Последний аргумент не может быть пустым указателем (как это допустимо для последнего аргумента функции tcp_l ] sten), поскольку длина структуры адреса сокета требуется в любых вызовах функций sendto и recvfrom.
ПРИМЕЧАНИЕ
saptr должен быть объявлен как struct sockadd г **. Мы используем тип данных void **, потому что определяем другую версию этой функции для XTI в разделе 31.3, которой необходим этот аргумент, чтобы хранить адрес указателя на другой тип структуры. Это значит, что наши вызовы этой функции должны содержать приведение типа (void **).
В листинге 11.7 показан исходный код для этой функции.
Функция getaddnnfo преобразует аргументы hostname и service. Создается дейтаграммный сокет. Выделяется память для одной структуры адреса сокета, и структура адреса сокета, соответствующая созданному сокету, копируется в память.
Листинг 11.7. Функция udp_client: создание неприсоединенного сокета UDP
//lib/udp_client с
#include "unp h"
mt
udp_cllent(const char *host, const char *serv. void **saptr. socklen_t *lenp)
{ --
intsockfd, n,
struct addrinfo hints. *「es. *ressave.
bzero(&hints. sizeof(struct addmnfo)),
hints ai_family = AFJNSPEC,
hints ai"socktype = &)CK_DGRAM.
10 if ( (n = getaddnnfo(host, serv. &hints. &res)) !•= 0)
err_qui t ('* udp_cl i ent error for Zs. %s: %s".
-host."serv. gai_strerror(n)):
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).
19if (res == NULL)/* значение errno устанавливается при последнем вызове
функции socketО */
20err_sys("udp_client error for Ms, Ms", host, serv),
21 *saptr = Malloc(res->ai_addrlen).
22 memcpy(*saptr. res->ai_addr, res->ai_addrlen).
23 *lenp = res->ai_addrlen.
24 freeaddnnfo(ressave),
25 eturn (sockfd),
26 }
Пример: не зависящий от протокола клиент времени и даты
Теперь мы перепишем наш клиент времени и даты, показанный в листинге 11.3, так, чтобы в нем использовалась наша функция udp_cl lent* В листинге 11.8 представлен не зависящий от протокола исходный код.
Листинг 11.8. UDP-клиент времени и даты, использующий нашу функцию udp_client
//names/daytimeudpcli1 c
2 int2 int1 include *'unp.h"
2 int
2 int
продолжение 少
3 4 5 6 7 83 4 5 6 7 8Листинг 11.8 (продолжение) maindnt argc, char **argv) { mt sockfd. n. char recvline[MAXLINE + 1]. socklen t salen. struct sockaddr *sa
3 4 5 6 7 8
3 4 5 6 7 8
if (argc ,= 3)
err_quit("usage daytimeudpclil <hostname/IPaddress> <service/port#>M);
sockfd = Udp_client(argv[1]. argv[2J. (void **) &sa. &salen).
printfCsending to ^s\en", Sock_ntop_host(sa salen)).
SendtoCsockfd. "". 1. 0. sa. salen). /* посылается 1-байтовая дейтаграмма */
n = Recvfrom(sockfd, recvline, MAXLINE 0. NULL. NULL).
recvlmefn] = 0./* завершающий пустой байт */
Fputs(recvl1ne. stdout).
exit(0)
}
1-16 Мы вызываем нашу функцию udp cl ient и затем выводим IP-адрес и порт сервера, которому мы отправим нашу дейтаграмму UDP. Мы посылаем однобайтовую дейтаграмму и затем читаем и выводим ответ сервера.
ПРИМЕЧАНИЕ
Нам нужно отправить дейтаграмму, содержащую 0 байт, поскольку ответ сервера времени и даты инициируется самим получением дейтаграммы от клиента, независимо от ее длины и содержания. Но многие реализации SVR4 не допускают нулевой длины дейтаграмм UDP.
Мы запускаем наш клиент, задавая имя узла с записью типа АААА и типа А. Поскольку функция getaddn nfo в первую очередь возвращает структуру с записью типа АААА, создается сокет IPv6:
solans % daytimeudpclil aix daytime
sending to 5flb dfOO ce3e 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
Функция udp_connect
Наша функция udp_connect создает присоединяющийся сокет UDP.
include "unp h"
int udp_connect(const char *hostndme, const char *service).
Возвращает дескриптор присоединенного сокета в случае успешного выполнения. в случае ошибки ничего не возвращает
В случае присоединненного сокета UDP два последних аргумента, которые требуются в функции udp_cl?ent, больше не нужны. Вызывающий процесс может вызвать функцию write вместо sendto; таким образом, нашей функции не нужно возвращать структуру адреса сокета и ее длину.
В листинге 11.9 представлен исходный код.
Эта функция почти идентична функции tcp_connect. Однако отличие в том, что при вызове функции connect для сокета UDP ничего не отправляется собеседнику. Если что-то не в порядке (собеседник недоступен или на заданном порте не запущен сервер), вызывающий процесс не обнаружит этого, пока не пошлет собеседнику дейтаграмму.
Листинг 11.9. Функция udp_connect: создание присоединенного сокета UDP
//lib/udp_connect с
findude *'unp h"
int
udp_connect(const char *host. const char *serv)
{'
intsockfd.n.
struct addrinfohints. *res.*ressave.
bzero(&hints. sizeof(struct addrwfo)).“
hintsai^family= AF_UNSPEC,
hintsai_socktype = SOCK__DGRAM.
if ( (n = getaddrmfo(host serv. &hints. &res)) !*= 0)
err_quit( "udp^onnect error for Zs. Ms Zs".
一 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)-
17continue,/* игнорируем этот адрес */
18 】f (connect(sockfd, res->ai_addr. res->ai_add「len) «• 0)
19break./* успех */
20Close(sockfd)./* игнорируем этот адрес */
21} while ( (res = res->ai_next) !== NULL).
22 if (res == NULL) /* значение errno устанавливается при последнем вызове функции connectО */
23err_sys(wudp_connect error for Ms. Zs”. host. serv).
24freeaddm nfo( ressave).
25 return (sockfd),
26 }
Функция udp_server
Наша последняя функция, предоставляющая более простой интерфейс для функ* ции getaddnnfo, — это функция udp_server.
#indude "unp h"
mt udp_server(const char ^hostname, const char ^service, socklen_t *lenptr)
Возвращает дескриптор неприсоединенного сокета в случае успешного выполнения, в случае ошибки не возвращает ничего
Аргументы функции те же, что и для функции tcp_listen: необязательный hostname, обязательный service (для связывания номера порта) и необязательный указатель на переменную, в которой возвращается размер структуры адреса сокета.
В листинге 11.10 представлен исходный код.
Листинг 11.10. Функция udp_server: создание неприсоединенного сокета для сервера UDh
//lib/udp_server с
#include"unp h'
mt
udp_server(const char *host. const char *serv. socklen_t *addrlenp)
{一—
mt sockfd n.
struct addrinfo hints. *res, *ressave,
bzero(&hmts. sizeof(struct addnnfo)),
hints ai_flags = AI_PASSIVE.
hints ai_fannly = AF_UNSPEC.
hints ai_socktype = SOCK_DGRAM
if ( (n = getaddrinfo(host. serv &hints &res)) != 0)
err_quit('*udp_server error for 弘s, Ms 弘s"
host, serv, gai_strerror(n)).
ressave = res.
15 do {
16 sockfd = socket(res->ai_fami ly. 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->a?_next) ,= NULL).
23 if (res == NULL)/* значение errno устанавливается при последнем
вызове функции socket О or bmd() */
24err_sys("udp_server error for 灯 犯"host, serv)
25 if (addrlenp)
26 *addrlenp = res->ai_addrlen /* возвращается размер адреса протокола */
27freeaddrinfo(ressave),
28 return (sockfd).
29 }
Эта функция практически идентична функции tcp_listen, в ней нет только вызова функции 】i sten. Мы устанавливаем семейство адресов AF_UNSPEC, но вызывающий процесс может использовать ту же технологию, которую мы описали
при рассмотрении листинга 11.6, чтобы потребовать использование определенного протокола (IPv4 или IPv6).
Мы не устанавливаем параметр сокета SO_REUSEADDR для сокета UDP, поскольку этот параметр сокета может допустить связывание множества сокетов с одним и тем же портом UDP на узлах, поддерживающих многоадресную передачу, как мы говорили в разделе 7.5. Поскольку у сокета UDP нет аналога состояния Т1МЕ_ WAIT, свойственного сокетам TCP, нет необходимости устанавливать этот параметр при запуске сервера.
Пример: не зависящий от протокола сервер времени и даты
В листинге 11.11 представлен наш сервер времени и даты, полученный путем модификации листинга И.би предназначенный для использования UDP.:
Листинг 11-11. Не зависящий от протокола UDP-сервер времени и даты //names/daytimeudpsrv2 с
#i nd ude"unp h"
#include<time h>
int
main(mt argc char **a「gv)
{
mtsockfd
ssize_tn
charbuff[MAXLINE]
9
time_t
ticks.
10
sockTen
t addrlen len
11 struct sockaddr *cliaddr.
if (argc == 2)
sockfd = Udp_server(NULL argv[l] &addrlen)
else if (argc 三 3)
sockfd = Udp_server(argv[l], argv[2] &addrlen).
16 else 一
17 err_quit("usage daytimeudpsrv [ <host> ] <service or port>"):
18cliaddr = Malloc(addrlen).
19 for ( ) {
20 len = addrl en
21 n = Recvfrom(sockfd. buff. MAXLINE. 0, cliaddr, &len).
22 pmntf("datagram from 犯\en". Sock_ntop(cliaddr, len)).
23ticks = time(NULL)
24snpnntf(buff. sizeof(buff). "% 24s\er\en'\ ctime(&ticks)).
25 Sendto(sockfd buff, strlen(buff). 0, cliaddr. len),
26}
27 }
Функция getnameinfo
Эта функция дополняет функцию getaddnnfo: она получает адрес сокета и возвращает одну символьную стро1<у с описанием узла и другую символьную строку с описанием службы. Эта функция предоставляет указанную информацию в не
зависящем от протокола виде, то есть вызывающему процессу неважно, какой тип адреса протокола содержится в структуре адреса сокета, поскольку эти подробности обрабатываются функцией.
#include <netdb h>
mt getnamemfo(const struct sockaddr *sockaddr socklen_t addrlen char *host. size_t hostlen char *serv, size_t servlen 7nt flags)
Возвращает 0 в случае успешного выполнения, -1 в случае ошибки
Аргумент sockaddr указывает на структуру адреса сокета, содержащую адрес протокола, преобразуемый в строку, удобную для человеческого восприятия, а аргумент addrl еп содержит длину этой структуры. Эта структура и ее длина обычно возвращаются любой из перечисленных ниже функций: accept, recvf rom, getsockname или getpeername.
Вызывающий процесс выделяет в памяти пространство для двух строк, удобных для человеческого восприятия: аргументы host и hostl еп определяют строку, описывающую узел, а аргументы serv и servlen определяют строку, которая описывает службы. Если вызывающему процессу не нужна возвращаемая строка с описанием узла, задается нулевая длина этой строки (hostlen). Аналогично нулевое значение аргумента servlen означает, что не нужно возвращать информацию о службе Чтобы упростить выделение памяти под массивы для хранения этих двух строк, при включении заголовочного файла <netdb h> определяются константы, показанные в табл. 11.4.
Таблица 11.4. Константы для размеров возвращаемых строк из функции getnamemfo
Константа
Описание
Значение
NI—MAXHOST
Максимальный размер возвращаемой строки с описанием узла
1025
NI—MAXSERV
Максимальный размер возвращаемой строки с описанием службы
32
Разница между функциями sock_ntop и getnameinfo состоит в том, что первая не задействует DNS, а только возвращает IP-адрес и номер порта. Последняя же обычно пытается получить имя и для узла, и для службы.
В табл. 11.5 показаны пять флагов, которые можно задать для изменения действия, выполняемого функцией getnamemfo.
Таблица 11.5. Флаги функции getnamemfo
Константа
Описание
NI—DGRAM
Дейтаграммный сокет
NI—NAMEREQD
Возвращать ошибку, если невозможно получить имя узла по е】о адресу
NI_NOFQDN
Возвращать только ту часть FQDN, которая содержит имя узла
NI_NUMERICHOST
Возвращать численное значение адреса вместо имени узла
NI_NUMERICSERV
Возвращать номер порта вместо имени службы
Флаг NI_DGRAM доЛжен быть задан, когда вызывающий процесс знает, что работает с дейтаграммным сокетом. Причина в том, что если функции getnamei nfo задать только IP-адрес и номер порта в структуре адреса сокета, она не сможет определить протокол (TCP или UDP) Существует несколько номеров пор-
тов, которые в случае 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, она возвратит в имени узла только al pha.
爹 Флаг NI—NUMERICHOST сообщает функции getnamei nfo, что не нужно вызывать DNS (поскольку это занимает некоторое время). Вместо этого возвращается численное представление IP-адреса, вероятно, при помощи вызова функции 1 netjuop.
爰 Аналогично, флаг NI_NUMERICSERV определяет, что вместо имени службы должен быть возвращен десятичный номер порта. Обычно серверы должны задавать этот флаг, поскольку номера портов клиента, как правило, не имеют соответствующего имени службы — это динамически назначаемые порты.
Можно объединять несколько флагов путем логического сложения, если их сочетание имеет смысл, например NI_DGRAM и NI_NUMERICHOST, так как некоторые сочетания бессмысленны, например NI_NAMEREQD и NI_NUMERICHOST.
ПРИМЕЧАНИЕ
Функция getnameinfo рассматривалась в Posix.lg, но она определяется в RFC 2133.
Функции, допускающие повторное вхождение
Функция gethostbyname из раздела 9.3 имеет интересную особенность, которую мы еще не рассматривали: она не допускает повторное вхождение (nonreentrant). Мы еще столкнемся с этой проблемой в главе 23, когда будем обсуджать потоки, но не менее интересно найти решение этой проблемы сейчас, без необходимости обращаться к понятию потоков.
Сначала посмотрим, как эта функция работает. Если мы изучим ее исходный код (это несложно, поскольку исходный код для всей реализации BIND свободно доступен), то увидим, что обе функции — и gethostbyname, и gethostbyaddr — содержатся в одном файле, который имеет следующий вид: static struct hostent host. /* здесь хранится результат */
struct hostent *
gethostbyname(const char *hostname)
return(getnostbyname2(hostname family)) /*Листинг 9 2*/ }
struct hostent *
gethostbyname2(const char *hostname int family)
{
/* вызов функций DNS для запроса А или AAAA */
/* заполнение структуры адреса узла */
return(&host).
struct hostent * gethostbyaddr(const char *addr, size_t len mt family) {-
/* вызов функций DNS для запроса PTR в домене m-addr.arpa */
/* заполнение структуры адреса узла */
return(&host)
Мы выделили полужирным шрифтом спецификатор класса памяти stati с итоговой структуры, потому что основная проблема в нем. Тот факт, что эти три функции используют общую переменную host, представляет другую проблему, которую мы обсуждали в упражнении 9.1.( Вспомните листинг 9.2.) Функция gethostbynanie2 появилась в BIND 4.9.4 с добавлением поддержки IPv6. Мы будем игнорировать тот факт, что когда мы вызываем функцию gethostbyname, задействуется функция gethostbyname2, поскольку это не относится к предмету обсуждения.
Проблема повторного вхождения может возникнуть в нормальном процессе Unix, вызывающем функцию gethostbyname или gethostbyaddr и из управляющего элемента главного потока, и из обработчика сигнала. Когда вызывается обработчик сигнала (допустим, это сигнал SIGALRM, который генерируется раз в секунду), главный поток управляющего элемента процесса временно останавливается и вызывается функция обработки сигнала. Рассмотрим следующую ситуацию: main() {
struct hostent *hptr
signal(SIGALRM sig_alrm).
hptr = gethostbyname()
}
void
sig_al「m(int signo)
{ 一
struct hostent *hptr.
hptr * gethostbyname( );
Если главный поток управления в момент остановки находится в середине выполнения функции gethostbyname (допустим, функция заполнила переменную host и должна сейчас возвратить управление), а затем обработчик сигналов вызывает функцию gethostbyname, то поскольку в процессе существует только один экземпляр переменной host, эта переменная используется снова. При этом значения переменных, вычисленные при вызове из главного потока управления, заменяются значениями, вычисленными при вызове из обработчика сигнала.
Если мы посмотрим на функции преобразования имен и адресов, представленные в этой главе и в главе 9, вместе с функциями 】net_XX^ из главы 4, мы заметим следующее:
з Функции gethostbyname, gethostbyname2, gethostbyaddr, getservbyname и getservbyport традиционно не допускают повторного вхождения, поскольку все они возвращают указатель на статическую структуру.
Некоторые реализации, поддерживающие программные потоки (Solans 2.x), предоставляют версии этих четырех функций, допускающие повторное вхождение, с именами, оканчивающимися суффиксом _г. О них рассказывается в следующем разделе.
В качестве альтернативы некоторые реализации с поддержкой программных потоков (Digital Unix 4 0 и HP_UX 10.30) предоставляют версии этих функций, допускающие повторное вхождение за счет использования собственных данных программных потоков.
Функции 1 net jDton и 】net_ntop всегда допускают повторное вхождение.
Исторически функция inet_ntoa не допускает повторное вхождение, но некоторые реализации с поддержкой потоков предоставляют версию, допускающую повторное вхождение, которая строится на основе собственных данных потоков.
Функция getaddrmfo допускает повторное вхождение, только если она сама вызывает функции, допускающие повторное вхождение, то есть если она вызывает соответствующую версию функции gethostbyname или getservbyname для имени узла или имени службы. Одной из причин, по которым вся память для результатов ее выполнения выделяется динамически, является возможность повторного вхождения,
轡 Функция getnamei nfo допускает повторное вхождение, только если она сама вызывает такие функции, то есть если она вызывает соответствующую версию функции gethostbyaddr для получения имени узла или функции getservbyport для получения имени службы. Обратите внимание, что обе результирующих строки (для имени узла и для имени службы) размещаются в памяти вызывающим процессом, чтобы обеспечить возможность повторного вхождения.
Похожая проблема возникает с переменной ermo. Исторически существовало по одной копии этой целочисленной переменной для каждого процесса. Если процесс выполняет системный вызов, возвращающий ошибку, то в этой переменной хранится целочисленный код ошибки. Например, функция close из стандартной библиотеки языка С может выполнить примерно такую последовательность действий:
必 поместить аргумент системного вызова (целочисленный дескриптор) в регистр; 卖 поместить значение в другой регистр, указывая, что был сделан системный вызов функции close;
尸 активизировать системный вызов (переключиться на ядро со специальной инструкцией);
碑 проверить значение регистра, чтобы увидеть, что произошла ошибка;
懲 если ошибки нет, возвратить (0);
嚟 сохранить значение какого-то другого регистра в переменной еггпо;
* возвратить (-1).
Прежде всего заметим, что если ошибки не происходит, значение переменной errno не изменяется. Поэтому мы не можем посмотреть значение этой переменной, пока мы не узнаем, что произошла ошибка (обычно на это указывает возвращаемое функцией значение -1).
Будем считать, что программа проверяет возвращаемое значение функции close и затем выводит значение переменной errno, если произошла ошибка, как в следующем примере: if (close(fd) < 0) {
fpnntfCstderr "close error, errno = ^d\en' errno) exit(l).
}
Существует небольшой промежуток времени между сохранением кода ошибки в переменной errno в тот момент, когда системный вызов возвращает управление, и выводом этого значения программой. В течение этого промежутка другой программный поток внутри процесса (то есть обработчик сигналов) может изменить значение переменной errno. Если, например, при вызове обработчика сигналов главный поток управления находится между close и fpnntf и обработчик сигналов делает какой-то другой системный вызов, возвращающий ошибку (допустим, вызывается функция write), то значение переменной errno, записанное при вызове функции cl ose, заменяется на значение, записанное при вызове функции write.
При рассмотрении этих двух проблем в связи с обработчиками сигналов одним из решений проблемы с функцией gethostbyname (возвращающей указатель на статическую переменную) будет не вызывать из обработчика сигнала функции, которые не допускают повторное вхождение. Проблемы с переменной errno (одна глобальная переменная, которая может быть изменена обработчиком сигнала) можно избежать, перекодировав обработчик сигнала так, чтобы он сохранял и восстанавливал значение переменной errno следующим образом: void signalrm(int sign。) Г
mt errno_save.
er「no_save = errno. /* сохраняем значение этой переменной при вхождении */
if (write( ) J nbytes)
fprintfCstderr "write error, errno = %d\en". errno).
errno >« errno save. /* восстанавливаем значение этой переменной при завершении
*/
В этом коде мы также вызываем функцию fpn ntf, стандартную функцию ввода-вывода, из обработчика сигнала. Это еще одна проблема повторного вхождения, поскольку многие версии функций стандартной библиотеки ввода-вывода не допускают повторного вхождения: стандартные функции ввода-вывода не должны вызываться из обработчиков сигналов.
Мы вернемся к проблеме повторного вхождения в главе 23 и увидим, как проблема с переменной errno решается с помощью потоков. В следующем разделе описываются некоторые версии функций имен узлов, допускающие повторное вхождение.
Функции gethostbyname_r и gethostbyaddr_r
Чтобы превратить функцию, не допускающую повторное вхождение, такую как gethostbyname, в повторно входимую, можно воспользоваться двумя способами.
Вместо заполнения и возвращения статической структуры вызывающий процесс размещает в памяти структуру, и функция, допускающая повторное вхождение, заполняет эту структуру Эта технология используется для перехода от функции gethostbyname (которая не допускает повторное вхождение) к функции gethostbyname_r (которая допускает повторное вхождение). Но это решение усложняется, поскольку помимо того, что вызывающий процесс должен предоставить структуру hostent для заполнения, эта структура также указывает на другую информацию: каноническое имя, массив указателей на псевдонимы, строки псевдонимов, массив указателей на адреса и сами адреса (см., например, рис. 9.2). Вызывающий процесс должен предоставить один большой буфер, используемый для дополнительной информации, и заполняемая структура hostent будет содержать различные указатели на этот буфер. При этом добавляется как минимум три аргумента функции: указатель на заполняемую структуру hostent, указатель на буфер, используемый для всей прочей информации, и размер этого буфера. Требуется также четвертый дополнительный аргумент — указатель на целое число, в котором будет храниться код ошибки, поскольку глобальная целочисленная переменная h_errno больше не может использоваться. (Глобальная целочисленная переменная h_errno создает ту же проблему повторного вхождения, что описана нами для переменной errno.)
Эта технология также используется функциями getnameinfo и inet__ntop.
Входящая функция вызывает функцию mall ос и динамически выделяет память. Это технология, используемая функцией getaddrinfo. Проблема при таком подходе заключается в том, что приложение, вызывающее эту функцию, должно вызвать также функцию freeaddri nfo, чтобы освободить динамическую память. Если эта функция не вызывается, происходит утечка памяти'. каждый раз, когда процесс вызывает функцию, выделяющую память, объем памяти, задействованной процессом, возрастает. Если процесс выполняется в течение длительного времени (что свойственно сетевым серверам), то потребление памяти этим процессом с течением времени неуклонно растет.
Обсудим функции Solaris 2.x, допускающие повторное вхождение, не используемые для сопоставления имен с адресами и наоборот (то есть для разрешения имен).
#include <netdb h>
struct hostent *gethos Wyname_r (cons t ch" ^hostnsme, struct hostent * result, char *buf. mt buflen int *h_errnop).
struct hostent *gethostbyaddr_r(const char *аШг. int len. mt type, struct hostent ★result, char 也/Г. int buflen. int *h_errnop).
Обе функции возвращают непустой указатель в случае успешного выполнения. NULL в случае ошибки
Для каждой функции требуется четыре дополнительных аргумента. Аргумент result — это структура hostent, размещенная в памяти вызывающим процессом и заполняемая данной функцией. При успешном выполнении функции этот указатель также является возвращаемым значением.
Аргумент buf — это буфер, размещенный в памяти вызывающим процессом, a buf 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 объединены в новую структуру hosten Jdata (которая должна быть размещена в памяти вызывающим процессом), а указатель на эту структуру — это третий и последний аргумент. Обычные функции gethostbyname и gethostbyaddr в Digital Unix 4.0 и в HP-UX 10.30 допускают повторное вхождение при использовании собственных .данных потоков (см. раздел 23.5). Интересный рассказ о разработке функций _r Solaris 2.x содержится в [60].
Наконец, хотя версия функции gethostbyname, допускающая повторное вхождение, может обеспечить безопасность, когда се одновременно вызывают несколько различных потоков, это ничего не говорит нам о возможности повторного вхождения для лежащих в ее основе функций распознавателя. На момент написания этой книги функции оаспозиавателя в BIND не допускают повторного вхождения.
Реализация функций getaddrinfo и getnameinfo
Рассмотрим реализацию функций getaddn nfo и getname】 nfo. Разработка собственной реализации первой из них даст нам возможность более детально представить, как она работает. Наша реализация также поддерживает доменные сокеты Unix, как мы отмечали в разделе 11.5.
ПРИМЕЧАНИЕ
Все части кода, рассматриваемого в этом разделе, зависящие от поддержки протоколов IPv4, IPv6 или домена Unix, ограничены определениями #ifdef и #endif соответствующей константы: IPv4, IPv6 или UNIXDOMAIN. Это позволяет компилировать код в системе, поддерживающей любые сочетания этих трех протоколов. Однако мы удалили эти выражения препроцессора из показываемого нами кода, потому что они ничего не добавляют к нашему обсуждению и делают код труднее для понимания.
Заметим также, что до главы 14 мы не рассматриваем подробно доменые сокеты Unix.
На рис. 11.2 показаны функции, вызываемые функцией getaddn nfo. Все они начинаются с префикса да_.
Рис-11.2. Функции, вызываемые нашей реализацией функции getaddr inf о
Первый файл — это наш заголовочный файл gai_hdr h, показанный в листинге 11.12, который включается во все наши исходные файлы.
Мы включаем наш обычный заголовочный файл unp h и один дополнительный заголовочный файл. Использование нашего флага AI_CLONE и нашей структуры search мы скоро рассмотрим. Оставшаяся часть заголовочного файла задает прототипы функций, которые мы покажем в этом разделе.
Листинг 11-12. Заголовок gai_hdr.h
//libgai/gai_hdr с
#i nd ude "unp h'*
#i nd ude <ctype h> /* isxdigitO и т д */
/* следующий внутренний флаг не может перекрываться с другими флагами AI ххх */
ППсАс nWPNMP -Р*
Листинг 11.12 (продолжение)
4 #define AI_CLONE4 #define AI_CLONEstruct search {const char *host;int family.}.struct search {const char *host;int family.}./* клонируется для других типов сокетов */
4 #define AI_CLONE
4 #define AI_CLONE
struct search {
const char *host;
int family.
}.
struct search {
const char *host;
int family.
}.
/* строка с именем или адресом узла */ /* AF_xxx */
/* прототипы функций для наших собственных внутренних функций */
intga_aistruct(struct addrinfo ***, const struct addrinfo *.
const void *. int).
struct addrinfo *ga_clone(struct addrinfo *).
intga_echeck(const char *, const char *, int mt, mt, mt),
intga^nsearchCconst char *, const struct addrinfo *. struct search*),
intga_port(struct addrinfo *. mt. int).
mtga二serv(struct addrinfo 女.const struct addnnfo *. const char*).
intga_unix(const char *. struct addrinfo *. struct addrinfo **)
intgn_ipv46(char *. size_t. char *, size_t» void *. size_t.
int. int. int).
В листинге 11.13 показана первая часть нашей функции getaddri nfo.
Листинг 11.13. Функция getaddrinfo: первая часть, инициализация
/ /11 bgai /getaddn nfo с
#include ”gai_hdr h"
#inc】ude<arpa/nameser h> /* необходимо для <resolv h> */
include<resolv h> /* res_imt, 一res */
int
getaddrinfo(const char *hostname. const char *servname,
const struct addrinfo *hintsp, struct addrinfo **result)
{
int rc. error, nsearch.
char **ap. *canon.
structhostent *hptr.
structsearch search[3] *sptr,
structaddrinfo hints *aihead, **aipnext:
/*
* Если мы встретим ошибку, нам потребуется
освободить с помощью функции freeО всю динамически
выделенную память
* Здесь расположен макрос для упрощения кода
*/
#define error(e) { error = (е) goto bad. }
aihead = NULL./* инициализация перененнЬх автоматического класса паня4и*/
aipnext = &aihead:''
canon = NULL,
if (hmtsp — NULL) {
bzero(&hints. sizeof(Mnts)).
hints a 1 _fami1y = AF_UNSPEC
} else
hints = *h】ntsp. /* копия структуры */
26/* сначала проверка основных ошибок */
27 if ( (rc - ga_echeck(hostname, servname. hints ai^flags, hints ai_fami】y.
28 hints a 1 _socktype. hints ai__protocol)) != 0)
29 error(rc),-
30/* сначала проверка для случая домена Unix */
31 if (hostname != NULL &&
32 (strcnp(hostname, "/local") == 0 || strcnpChostname. "ZumxM) — 0) &&
33 (servname != NULL && servname[0] =■ '/'))
34 return (ga_umx(servname. &hints. result)).
Задание макроса error
13-17 Более чем в двенадцати точках этой функции нам потребуется освободить всю выделенную нами память и возвратить соответствующий код, если случится ошибка. Чтобы упростить код, мы задаем этот макрос, который сохраняет возвращаемый код в переменной error и переходит к метке bad в конце функции (см. листинг 11.19).
Инициализация автоматических переменных
18-20 Инициализируются несколько автоматических переменных (которые создаются при входе в блок, где они объявлены, и уничтожаются при выходе из него). Мы описываем указатели a i head и aipnext в листинге 11.24.
Копирование структуры hints вызывающего процесса
21-25 Если вызывающий процесс предоставляет структуру hi nts, мы копируем ее в нашу собственную локальную переменную, поэтому мы можем затем изменить ее. В противном случае мы начинаем с нулевой структуры, отличной от d】_family, которая инициализируется в AF_UNSPEC. Последняя обычно задается нулевой, хотя Posix. 1g этого не требует.
Проверка аргументов
26-29 Мы вызываем нашу функцию ga_echeck, показанную в листинге 11.26, чтобы проверить некоторые из наших аргументов.
Проверка полного имени домена Unix
30-34 Если имя узла — или /local, или /uniх, и имя службы начинается с косой черты, мы обрабатываем этот аргумент как полное имя в домене Unix. Наша функция ga_unix (см. листинг 11.23) полностью обрабатывает полное имя.
Оставшаяся часть нашей функции getaddnnfo (которая продолжается в листинге 11.17) работает с сокетами IPv4 и IPv6. Наша функция ga_nsearch, первая часть которой представлена в листинге 11.14, вычисляет, сколько раз мы выполняем поиск имени узла. Если вызывающий процесс задает семейство адресов AF_INET или AF_INET6, имя узла мы ищем только один раз. Но если семейство адресов не задано (AF UNSPEC), мы выполняем операцию поиска дважды: для имени узла IPv6 и для имени узла IPv4. Эту функцию мы показываем для трех случаев: “ нет имени узла, установлен флаг AI_PASSIVE;
'工 нет имени узла, флаг AI_PASSIVE не установлен (активный сокет);
讓 задано имя узла.
Эти три случая соответствуют трем основным частям табл. 11.3.
Листинг 11.14. Функция ga_nsearch: нет имени узла, пассивный сокет //libgai/ga_nsearch с
mt
ga_nsearch(const char *hostname. const struct addnnfo *hintsp
一 struct search *search)
{
mt nsearch = 0
if (hostname == NULL || hostname[0]二'\e0') {
if (hintsp->ai_flags & AI_PASSIVE) {
/* нет имени-узла, установлен флаг AI_PASSIVE подразумевается связывание с
помощью символов подстановки */
switch (hintsp->ai_fdmily) {
case AF_INET
search[nsearch] host = "0 0 0 0",
search[nsearch] family = AFINET,
nsearch++
break
case AF_INET6
search[nsearch]host ="0 O'.
search[nsearch]faun Iу= AF_INET6.
1 nsearch++
break.
case AFJJNSPEC
search[nsearch]host ="0 0" /* сначала IPv6, затем IPv4 */
searchfnsearch]family= AF_IWET6,
” nsearch++.
search[nsearch]host =0000".
search[nsearch]family= AF」NET
nsearch++
break
}
Отсутствие имени узла, пассивный сокет
11-13 Если вызывающий процесс не указывает имя узла и задает флаг AI_PASSIVE, мы возвращаем информацию для создания одного или более пассивных сокетов, которые будут связаны с универсальными адресами. Переключение (оператор 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_PASSI VE не установлен. При этом подразумевается, что вызывающий процесс хочет создать активный сокет для локаль
ного V3JTH
Листинг 11.15. Функция ga nsearch: нет имени узла, не пассивный сокет
//libgai/ga_nsearch с
34} eTse {
35/* нет имени узла и не установлен флаг AI_PASSIVE соединение с локальным узлом */
36switch (hintsp->a3_farmly){
37case AF_INET
38search[nsearch] host = "localhost',/* 127 0 0 1 */
39search[nsearch] family = AF_INET.
40nsearch++
41break
42case AFJNET6
43search[nsearch] host = "0 Iм,
44search[nsearch] family = AF_INET6:
45nsearch++.
46break.
47case AF—UNSPEC
48search[nsearch] host = "0 1" /* сначала IPv6, затем IPv4 */
49search[nsearch] family = AF_INET6
50nsearch++
51search[nsearch] host = 'localhost"
52search[nsearch] family = AF_INET.
53nsearch++
54break
55}
56}
34 56 Для IPv4 мы считаем, что имя узла 1 оса 1 host возвратит адрес закольцовки, обычно 127.0.0.1. Общепринятого имени для локального узла с IPv6 не существует, поэтому мы возвращаем адрес закольцовки 0 1. В случае пассивного сокета, если не задано семейства адресов, мы возвращаем две структуры: одну для IPv6 и далее одну для IPv4.
В листинге 11.16 показана последняя часть нашей функции — часть else исходного оператора ч f. Код выполняется, когда задано имя узла.
Листинг 11.16. Функция ga_nsearch: задано имя узла
//11bgai/ga_nsearch с
57} else {/* ззпано имя узла */
58 switch (hintsp->di_fannly) {
59
60
case AF—INET
search[nsearch] host =-
hostname
61
search[nsearch] family
=AF—INET.
62
nsearch++
63
break.
64
case AFJNET6
65
search[nsearch] host =
hostname
search[nsearch] family
=AF_INET6.
5
nsearch++
68
break
69
case AF_UNSPEC
70
search[nsearch] host =
hostname
71
search[nsearch] family
=AF_INET6 /* сначала IPv6 */
72
nsearch++.
73
search[nsearch] host =
hostname
74
search[nsearch] family
=AF_INET /* затем IPv4 */
75
nsearch++.
76 break,
77}
Листинг 11.16 (продолжение)
78}
79 if (nsearch < 1 11 nsearch > 2)
80 err_quit("nsearch 父 %d". 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[0])) {
40struct in_addr inaddr.
41 if (inet_pton(AF_INET, sptr->host. &inaddr) == 1) {
42if (hints aijarmly «= AF_UNSPEC &&
43hints ai_fam1y •= AF^INET)
44e r ror(EAl_ADDRFAMILY)7
45if (sptr->famlyJ=AFJNET)
46continue. /* игнорируем*/
47rc = ga_ai struct(&aipnext&hmts. &1naddr. AF_INET):
48if (rc T= 0)
49error(rc),
50continue.
53/* проверяем шестнадцатеричный адрес IPv6 */
54 if ( (isxdigit(sptr->host[0]) || sptr->host[0] — ':') &&
55(strchr(sptr->host. '•') ,= NULL)) {
56struct in6_addr in6addr.
57 if (1net_pton(AF_INET6, sptr->host. &in6addr) — 1) {
58if (hints ai^family '= AFJJNSPEC &&
59hints ai_family H AF INET6)
60error(EAI_ADDRFAMILY)7
61if (sptr->family != AF_INET6)
62continue. /* игнорируем */
63 rc = ga_aistruct(&aipnext, &hints. &in6addr. AF_INET6):
64if (rc 7= 0)-
65error(rc).
66continue,
67}
68}
Вызов функции ga_nsearch
Мы вызываем нашу функцию ga_nsea rch, заполняющую массив search и возвращающую число структур в массиве — одна или две.
Цикл по всем структурам search
Мы выполняем цикл для каждой структуры search, созданной функцией ga_nsea「ch.
Проверка строки в точечно-десятичной записи IPv4
39-44 Если первый символ имени узла является цифрой, мы проверяем, действительно ли имя узла является строкой в точечно-десятичной записи. Для выполнения этой проверки и преобразования мы вызываем функцию inet_pton. Если функция выполняется успешно, но вызывающий процесс задает семейство адресов, отличное от AF INET, это приводит к ошибке.
45-46 Мы проверяем, что элемент family структуры search также равен AF_INET, хотя несовпадение в данном случае вызывает только игнорирование этой структуры. Этот сценарий может иметь место, если, например, вызывающий процесс указывает имя узла 192.3.4.5, но не задает никакого семейства адресов. Функция да_ nsearch создает две структуры search: одну для IPv6 и одну для IPv4. В первый раз в цикле for вызов функции inet_pton оказывается успешным, но поскольку элемент fami 1у структуры search — зто AF_INET6, мы хотим игнорировать эту структуру, а не генерировать ошибку.
Создание структуры addrinfo
47-52 Наша функция ga_a т struct создает структуру addn nfon добавляет ее к создаваемому связному списку (указатель aipnext).
Проверка строки адреса IPv6
53-60 Если первый символ имени узла является либо шестнадцатеричным числом, либо двоеточием и строка содержит двоеточие, мы проверяем, является ли имя узла строкой адреса IPv6, вызывая функцию inet_pton. Если она выполняется успешно, но вызывающий процесс задает семейство адресов, отличное otAF_INET6, то это ошибка.
61-62 Мы проверяем, что элемент family структуры search также равен AF_INET6, но несовпадение в данном случае вызовет только игнорирование этой структуры search.
Создание структуры addrinfo
63-68 Наша функция ga_a】 struct создает структуру addn nfo и добавляет ее к создаваемому связному списку.
В первых двух операторах if в цикле for (см. листинг 11.17) осуществляется проверка строки в точечно-десятичной записи (адрес IPv4) или строки шестнадцатеричных чисел (адрес IPv6).
В оставшейся части цикла, показанной в листинге 11.18, ведется поиск имени узла при помощи вызова либо функции gethostbyname, либо функции gethostbyname2.
Листинг 11.18. Функция getaddnnfo: поиск имени узла
//11bgai/getaddrinfo с
69/* остальная часть цикла for() для поиска имени узла */
70if ( (_res options & RESJNIT) == 0)
71 resjmt()/* необходимо для установления _res.options */
72 if (nsearch =- 2) {
73 _res options &= ~RES_USE」NET6,
74 hptr = gethostbyname2(sptr->host sptr->family)
75} else {
76if (sptr~>family == AF_INET6)
_res options |e RES_USE_INET6.
else
79res options &= ~RES_USE_INET6.
80hptr н gethostbyname(sptr->host).
81},
82if (hptr == NULL) {
83if (nsearch = 2)
84continue. /* если вызов неудачен, но nsearch равно двум, это не ошибка */
85switch (h__errno) {
86. case HOST 二NOT_FOUND
87error(EAI_NONAME),
88I case TRY_AG^IN
89' error(D\I_AGAIN).
90case NO_RECOVERY
91error?EAI__FAIL).
92case N0_DATA
93error?EAI_NODATA).
94default
95error(EAI_NONAME).
96}
97}
98/* если семейство адресов задано проверяем нет ли расхождений */
99if (hints ai family '= AFJJNSPEC 8& hints ai_family '= hptr->h_addrtype)
100 errorCEACADDRFAMILY)-一
101/* сохраняем каноническое имя */
102 if (hostname != NULL && hostname[0] != '\e。' &&
103 (hints aijlags & AI_CANONMME) && canon =- NUll) {
104 if ( (canon = strdup?hptr->h_name)) =° NULL)
105error(EAI_MEMORY).
106}
107/* создаем одну структуру addnnfo{} для каждого возвращенного адреса */
108 for (ар = hptr->h_addr_list. *ар '= NULL ар++) {
109 rc = да aistructC&aipnext. &hints. *ap, hptr->h_addrtype),
110 if (rc “ 0)一
111error(rc)
112}
113}
114 if (aihead = NULL)
115 error(EAI_NONAME) /* ничего не найдено */
Инициализация распознавателя
70-71 Мы вызываем функцию распознавателя「es_1nit, если она не была вызвана раньше.
Вызов функции gethostbyname2, когда поиск выполняется дважды
72~74 Если nsearch равно 2, мы проходим цикл for дважды: сначала для адреса IPv6, затем для адреса IPv4. Если аргумент имени узла имеет адрес только в одном из двух семейств адресов, мы хотим возвратить только один адрес. Например, наш узел sol an s в разделе 9.2 имеет в DNS запись Tima АААА и запись типа А. Проходя цикл в первый раз, мы хотим найти запись типа АААА, а во второй — запись типа А. Но если имя узла имеет только запись типа А, мы не хотим обрабатывать эту запись во время первого прохождения цикла, когда элемент 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 равно двум, это не ошибка, поскольку один из проходов через цикл может оказаться успешным. (В конце цикла мы проверяем, что возвращается как минимум одна структура addnnfo.) Но если это был единственный вызов распознавателя, мы возвращаем ошибку, соответствующую значению h_errno распознавателя.
Проверка несовпадения семейства адресов
98-100 Если вызывающий процесс задает семейство адресов, но возвращаемое распознавателем семейство отличается от заданного, это ошибка.
Сохранение канонического имени
101-106 Если вызывающий процесс задает имя узла и флаг AI_CANONNAME, мы сохраняем первое каноническое имя, возвращаемое распознавателем. (Вспомните листинг 11.15, когда мы вызываем распознаватель для имени local host, даже если вызывающий процесс не задает имени узла.) Мы дублируем строку, возвращаемую распознавателем, и сохраняем указатель на нее в переменной canon.
Создание структуры addrinfo для каждого адреса
107-112 Для каждого адреса, возвращаемого распознавателем в массиве h_dddr_】ist, мы вызываем нашу функцию ga_aistruct для создания структуры addrinfo и добавления ее к создаваемому связному списку структур.
Проверка отсутствия совпадений
114-115 Если начало связного списка структур addrinfo по-прежнему является пустым указателем, это означает, что все итерации в цикле for оказались неудачными.
В листинге И 19 показана последняя часть функции getaddri nfo
Листинг 11.19. Функция getaddrinfo обработка имени службы
//1т bgai/getaddrinfo с
116/* возвращается каноническое имя */
117 if (hostname 』NULL && hostname[0] '= \e0 &&
118 hints ai_flags & AI_CANONNAME) {
119 if (canon J NULL)"
120 a1 head->ai_canonname = canon
121else { 一
122 if ( (aihead->ai_canonname = strdup(search[O] host)) — NULL)
123error(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
134freeaddr1nfo(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_ххх
Наша функция ga_serv, которая вызывалась из программы в листинге 11.19 для обработки имени службы, показана в листинге 11 20
5 6 7 8 9 05 6 7 8 9 01112131415161718192021222324252627282930313233343536373839404142434411121314151617181920212223242526272829303132333435363738394041424344
5 6 7 8 9 0
5 6 7 8 9 0
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
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
Листинг 11.20. Функция ga_serv 〃libgai/ga_serv с int ga_serv(struct addrinfo *aihead const struct addrinfо *hintsp -const char *serv) { int port rc nfound struct servent *sptr
nfound = 0
if (isd】git(serv[0])) {/* сначала проверяется строка с номером пбрта */
port = htons(atcn(serv))
if (hmtsp->ai_socktype) {
/* вызывающий процесс задает тип сокета */
if ( (rc = ga_port(aihead port hi ntsp->ai_socktype)) < 0)
return (EAI_MEMORY)
nfound += rc
} else {
/* вызывающий процесс не задает тип сокета */
if ( (rc = ga_port(aihead port SOCK_STREAM)) < 0)
return (EAI_MEMORY)
nfound += rc
if ( (rc = ga_port(aihead port SOCK_DGRAM)) < 0)
return (EA匸 MEMORY)
nfound += rc
}
} else {
/* пробуем имя службы TCP затем UDP */
if (hintsp->ai_socktype == 0 || hintsp >ai_socktype == SOCK_STREAM) {
if ( (sptr = getservbyname(serv tcp )) ,= NULL) {
if ( (rc == ga_port(aihead sptr->s_port SOCK_STREAM)) < 0)
return (EAI_MEMORY)
nfound += rc
}
}
if (hintsp->ai_socktype = 0 || hintsp >ai_socktype == SOCK_DGRAM) {
if ( (sptr = getservbyname(serv udp ))『NULL) {
if ( (rc = ga_port(aihead sptr->s_port SOCK_DGRAM)) < 0)
return (EAI_MEMORY).
nfound += rc
}
45 if (nfound == 0) {
46】f (hintsp->ai_socktype = 0)
47 return (EAI_NONAME) /* все вызовы функции getservbyname() окончились неудачно */
48else
49 return (EAI_SERVICE) /* эта служба не поддерживается для данного типа сокета */
50}—
51return (0)
52 }
Проверка строки с номером порта
12-27 Если первый символ в имени службы — это цифра, мы считаем, что имя службы задано через номер порта, и вызываем функцию atoi для преобразования его в двоичное число Если вызывающий процесс задает определенный тип сокета
(SOCK_STREAM или SOCK_DGRAM), то тогда наша функция ga^port вызывается один раз для этого типа сокета. Но если тип сокета не задан, наша функция ga_port вызывается дважды — один раз для TCP, второй для UDP. (Вспомните табл. 11.1.) Мы подсчитываем, сколько раз функция ga_port завершается успешно, и возвращаем ошибку, только если при завершении выполнения функции полученное число равно 0.
Вызов функции getservbyname для TCP
28-36 Если тип сокета не задан или задан сокет TCP, вызывается функция getservbyname со вторым аргументом Mtcp". Если вызов оказывается успешным, вызывается наша функция 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 addnnfo *aihead. Int port, int socktype)
29/* номер порта должен быть в сетевом порядке байтов */
30 {
31int nfound = 0:
32struct addгinfo *ai,
33 for (ai = aihead, an != NULL, ai = a»ai_next) {
34 if (ai->ai_flags & AI_CLONE) {-
35 if (ai->ai_socktype != 0) {
36 if ( (ai = ga_clone(3i)) == NULL)
37return (-1)/* ошибка при выделении памяти */
38/* ai указывает на вновь клонированный элемент, который нам нужен */
39}
40} else if (ai->ai_socktype != socktype)
41continue./* игнорируем несовпадение типов сокетов */
42av>ai_socktype =socktype.
43switch (ai->ai_family){
44case AF_INET: ~
45 ((struct sockaddrjn *) ai->ai_addr)->sinj)ort = port.
46nfound++.
47break.
48case AFJNET6:
49((struct sockaddr_in6 *) ai->ai_addr)->sin6_port = port;
50nfound++.
51 break:
52}
53}
54 return (nfound):
55 }
Цикл по всем структурам addrinfo
33 Мы выполняем цикл по всем структурам addn nfo, созданным при помощи функции ga_ai struct в листингах 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) создает две структуры addri nfo, по одной для каждого 1Р-ад- реса, возвращаемого функцией gethostbynsme. Флаг AI_CLONE также установлен в каждой структуре, поскольку не задан тип сокета. Итоговый связный список представлен на рис. 11.3.
aiheadaddrinfo{}addrinfo{}
Рис. 11.3. Структуры addrinfo при первом вызове функции ga_port
Функция ga_serv вызывается в листинге 11.19. Поскольку имя службы domain действительно и для TCP, и для UDR функция getservbyname вызывается два раза, и поэтому функция ga_port также вызывается дважды: сначала ее последний аргумент равен SOCK_STREAM, а при втором вызове последний аргумент равен SQCK DGRAM. При первом вызове функция gd_port начинает со связного списка, показанного
на рис. 11.3. В листинге 11.21 флаг AI_CLONE установлен для обеих структур, но тип сокета нулевой. Следовательно, все, что происходит со структурой addnnfo, когда функция ga_port вызывается в первый раз, — в структуре адреса сокета элемент ai_socktype принимает значение SOCK_STREAM, а номер порта — значение 53. Флаг AI CLONE остается установленным. При этом мы получаем связный список, изображенный на рис. 11.4.
aiheadaddrinfo(}addrinfo(}
Рис. 11.4. Структуры addrinfo после первого вызова функции ga_port
Но когда функция ga_port вызывается во второй раз (с последним аргументом SOCK_DGRAM), поскольку флаг AI_CLONE установлен и тип сокета ненулевой, функция ga_cl one вызывается для каждой структуры addrinfo. Элемент ai_socktype каждой заново клонированной структуры принимает значение SOCK_DGRAM, и мы получаем связный список, показанный на рис. 11.5. На этом рисунке вторая структура addrinfo и ее структура адреса сокета клонированы из первого набора структур, а четвертая структура addn nfo и ее структура адреса сокета клонированы из третьего набора структур.
aihead addrinfo{}addrinfо{}addrinfo{}addrinfo{}
Рис. 11.5. Структуры addrinfo после второго вызова функции да_port
В листинге 11.22 показана наша функция ga_clone, которая была вызвана в листинге 11.21 для клонирования новой структуры addrinfo и ее структуры адреса сокета из существующего набора структур. ,
Листинг 11.22. Функция ga_clone
//1ibgai/ga_clone с
struct addnnfo *
ga__c1 one (struct addnnfo *ai)
{ ~
struct addrinfo *new;
if ( (new = callocd. sizeof(struct addrmfo))) == NULL)
return (NULL).
new->ai_next = ai->ai_next;
ai->ai_next = new,
new->ai_flags = 0:/* провернем, что флаг AIJLONE сброшен */
new->d匸 family = ai->ai_fannly,~
new->ai[socktype 工 ai->ai_socktype.
new->aij)rotoco1 = ai->ai_protocol:
new->ai_canonname » NULL;
new->ai_addrlen = ai->ai_addr1en,
if ( (new->ai_addr = malToc(ai->ai_addrlen))==NULL)
return (NULL).
memcpy(new->ai_addr. ai->ai_addr. ai->ai_addrlen):
return (new):
}
Выделение структуры addrinfo и включение ее в связный список
9-12 В памяти размещается новая структура addrinfo, и указатель ai_next клонируемой структуры (то есть предыдущей структуры в списке) становится указателем на эту новую структуру.
Инициализация из клонированной записи
13-22 Все поля новой структуры addrmfo копируются из клонируемой структуры, за исключением поля ai_fl ags, которое становится нулевым, и ai_canonname, которое становится пустым указателем. Указатель на заново созданную структуру — это возвращаемое значение функции.
Наша функция ga_uni х, которую мы показываем в листинге 11.23, была вызвана из листинга 11.13 для обработки полного имени в домене Unix.
Листинг 11.23. функция ga_unix
//1ibgai/ga_unix с
mt
ga_unix(const char *path. struct addrinfo *hmtsp. struct addnnfo **resu1t)
5广
int rc.
struct addrinfo *aihead, **aipnext.
aihead = NULL.
aipnext = &aihead.
if (hintsp->ai_farnily '= AF_UNSPEC && hmtsp->ai_family '= AF_LOCAL)
return (EAI_ADDRFAMILY). ~~~
if (hintsp->aT_socktype == 0) {
если не указан тип сокета. дейтаграммные */
сначала возвращаются потоковые сокеты, а затем продолжение 夕
Листинг 11.23. Функция ga_unix
hintsp">3i_socktype = SOCK_STREAM.
if ( (rc = ga_aistruct(&aipnext. hinlsp. path. AF_LOCAL)) f= 0)
return (rc),
hintsp->ai_socktype = SOCK_DGRAM
}一~
if ( (rc = ga_aistruct(&aipnext. hintsp. path. AF_L0CAD) ,= 0)
return (rc).
if (hintsp,ai_f1ags & AI_CANONNAME) {
struct utsn^me myname.
if (unarneC&myname) < 0)
return (EAI_SYSTEM),
if ( (aihead->ai_canonname = strdupCmyname.nodenarne)) == NULL)
return (EAI MEMORY),
}"
*result = aihead:/* указатель на первую структуру в связном списке */
return (0).
}
Функция ga_aistruct9 создание структур
10-20 Если тип сокета не задан, мы вызываем нашу функцию ga_ai struct дважды, чтобы создать две структуры addri nfo: один раз с типом сокета SOCK_STREAM, а второй раз — с типом сокета SOCK_DGRAM. Но если вызывающий процесс задает ненулевой тип сокета, наша функция ga_ai struct вызывается только один раз, создавая одну структуру addnnfo с этим типом сокета.
Возвращение канонического имени
21-27 Если вызывающим процессом был задан флаг AI_CANONNAME, мы вызываем функцию uname, чтобы получить имя системы, и возвращаем элемент nodenarne (см. раздел 9.7) в качестве канонического имени.
Значение указателей aihead и aipnext мы объясним вместе с функцией да_ ai struct, которая описывается следующей.
Наша функция ga_ai struct вызывается для создания структуры addri nfo в листингах 11,17 и 11.18 (для адреса IPv4 или IPv6) и в листинге 11.23 (для доменного сокета Unix). Первую часть этой функции мы показываем в листинге 11.24.
Листинг 11.24. Функция да aistruct: первая часть
//11Ьда1/да_аistruct с
int
да_аistruct(struct addrinfo ***paipnext const struct addnnfo *hintsp,
const void *addr. int family),
{
struct addrinfo *ai.
if ( (ai = ca1loc(l. sizeof(struct addnnfo))) == NULL)
return (EAI—MEMORY):
ai->ai_next = NULL:
at->ai_canonname = NULL.
**paipnext = ai.
*paipnext = &ai->ai_next.
if ( (ai->ai_socktype = hintsp->ai_socktype) ==0),
ai->ai_flags |= AI_CL0NE.
ai->ai_protocol * hintsp->ai_protoco1
Выделение структуры addrinfo и добавление ее к связному списку
10-15 В памяти размещается структура addrinfo и добавляется к создаваемому связному списку. Для построения связного списка используются два указателя: ai head и aipnext. Оба были размещены в памяти и проипициализированы в листинге 11.13 для сокета IPv4 или IPv6 и в листинге 11.24 для сокета домена Unix. Указатель а 1 head инициализируется как пустой указатель, a a i pnext — как указатель на a i head. Они показаны на рис. 11.6.
aihead
NULL
aipnext
Рис. 11.6. Инициализация указателей связного списка
Указатель aihead всегда указывает на первую структуру addrinfo в связном списке (следовательно, типом данных является struct addri nfo *). Указатель aipnext обычно указывает на элемент aipnext последней структуры в связном списке (поэтому его тип — struct addrinfo **). Мы употребляем по отношению к указателю aipnext слово «обычно», поскольку при инициализации он в действительности указывает на aihead, но после того, как первая структура размещена в памяти и помещена в список, он всегда указывает на элемент ai_next.
Возвращаясь к нашей функции ga_ai struct, отметим, что после выделения новой структуры выполняются два выражения:
**paipnext = ai,
*paipnext = &ai->ai_next.
Первое выражение устанавливает указатель ai_next последней структуры в списке (или указатель aihead, если эта новая структура является первой в списке) на вновь размещенную структуру, а второе — указатель aipnext на элемент aipnext вновь размещенной структуры. Дополнительный оператор разыменования необходим в обоих выражениях, поскольку адрес a i pnext — это аргумент функции (см. упражнение 11.4). Когда первая структура добавляется к списку, мы получаем структуры данных, изображенные на рис. 11.7.
aiheadaddrinfo_in{}
Перваяструктура
aipnext
аз. nextаз. nextNULLNULL
аз. next
аз. next
NULL
NULL
Рис. 11.7. Связный список после добавления первой структуры
Когда наша функция ga_ai struct вызывается в следующий раз для размещения второй структуры и добавления ее к списку, мы получаем структуры данных, изображенные на рис. 11.8.
aiheadaiheadПерваяструктураПерваяструктураaipnextaipnextai nextai nextaddrinfo_in{}addrinfo_in{}Рис. 11.8. Связный список после добавления второй структурыРис. 11.8. Связный список после добавления второй структуры
aihead
aihead
Перваяструктура
Перваяструктура
aipnext
aipnext
ai next
ai next
addrinfo_in{}
addrinfo_in{}
Рис. 11.8. Связный список после добавления второй структуры
Рис. 11.8. Связный список после добавления второй структуры
9 0 1 23456789012345 6 7 8 9 122 22222222333333 33339 0 1 23456789012345 6 7 8 9 122 22222222333333 3333
9 0 1 23456789012345 6 7 8 9 122 22222222333333 3333
9 0 1 23456789012345 6 7 8 9 122 22222222333333 3333
Установка типа сокета
16-17 Элемент ai_socktype устанавливается равным типу сокета, который задан вызывающим процессом, и если он нулевой, устанавливается флаг Ai_CLONE.
В листинге 11 25 показана вторая часть нашей функции. Оператор switch отводит для каждого семейства адресов отдельный блок case, в котором осуществляется размещение в памяти и инициализация структуры адреса сокета.
Листинг 11.25. Функция ga_aistruct: вторая часть libgai/ga_aistruct с switch ((ai->ai_fami1y = family)) { case AF_INET { 一 struct sockaddr_m *sinptr
/* размещаем структуру sockbddr_in{} и заполняем ее кроме номера порта*/ if ( (sinptr 工 callocCl sizeoflstruct sockaddr^in))) = NULL)
return (EAI_MEMORY)
#ifdef HAVE_SOCKADDR-SAjEN smptr->sinjen = sizeof(struct sockaddrjn)
#endif~一
smptr->sin__family = AF_INET. memcpy(&sinptr->sin_addr addr sizeofCstruct in_addr)) ai->ai_addr 工(struct sockaddr *) sinptr - ai->ai_addrlen » sizeofCstruct sockaddjin), break 一~
}
case AFJNET6 { struct sockaddr_in6 *sir)6ptr
/* размещаем структуру sockaddr_in6{} и заполняем ее кроме номера порта */ if ( (sin6ptr = callocCl sizeofCstruct sockaddr_in6))) 一 NULL) return (EAI_MEMORY)
#ifdef HAVE SOCKADDR SA LEN
40
sin6ptr->sin6_len = sizeofCstruct
sockaddr_m6)
41
#endif-
42
sin6pt r->sin6_fami1у = AF_INET6
43
memcpy(&sm6ptr >sin6_addF addr
sizeof(struct in”ddr)):
44
ai >ai addr = (struct sockaddr *)
sin6ptr~
45
ai->ai addrlen = sizeofCstruct sockaddr m6)
46
break
}
case AF_L0CAL {
struct sockaddr^un *unp
/* размещаем структуру sockaddr_un{} м заполняем ее */
if (strlen(addr) >» sizeof(unp->sun path))
return(EAI_SERVICE)-
if ( (unp » callocCl. sizeof(struct sockadd「_un))) ==• NULL)
return(EAI_MEMORY),~
unp->sun_fami1y = AF_LOCAL
strcpy(unp->sunj)ath addr)
#ifdef HAVE_SOCKADDR_SA_LEN
unp->sun_len = GlJN_LEN(unp)
#endif
ai->ai_addr = (struct sockaddr *) unp
ai->ai_addrlen » sizeof(struct sockaddr_un)
if (hintsp->ai_flags & AI_PASSIVE)
unlink(unp->sun_pdth) /* если выполняется неудачно, это не ошибка */
break
}
}
return (0)
}
Выделение и инициализация структуры
адреса сокета IPv4
20-33 Выделяется структура sockaddr_i п, и указатель ai_addr в структуре addri nfo устанавливается для указания на нее. Происходит инициализация IP-адреса, семейства адресов и элементов длины структуры адреса сокета. Номер порта не инициализируется до тех пор, пока не будет вызвана функция ga_serv, которая, в свою очередь, вызывает функцию ga_port.
Выделение и инициализация структуры адреса
сокета IPv6
34-47 Выделяется и инициализируется структура sockaddr」пб.
Выделение и инициализация структуры адреса доменного сокета Unix
48-65 Выделяется и инициализируется структура sockaddr_un. Адрес — это полное имя, и если вызывающим процессом был задан флаг AI_PASSIVE, мы пытаемся с помощью функции unlink отсоединить имя, чтобы не допустить возвращения ошибки при вызове функции bi nd. Но если функция unlink выполнится неудачно, это не будет ошибкой.
Наша функция ga_echeck, показанная в листинге 11 26, была вызвана в листинге 11.13 для того, чтобы выполнить некоторую начальную проверку ошибок в аргументах вызывающего процесса
Листинг 11.26. функция ga_echeck
//libgai/ga_echeck с
mt
ga__echeck(const char *hostname const char *servnameпподолжение 夕
Листинг 11.26 (продолжение)
int flags, int family, int socktype int protocol)
{
if (flags & ~(AI_PAS$IVE | AI_CANONNAME))
return (EAI_BADFLAGS) /* неизвестный флаг */
15
switch (family) {
16
17
18
19
20
21
22
23
case AF—UNSPEC break
case AF_INET
if (socktype *= 0 &&
(socktype '= SOCK_STREAM &&
socktype '= SOCK.DgRAM &&
socktype 1= S0CK_RAW))
return (EAI_SOCKTYPE) /* недопустимый тип сокета */
24
25
26
27
28
29
30
break,
case AF_INET6
if (socktype |= 0 &&
(socktype i= SOCK_STREAM &&
socktype i= SOCKjGRAM &&
socktype 」SOCK^RAW))
return (EAI^SOCKTYPE). /* недопустимый тип сокета */
31
32
33
34
35
36
break.
case AF_LOCAL
if (socktype != 0 &&
(socktype 』SOCK_STREAM &&
socktype i= SOCK_DGRAM))
return (EAI_SOCKTYPE). /* недопустимый тип сокета */
11 if (hostname == NULL || hostname[0] == '\e0') {
12 if (servname == NULL || servname[0] == '\e0')
13 return (EAI—NONAME) /* должно быть указано имя узла или службы */
14}
37 break,
38 default
39 return (EAI FAMILY)./* неизвестное семейство протоколов */
40)一皿
41 return (0)
42 }'
•-14 Проверяются флаги. Кроме того, должно быть задано либо имя узла, либо имя службы.
5-41 В зависимости от семейства адресов поддерживаются только определенные типы сокетов, при этом проверяется тип сокета.
Мы не проверяем рекомендацию ai_proto81 вызывающего процесса, если она есть, поскольку лишь немногие приложения задают это значение (которое становится третьим аргументом функции socket). Если задается неверное сочетание, такое как тип сокета SOCK_DGRAM и протокол IPPROTO TCP, вызывающему процессу возвращается рекомендация ai_protocol, как показано в листинге 11.24, а если вызывающий процесс использует это значение для вызова функции socket, возвращается ошибка EPROTONOSUPPORT.
Мы закончили описание функции getaddri nfo, а также всех внутренних функций, которые она вызывает. В листинге 11.27 представлена функция freeaddri nfo, освобождающая всю память в связном списке. Мы вызываем эту функцию в листинге 11.19, если происходит ошибка, а пользователь также вызывает ее, чтобы освободить связный список структур..
Листинг 11.27. Функция freeaddnnfo: первая часть
//11Dga1/freeaddrinfo с
1 ^include “gai hdr h*
void
freeaddrinfo(struct addrinfo *aihead)
{
struct addnnfo *ai, *ainext
for (ai = aihead, ai i= NULL, ai = ainext) {
if (ai->ai_addr l= NULL)
free(ai->ai__addr)./* структура адреса сокета */
if (ai->ai_canonnarne ,= NULL)
free(ai->ai_canonname).
ainext = di・>ai_next,/* невозможно получить содержимоеуказателя aijiexv'
после выполнения функции freeО */'
free(ai),/* сама структура addnnfo{} */
}
}
6-13 Мы перебираем связный список структур add门 nfo. Если была выделена память для структуры адреса сокета, она освобождается. Если была выделена память доя строки канонического имени, она также освобождается. Наконец, освобождается память, занятая самой структурой addrmfo. Обратите внимание, что следует сохранить содержимое указателя ai_next структуры до ее освобождения, так как мы не сможем ссылаться на эту структуру после того, как будет выполнена функция free.
В листинге 11.28 показана наша реализация функции getnamemfo. Она состоит из оператора switch с одним блоком case для каждого семейства адресов.
Листинг 11.28. Функция getnameinfo'
//1ibgai/getnameinfo с
int
getnameinfo(const struct sockaddr *sa sock!en_t salen
char *host size_t hostlen.
char *serv. size_t servlen int flags) .
{一
789789switch (sa->sa_fami1y) {
7
8
9
7
8
9
case AF_INET {
struct sockaddr_in *sain = (struct sockaddr_in *) sa.
return (gn_ipv46(host. hostlen serv servlen
&sain->sin_addr. sizeof(struct m_addr),
AF_INET. sain->sm_port, flags))
}—
case AF_INET6 {•
15 struct sockaddr_in6 *sam = (struct sockaddr_in6 *) sa:
16 return (gn_ipv46(host hostlen serv, serv!en
17,&sain->sin6_addr. sizeof(struct in6_addr).
18'AF_INET6. sam->sin6_port. flags))/
продолжение 按продолжение 按19}一-
продолжение 按
продолжение 按
Листинг 11.28 (продолжение)
case AF_LOCAL:{
struct sockaddr_un *un » (struct sockaddr^un *) sa:
if (hostlen> 0)
snprintf(host. hostlen, *'^s". '7local"):
if (servlen> 0)
snpnntf(serv. servlen. "Xs°. un->sun_path);
return (0):
}
default:
return (1):
}
}
Обработка структур адреса сокета IPv4 и IPv6
-19 Мы вызываем нашу функцию gn_i pv46 (она показана следующей) для обработки структур адресов сокетов IPv4 и IPv6.
Обработка структур адреса доменного сокета Unix
0-27 Для структуры адреса доменного сокета Unix мы возвращаем имя узла /local, а в качестве имени службы возвращаем имя, связанное с сокетом. Если с сокетом не связано никакого полного имени, возвращаемое имя службы будет пустой строкой.
ПРИМЕЧАНИЕ
Мы возвращаем имя узла и имя службы, используя функцию snprintf вместо функции strncpy. Если бы мы использовали последнюю, мы могли бы написать: strncpy(host. "/local", hostlen):
Хотя это гарантирует, что мы не переполним буфер вызывающего процесса, но если hostlen меньше или равно 6, буфер вызывающего процесса не будет содержать символа конца строки (нуля). Но мы пишем библиотечную процедуру и всегда должны возвращать строку, оканчивающуюся нулем, если вызывающий процесс предполагает это. Это может вызвать проблемы для вызывающего процесса далее по ходу программы. Поэтому следует всегда писать:
strnCpy(host・ “/locdl”. hostlen-1).
hostChostlen-1] » '\e0'.
Тогда мы сможем быть уверены, что не переполним буфер вызывающего процесса и что наш результат оканчивается нулем (то есть имеется символ конца строки). Вместо двух указанных операторов мы можем использовать snprintf. В качестве альтернативы можно определить нашу собственную библиотечную функцию, которая вызывает функцию strncpy и добавляет к результату символ конца строки (нуль), однако вызов существующей функции snprintf выглядит проще.
В листинге 11.29 представлена наша функция gn_ipv46, обрабатывающая структуры адреса сокета IPv4 и IPv6 для функции getnamei nfo.
Листинг 11 -29. Функция gn_ipv46: обработка структур адреса сокета IPv4 и IPv6
//libgai/gn_ipv46 с
int
gn_ipv46(char *host. size_t hostlen, char *serv. size_t servlen,
- void *aptr, size_t alen. int family, int port, int flags)
121314151617181920212223242526272829303132333435363738394041424344121314151617181920212223242526272829303132333435363738394041424344
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
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
{
9char*ptr,
10structhostent*hptr:
11structservent*sptr;
if (hostlen > 0) {
if (flags & NI_NUMERICHOST) {
if (inet_nto0(family, aptr. host, hostlen) «•« NULL)
return (1):
} else {
hptr ■ gethostbyaddrCaptr, alen. family).
if (hptr У NULL && hptr«>h_name != NULL) {
if (flags & NI_N0FQDN) {
if ( (ptr = strchr(hptr->h_name, '.')) !■ NULL)
*ptr = 0:/* отбрасываем все после первой точки V
}
snprintf(host, hostlen, ''Xs". 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) {
snpnntfCserv, serv!en."痴”.ntohs(port)):
} else {'
sptr = getservbyport(port. (flags & NI_DGRAM) ? "udp" : NULL):
if (sptr '» NULL && sptr->s_name '» NULL)
snprintf(serv, servlen. "Is", sptr->s_name), else
snprintf(serv. servlen. "Zd". ntohs(port)).
}
return (0).
Возвращение имени узла
12-23 Если задан флаг NI_NUMERICHOST, мы вызываем функцию inet_ntop, чтобы возвратить формат представления IP-адреса; в противном случае функция gethostbyaddr ищет имя узла, соответствующее IP-адресу. Если функция gethostbyaddr выполняется успешно и если задан флаг NI__NOFQDN (означающий, что не требуется возвращать полное доменное имя), возвращается имя узла, усеченное до первой точки (см. табл. 11.5).
Обработка неудачного выполнения функции gethostbyaddr
24-29 Если функция gethostbyaddr выполняется неудачно (к сожалению, это обычное явление при существующем количестве неверно сконфигурированных серверов DNS в Интернете, см. раздел 14.8 [95]) и если был задан флаг NI_NAMEREQD, возвращается ошибка. В противном случае строка адреса, соответствующая IP-адресу, формируется функцией i net_ntop.
Возвращение строки с именем службы
2 42 Если был задан флаг NI_NUMERICSERV, возвращается десятичный номер порта. В противном случае вызывается функция getservbyport. Последний аргумент — это пустой указатель, если не задан флаг NI_DGRAM Если функция getservbyport выполняется неудачно, возвращается десятичный номер порта.
Резюме
Функция getaddri nfo — это полезная функция, позволяющая нам писать код, не зависящий от протокола. Но ее непосредственное выполнение включает несколько шагов, причем для различных сценариев нужно обрабатывать повторяющиеся особенности: просматривать все возвращаемые структуры, игнорировать возвращение ошибки из функции socket, устанавливать параметр сокета SO_REUSEADDR для серверов TCP и т. п. Мы упрощаем все эти подробности с помощью наших функций tcp^connect, tcp_l i sten, udp_cl i ent, udp_connect и udp_server. Мы показали использование этих функций при создании версий наших клиентов и серверов TCP II UDP, не зависящих от протокола.
Функции gethostbyname и gethostbyaddr также служат примером функций, которые обычно не допускают повторного вхождения. Эти функции совместно используют общую статическую структуру для записи результата, указатель на которую они обе возвращают. Мы столкнемся снова с проблемой повторного вхождения в главе 23 при описании потоков и обсудим пути ее решения. Мы рассмотрели предоставляемые некоторыми производителями версии _г этих двух функций, что является приемлемым решением, однако требует изменения всех приложений, вызывающих эти функции.
Упражнения
. В листинге 11.4 вызывающий процесс должен передать указатель на целое число, чтобы получить размер адреса протокола. Если вызывающий процесс пе делает этого (то есть передает пустой указатель в качестве последнего аргумента)» как может вызывающий процесс получить действительный размер адресов протокола?
Измените листинг 11.6, чтобы вызвать функцию getnamei nfo вместо функции sock_ntop. Какие флаги вы должны передать функции getnamei nfo?
В разделе 7.5 мы обсуждали завладение портом с помощью параметра сокета SO_REUSEADDR. Чтобы увидеть, как это происходит, создайте не зависящий от протокола сервер времени и даты UDP, показанный в листинге 11.1. Запустите один экземпляр сервера в одном окне, свяжите его с универсальным адресом и некоторым портом, который вы выберете. Запустите в другом окне клиент и проверьте, что этот сервер выполняет обработку клиента (отметьте вызов функции printf на узле сервера). Затем запустите другой экземпляр сервера в другом окне, и на этот раз свяжите его с одним из адресов направленной передачи узла и тем же портом, что и первый сервер. С какой проблемой вы сразу же столкнетесь? Устраните эту проблему и перезапустите второй сервер Запустите клиент, отправьте дейтаграмму л проверьте, что второй сервер захва-
тил порт первого сервера. Если возможно, запустите второй сервер снова с учетной записью, отличной от учетной записи первого сервера, чтобы проверить, происходит ли по-прежнему захват порта, поскольку некоторые производители не допускают второго связывания, если идентификатор пользователя отличен от идентификатора процесса, уже связанного с портом.
Обсуждая листинг 11.24, мы отметили, что адрес указателя aipnext — это аргумент функции ga_ai struct, требующий дополнительного оператора разыменования при ссылке на переменную. Почему бы нам не сделагь aipnext глобальной переменной, вместо того чтобы передавать ее адрес в качестве аргумента?
В нашем обсуждении домена Unix в конце раздела 11.5 мы отметили, что ни одно из имен служб IANA не начинается с косой черты. Содержит ли какое- нибудь из этих имен косую черту?
В конце раздела 2.10 мы показали два примера Telnet: сервер времени и даты и эхо-сервер. Зная, что клиент проходит через два этапа — функцию gethost- bynarue и функцию connect, определите, к каким этапам относятся строки вывода клиента.
Функции gethostbyaddr может потребоваться длительное время (до 80 секунд) на возвращение ошибки, если для IP-адреса не может быть найдено имя узла. Напишите новую функцию getnameinfo tirue。,которая получает дополнительный целочисленный аргумент, задающий максимальную длительность ожидания ответа в секундах. Если время таймера истекает и флаг NI_NAMEREQD не задан, вызовите функцию met_ntop и возвратите строку адреса.
ГЛАВА 12
Процессы-демоны и суперсервер inetd
Введение
Демон {daemon) — это процесс, выполняющийся в фоновом режиме и не зависящий от управления со всех терминалов. Системы Unix обычно имеют множество процессов (от 20 до 50), которые являются демонами, работают в фоновом режиме и выполняют различные административные задачи.
Независимость от всех терминалов нужна в том случае, если демон запускается с терминала (в противоположность запуску из сценария инициализации). Нам нужна возможность использовать этот терминал позже для других задач. Например, если мы запустим демон с терминала, а затем выйдем из терминала и в него войдет кто-то еще, то во время сеанса следующего пользователя не должны появляться какие-либо сообщения об ошибках, связанные с нашим демоном. Аналогично, сигналы, генерируемые клавишами терминалов (например, сигнал прерывания), не должны влиять на какие-либо демоны, которые мы раньше запустили с терминала. Хотя наш сервер несложно запустить в фоновом режиме (заканчивая командную строку оболочки символом амперсанд &), нам нужно, чтобы наша программа сама автоматически помещала себя в фоновый режим независимо от терминалов.
Существует несколько способов запустить демон:
Во время запуска системы многие демоны запускаются сценариями инициализации системы. Эти сценарии часто находятся в каталоге /etc или в каталоге, имя которого начинается с /etc/rc, но их расположение и содержание зависят от реализации. Такие демоны запускаются с правами привилегированного пользователя.
Некоторые сетевые серверы часто запускаются из этих сценариев: суперсервер inetd (следующий пункт, который мы рассмотрим), сервер Web и почтовый сервер (часто sendmail). Демон syslogd, обсуждаемый в разделе 12.2, обыч~ но запускается одним из этих сценариев.
Многие сетевые серверы запускаются суперсервером i netd, который мы опишем далее в этой главе. Сам i netd запускается в одном из сценариев на шаге 1. Суперсервер inetd прослушивает сетевые запросы (Telnet, FTP и т. д.), и когда приходит запрос, активизирует требуемый сервер (сервер Telnet, сервер FTP и т. д.).
За периодические процессы в системе отвечает демон cron, и программы, которые он активизирует, выполняются как демоны. Сам демон cron запускается на шаге 1 во время загрузки системы.
Если программа должна быть выполнена в определенный момент времени в будущем, применяется команда at. Демон cron обычно инициирует эти программы, когда приходит время их выполнения, поэтому они выполняются как демоны.
Демоны можно запускать с пользовательских терминалов, как в основном, так и в фоновом режимах. Это часто выполняется при тестировании демона или перезапуске демона, завершенного по некоей причине.
Поскольку у демона нет управляющего терминала, ему необходимы средства для вывода сообщений о некоторых событиях — это могут быть обычные информационные сообщения или экстренные сообщения об аварийных ситуациях, которые должен обрабатывать администратор. Использование функции syslog — это стандартный путь вывода таких сообщений. Эта функция посылает сообщения демону syslogd.
Демон syslogd
Системы Unix обычно запускают демон syslogd в одном из сценариев инициализации системы, и он функционирует, пока система работает. Реализации sysl ogd, происходящие от Беркли, выполняют при запуске следующие действия:
Считывается файл конфигурации, обычно /etc/syslog conf, в котором указано, что делать с каждым типом сообщений, получаемых демоном. Эти сообщения могут добавляться в некоторый файл (особой разновидностью такого файла является /dev/console, который записывает сообщение на консоль), передаваться определенному пользователю (если этот пользователь вошел в систему) или передаваться демону syslogd на другом узле.
Создается доменный сокет Unix и связывается с полным именем /var/run/log (в некоторых системах /dev/log).
Создается сокет UDP и связывается с портом 514 (служба syslog).
Открывается файл (устройство) /dev/kl од. Любые сообщения об ошибках внутри ядра появляются как входные данные на этом устройстве.
Демон syslogd, выполняется в бесконечном цикле, в котором вызывается функция sei ect, ожидающая, когда один из трех его дескрипторов (из пунктов 2, Зи 4) станет готов для чтения. Этот демон считывает сообщение и выполняет то, что предписывает делать с этим сообщением файл конфигурации. Если демон получает сигнал SIGHUP, он заново читает файл конфигурации.
Мы можем отправлять сообщения о событиях для записи в журнал (log messages) демону syslogd из наших демонов, создав дейтаграммный доменный сокет Unix н указывая при отправке полное имя, с которым связан демон, но более простым интерфейсом является функция syslog, которую мы описываем в следующем разделе. В качестве альтернативы мы можем создать сокет UDP н отправлять наши сообщения на адрес закольцовки и порт 514.
ПРИМЕЧАНИЕ
Более новые реализации отключают возможность создания сокета UDP, если она не задана администратором, поскольку если позволить кому угодно отправлять дейтаграммы UDP на этот порт (возможно, заполняя приемный буфер его сокета), это может привести к тому, что законные сообщения пе будут получены.
Между различными реализациями демона syslogd существуют отличия. Например, доменные сокеты Unix используются Беркли-реализациям и, а реализации System V использую г потоковый драйвер (streams log driver). Различные реализации, происходящие от Всрклм, используют для доменных сокетов Unix различные полные имена. Мы , можем игнорировать все эти подробности, если используем функцию syslog.
Функция syslog
Поскольку у демона нет управляющего терминала, он не может просто вызвать функцию fpri ntf для вывода в стандартный поток сообщений об ошибках (stderr). Обычная техника записи в журнал сообщений демона — это вызов функции sysl од. #include <syslод h>
void syslog(】nt priority const char *message. ).
ПРИМЕЧАНИЕ
* Хотя эта функция изначально разрабатывалась для BSD, в насюящсс время она предоставляется большинством производителей сис гем Unix. В Posix о функции syslog нс говорится шгчего, но ее требует Unix 98.
Аргумент priority — это комбинация аргументов level и faci lity, которые мы показываем в табл. 12.1 и 12.2. Аргумент message аналогичен строке формата функции printf с добавлением спецификации %т, которая заменяется сообщением об ошибке, соответствующем текущему значению переменной errno. Символ перевода строки может появиться в конце строки message, но он не является обязательным.
Сообщения для журнала имеют значение level (уровень) от 0 до 7, что мы показываем в табл. 12.1. Это упорядоченные значения. Если отправитель не задает значение 1 evel, используется значение по умолчанию LOG_NOTICE.
Таблица 12.1. Аргумент level журнальных сообщений
Level
Значение
Описание
LOG_EMERG
0
Система нс может функционировать, экстренная ситуация
(наивысший приоритет)
LOG_ALERT
1
Следует немедленно принять меры, срочная ситуация
LOG—CRIT
2:
Критическая ситуация
LOGJERR
3
Состояние ошибки
LOG_WARNING
4
Преду 11режде1 шсг
LOG_NOTICE
5
Необычное, хотя и нс ошибочное состояние (значение
аргумента level по умолчанию)
LOG」NFO
6-
'Информационное сообщение
LOG—DEBUG
7
Отладочные сообщения (низший приоритет)
Сообщения также содержат аргумент facility для идентификации типа процесса, посылающего сообщение. Мы показываем его различные значения в табл. 12.2. Если не задано значение аргумента faci 1 ity, используется его значение по умолчанию —LOG_USER.
Таблица 12.2. Аргумент facility журнальных сообщений
facility
Описание
LOG_AUTH
Сообщения n。безопаспости/авторизации
LOG—AUTHPRIV
CooCuieiniM no безопасности/авторизапии (частные)
LOG_CRON
Демон cron
LOG_DAEMON
Системные демоны
LOG_FTP
Демол FTP
LOG_KERN
Сообщения ядра
LOG_LOCALO
Локальное использование
LOG_LOCAL1
Локальное использование
LOG_LOCAL2
Локальное использование
LOG_LOCAL3
Локальное использование
LOG_LOCAL4
Локальное использование
LOG_LOCAL5
Локальное использование
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. "rerame(^s, Is) Im", filel f”e2)
Назначение аргументов faci lity и level в том, чтобы все сообщения, которые посылаются процессами определенного типа (то есть с одним значением аргумента facility), могли обрабатываться одинаково в файле /etc/syslog.conf или чтобы все сообщения одного уровня (с одинаковым значением аргумента level) обрабатывались одинаково. Например, файл конфигурации может содержать строки kern.*/dev/console
local7 debug/var/log/cisco log
для указания, что все сообщения ядра направляются на консоль, а сообщения относительно отладки со значением аргумента faci I ity, равным local7, добавляются в файл /var/log/cisco log.”
Когда приложение впервые вызывает функцию syslog, она создает дейтаграммный доменный сокет Unix и затем вызывает функцию connect для сокета с заранее известным полным именем, которое создано демоном syslogd (например, /var/run/log). Этот сокет остается открытым, пока процесс не завершится. Другим вариантом является вызов процессом (Ъунгший cnenlca и clnseloa.
include <syslog h>
void openlog (const char *2en" int options, int facility), void closelog(void).
Функция openl og может быть вызвана перед первым вызовом функции syslog, а функция cl osel og — когда приложение закончит отправлять сообщения в журнал.
Аргумент ident — это строка, которая будет добавлена в начало каждого журнального сообщения функцией syslog. Часто это имя программы.
Аргумент opti ons может принимать одно из перечисленных в табл. 12.3 значений. Обычно он формируется путем применения операции логического ИЛИ к константам из табл. 12.3.
Таблица 12.3. Аргумент options (параметр) для функции openlog
Параметр
Описание
LOG_CONS
Выводить журнал на консоль, если невозможно послать сообщение демону syslogd
LOG_NDELAY
Не откладывать создание сокета, открыть его сейчас
LOG_PERROR
Записывать сообщение в stderr, а также посылать его демону syslogd
LOG—PID
Включать идентификатор процесса (PID) в каждую запись журнала
Обычно доменный сокет Unix не создается при вызове функции openl og. Вместо этого сокет открывается при первом вызове функции syslog. Параметр LOG_ NDELAY указывает, что сокет должен создаваться при вызове функции openl од.
Аргумент faci lity функции openlog задает значение fash ty, используемое по умолчанию для любого последующего вызова функции syslog, при котором не задается аргумент faci lity. Некоторые демоны вызывают функцию openl og и задают значение аргумента facility (которое обычно не изменяется для данного демона) и затем в каждом вызове функции syslog задают только аргумент level (поскольку level может изменяться в зависимости от ошибки).
Сообщения для записи в журнал могут также генерироваться командой 1 ogger. Это может использоваться в сценариях интерпретатора команд, например для отправки сообщений демону sys 1 ogd.<
Функция daemonjnit
В листинге 12.11 показана функция, называемая daemon_init, которую мы мож^^4 вызвать (обычно с сервера), чтобы придать процессу свойства демона.
Листинг 12.1. Функция daemonjnit: придание процессу свойств демона //daemon_init с
#mcTude"unp.h"
#ijx】ude<syslog h>
#define MAXFD 64
extern int daemon_p「oc;/* определено в error c */
Все исходные коды программ, опубликованные в этой книг% вц нож可е и^йр< гр адресу ПЦр^/ www.piter.com/download.
void
daemon_wit(const char *pname, int facility)
{~
7nt1,
pid_tpid.
if ( (pid = ForkO) != 0)
exit(0). /* заверненме работы родителя */
/* первый дочерний процесс продолжает работу */
setsidO,/* становится главным в сеансе */
14 Signal(SIGHUP. SIGJGN).
15 if ( (pid = ForkO) != 0)
16 exit(0); /* первый дочерний процесс завершается *^
17/* вт叩о府 дочерний процесс продолжает работу */
18
daemon^proc =
=1.
/* для наших функций егг_ХХХ() */
19
chdir('7").
/* изменение рабочего каталога */
20
umask(0);
/* маске режима создания файла сбрасывается ^ 0 */
21 for (1 = 0. 1 < MAXFD, 1++)
22 close(i);
23 openlog(pname. LOG_PID. facility),
24 }』
Вызов функции fork
10-11 Сначала мы вызываем функцию fork, после чего родительский процесс завершается, а дочерний продолжается. Если процесс был запущен из интерпретатора команд в фоновом режиме, то когда родительский процесс завершается, оболочка считает, что команда выполнена. Это автоматически запускает дочерний процесс в фоновом режиме. Дочерний процесс наследует идентификатор группы процессов от родительского процесса, но получает свой собственный идентификатор процесса. Это гарантирует, что дочерний процесс не является главным в группе процессов, что требуется для следующего вызова функции sets id.
Вызов функции setsid
12-13 Функция setsid — это функция Posix.l, создающая новый сеанс. (В главе 9 [93] подробно рассказывается о взаимоотношениях процессов.) Процесс становится главным в новом сеансе, становится главным в новой группе процессов и не имеет управляющего терминала.
Игнорирование сигнала SIGHUP и новый вызов функции fork
14-16 Мы игнорируем сигнал SIGHUP и снова вызываем функцию fork. Когда эта функция завершается, родительский процесс на самом деле является первым дочерним процессом, и он завершается, оставляя выполняться второй дочерний процесс. Назначение второй функции fork — гарантировать, что демон не сможет автоматически получить управляющий терминал, если потом он откроет устрой-
ство терминала. В SVR4, когда главный процесс сеанса бЬз управляющего терминала открывает устройство терминала (которое в этот момент не является управляющим терминалом для другого сеанса), терминал становится управляющим терминалом главного процесса сеанса. Но вызывая второй раз функцию fork, мы гарантируем, что второй дочерний процесс больше не является главным в сеансе, поэтому он не может получить управляющий терминал. Сигнал SIGHUP следует игнорировать, поскольку, когда главный процесс сеанса завершает работу (первый дочерний процесс), всем процессам в сеансе (наш второй дочерний процесс) посылается сигнал SIGHUP.
Установка флага для функций ошибок
7-18 Мы присваиваем глобальной переменной daemon^proc ненулевое значение. Эта внешняя переменная задается нашими функциями err_ХХХ (см. раздел Г.4), и ее ненулевое значение сообщает этим функциям, что нужно вызвать функцию sysl од вместо функции fprintf (которая выводит сообщение об ошибке в стандартный поток сообщений об ошибках). Это спасает нас от необходимости проходить через весь наш код и вызывать одну из наших функций ошибок, если сервер не работает как демон (то есть когда мы проверяем сервер), а при работе в режиме демона заменять все вызовы на вызовы sysl од.
Изменение рабочего каталога и сброс всех битов в маске режима создания файла
9-20 Мы изменяем рабочий каталог на корневой каталог, хотя у некоторых демонов могут быть причины изменить рабочий каталог на какой-либо другой. Например, демон печати может изменить его на каталог, в котором накапливается содержимое заданий для принтера и происходит вся работа по выводу данных на печать. Если демоном сбрасывается дамп (файл core), on появляется в текущем рабочем каталоге. Другой причиной для изменения рабочего каталога является то. что демон мог быть запущен в любой файловой системе, и если он там останется, эту систему нельзя будет размонтировать. Маска режима создания файла устанавливается в 0, чтобы в том случае, если демон создает свои собственные файлы, биты разрешения в унаследованной маске создания режимного кода файла не повлияли на биты разрешения в новых файлах.
Закрытие всех открытых дескрипторов
1-22 Мы закрываем все открытые дескрипторы, которые наследуются от процесса, выполнившего демон (обычно оболочка). Проблема состоит в определении наибольшего используемого дескриптора: в Unix нет ни одной функции, предоставляющей это значение. Есть способы определения максимального числа дескрипторов, которое может открыть процесс, но даже это достаточно сложно [93, с. 43], поскольку предел может быть бесконечным Наше решение — закрыть первые 64 дескриптора, даже если большинство из них, возможно, не было открыто.
Некоторые демоны открывают /dev/null для чтения и записи и подключают к нему дескрипторы стандартных потоков ввода, вывода и сообщений об ошибках. Это гарантирует, что наиболее типичные дескрипторы открыты и операция чтения из любого из них возвращает 0 (конец файла), а ядро игнорирует все, что записано в любой из этих трех дескрипторов. Причина, по которой требуется от-
крыть эти дескрипторы, заключается в том, что любая библиотечная функция, вызываемая демоном и считающая, что она может читать из стандартного потока ввода или записывать либо в стандартный поток вывода, либо в стандартный поток сообщений об ошибках, не завершится с ошибкой. Некоторые демоны используют альтернативный вариант — открывают файл журнала (log file), в который они будут записывать сообщения во время выполнения, и подключают к нему стандартные потоки ввода, вывода и сообщений об ошибках.
Использование демона syslogd для вывода сообщений об ошибках
23 Вызывается функция openlog. Первый ее аргумент берется из вызывающего процесса и обычно является именем программы (например, argv[0]). Мы указываем, что идентификатор процесса должен добавляться к каждому сообщению. Аргумент facility также задается вызывающим процессом, и его значением может быть константа из табл 12 2 либо, если приемлемо значение по умолчанию LOG_USER, нулевое значение.
Отметим, что поскольку демон выполняется без управляющего терминала, он никогда не должен получать сигнал SIGHUP от ядра. Следовательно, многие демоны используют этот сигнал в качестве уведомления от админист ратора, что файл конфигурации демона изменился и демон должен еще раз считать файл Два других сигнала, которые демон никогда пе должен получать, — это сигналы SIGINT и IGWINCH, и они также могут использоваться для уведомления демона о некоторых изменениях.
Пример: сервер времени и даты
в качестве демона
В листинге 12.2 представлено изменение нашего сервера времени и даты, не зависящего от протокола. В отличие от сервера, показанного в листинге 11.6, в нем вызывается функция daemon_imt, чтобы этот сервер мог выполняться в качестве демона.
Листинг 12.2. Не зависящий от протокола сервер времени и даты, работающий в качестве демона
//1netd/daytimetcpsrv2 с
include "unp h*
#include 〈time h>
int
main(int argc char **argv)
{
int1istenfd connfd
socklen_t addrlen len
struct sockaddr *cliaddr
char buff[MAXLINE]
time_t ticks
aaemon_imt(argv[OJ 0)
if (argc == 2)
listenfd 工 Tcp_hsten(NULL. argv[l] &addrlen).
Y>11f\f}f\ П Ъ/ f»4H£f ^^
Листинг 12.2 (продолжение)
else if (агде == 3)
15 listenfd = Tcpjisten(argv[l]. argv[2]. &addrlen).
16 else
17err_quit("usage daytimetcpsrv2 [ <host> ] <service or port>'*):
18 cliaddr = Malloc(addrlen).
19 for (..) {
20len = addrlen.
21connfd = Accept(listenfd. cliaddr. &len).
22err_msg("connect?or? from £s". Sock_ntop(cl】addr.len)).
23ticks = time(NULL),
24snprmtf(buff. sizeof(buff). n% 24s\erVen0.ctime(&ticks)):
25Write(connfd. buff, strlen(buff))»
26Close(connfd).
27}
28 }
Изменений всего два: мы вызываем нашу функцию daemon__imt, как только программа запускается, а затем вызываем нашу функцию erjmsg вместо pnntf, чтобы вывести IP-адрес и порт клиента. На самом деле, если мы хотим, чтобы наши программы могли выполняться как демоны, мы должны исключить вызов функций printf и fprintf и вместо них использовать нашу функцию err_msg.
Если мы запустим эту программу на нашем узле sol ап$и затем проверим файл /var/sdm/niessages (куда мы отправляем все сообщения LOG_USER) после соединения с нашим узлом bsdi, мы получим:
Jun 4 15 15 33 solans kohald com daytimetcpsrv2[ 14882] connection from
ffff 206 62 226 35 3356
(Мы разбили одну длинную строку на две строки.) Дата, время и FQDN автоматически ставятся в начале сообщения демоном syslogd.
Демон inetd
В типичной системе Unix может существовать много серверов, ожидающих запроса клиента. Примерами являются FTP, Telnet, Rlogin, TFTP и т. д. В системах, предшествующих 4.3BSD, каждая из этих служб имела связанный с ней процесс. Этот процесс запускался во время загрузки из файла /etc/rc, и каждый процесс выполнял практически идентичные задачи запуска: создание сокета, связывание при помощи функции bind заранее известного порта с сокетом, ожидание соединения (TCP) или получения дейтаграммы (UDP) и последующее выполнение функции fork. Дочерний процесс выполнял обслуживание клиента, а родительский процесс ждал, когда поступит следующий запрос клиента. Эта модель связана с двумя проблемами.
Все эти демоны содержали практически идентичный код запуска, направленный сначала на создание сокета, а затем на превращение процесса в процесс демона (аналогично нашей функции daemon_init).
Каждый демон занимал некоторое место в таблице процессов, но при этом большую часть времени находился в состоянии ожидания.
Реализация 4.3BSD упростила ситуацию, предоставив суперсервер (superservef) Интернета — демон inetd. Этот демон может применяться серверами, использующими TCP или UDP, и не поддерживает других протоколов, таких как доменные сокеты Unix. Демон inetd решает две вышеупомянутые проблемы.
Он упрощает написание процессов демонов, поскольку обрабатывает большинство подробностей запуска. Таким образом устраняется необходимость вызова нашей функции daemon^imt для каждого сервера.
Этот демон позволяет одиночному процессу (inetd) ждать входящие клиентские запросы ко множеству служб (вместо одного процесса для каждой службы). Это сокращает общее число процессов в системе.
Процесс inetd сам становится демоном, используя технологии, которые мы изложили при описании функции daemon一i mt. Затем он считывает и обрабатывает файл конфигурации, обычно файл /ect/inetd conf. Этот файл задает, какие службы должен обрабатывать суперсервер, а также что нужно делать, когда приходит запрос к одной из этих служб. Каждая строка содержит поля, показанные в табл. 12.4. Вот несколько строк в качестве примера:
ftp telnet login tftpftp telnet login tftpstream stream stream dgramstream stream stream dgramtcp nowait tcp nowait tcp nowait udp waittcp nowait tcp nowait tcp nowait udp waitroot /usr/bin/ftpd root /usr/bm/telnetd root /usr/bin/rlogind nobody /usr/bin/tftpdroot /usr/bin/ftpd root /usr/bm/telnetd root /usr/bin/rlogind nobody /usr/bin/tftpdftpd -1 telnetd rlogind -s tftpd -s /tftpbootftpd -1 telnetd rlogind -s tftpd -s /tftpboot
ftp telnet login tftp
ftp telnet login tftp
stream stream stream dgram
stream stream stream dgram
tcp nowait tcp nowait tcp nowait udp wait
tcp nowait tcp nowait tcp nowait udp wait
root /usr/bin/ftpd root /usr/bm/telnetd root /usr/bin/rlogind nobody /usr/bin/tftpd
root /usr/bin/ftpd root /usr/bm/telnetd root /usr/bin/rlogind nobody /usr/bin/tftpd
ftpd -1 telnetd rlogind -s tftpd -s /tftpboot
ftpd -1 telnetd rlogind -s 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; либо tcp, либо 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.
Рис. 12.1. Шаги, выполняемые демоном metd
При запус/се демон читает файл /etc/inetd conf и создает сокет соответствующего типа (потоковый или дейтаграммный сокет) для всех служб, заданных в файле Максимальное число серверов, которое может обрабагывать демон ina+d члвигит от максимального числа дескрипторов, которое он может со-
здать Каждый новый сокет добавляется к набору дескрипторов, который будет использован для вызова функции select
Для сокета вызывается функция bind, задающая заранее известный порт для сервера и универсальный IP-адрес Этот номер порта TCP или UDP получается при вызове функции getservbyname с полями service-name и protocol из файла конфигурации в качестве аргументов.
Для сокетов TCP вызывается функция listen, так что принимаются входящие запросы на соединение Этот шаг не выполняется для дейтаграммных сокетов.
После того как созданы все сокеты, вызывается функция sei ect, ожидающая, когда какой-либо из сокетов станет готов для чтения. Вспомните из раздела 6 3, что прослушиваемый сокет TCP становится готов для чтения, когда новое соединение готово быть принятым с помощью функции accept, а сокет UDP становится готов для чтения, когда приходит дейтаграмма. Демон i netd большую часть времени блокирован в этом вызове функции sei ect, ожидая, когда сокет станет готов для чтения
Когда функция sei ect определяет, что сокет готов для чтения, то если этот сокет является сокетом TCP, вызываегся функция accept для принятия нового соединения
Демон inetd запускает функцию fork, и дочерний процесс обрабатывает запрос клиента. Это аналогично стандартному параллельному серверу (см раздел 4.8).
Дочерний процесс закрывает все дескрипторы, кроме дескриптора, который он обрабатывает новый присоединенный сокет, возвращаемый функцией accept для сервера TCP, или исходный сокет UDP.
Дочерний процесс трижды вызывает функцию dup2, подключая сокет к дескрипторам 0, 1 и 2 (стандартные потоки ввода, вывода и сообщений об ошибках) Исходный дескриптор сокета затем закрывается. При этом в дочернем процессе открытыми остаются только дескрипторы 0, 1 и 2. Если дочерний процесс читает из стандартного потока ввода, он читает из сокета, и все, что он записывает в стандартный поток вывода или стандартный поток сообщений об ошибках, записывается в сокет
Дочерний процесс вызывает функцию getpwnam, чтобы получить значение поля login-паше, заданного в файле конфигурации. Если это не поле root, дочерний процесс становится указанным пользователем при помощи функций setgid и setuid. (Поскольку процесс inetd выполняется с идентификатором пользователя, равным 0, дочерний процесс наследует этот идентификатор пользователя при выполнении функции fork, поэтому он имеет возможность стать любым пользователем по своему выбору.)
Теперь дочерний процесс вызывает функцию ехес, чтобы выполнить соответ- ствующую программу сервера (поле server program) для обработки запроса, передавая аргументы, указанные в файле конфигурации.
У. Если сокет является потоковым сокетом, родительский процесс должен закрыть присоединенный сокет (как наш стандартный параллельный сервер) Родительский процесс снова вызывает функцию select, ожидая, когда следующий сокет станет готов для чтения
Чтобы рассмотреть более подробно, что происходит с дескрипторами, на рис. 12.2 показаны дескрипторы в демоне i netd в момент прихода нового запроса на соединение от клиента FTP.
inetd仆 Порт TCP номер 21仆 Порт TCP номер 23仆 Порт UDP номер 69仆 Порт TCP номер 21inetd仆 Порт TCP номер 21仆 Порт TCP номер 23仆 Порт UDP номер 69仆 Порт TCP номер 21Прослушиваемые сокеты TCPи сокеты UDP, ожидающиетого момента, когда онистанут готовы для чтения
inetd
仆 Порт TCP номер 21
仆 Порт TCP номер 23
仆 Порт UDP номер 69
仆 Порт TCP номер 21
inetd
仆 Порт TCP номер 21
仆 Порт TCP номер 23
仆 Порт UDP номер 69
仆 Порт TCP номер 21
} Присоединенный сокет TCP, возвращенный функцией accept
Рис. 12.2. Дескрипторы демона inetd в тот момент, когда приходит запрос на порт 21 TCP
Запрос на соединение направляется на порт 21 TCP, но новый присоединенный сокет создается функцией accept.
inetd (дочерний процесс)inetd (дочерний процесс)Рис. 12.3. Дескрипторы демона inetd в дочернем процессеРис. 12.3. Дескрипторы демона inetd в дочернем процессеНа рис. 12.3 показаны дескрипторы в дочернем процессе после вызова функции fork, после того как дочерний процесс закрывает все остальные дескрипторы, кроме дескрипторов присоединенного сокета
inetd (дочерний процесс)
inetd (дочерний процесс)
Рис. 12.3. Дескрипторы демона inetd в дочернем процессе
Рис. 12.3. Дескрипторы демона inetd в дочернем процессе
Следующий шаг для дочернего процесса — подключение присоединенного сокета к дескрипторам 0,1 и 2 и последующее закрытие присоединенного сокета. При этом мы получаем дескрипторы, изображенные на рис. 12.4.
Затем дочерний процесс вызывает функцию ехес, и, как сказано в разделе 4 7, во время выполнения функции ехес все дескрипторы обычно остаются открытыми, поэтому реальный сервер, на котором выполняется функция ехес, использует любой из дескрипторов 0,1 и 2 для взаимодействия с клиентом. Эти дескрипторы должны быть единственными открытыми на стороне сервера дескрипторами.
Описанный нами сценарий относится к ситуации, при которой файл конфигурации задает в поле wait-fl ад значение nowait для сервера. Это типично для всех служб TCP и означает, что демону inetd не нужно ждать завершения его дочернего процесса, перед тем как он примет другое соединение для данной службы. Если приходит другой запрос на соединение для той же службы, он возвращается родительскому процессу, как только тот снова вызовет функцию select.
Рис. 12.4. Дескрипторы демона inetd после выполнения функции dup2Рис. 12.4. Дескрипторы демона inetd после выполнения функции dup2ВыполнениеВыполнениесерверной программусерверной программу
Рис. 12.4. Дескрипторы демона inetd после выполнения функции dup2
Рис. 12.4. Дескрипторы демона inetd после выполнения функции dup2
Выполнение
Выполнение
серверной программу
серверной программу
Шаги 4,5 и 6, перечисленные выше, выполняются снова, и новый запрос обрабатывается другим дочерним процессом.
Задание флага wait для службы UDP изменяет шаги, выполняемые родительским процессом. Флаг указывает на то, что демон inetd должен ждать завершения своего дочернего процесса, прежде чем снова вызвать функцию select для определения готовности этого сокета UDP для чтения. Происходят следующие изменения:
После выполнения функции fork в родительском процессе сохраняется идентификатор дочернего процесса. Это дает возможность родительскому процессу узнать, когда завершается определенный дочерний процесс, анализируя значение, возвращаемое функцией waitpid.
Родительский процесс отключает способность сокета выполнять последующие функции sei ect, сбрасывая соответствующий бит в наборе дескрипторов с помощью макроса FD_CLR. Это значит, что дочерний процесс завладевает сокетом до своего завершения.
Когда завершается дочерний процесс, родительский процесс уведомляется об этом с помощью сигнала SIGCHLD, и обработчик сигналов родительского процесса получает идентификатор завершающегося дочернего процесса. Он снова включает функцию select для соответствующего сокета, устанавливая бит для этого сокета в своем наборе дескрипторов.
Причина, по которой дейтаграммный сервер должен завладевать сокетом, пока он не завершит работу, лишая тем самым демона i netd возможности выполнять функцию select на этом сокете для проверки готовности его для чтения (в ожидании другой дейтаграммы клиента), в том, что для сервера дейтаграмм существует только один сокет, в отличие от сервера TCP, у которого имеется прослушиваемый сокет и по одному присоединенному сокету для каждого клиента Если демон 1 netd не отключил чтение на сокете дейтаграмм и, допустим, родительский процесс (inetd) завершил выполнение перед дочерним, дейтаграмма от клиента все еще будет находиться в приемном буфере сокета Это приводит к тому, что функция select снова сообщает, что сокет готов для чтения, и демон inetd снова выполняет функцию fork, порождая другой (ненужный) дочерний процесс. Демон 1 netd должен игнорировать дейтаграммный сокет до тех пор, пока он не узнает, что дочерний процесс прочитал дейтаграмму из приемного буфера сокета. Демон 1 netd узнает, что дочерний процесс закончил работу с сокетом, путем по-
лучения сигнала 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 и exec. Серверы Web используют множество технологий для минимизации накладных расходов при порождении процессов для обслуживания клиентов, как мы покажем в главе 27.
Функция daemonjnetd
В листинге 12.3 показана функция daemon_i netd, которую мы можем вызвать с сервера, запущенного демоном inetd.「
Листинг 12.3. Функция daemonjnetd для придания свойств демона процессу, запущенному демоном inetd
//daemon_metd с
#incTude"unp h"
#include<syslog h>
extern int daemon_proc: /* определено в error.с*/
void
daemon_inetd(const char *pname. int facility)
{-
daemon_proc = 1, /* для наших функций err XXXO */
openlog(pname. LOG_PID, facility).
}一
Эта функция тривиальна по сравнению с daemon_mit, потому что все шаги выполняются демоном inetd при запуске. Все, что мы делаем — устанавливаем флаг daemon_proc для наших функций ошибок (см. табл. Г.1) и вызываем функцию open]од с теми же аргументами, что и при вызове функции daemon 】 mt, представленной в листинге 12.1.
Пример: сервер времени и даты, активизированный демоном inetd
Листинг 12.4 представляет собой модификацию нашего сервера времени и даты, показанного в листинге 12.2, который может быть активизирован демоном inetd.
Листинг 12.4. He зависящий от протокола сервер времени и даты, который может быть активизирован демоном inetd
//1netd/daytimetcpsrv3 c
#mclude"unp h“
include〈time h>
mt
maindnt argc. char **argv)
{
socklen_t len.
struct sockaddr *cliaddr.
char buff[MAXLINE],
time_t ticks:•
daemon_inetd(argv[0], 0).
cliaddr = Ma11oc(MAXSOCKADDR).
len = MAXSOCKADDR.
Getpeername(0, cliaddr. &1en).
err_msg("connection from 犯".Sock_ntop(cliaddr. len)).
ticks = time(NULL).
snpnntf(buff. sizeof(buff), "% 24s\e「\en" ctime(&ticks)):
Wnte(0. buff, strlen(buff)),
Close(O),/* закрываем соединение TCP */
exit(0),
}
В программе сделано два важных изменения. Во-первых, исчез весь код создания сокета: вызовы функций tcp_l i sten и accept. Эти шаги выполняются демоном inetd, и мы ссылаемся на соединение TCP, используя нулевой дескриптор (стандартный поток ввода). Во-вторых, исчез бесконечный цикл for, поскольку сервер активизируется по одному разу для каждого клиентского соединения. После предоставления сервиса клиенту сервер завершает свою работу.
Вызов функции getpeername
11-14 Поскольку мы не вызываем функцию tcp_l т sten, мы не знаем размера структуры адреса сокета, которую она возвращает, а поскольку мы не вызываем функцию accept, мы не знаем и адреса протокола клиента. Следовательно, мы выделяем буфер для структуры адреса сокета, используя нашу константу MAXSOCKADDR и вызываем функцию getpeername с нулевым дескриптором в качестве первого аргумента.
Чтобы выполнить этот пример в нашей системе BSD/OS, сначала мы присваиваем службе имя и порт, добавляя следующую строку в /etc/services: mydaytime9999/tcp
Затем мы добавляем строку в /etc/inetd conf:
mydaytime stream tcp nowait rstevens
/usr/home/rstevens/daytimetcpsrv3 daytimetcpsrv3
(Мы разбили длинную строку на более короткие.) Мы помещаем выполняемый код в заданный файл и отправляем демону inetd сигнал SIGHUP, сообщающий ему, что нужно заново считать файл конфигурации. Следующий шаг — выполнить
программу netstat, чтобы проверить, что на порте TCP 9999 создан прослушиваемый сокет*
bsdi % netstat -па | grep 9999
tcp 0 0* 9999* *LISTEN
Затем мы запускаем сервер с другого узла:
alpha % tel net 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/log/messages (в который, как указано в нашем файле /etc/sysl og conf, должны направляться наши сообщения с аргументом fac】lity=LOG_USER) содержит запись:
Jun 5 11 13 50 bsdi ddytimetcpsrv3[28724] connection from 206 62 226 42 1042
Резюме
Демоны — это процессы, выполняемые в фоновом режиме независимо от управления с терминалов. Многие сетевые серверы работают как демоны. Все выходные данные демона обычно отправляются демону syslogd при помощи вызова функции syslog. Администратор полностью контролирует все, что происходит с этими сообщениями, основываясь на том, какой демон отправил данное сообщение, и насколько оно серьезно.
Чтобы запустить произвольную программу и выполнять ее в качестве демона, требуется пройти несколько шагов: вызвать функцию fork для запуска в фоновом режиме, вызвать функцию sets id для того, чтобы создать новый сеанс Posix 1 и стать главным процессом сеанса, снова вызвать функцию fork, чтобы избежать перехода в режим управления с терминала, изменить рабочий каталог и маску режима создания файла и закрыть все ненужные файлы. Наша функция daemon_ imt выполняет все эти шаги.
Многие серверы Unix запускаются демоном 】netd. Он осуществляет все необходимые шаги по превращению процесса в демон, и при запуске действительного сервера открывается сокет для стандартных потоков ввода, вывода и сообщений об ошибках. Это позволяет нам опустить вызовы функций socket, bind, listen и accept, поскольку все эти шаги выполняются демоном inetd.
Упражнения
1 Что произойдет в листинге 12 2, если мы отложим вызов функции daemon.】 mt до завершения обработки аргументов командной строки, и функция err_qu7t будет вызвана до того, как программа станет демоном?
Как вы думаете, какие из 10 серверов, перечисленных в табл. 2.1 (учитываются версии TCP и UDP для каждой из пяти служб, управляемых демоном inetd), реализуются с помощью вызова функции fork, а какие не требуют этой функции?
Что произойдет, если мы создадим сокет UDP, свяжем порт 7 с сокетом (стандартный эхо-сервер в табл 2 1) и отправим дейтаграмму UDP серверу chargen?
В руководстве Solans 2.x для демона inetd описывается флаг -t, заставляющий демон inetd вызывать функцию sysl од (с аргументами faci 1 ity=LOG_DAEMON и level =LOG_NOTICE) для протоколирования клиентского IP-адреса и порта любой службы TCP, которые обрабатывает демон i netd Как демон i netd получает эту информацию?
В этом же руководстве сказано, что демон inetd не может выполнить это для сокета UDP. Почему?
Есть ли способ обойти эти ограничения для служб UDP?
ГЛАВА 13
Дополнительные функции ввода-вывода
Введение
Эта глава охватывает разнообразные функции и технологии, которые мы помещаем в общую категорию «расширенного ввода-вывода». Сначала мы описываем установку тайм-аута для операции ввода-вывода, которую можно выполнить тремя различными способами.
Затем мы рассматриваем три варианта функций read н write: recv и send, допускающие четвертый аргумент, содержащий флаги, передаваемые от процесса к ядру; readv и wntev, позволяющие нам задавать массив буферов для ввода или вывода; recvmsg и sendmsg, объединяющие все свойства других функций ввода- вывода н обладающие новой возможностью получения и отправки вспомогательных данных.
Мы также рассказываем о том, как определить, сколько данных находится в приемном буфере сокета и как использовать с сокетами стандартную библиотеку ввода-вывода С. Эту главу мы заканчиваем кратким обзором протокола Т/ТСР — TCP для транзакций, позволяющего избежать трехэтапного рукопожатия.
Тайм-ауты сокета
Существует три способа установки тайм-аута для операции ввода-вывода через сокет.
Вызов функции alarm, которая генерирует сигнал SIGALARM, ксада истекает заданное время. Это подразумевает обработку сигналов, которая может варьироваться от одной реализации к другой. К тому же такой подход может стать помехой другим существующим вызовам функции alarm в данном процессе.
Блокирование при ожидании ввода-вывода в функции sei ect, имеющей встроенное ограничение времени, вместо блокирования в вызове функции read или write.
Использование более новых параметров сокета — SO_RCVTIMEO и SO__SNDTIMEO. Проблема при использовании этого подхода заключается в том, что не все реализации поддерживают эти параметры сокета.
Все три технологии работают с функциями ввода и вывода (такими как read, write и их вариации, например recvfrom и sendto), но нам также хотелось бы иметь
технологию, работающую с функцией connect, поскольку процесс соединения TCP может занять длительное время (обычно 75 секунд). Функцию select можно использовать для установки тайм-аута функции connect, только когда сокет находится в неблокируемом режиме (который мы рассматриваем в разделе 15.3), а параметры сокетов, устанавливающие тайм-аут, не работают с функцией connect. Мы также должны отметить, что первые две технологии работают с любым дескриптором, в то время как третья технология только с дескрипторами сокетов.
Теперь мы представим примеры применения всех трех технологий.
Тайм-аут для функции connect (сигнал SIGALRM)
В листинге 13.11 показана наша функция connect」】 шео, вызывающая функцию connect с ограничением по времени, заданным вызывающим процессом. Первые три аргумента — это аргументы, которых требует функция connect, а четвертый аргумент — это длительность ожидания в секундах.
Листинг 13.1. Функция connect с тайм-аутом //11Ь/connect timeo с
12345678910111213141516171234567891011121314151617include "unp h"static void connect^ 1 a rm (i nt).mt、connect_timeo(mt sockfd. const SA *saptr. socklen』salen. int nseC)include "unp h"static void connect^ 1 a rm (i nt).mt、connect_timeo(mt sockfd. const SA *saptr. socklen』salen. int nseC)Sigfunc *sigfunc.mt n.sigfunc = Signal(SIGALRMif (alarm(nsec) 「 0)e rr_msg("connect_timeo if ( fn = connect(sockfd,close(sockfd), if (errno == EINTR)errno = ETIMEDOUT.Sigfunc *sigfunc.mt n.sigfunc = Signal(SIGALRMif (alarm(nsec) 「 0)e rr_msg("connect_timeo if ( fn = connect(sockfd,close(sockfd), if (errno == EINTR)errno = ETIMEDOUT.connect alarm)connect alarm)alarm was already set"); saptr, salen)) < 0) {alarm was already set"); saptr, salen)) < 0) {18191819alarm(O),Signal(SIGALRM. sigfunc)alarm(O),Signal(SIGALRM. sigfunc)return (n)return (n)/* отключение alarm *//* восстанавливаем прежний обработчик 七йгйа眦“/* отключение alarm *//* восстанавливаем прежний обработчик 七йгйа眦“
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include "unp h"
static void connect^ 1 a rm (i nt).
mt、
connect_timeo(mt sockfd. const SA *saptr. socklen』salen. int nseC)
include "unp h"
static void connect^ 1 a rm (i nt).
mt、
connect_timeo(mt sockfd. const SA *saptr. socklen』salen. int nseC)
Sigfunc *sigfunc.
mt n.
sigfunc = Signal(SIGALRM
if (alarm(nsec) 「 0)
e rr_msg("connect_timeo if ( fn = connect(sockfd,
close(sockfd), if (errno == EINTR)
errno = ETIMEDOUT.
Sigfunc *sigfunc.
mt n.
sigfunc = Signal(SIGALRM
if (alarm(nsec) 「 0)
e rr_msg("connect_timeo if ( fn = connect(sockfd,
close(sockfd), if (errno == EINTR)
errno = ETIMEDOUT.
connect alarm)
connect alarm)
alarm was already set"); saptr, salen)) < 0) {
alarm was already set"); saptr, salen)) < 0) {
18
19
18
19
alarm(O),
Signal(SIGALRM. sigfunc)
alarm(O),
Signal(SIGALRM. sigfunc)
return (n)
return (n)
/* отключение alarm */
/* восстанавливаем прежний обработчик 七йгйа眦“
/* отключение alarm */
/* восстанавливаем прежний обработчик 七йгйа眦“
20 static void
21 connect_alarm(int sign。)
22 {'
/* просто прерываем connect() *//* просто прерываем connect() */23 return.
/* просто прерываем connect() */
/* просто прерываем connect() */
24 }
Установка обработчика сигналов
Для SIGALRM устанавливается обработчик сигнала. Текущий обработчик сигнала (если таковой имеется) сохраняется, и таким образом мы можем восстановить его в конце функции.
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http.// www piter com/download
Установка таймера
9-10 Таймер для процесса устанавливается на время (число секунд), заданное вызывающим процессом. Возвращаемое значение функции al arm — это число секунд, остающихся в таймере для процесса (если он уже установлен для процесса) в настоящий момент или 0 (если таймер не был установлен прежде). В первом случае мы выводим сообщение с предупреждением, поскольку мы стираем предыдущую установку таймера (см. упражнение 13.2).
Вызов функции connect
11-15 Вызывается функция connect, и если функция прерывается (EINTR), мы присваиваем переменной errno значение ETIMEDOUT. Сокет закрывается, чтобы не допустить продолжения трехэтапного рукопожатия.
Выключение таймера и восстановление предыдущего обработчика сигнала
16-18 Таймер при обнулении выключается, и восстанавливается предыдущий обработчик сигналов (если таковой имеется).
Обработка сигнала SIGALRM
20-24 Обработчик сигнала просто возвращает управление. Предполагается, что это прервет ожидание функции connect, заставив ее возвратить ошибку EINTR. Вспомните нашу функцию signal (см. листинг 5.5), которая не устанавливает флага S/^RESTART, когда перехватываемый сигнал — это сигнал SIGALRM.
Одним из важных моментов в этом примере является то, что мы всегда можем сократить период ожидания для функции connect, используя эту технологию, но мы не можем увеличить период, заданный для ядра. В Беркли-ядре тайм-аут для функции connect обычно равен 75 секундам. Мы можем задать меньшее значение для нашей функции, допустим 10, но если мы задаем большее значение, скажем 80, тайм-аут самой функции connect все равно составит 75 секунд.
Другой важный момент в данном примере — то, что мы используем возможность прерывания системного вызова (connect) для того, чтобы возвратить управление, прежде чем истечет время ожидания ядра. Такой подход допустим, когда мы выполняем системный вызов и можем обработать возвращение ошибки EINTR. Но в разделе 26.6 мы встретимся с библиотечной функцией, выполняющей системный вызов, которая сама выполняет заново системный вызов при возвращении ошибки EINTR. Мы можем продолжать работать с сигналом SIGALRM и в этом случае, но в листинге 26.6 мы увидим, что нам придется воспользоваться функциями sigsetjmp и siglongj叩,поскольку библиотечная функция игнорирует ошибку EINTR.
Тайм-аут для функции recvfrom (сигнал SIGALRM)
В листинге 13.2 показана новая версия функции dg_cl i, приведенной в листинге 8.4, в которую добавлен вызов функции al arm для прерывания функции recvfrom при отсутствии ответа в течение 5 секунд.
Листинг 13.2- Функция dg_ch, в которой при установке тайм-аута для функции recvfrom используется функция alarm
//advio/daclitimeo3 с
static void sig_alrm(mt).
void
dg_cl】(FILE *fp, int sockfd, const SA *pservaddr. socklen_t servlen)
{一_
mt n.
char sendline[MAXLINE],recvline[MAXLINE + 1]:
Signal (SIGALRM. signal rm).
while (FgetsCsendline. MAXLINE, fp) •= NULL) {
Sendto(sockfd. sendl7ne. strlen(sendline), 0. pservaddr, servlen).
alarm(5).
if ( (n = recvfrom(sockfd. recvlme, MAXLINE 0. NULL. NULL)) < 0) {
if (errno в EINTR)
fpnntf(stderr, "socket timeout\en")
else
err_sys("recvfrom errorH),
} else {
alarm(O)
recvline[n] = 0./* завершающий нуль */
Fputs(recvlme. stdout).
}
}
}
static void
signalrm(int signo)
{-
return./* просто прерываем recvfromO */
}
Обработка тайм-аута из функции recvfrom
8-22 Мы устанавливаем обработчик для сигнала SIGALRM и затем вызываем функцию alarm для 5-секундного тайм-аута при каждом вызове функции recvfrom. Если функция recvfrom прерывается нашим обработчиком сигнала, мы выводим сообщение об ошибке и продолжаем работу. Если получена строка от сервера, мы отключаем функцию al arm и выводим ответ.
Обработчик сигнала SIGALRM
24-28 Наш обработчик сигналов возвращает управление, прерывая блокированную функцию recvfrom.
Этот пример работает корректно, потому что каждый раз, когда мы устанавливаем функцию alarm, мы читаем только один ответ. В разделе 18.4 мы используем ту же технологию, но поскольку мы читаем множество ответов для данной функции alarm, возникает ситуация гонок, которую мы должны обработать.
Тайм-аут для функции recvfrom (функция select)
Мы демонстрируем вторую технологию для установки тайм-аута (использование функции sei ect) в листинге 13.3. Здесь показана наша функция readab] e_timeo, которая ждет, когда дескриптор станет готов для чтения, но не более заданного
числа секунд.
Листинг 13.3. Функция readable_timeo: ожидание, когда дескриптор станет готов для чтения
//lib/readable_tioieo с
intint2 3 4 5 62 3 4 5 6include "unp h"
int
int
2 3 4 5 6
2 3 4 5 6
readable_timeo(int fd. int sec)
{ 一
fd_set rset.
struct timeval tv:
FD_ZERO(&rset).
FD[SET(fd. &rset).
tv tv_sec = sec.,r,
tv tv_usec = 0:
return (select(fd + 1. &rset. NULL. NULL. &tv)).
/* > если дескриптор готов для чтения */
}
Подготовка аргументов для функции select
7-10 В наборе дескрипторов для чтения включается бит, соответствующий данному дескриптору. В структуре timeval устанавливается время (число секунд), в течение которого вызывающий процесс готов ждать.
Блокирование в функции select
11-12 Функция select ждет, когда дескриптор станет готов для чтения или истечет заданное время ожидания. Возвращаемое значение этой функции — это возвращаемое значение функции sei ect: -1 в случае ошибки; 0, если истекло время ожидания, и положительное значение, задающее число готовых дескрипторов, если таковые появились.
Эта функция не выполняет операции чтения — она просто ждет, когда дескриптор будет готов к чтению. Следовательно, эту функцию можно использовать с любым типом сокета — TCP или UDP.
Создание аналогичной функции, называемой wntable_timeo, тривиально. Эта функция ждет, когда дескриптор будет готов для записи.
Мы используем эту функцию в листинге 13.4, где показана еще одна версия нашей функции dg_cli, приведенной в листинге 8.4. Эта новая версия вызывает функцию recvfrom, только когда наша функция readable_timeo возвращает положительное значение.
Мы не вызываем функцию recvfrom, пока функция readable_t^o не сообщит нам, что дескриптор готов для чтения. Тем самым мы гарантируем, что функция recvfrom не заблокируется.
Листинг 13.4. Функция dg_cli, вызывающая функцию readable_timeo для установки тайм-аута
//advio/dgclitimeol с
#include '*unp h"
void
dg_cli(FILE *fp. int sockfd. const SA *pservaddr. socklen t servlen)
{у,疝
intn,
charsendline[MAXLINE] recvline[MAXLINE + 1]:
while (Fgets(sendline. MAXLINE, fp) 「 NULL) {
SendtoCsockfd. sendlme, strlen(sendline), 0. pservaddr. servlen):
if (Readable_timeo(sockfd. 5) = 0) {
fprintf(stderr, "socket timeout\enH).
} else {
n = Recvfrom(sockfd, recvlme. MAXLINE. 0. NULL. NULL):
recvline[n] = 0,/* завершающий нуль */
Fputs(recvlme. stdout),
}
}
}
Тайм-аут для функции recvfrom (параметр сокета SO_RCVTIMEO)
В нашем последнем примере демонстрируется применение параметра сокета SO_RCVTIMEO. Мы устанавливаем этот параметр один раз для дескриптора, задавая значение тайм-аута, и этот тайм-аут затем применяется ко всем операциям чтения этого дескриптора. Одна из замечательных особенностей этого метода состоит в том, что мы устанавливаем данный параметр только один раз, тогда как предыдущие два метода требовали выполнения некоторых действий перед каждой операцией, для которой мы хотели задать временной предел. Но этот параметр сокета применяется только к операциям чтения. Аналогичный параметр SO_SNDTIMEO применяется только к операциям записи, и ни один параметр сокета не может использоваться для установки тайм-аута для функции connect.
Листинг 13.5. Функция dg_cli, использующая параметр сокета SO_RCVTIMEO для установки тайм-аута
//adV1o/dgclitimeo2 c
#incTude "unp h"
void
dg_cli(FILE *fp. mt sockfd. const SA *pservaddr socklen_t servlen)
{-—
mt n.
charsend" ne [MAXLINE], recvlme [MAXLINE + 1],
struct timeval tv:
tv tv_sec = 5.
tv tV—iisec = 0.
Setsockopt(sockfd. SOL_SOCKET. SO_RCVTIMEO. &tv, sizeof(tv)):
И while (Fgets(sendline. MAXLINE, fp) ,= NULL) {
SendtoCsockfd. sendline. strlen(sendline). 0. pservaddr, servlen);
n = recvfrom(sockfd. recvlme. MAXLINE, 0. NULL, NULL),
if (n < 0) {
if (errno == EWOULDBLOCK) {
fpnntf(stderr, "socket timeout\en"). ,
continue.
} elseпродолжение 余
Листинг 13.5. (продолжение)
err_sys(”recvfrom error*'):
}“
recvlmetn] = 0. /* завершающий нуль */
Fputs(recvline. stdout):
}
}
Установка параметра сокета
8-10 Четвертый аргумент функции setsockopt — это указатель на структуру timeval, в которую записывается желательное значение тайм-аута.
Проверка тайм-аута
15-17 Если тайм-аут операции ввода-вывода истекает, функция (в данном случае recvfrom) возвращает ошибку EWOULDBLOCK.
Функции recv и send
Эти две функции аналогичны стандартным функциям read и write, но для них требуется дополнительный аргумент.
#include <sys/socket h>
ssize_t recv(mt sockfd void *buff, size_t nbytes mt flags).
ssize_t send(mt sockfd const void *buff size』nbytes. int flags)
Обе функции возвращают количество прочитанных или записанных байтов в случае успешного выполнения -1 в случае ошибки
Первые три аргумента функций recv и send совпадают с тремя первыми аргументами функций read и write. Аргумент fl ags либо имеет нулевое значение, либо формируется в результате применения операции логического ИЛИ к константам, представленным в табл. 13.1,
Таблица 13.1. Аргумент flags для функций ввода-вь旧ода
flags
Описание
recv
send
MSG—DONTROUTE
Не искать в таблице маршрутизации
•
MSG_DONTWAIT
Только эта операция является неблокируемой
•
•-
MSG—OOB
Отправка или получение внеполосных данных
•
•г
MSG_PEEK
Просмотр приходяших сообщений
•
MSG—WAITALL
Ожидание всех данных
•
v MSG—DONTROUTE. Этот флаг ообщает ядру, что получатель находится в нашей сети, и поэтому не нужно выполнять поиск в таблице маршрутизации. Дополнительную информацию об этом свойстве мы приводим при описании параметра сокета SO_DONTROUTE (см. раздел 7.5). Это свойство можно включить для одной операции вывода с флагом MSG_DONTROUTE или для всех операций вывода данного сокета, используя указанный параметр сокета.
犠 MSG_DONTWAIT. Этот флаг указывает, что отдельная операция ввода-вывода является неблокируемой. Таким образом, отпадает необходимость включать флаг
отсутствия блокировки для сокета, выполнять операцию ввода-вывода и затем выключать флаг отсутствия блокировки. Неблокируемый ввод-вывод мы опишем в главе 15 вместе с включением и выключением флага отсутствия блокировки для всех операций ввода-вывода через сокет.
ПРИМЕЧАНИЕ
Этот флаг введен в Net/З и может не поддерживаться в некоторых системах.
MSG—OOB. С функцией send этот флаг указывает, что отправляются внеполосные данные. В случае TCP в качестве внеполосных данных должен быть отправлен только 1 байт, как показано в главе 21. С функцией recv этот флаг указывает на то, что вместо обычных данных должны читаться внеполосные данные.
奴:MSG_PEEK. Этот флаг позволяет нам просмотреть пришедшие данные, готовые для чтения, при этом после выполнения функции recv или reevfrom данные не сбрасываются (при повторном вызове этих функций снова возвращаются уже просмотренные данные). Подробнее мы поговорим об этом в разделе 13.7.
MSG WAITALL. Этот флаг был впервые введен в 4.3BSD Reno. Он сообщает ядру, что операция чтения должна выполняться до тех пор, пока не будет прочитано запрашиваемое количество байтов. Если система поддерживает этот флаг, мы можем опустить функцию readn (см. листинг 3.8) и заменить ее макроопределением:
fdefine readnCfd. ptr n) recv(fd, ptr. n MSG_WAITALL)
Даже если мы задаем флаг MSG_WAITALL, функция может возвратить количество байтов меньше запрашиваемого в том случае, если перехватывается сигнал, или соединение завершается, или есть ошибка сокета, требующая обработки.
Существуют дополнительные флаги, используемые протоколами, отличными от TCP/IP. Например, транспортный уровень OSI основан на записях (а не на потоке байтов, как TCP) н для операций вывода поддерживает флаг MSG_EOR, задающий конец логической записи. Т/ТСР (TCP для транзакций, описываемый в разделе 13.9), поддерживает новый флаг MSG_EOF для объединения операции вывода с отправкой сегмента FIN.
С аргументом flags связана одна фундаментальная проблема: он передается по значению и не является аргументом типа «значение-результат». Следовательно, он может использоваться только для передачи флагов от процесса к ядру. Ядро не может передать флаги обратно процессу. Это не представляет проблемы с ТCP/IP, поскольку очень редко бывает необходимо передавать флаги обратно процессу от ядра. Но когда к 4.3BSD Reno были добавлены протоколы OSI, появилась необходимость возвращать процессу флаг MSG_EOR при операции ввода. В 4.3BSD Reno было принято решение оставить аргументы для общеупотребительных функций (recv и reevfrom) как есть и изменить структуру msghdr, которая используется с функциями reevmsg и sendmsg. В разделе 13.5 мы увидим, что в эту структуру был добавлен целочисленный элемент msg_flags, и поскольку структура передается по ссылке, ядро может изменить эти флаги по завершении функции. Это значит также, что если процессу необходимо, чтобы флаги изменялись ядром, процесс должен вызвать функцию reevmsg вместо вызова функции recv или reevfrom.
Функции readv и writev
Эти две функции аналогичны функциям read и write, но readv и writev позволяют использовать для чтения или записи один или более буферов с помощью одного вызова функции. Эти операции называются операциями распределяющего чтения {scatter read) (поскольку вводимые данные распределяются по нескольким буферам приложения) и объединяющей записи (gather write) (поскольку данные из нескольких буферов объединяется для одной операции вывода).
include <sys/uio h>
ssize_t readv(int filedes. const struct lovec *7。乙 int lovcnt).
ssize_t writev(mt filedes. const struct lovec *wv. int wvcnt).
Обе функции возвращают количество считанных или записанных байтов, -1 в случае ошибки
Второй аргумент обеих функций — это указатель на массив структур lovec, для определения которого требуется включить заголовочный фа血刀 <sys/uio h>: struct lovec {
void *iov_base. /* начальный адрес буфера */
size_t iov_len, /* размер буфера */
}.- -
ПРИМЕЧАНИЕ ——
Функции readv и writev еще не стандартизованы Posix. Но структура iovec используется также с функциями resvmsg и sendmsg (см, раздел 13.5) и стандартизована в Posix. 1g. Типы данных элементов структуры iovec определяются Posix. 1g. Вам могут встретиться реализации, определяющие iov base как char *, a iov len как int.
Существует некоторый предел числа элементов в массиве структур iovec, зависящий от реализации. 4.4BSD, например, допускает до 1024 элементов, в то время как предел Solaris 2.5 равен 16. Posix. 1g требует, чтобы константа IOV_MAX определялась включением заголовочного файла <sys/uw h> и чтобы ее значение было не менее 16.
Функции readv и wri tev могут использоваться с любым дескриптором, но только не с сокетами. Кроме того, writev является атомарной операцией. Для протокола, основанного на записях, такого как UDP, один вызов функции writev генерирует одну дейтаграмму UDP.
Мы отметили одно использование функции writev с параметром сокета ТСР_ NODELAY в разделе 7.9. Мы сказали, что при записи с помощью функции wri tev 4 байт и затем 396 байт может активизироваться алгоритм Нагла, и предпочтительное решение в данном случае заключается в вызове функции wn tev для двух буферов.
Функции recvmsg и sendmsg
Эти две функции являются наиболее общими для всех операций ввода-вывода. Действительно, мы можем заменить все вызовы функций read, readv, recv и recvfrom вызовами функции recvmsg. Аналогично, все вызовы различных функций вывода можно заменить вызовами функции sendmsg.
#include <sys/socket h>
ssize t recvmsg(int sockfd, struct msghdr *msg. int flags)
ssize_t sendmsg(int sockfd, struct msghdr *msg. int flags).
Обе функции возвращают количество прочитанных или записанных байтов в случае успешного выполнения. -1 в случае ошибки
Большинство аргументов обеих функций скрыто в структуре msghdr: struct msghdr {
void
*msg_name.
/* адрес протокола */
socklen_t
msg_ndmelen.
/* размер адреса протокола */
struct lovec
*msg_iov;
/* массив буферов */
size_t
msg_iovlen.
/* количество элементов в массиве msg_iov */
void
*msg_control,
/* вспомогательные данные, должны быть выровнены для струк
туры emsghdr' */
socklen_t
msg_controllen.
/* размер вспомогательных данных */
int
msg_flags.
/* флаги, возвращенные функцией reevmsgО */
};
ПРИМЕЧАНИЕ
Показанная нами структура 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_nanie и msg_namelen используются, когда сокет не является присоединенным (например, неприсоединенный сокет UDP). Они аналогичны пятому и шестому аргументам функций reevfrom и sendto: msg_nanie указывает на структуру адреса сокета, в которой вызывающий процесс хранит адрес протокола получателя для функции sendmsg или функция reevmsg хранит адрес протокола отправителя. Если нет необходимости задавать адрес протокола (например, сокет TCP или присоединенный сокет UDP), элемент msg_name должен быть пустым указателем. Элемент msg_namelen является аргументом типа «значение» для функции sendmsg, но для функции reevmsg это аргумент типа «значение-результат».
Элементы msg_iov и msg_i ovl еп задают массив буферов ввода и вывода (массив структур iovec), аналогичный второму и третьему аргументам функций readv и writev.
Элементы msg_control и msg controllen задают расположение и размер необязательных вспомогательных данных. Элемент msg_control 1 еп — это аргумент типа «значение-результат» функции reevmsg. Вспомогательные данные мы рассматриваем в разделе 13.6.
Работая с функциями reevmsg и sendmsg, следует учитывать различие между двумя флаговыми переменными: это аргумент flags, который передается по значению, и элемент msg_fl ags структуры msghdr, который передается по ссылке (поскольку функции передается адрес этой структуры).
: Элемент msg_fl ags используется только функцией reevmsg. Когда вызывается функция reevmsg, аргумент fl ags копируется в элемент msg_f 1 ags [105, с. 502], и это значение используется ядром для управления приемом данных. Затем это значение обновляется в зависимости от результата функции reevmsg.
Элемент msg_flags игнорируется функцией sendmsg, поскольку эта функция использует аргумент flags для управления выводом данных. Это значит, что если мы хотим установить флаг MSG_DONTWAIT при вызове функции sendmsg, то мы должны присвоить это значение аргументу flags, а присваивание значения MSG_DONTWAIT элементу msg_f 1 ags не имеет никакого эффекта.
В табл. 13.2 показано, какие флаги проверяются ядром для функций ввода и вывода и какие элементы msg_f 1 ags может возвращать функция recvmsg. Для элемента sendmsg msg_f 】ags нет колонки, потому что, как мы отмечали, он не используется.
Таблица 13.2. Флаги для различных функций ввода-вывода
Флаг
Проаеряются функциями send flags sendto flags sendmsg flags
Проверяются функциями recv flags recvfrom flags recvmsg flags
Возвращаются функцией recvmsg msgjlags
MSG_DONTROUTE
MSG_DONTWAIT
•
•
MSG—PEEK
•
MSG_WAITALL
MSG_EOR
•
MSG_OOB
•
•
MSG—BCAST
MSG_MCAST
MSG—TRUNC
MSG_CTRUNC
Первые четыре флага только проверяются и никогда не возвращаются, вторые два проверяются и возвращаются, а последние четыре флага только возвращаются. Следующие ниже комментарии относятся к шести флагам, возвращаемым функцией recvmsg.
* MSG_BCAST. Этот флаг введен в BSD/OS и возвращается, если дейтаграмма была получена как широковещательна牙 дейтаграмма канального уровня или если ее IP-адрес получателя является широковещательным адресом. Этот флаг предоставляет более удачную возможность определить, что дейтаграмма UDP была отправлена на широковещательный 燄рес, чем параметр сокета IP^RECVDST - ADDR.一
MSG_MCAST. Этот флаг введен в BSD_OS и возвращается, если дейтаграмма была получена как дейтаграмма многоадресной передачи канального уровня.
' MSG_TRUNC. Этот флаг возвращается, если дейтаграмма была усечена: у ядра имеется больше данных для возвращения, чем позволяет пространство в памяти, выделенное для них процессом (сумма всех элементов iov_len). Более подробно мы рассмотрим это в разделе 20.3.
> 之 MSG_CTRUNC. Этот флаг возвращается, если были усечены вспомогательные данные: у ядра имеется больше вспомогательных данных для возвращения, чем позволяет выделенное для них процессом пространство в памяти (msg_con- trollenl
丁 MSG E0R. Этот флаг означает конец записи. Он сбрасывается, если возвращаемые данные не заканчивают запись. Если же возвращаемые данные заканчивают логическую запись, этот флаг устанавливается. TCP не использует этот флаг, поскольку это протокол потока байтов.
MSG_OOB. Этот флаг никогда не возвращается для внеполосных данных TCP. Он возвращается другими наборами протоколов (например, протоколами OSI). Реализации могут возвращать некоторые из входных аргументов fl ags в элементе msg_f ] ags, поэтому мы должны проверять только те значения флагов, которые нас интересуют (например, последние шесть в табл, 13.2).
На рис. 13.1 представлена структура msghdr и информация, на которую она указывает. На этом рисунке отражена ситуация, предшествующая вызову функции recvmsg для сокета UDP.
Рис. 13.1. Структуры данных в тот момент, когда функция recvmsg вызывается для сокета UDPРис. 13.1. Структуры данных в тот момент, когда функция recvmsg вызывается для сокета UDP
Рис. 13.1. Структуры данных в тот момент, когда функция recvmsg вызывается для сокета UDP
Рис. 13.1. Структуры данных в тот момент, когда функция recvmsg вызывается для сокета UDP
Для адреса протокола в памяти выделяется 16 байт, а для вспомогательных данных — 20 байт. Инициализируется массив из трех структур iovec: первая задает 100-байтовый буфер, вторая — 60-байтовый буфер, третья — 80-байтовый буфер. Мы также предполагаем, что был установлен параметр сокета IP_RECVSTADDR для получения IP-адреса получателя из дейтаграммы UDP.
Затем будем считать, что с адреса 198.69.10.2, порт 2000, приходит 17 0-байтовая дейтаграмма UDP, предназначенная для нашего сокета UDP с IP-адресом получателя 206.62.226.35. На рис. 13.2 показана вся информация, содержащаяся в структуре msghdr в момент завершения функции recvmsg.
Затемненными показаны поля, изменяемые функцией recvmsg. По сравнению с рис. 13.1 на рис. 13.2 изменяется следующее:
糞 В буфер, на который указывает элемент msg_name, записывается структура адреса сокета Интернета, содержащая IP-адрес и UDP-порт отправителя, опре- ПР TTAWWKtn ПС пп ТГ\Ги^1Т1ТГ\ТЛ ТТОТЛТОГЧ-^ОХК^Л!
Рис. 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_controllen — его новым значением становится фактический размер записанных вспомогательных данных. Этот аргумент также является аргументом типа «значение-результат», и его результат по завершении функции равен 16.
剝 Элемент msg_f 1 ags изменяется функцией recvmsg, но процессу никакие флаги не возвращаются.
В табл. 13.3 показаны различия между рассмотренными пятью группами функций ввода-вывода.
Таблица 13.3. Сравнение пяти групп функций ввода-вы вода
Функция
Произ- Только
Один бу-
Распреде- Наличие
Указание
Управляю*
вольный дескрип-
фердля
ляющее флагов
адреса
щая ин-
дескрип- тор со-
чтения и
чтение,
собесед-
формация
торкета
записи
объеди-
ника
няющая
запись
read, wnte
•
readv,
•
•
wntev
recv, send
recvfrom,
sendto
recvmsg,
•
••
.
sendsg
Вспомогательные данные
Вспомогательные данные (ancillary data) можно отправлять и получать, используя элементы msg_control и msg_controllen структуры msghdr с функциями sendmsg и recvmsg. Другой термин, используемый для обозначения вспомогательных данных, —управляющая информация (control information). В этом разделе мы рассматриваем данное понятие и показываем структуру и макросы, используемые для создания и обработки вспомогательных данных. Примеры программ мы откладываем до следующих глав, в которых рассказывается о применении вспомогательных данных.
В табл. 13.4 приводится обобщение различных вариантов применения вспомогательных данных, рассматриваемых в этой книге.
Таблица 13.4. Использование вспомогательных данных
as5&atfc«pjase90sssia2sass8«ss4»tw®8sw94^^
Протокол
cm sgJewel
cmsg_type
Описание
IPv4
IPPROTO_IP
IP_RECDVSTADDR
Получает адрес получателя с дейтаграммой U DP
IP—RECVIF
Получает индекс интерфейса с дейтаграммой UDP
IPv6
IPPROTO_IPV6
IPV6_DSTOPTS
Задает/получает параметры получателе
IPV6_HOPLIMIT
Задает /получает предел количества
транзитных узлов
IPV6_HOPOPTS
Задает/получает параметры для 1 ранзитиых узлов
IPV6_NEXTHOP
Задает следующий транзитный адрес
IPV6_PKTINFO
Задает/получает информацию о пакете
IPV6_RTHDR
Задает/получает информацию о пакете
Домен Unix
SOL_SOCKET
SCM_RIGHTS
Посылает/получает дескрипторы
SCM_CREDS
Посылает/получает данные, идентифицирующие пользователя
Набор протоколов OSI также использует вспомогательные данные для различных целей, которые мы не рассматриваем в этой книге.
Вспомогательные данные состоят из одного или более объектов вспомогательных данных (ancillary data objects), каждый из которых начинается со структуры emsghdr, определяемой подлючением заголовочного файла <sys/socket .h>: struct emsghdr {
socklen_t cmsg_】en. /* длина в байтах, включая эту структуру */
int cmsg_level, /* исходящий протокол */
int cmsg_type. /* тип данных, специфичный для протокола */
/* далее следует массив символов без знака cmsg_data[] */
}'
Мы уже видели эту структуру на рис. 13.2, когда она использовалась с параметром сокета IP_RECVDSTARRD для возвращения IP-адреса получателя дейтаграмм мы UDP. Вспомогательные данные, на которые указывает элемент msg_cont「ol, должны быть соответствующим образом выровнены для структуры emsghdr. Один из способов выравнивания мы показываем в листинге 14.9.
На рис. 13.3 приводится пример двух объектов вспомогательных данных, содержащихся в буфере управляющей информации.
msg control
▼▼GST TOJ+JU OOI6 seGST TOJ+JU OOI6 se《)йым—owsoсфн—^ИБи《)йым—owsoсфн—^ИБиcmsg_len emsg level cmsg_type Заполнениеcmsg_len emsg level cmsg_type ЗаполнениеДанныеДанныеcmsghdr{ }cmsghdr{ }Обьекшвспомогательныхданныхcmsg_space ()Обьекшвспомогательныхданныхcmsg_space ()с§^ёиUST—бгошис§^ёиUST—бгошиЗаполнен怨 cmsg_len cmsg_level cmsg_type ЗаполнениеЗаполнен怨 cmsg_len cmsg_level cmsg_type ЗаполнениеДанныеДанныеcmsghdr{}cmsghdr{}Объекты\ вспомогательных/ данныхCMSG—SPACE()Объекты\ вспомогательных/ данныхCMSG—SPACE()
▼
▼
GST TOJ+JU OOI6 se
GST TOJ+JU OOI6 se
《)йым—owsoсфн—^ИБи
《)йым—owsoсфн—^ИБи
cmsg_len emsg level cmsg_type Заполнение
cmsg_len emsg level cmsg_type Заполнение
Данные
Данные
cmsghdr{ }
cmsghdr{ }
Обьекшвспомогательныхданныхcmsg_space ()
Обьекшвспомогательныхданныхcmsg_space ()
с§^ёиUST—бгоши
с§^ёиUST—бгоши
Заполнен怨 cmsg_len cmsg_level cmsg_type Заполнение
Заполнен怨 cmsg_len cmsg_level cmsg_type Заполнение
Данные
Данные
cmsghdr{}
cmsghdr{}
Объекты
\ вспомогательных
/ данных
CMSG—SPACE()
Объекты
\ вспомогательных
/ данных
CMSG—SPACE()
JTГ 7
Рис. 13.3. Два объекта вспомогательных данных
Элемент msg_contr。】 указывает на первый объект вспомогательных данных, а общая длина вспомогательных данных задается элементом msg_control 1 еп. Каждому объекту предшествует структура emsghdr, которая описывает объект. Между элементом cmsg_type и фактическими данными может существовать заполнение, а также заполнение может быть в конце данных, перед следующим объектом вспомогательных данных. Пять макросов CMSG_xxx, которые мы описываем чуть ниже, учитывают это возможное заполнение.
ПРИМЕЧАНИЕ
Не все реализации поддерживают наличие нескольких объектов вспомогательных данных в буфере управляющей информации.
На рис. 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
Дескриптор
fcred{}
Рис. 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_FIRSTHDR(struct msghdr *mhdrptr),
Возвращает указатель на первую структуру cmsghdr или NULL, если нет вспомогательнцх , данных
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr struct cmsghdr *cmsgptr):*
Возвращает указатель на структуру cmsghdr или NULL, если нет больше объектов вено。。修'' тельных данных
unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr).
Возвращает указатель на первый байт данных связанных со структурой cmsghdr
unsigned int CMSG_LEN(unsigned int length)
Возвращает значение, которое записывается в cmsg_Ien
unsigned int CMSG_SPACE(unsigned int length).
ПРИМЕЧАНИЕ
В Posix. 1g определены первые пять макросов, а в [96] определены последние два.
Эти макросы будут использованы в следующем псевдокоде: struct msghdr msg. struct cmsghdr *cmsgptr, /* заполнение структуры msg */ /* вызов recvmsg() */
for (cmsgptr = CMSG FIRSTHDR(&msg) cmsgptr !■ 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 никогда пе используют элемент msg controllen и просто возвращают значение cmsg control. В листинге 20.2 мы проверяем значение msg controllen перед вызовом макроопределения.
Разница между макросами CMSG_LEN и CMSG_SPACE заключается в том, что первый возвращает длину объекта вместе с дополняющими нулями (это значение хранится в cmsg_l еп), а последний возвращает длину собственно объекта (это значение может использоваться для динамического выделения памяти под объект).
Сколько данных находится в очереди?
Иногда требуется узнать, сколько данных находится в очереди данного сокета для чтения, не считывая эти данные. Для этого имеется три способа.
Если нашей целью не является блокирование в ядре (поскольку мы можем выполнять другие задачи, пока данные для чтения еще не готовы), может использоваться неблокируемый ввод-вывод. Мы обсуждаем его в главе 15.
Если мы хотим проверить данные, но при этом оставить их в приемном буфере для считывания какой-либо другой частью процесса, мы можем использовать флаг MSG PEEK (см. табл. 13.1). Если мы не уверены, что какие-либо данные готовы для чтения, мы можем объединить этот флаг с отключением блокировки для сокета или с флагом MSG_DONTWAIT. Помните о том, что для потокового соке-
та количество данных в приемном буфере может изменяться между двумя последовательными вызовами функции recv. Например, предположим, что мы вызываем recv для сокета TCP, задавая буфер длиной 1024 и флаг MSG_PEEK, и возвращаемое значение равно 100. Если затем мы снова вызовем функцию recv, возможно, возвратится более 100 байт (мы задаем длину буфера больше 100), поскольку в промежутке между двумя нашими вызовами recv могут быть получены дополнительные данные. А что произойдет в случае сокета UDP, когда в приемном буфере имеется дейтаграмма? При вызове recvfrom с флагом MSG_PEEK, за которым последует другой вызов без задания MSG PEEK, возвращаемые значения обоих вызовов (размер дейтаграммы, ее содержимое и адрес отправителя) будут совпадать, даже если в приемный буфер сокета между двумя вызовами добавляются дополнительные дейтаграммы. (Мы считаем, конечно, что никакой другой процесс не использует тот же дескриптор и не осуществляет чтение из данного сокета в это же время.)
Некоторые реализации поддерживают команду FIONREAD функции loctl. Третий аргумент функции ioctl — это указатель на целое число, а возвращаемое в этом целом числе значение — это текущее число байтов в приемном буфере сокета [105, с. 553]. Это значение является общим числом установленных в очередь байтов, которое для сокета UDP включает все дейтаграммы, установленные в очередь. Также помните о том, что значение, возвращаемое для сокета UDP, в Беркли-реализациях включает пространство, требуемое для структуры адреса сокета, содержащей IP-адрес отправителя и порт для каждой дейтаграммы (16 байт для IP4, 24 байта для IP6).
Сокеты и стандартный ввод-вывод
Во всех наших примерах мы применяли то, что иногда называется вводом-выводом Unix: вызывали функции read и write и их разновидности (recv, send и т. д.). Эти функции работают с дескрипторами и обычно реализуются как системные вызовы внутри ядра Unix.
Другой метод выполнения ввода-вывода заключается в использовании стандартной библиотеки ввода-вывода. Она задается стандартом ANSI С и была задумана как библиотека, совместимая с He-Unix-системами, поддерживающими ANSI С. Стандартная библиотека ввода-вывода обрабатывает некоторые моменты, о которых мы должны заботиться сами при использовании функций ввода-вывода Unix, такие как автоматическая буферизация потоков ввода и вывода. К сожалению, ее обработка буферизации потока может представить новый ряд проблем, о которых следует помнить. Глава 5 [93] подробно описывает стандартную библиотеку ввода-вывода, а в [79] представлена полная реализация стандартной библиотеки ввода-вывода и ее обсуждение.
ПРИМЕЧАНИЕ
При обсуждении стандартной библиотеки ввода-вывода используется термин «поток» в выражениях типа «мы открываем поток ввода» или «мы очищаем поток вывода». Не путайте это с подсистемой потоков System V, которую мы обсуждаем в главе 33.
Стандартная библиотека ввода-вывода может использоваться с сокетами, но есть несколько моментов, которые необходимо при этом учитывать.
‘ Стандартный поток ввода-вывода может быть создан из любого дескриптора при помощи вызова функции fdopen. Аналогично, имея стандартный поток ввода-вывода, мы можем получить соответствующий дескриптор, вызывая функцию fileno. С функцией flleno мы впервые встретились в листинге 6.1, когда мы хотели вызвать функцию sei ect для стандартного потока ввода-вывода. Функция select работает только с дескрипторами, поэтому нам необходимо было получить дескриптор для стандартного потока ввода-вывода.
繼 Сокеты TCP и UDP являются двусторонними. Стандартные потоки ввода- вывода также могут быть двусторонними: мы просто открываем поток типа г+, что означает чтение-запись. Но в таком потоке за функцией вывода не может следовать функция ввода, если между ними нет вызова функции ffl ush, fseek, fsetpots или rewind. Аналогично, за функцией вывода не может следовать функция ввода, если между ними нет вызова функции fseek, fsetpots, rewind, в том случае, когда при вводе не получен признак конца файла. Проблема с последними тремя функциями состоит в том, что все они вызывают функцию 1 seek, которая не работает с сокетами.
: Простейший способ обработки подобной проблемы чтения-записи — это открытие двух стандартных потоков ввода-вывода для данного сокета: одного для чтения и другого для записи.
Пример: функция str_echo, использующая стандартный ввод-вывод
Сейчас мы модифицируем наш эхо-сервер TCP (см. листинг 5.2) для использования стандартного ввода-вывода вместо функций readl т пе и wnten. В листинге 13.6 представлена версия нашей функции str~echo, использующая стандартный ввод-вывод. (С этой версией связана проблема, которую мы вскоре опишем.)
Листинг 13.6. Функция str^echo, переписанная с использованием стандартного ввода-вывода
//adVIo/str_echo_stdiо02 с
include unp h"
void
str echo(int sockfd)
{'
charlme[MAXLINE]
FILE*fpin *fpout
fpin =Fdopen(sockfd "rM),
fpout= Fdopen(sockfd"w").
for () {
if (FgetsCline MAXLINE fpin) == NULL) ,
return /* соединение закрывается удаленным концом */
Fputsdine fpout).
}
}
Преобразование дескриптора в поток ввода и поток вывода
7-13 Функцией fdopen создаются два стандартных потока ввода-вывода: один для ввода и другой для вывода. Вызовы функций readline и wnten заменены вызовами функций fgets и fputs.
hello, world and hi hello??thello world and hi hello??hello, world and hi hello??thello world and hi hello??Если мы запустим наш сервер с этой версией функции str_echo и затем запустим наш клиент, мы увидим следующее: solans ■ tcpcl?02 206.62.226.33
hello, world and hi hello??
t
hello world and hi hello??
hello, world and hi hello??
t
hello world and hi hello??
мы набираем эту строку “о не получаем отражения и на эту строку “ет ответа и на эту строку нет ответа
наш символ нонца файла
и затем выводятся три отраженные строки
Здесь возникает проблема буферизации, поскольку сервер ничего не отражает, пока мы не введем наш символ конца файла. Выполняются следующие шаги:
Мы набираем первую строку ввода, и она отправляется серверу.
, Сервер читает строку с помощью функции fgets и отражает ее с помощью функции fputs.
' Но стандартный поток ввода-вывода сервера полностью буферизован стандартной библиотекой ввода^вывода. Это значит, что библиотека копирует отраженную строку в свой стандартный буфер ввода-вывода для этого потока, но не выдает содержимое буфера в дескриптор, поскольку буфер не заполнен.
Мы набираем вторую строку ввода, и она отправляется серверу.
Сервер читает строку с помощью функции fgets и отражает ее с помощью функции fputs.
и Снова стандартная библиотека ввода-вывода сервера только копирует строку в свой буфер, но не выдает содержимое буфера в дескриптор, поскольку он не заполнен.
< По тому же сценарию вводится третья строка.
Мы набираем наш символ конца файла, и функция str_cl т (см. листинг 6.2) вызывает функцию shutdown, посылая серверу сегмент FIN.
TCP сервера получает сегмент FIN, который читает функция fgets, в результате чего функция fgets возвращает пустой указатель.
" Функция str_echo возвращает серверу функцию mam (см. листинг 5.9), и дочерний процесс завершается при вызове функции exit.
“ Библиотечная функция ехт t языка С вызывает стандартную функцию очистки ввода-вывода [93, с. 162-164], и буфер вывода, который был частично заполнен нашими вызовами функции fputs, теперь выводит скопившиеся в нем данные.
; Дочерний процесс сервера завершается, в результате чего закрывается его присоединенный сокет, клиенту отсылается сегмент FIN и заканчивается последовательность завершения соединения TCP.
' Наша функция str cl i получает и выводит три отраженных стоки.
Затем функция str_cli получает символ конца файла на своем сокете, и клиент завершает свою работу.
Проблема здесь заключается в том, что буферизация на стороне сервера выполняется автоматически стандартной библиотекой ввода-вывода. Существует три типа буферизации, выполняемой стандартной библиотекой ввода-вывода.
Полная буферизация (fully buffered) означает, что ввод-вывод имеет место, только когда буфер заполнен, процесс явно вызывает функцию ffl ush или процесс завершается посредством вызова функции exit. Обычный размер стандартного буфера ввода-вывода — 8192 байт.
Вуфер篡защ!я по строкам {line buffered) означает, что ввод-вывод имеет место, только когда встречается символ перевода строки, процесс вызывает функцию fflush или процесс завершается вызовом функции exit.
Отсутствие буферизации (unbuffered) означает, что ввод~вывод имеет место каждый раз, когда вызывается функция стандартного ввода-вывода.
Большинство реализаций Unix стандартной библиотеки ввода-вывода используют следующие правила:
Стандартный поток ошибок никогда не буферизуется.
Стандартные потоки ввода и вывода полностью буферизованы, если они не подключены к терминальному устройству, в случае чего они буферизуются по строкам.
Все остальные потоки полностью буферизованы, если они не подключены к терминалу, в случае чего они буферизованы по строкам.
Поскольку сокет не является терминальным устройством, проблема, отмеченная в нашей функции str_echo в листинге 13.6, заключается в том, что поток вывода (fpot) полностью буферизован. Есть два решения: мы можем сделать поток вывода буферизованным по строкам при помощи вызова функции setvbuf либо заставить каждую отраженную строку выводиться при помощи вызова функции ffl ush после каждого вызова функции fputs. Применение любого из этих изменений скорректирует поведение нашей функции str_echo.
ПРИМЕЧАНИЕ
Другим решением будет вообще отказаться от стандартной библиотеки ввода-вывода и использовать библиотеку sfio. Этот вариант описан в [58], а исходный код является свободно доступным.
Будьте осторожны — некоторые реализации стандартной библиотеки ввода-вывода все еще вызывают проблемы при работе с дескрипторами, большими 255. Эта проблема может возникнуть с сетевыми серверами, обрабатывающими множество дескрипто* ров. Проверьте определение структуры FILE в вашем заголовочном файле <stdio.h>, чтобы увидеть, к какому тину переменных относится дескриптор.
Т/ТСР: TCP для транзакций
Т/ТСР — это незначительное изменение TCP, помогающее избежать трехэтапного рукопожатия между узлами, которые недавно взаимодействовали друг с другом. Т/ТСР подробно описывается в [9 5], RFC 1379 [И] и RFC 1644 [13].】
ПРИМЕЧАНИЕ
Наиболее широко распространена реализация Т/ТСР, имеющаяся в 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 sendto read (блокировка)Завершение функции readЗавершение функции readSYN,吧郵弁請詣^SYN,吧郵弁請詣^而諒嚼^端詬а而諒嚼^端詬аsocket, bind, listen, accept (блокировка)Завершение функции acceptФункция read считывает запрос.Сервер обрабатывает запрос.Функция send отправляет ответ клиенту closesocket, bind, listen, accept (блокировка)Завершение функции acceptФункция read считывает запрос.Сервер обрабатывает запрос.Функция send отправляет ответ клиенту closeЛРиемаЛРиема0T сервера0T сервераКлиент
socket sendto read (блокировка)
socket sendto read (блокировка)
Завершение функции read
Завершение функции read
SYN,吧郵弁請詣^
SYN,吧郵弁請詣^
而諒嚼^端詬а
而諒嚼^端詬а
socket, bind, listen, accept (блокировка)
Завершение функции accept
Функция read считывает запрос.
Сервер обрабатывает запрос.
Функция send отправляет ответ клиенту close
socket, bind, listen, accept (блокировка)
Завершение функции accept
Функция read считывает запрос.
Сервер обрабатывает запрос.
Функция send отправляет ответ клиенту close
ЛРиема
ЛРиема
0T сервера
0T сервера
Рис. 13>5. Схема минимальной транзакции Т/ТСР
ПРИМЕЧАНИЕ
Мы игнорируем здесь некоторые подробности, которые полностью раскрыты в [95]. Например, когда клиент в первый раз общается с сервером, гребуется трехэтапное рукопожатие. Но затем в течение некоторого времени этого можно избежать, пока на обоих концах соединения сохраняется некоторая кэшированная информация о соединении, если в течение этого времени ни один из концов соединения не выйдет из строя и не перезагрузится. Три показанных сегмента формируют минимальный обмен «запрос- ответ». Дополнительные сегменты требуются, если или запрос, или ответ не помещаются в один сегмент. Термин «транзакция» означает запрос клиента и ответ сервера. Общие примеры транзакций — это запрос DNS и ответ сервера или запрос HTTP и ответ сервера. Мы не говорим здесь о двухфазном протоколе с подтверждением завершения транзакций.
Преимуществом использования Т/ТСР в том, что он сохраняет надежность TCP (последовательные номера, тайм-ауты, повторные передачи и т. п.) в отличие от ненадежного варианта с применением UDP, при котором добавление надежности является проблемой. Кроме того, Т/ТСР поддерживает такие свойства TCP (часто отсутствующие у приложений UDP), как медленный старт и предотвращение перегрузок (переполнения буфера).
Т/ТСР требует минимальных изменений API сокетов. Следует отметить, что в системе, предоставляющей Т/ТСР, не требуется вносить изменения в приложения TCP, когда функции Т/ТСР не требуются. Все существующие приложения TCP продолжают работать, используя API сокетов, который мы уже описывали.
Клиент вызывает функцию sendto для объединения установления соединения с отправкой данных. Это заменяет отдельные вызовы функций connect и wn te. Адрес протокола сервера теперь передается функции sendto вместо функции connect.
讀 Применяется новый флаг вывода MSG_EOF (см. табл. 13.1), чтобы указать, что больше никаких данных через сокет отправлено не будет. Это позволяет объединить операцию вывода (функция send или sendto) с функцией shutdown. Задание этого флага с функцией sendto, которая также задает адрес сервера,— это способ, с помощью которого отправляется сегмент, содержащий SYN, FIN и данные. Обратите внимание, что на рис. 13.5 сервер отправляет свой ответ, используя функцию send, а не write. Причина в том, что для отправки сегмента FIN с ответом нужно задать флаг MSG EOF. (Не путайте этот новый флаг с существующим флагом MSG—EOR, который указывает конец записи для протоколов, ориентированных на записи.)
t Определяется новый паралсетр сокета TCP_NOPUSH со значением аргумента 】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] содержит пример кода клиента и сервера Т/ТСР.
Резюме
Существует три способа установить ограничение времени для операции с сокетом: 嬢 Использовать функцию alarm и сигнал SIGALRM.
嚟 Задать предел времени в функции sei ect.
W Использовать более новые параметры сокета SO^RCVTIMEO и SO_SNDTIMEO.
Первый способ легко использовать, но он включает обработку сигналов и, как показано в разделе 18.5, может привести к ситуации гонок. Использование функции select означает, что блокирование происходит в этой функции (с заданным в ней пределом времени) вместо блокирования в вызове функции read, write или connect. Другая альтернатива — использование новых параметров сокета — также проста в использовании, но предоставляется не всеми реализациями.
Функции recvmsg и sendmsg являются наиболее общими из пяти групп предоставляемых функций ввода-вывода. Они объединяют целый ряд возможностей, свойственных других функциям ввода-вывода, позволяя задавать флаг MSG xxx (как функции recv и send), возвращать или задавать адрес протокола собеседника (как функции recvfrom и sendto), использовать множество буферов (как функции readv и w门 tev). Кроме того, они обеспечивают две новых возможности: возвращение флагов приложению и получение или отправку вспомогательных данных.
В тексте книги мы описываем десять различных форм вспомогательных данных, шесть из которых появились в IPv6. Вспомогательные данные состоят из объектов вспомогательных данных. Перед каждым объектом идет структура cmsghdr, задающая его длину, уровень протокола и тип данных. Пять макросов, начинающихся с префикса CMSG_, используются для создания и анализа вспомогательных данных.
Сокеты могут использоваться со стандартной библиотекой ввода-вывода С, но это добавляет еще один уровень буферизации к уже имеющемуся в TCP. На самом деле недостаток понимания буферизации, выполняемой стандартной библиотекой ввода-вывода, является наиболее общей проблемой при работе с этой библиотекой. Поскольку сокет не является терминальным устройством, общем решением этой потенциальной проблемы будет отключение буферизации стандартного потока ввода-вывода.
Т/ТСР — это простое усовершенствование TCP, которое поможет избежать трехэтапного рукопожатия, допуская более быстрый ответ сервера на запрос клиента, если этот клиент и сервер недавно взаимодействовали. С точки зрения программирования клиента преимущество Т/ТСР заключается в том, что вместо обычной последовательности вызова функций connect, w门 te и shutdown требуется вызывать только функцию sendto.
Упражнения
Что происходит в листинге 13Л, когда мы переустанавливаем обработчик сигналов, если процесс не установил обработчик для сигнала SIGALRM?
В листинге 13.1 мы выводим предупреждение, если у процесса уже установлен таймер alarm. Измените эту функцию так, чтобы новое значение alarm для процесса задавалось после выполнения connect до завершения функции.
Измените листинг 11.3 следующим образом: перед вызовом функции read вызовите функцию recv с флагом MSG_PEEK. Когда она завершится, вызовите функ、 цию 1 octi с командой FIONREAD и выведите число байтов, установленных в оче~ редь в буфере приема сокета. Затем вызовите функцию read для фактического чтения данных.
Что происходит с оставшимися в стандартном буфере ввода-вывода данными, если процесс, дойдя до конца функции main, не обнаруживает там функции exit?
Примените каждое из двух изменений, описанных после листинга 13.6, и убе- дитесь в том, что каждое из них решает проблему буферизации.
ГЛАВА 14
Доменные протоколы Unix
Введение .
Доменные протоколы Unix — это не набор протоколов, а способ связи клиентов и серверов на отдельном узле, использующий тот же API, который используется для клиентов и серверов на различных узлах, — сокеты или XTL Доменные протоколы Unix представляют альтернативу методам IPC (Interprocess Communications —взаимодействие процессов), которым посвящен второй том1 этой серии, применяемым, когда клиент и сервер находятся на одном узле. Подробности действительной реализации доменных сокетов Unix в ядре, происходящем от Беркли, приводятся в третьей части [95].
В домене Unix предоставляются два типа сокетов: потоковые (аналогичные сокетам TCP) и дейтаграммные (аналогичные сокетам UDP). Хотя предоставляется также и символьный сокет, но его семантика никогда не документировалась, он не используется никакой из известных автору программ и не определяется в Posix. 1g.
Доменные сокеты Unix используются по трем причинам.
В реализациях, происходящих от Беркли, доменные сокеты Unix часто вдвое быстрее сокета TCP, когда оба собеседника находятся на одном и том же узле [95, с. 223-224] Преимущества при этом получает одно приложение — X Window System Когда клиент ХИ запускается и открывает соединение с сервером XI1, клиент проверяет значение переменной окружения DISPLAY, которая задает имя узла сервера, окно и экран. Если сервер находится на том же узле, что и клиент, клиент открывает потоковое соединение через доменный сокет Unix с сервером, иначе клиент открывает соединение TCP с сервером.
Доменные сокеты Unix используются при передаче дескрипторов между процессами на одном и том же узле Подобный пример мы приводим в разделе 14.7.
Более новые реализации доменных сокетов Unix предоставляют регистрационные данные клиента (идентификатор пользователя и идентификаторы группы) серверу, что можег служить дополнительной проверкой безопасности. Мы покажем это в разделе 14.8.
Адреса протоколов, используемые для идентификации клиентов и серверов в домене Unix, — это полные имена в обычной файловой системе. Вспомните, что IPv4 использует комбинацию 32-разрядных адресов и 16-разрядных номеров
портов для своих адресов протоколов, а IPv6 для своих адресов протоколов использует комбинацию 128-разрядных адресов и 16-разрядных номеров портов. Эти полные имена не являются обычными именами файлов Unix: мы не можем читать из этих файлов или записывать в них. Это может делать только программа, связывающая полное имя с доменным сокетом Unix.
Структура адреса доменного сокета Unix
В листинге 14.1! показана структура адреса доменного сокета Unix, задаваемая включением заголовочного файла <sys/un h>.
Листинг 14.1. Структура адреса доменного сокета Unix: socladdr_un struct sockaddr_un {
umt8_t sunjen.
sa__family_t sun_fami1y. /* AF_LOCAL */
char sun_path[104]. /* полное имя, оканчивающееся нулем */
}.-
ПРИМЕЧАНИЕ
Более ранние реализации BSD определяли размер массива sun_path равным 108 байт, а не 104, как показано в нашем примере. В Posix.lg требуется только, чтобы его размер был как минимум 100 байт. Причина этих ограничений связана с реализацией 4.2BSD, в которой требуется, чтобы эта структура соответствовала 128-битовому mbuf (буферу памяти ядра).
Полное имя, хранимое в символьном массиве sun_path, должно завершаться нулем. Имеется макрос SUN_LEN, который получает указатель на структуру sock- dddr_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 и выводит это полное имя.
Листинг 14.2. Связывание полного имени с доменным сокетом Unix um xdoma m/umxbind.c
include "unp h”
mt
main(int argc char **argv)
{
int sockfd.
socklen_t len:
struct sockaddrjjn addrl, addr2:
if (argc l= 2)•
er「_quit( "usage umxbind <pathname>"):
sockfd н Socket(AF_LOCAL, SOCK_STREAM. 0).
unlmk(argv[l]),/* игнорируем возможную ошибку */
bzero(&addrl. sizeof(addrl)).
addrl sun_faimly = AF_LOCAL.
strncpyCaddrl sun_path. argv[l], sizeof(addrl sun_path) - 1).
Bind(sockfd. (SA *) &addrl, SUN_LEN(&addrl)). “
len = sizeof(addr2), 17Getsockname(sockfd, (SA *) &add「2. &len).
pnntf("bound name = %s. returned len = ^d\en". addr2 sun_path, len):“W”
exit(0).
}
Удаление файла
11 Полное имя, которое функция bind должна связать с сокетом, — это аргумент командной строки. Если это полное имя уже существует в файловой системе, при выполнении функции bi nd возникает ошибка. Следовательно, мы вызываем функцию uni ink, чтобы удалить файл в том случае, если он уже существует. Если его не существует, функция uni т nk возвращает ошибку, которую мы игнорируем.
Вызов функций bind и getsockname
12-18 Мы копируем аргумент командной строки, используя функцию st г перу, чтобы избежать переполнения структуры в том случае, если полное имя слишком длинное. Поскольку мы инициализируем структуру нулем и затем вычитаем единицу из размера массива sun_path, мы знаем, что полное имя оканчивается нулем. Далее вызывается функция bind и мы используем макрос SUN_LEN для вычисления длипы аргумента функции. Затем мы вызываем функцию getsockname, чтобы получить имя, которое было только что связано с сокетом, и выводим результат.
Если мы запустим программу в BSD/OS. то получим следующие результаты: bsdi % umask сначала вводим наше значение umask 0002оно отображается 8 восьмеричной системе
bsdi % umxbind /tmp/foo.bar bound name = /tmp/foo bar. returned len 工 14 bsdi % umxbind /tmp/foo.bar снова запускаем программу bound name = /tmp/foo bar. returned len = 14 bsdi % Is -1 /tmp/foo.bar srwxrwxrwx 1 rstevens wheel 0 May 20 11 02 /tmp/foo bar
bsdi 岩 1s 1F /tmp/foo bar
srwxrwxrwx 1 rstevens wheel 0 May 20 11 02 /tmp/foo bar=
Сначала мы вводим наше значение umask, поскольку в Posix 1g указано, что права доступа к создаваемому объекту определяю хся этим значением Наше значение 2 выключает бит, разрешающий запись в файл для прочих пользователей (этот 6,п называется other-w rite или woi Id-wnte) Затем мы запускаем программу и видим, что длина, возвращаемая функцнен getsockname, равна 14 один байт для элемента sun_len, один байт для эпемента sun_family и 12 байт для полного имени (исключая завершающий нуль) Это пример аргумента типа «значение- результат», значение которого при завершении функции отличается от значения при вызове функции Мы можем вывести полное имя, используя спецификатор формата %s функции printf, поскольку полное имя, хранящееся в sun_path, представляет собой завершающуюся нулем строку Затем мы снова запускаем программу, чтобы проверить, что вызов функции uni ink удаляет соответствующий файл
Мы запускаем команду Is 1, чтобы увидеть биты разрешения для файла и тип файла В 4 4BSD тип файта — это сокет, что обозначается символом s Мы также замечаем, что все девять битов разрешения включены, так как 4 4BSD не изменяет принятые по умолчанию биты разрешения на наше значение umask Наконец, мы снова запускаем 1 s с параметром F, что заставляет 4 4BSD добавить знак равенства (соответствующий типу «сокег») к полному имени
ПРИМЕЧАНИЕ
Posix 2 ничего не знаего сокетах к определяет только чго параметр -Г выводит косую черту для катало! а звездочку для выполняемого файла п вер шкальную черту 八ня фаила типа I4FO
Теперь мы запустим ту же программу в 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 p1 rstevens otherl 0 May 20 11 36 /tmp/foo bar|
Первое огличие состоит в том, что длина, возвращаемая функцией getsockname равна 110, общему размеру структуры Solans sockaddr__un Это нормально, поскольку имя файла в элементе sun_path оканчивается нулем Мы также видим, что все биты разрешений файла (для чтения, записи и выполнения) по умолчанию сброшены Поскольку все биты разрешении сброшены, мы не можем сказав, использовалось паше значение umask или нет Наконец, Is 1 указывает, что файл с данным именем имеет тип именованного канала (FIFO) — это свойственно всем доменным сокетам Unix в SVR4, и параметр F выводит вертикальную черту для обозначения FIFO
Функция socketpair
Функция socketpair создает два сокета которые затем соединяются друг с другом Эта функция применяется только к доменным сокетам Unix.
include <sys/socket h>
int socketpairdnt \fIfamly\fP int \fItype\fP int \f I protocol \fP int \fIsockfd[2]\fP) Возвращает ненулевое значение в случае успешного выполнения -1 в случае ошибки
Аргумент fami 1у должен быть равен AF_L0CAL, а аргумент protocol должен быть нулевым Однако аргумент type может быть равен как SOCK_STREAM, так и SOCKJ3GRAM Два дескриптора сокета создаются и возвращаются как sockfd[0] и sockfd[l]
ПРИМЕЧАНИЕ
Эта функция аналси ична функции Unix pipe при сс вызове возвращаются два дескриптора, причем каждый дескриптор соединен с друшм Действительно Беркли-реализации реализую! функцию pipe, выполняя ле же внутренние операции, что и функция socketpair [95, с 253-2541
Два созданных сокета не имеют имен Это значит, что не было неявного вызова функции bind
Результат выполнения функции socketpair с аргументом type, равным SOCK_ STREAM, называется потоковым каналом (s〃e〃mp绸е) Потоковый канал является аналогом обычно!о канала Unix (который создается функцией pipe), ио он двусторонний, что позволяет использовать оба дескриптора и для чтения, и для записи Потоковый канал, созданный функцией socketpai * изображен па рис 14 1
ПРИМЕЧАНИЕ
Posix 1 нс требует поддержки двусторонних каналов В SVR4 функция pipe возвращает два двусторонних дсскригпора, в то время как ядра, происходящие от Беркли, традиционно возвращаю г односторонние дескрипторы (см рис 17 31 [95])
Функции сокетов
Функции сокетов применяются к доменным сокетам Unix с учетом некоторых особенностей и ограничении Ниже мы перечисляем требования Posix, указывая, где они применимы Отметим, что на сегодняшний день не все реализации соответствуют этим требованиям
Права доступа к файлу по умолчанию для полного имени, созданного функцией bind, задаются значением 0777 (чтение, запись и выполнение данного файла разрешены владельцу файла, группе пользователей, в которую он входит, и всем остальным пользователям) и могут быть изменены в соответствии с текущим значением umask
Имя, связанное с доменным сокетом Unix, должно быть абсолютным, а не относительным именем Причина, по которой нужно избегать относительного имени, в том, что в таком случае разрешение имени зависит от текущего рабочего каталога вызывающего процесса, то есть если сервер связывается с относительным именем, клиент должен находиться в том же каталоге, что и сервер (или должен знать этот каталог), для того чтобы вызов клиентом функции connect или sendto был успешным
ПРИМЕЧАНИЕ —
В Posix 1g сказано чю связывание отпоситслыгого имени с доменным сокетом Unix приводит к непредсказуемым результахам
Полное имя, заданное в вызове функции connect, должно быть именем, в настоящий момент связанным с открытым доменным сокетом Unix того же типа (потоковым или дейтаграммным). Ошибка происходит в следующих случаях: если имя существует, ио не является сокетом; если имя существует и является сокетом, но ни один открытый дескриптор с ним не связан; если имя существует и является открытым сокетом, ио имеет неверный тип (то есть потоковый доменный сокет Unix не может соединиться с именем, связанным с дейтаграммным доменным сокетом Unix, и наоборот).
С функцией connect доменного сокета Unix связана такая же проверка прав доступа, какая имеет место при вызове функции open для доступа к файлу только на запись.
Потоковые доменные сокеты Unix аналогичны сокетам TCP: они предоставляют интерфейс байтового потока без границ записей.
Если при вызове функции connect для потокового доменного сокета Unix обнаруживается, что буфер прослушиваемого сокета заполнен (см. раздел 4.5), немедленно возвращается ошибка ECONNREFUSED. В этом отличие от сокета TCP: прослушиваемый сокет TCP итерирует приходящий сегмент SYN, если очередь сокета заполнена, и выполняется несколько попыток отправки сегмента SYN.
Дейтаграммные доменные сокеты Unix аналогичны сокетам UDP: они предоставляют ненадежный сервис дейтаграмм, сохраняющий границы записей.
В отличие от сокетов UDP, при отправке дейтафаммы на неприсоедииениый дейтаграммный доменный сокет Unix с сокетом не связывается полное имя. (Вспомните, что отправка дейтаграммы UDP на иеприсоединениый сокет UDP заставляет динамически назначаемый порт связываться с сокетом.) Это означает, что получатель дейтаграммы не будет иметь возможности отправить ответ, если отправитель не связал со своим сокетом полное имя. Аналогично, в отличие от TCP и UDP, при вызове функции connect для дейтаграммного доменного сокета Unix с сокетом не связывается полное имя.
Клиент и сервер потокового доменного протокола Unix
Теперь мы перепишем наш эхо-клиент и эхо-сервер TCP из главы 5 с использованием доменных сокетов Unix. В листинге 14.3 показан сервер, который является модификацией сервера из листинга 5.9 и использует потоковый доменный протокол Unix вместо протокола TCP.
Листинг 14.3. Эхо-сервер потокового доменного протокола Unix
//urnxdomain/unixstrservOl с
include "unp h'
int
main(int argc. char **argv)
{
int 1istenfd connfd. •
pid_t chi1 dpid.
socklen t clilen.
struct sockaddr_un cliaddr servaddr
void sig_chld(mt)
“stenfd = Socket(AF_LOCAL. SOCK_STREAM 0).
unlink(UNIXSTR_PATH)
bzero(&servaddr. sizeof(servaddr)).
servaddr sun_family = AF_LOCAL
strcpy(servaddr sun_path UNIXSTR_PATH),
1515Bind(listenfd, (SA *) &servaddr.Bind(listenfd, (SA *) &servaddr.sizeof(servaddr)):sizeof(servaddr)):
15
15
Bind(listenfd, (SA *) &servaddr.
Bind(listenfd, (SA *) &servaddr.
sizeof(servaddr)):
sizeof(servaddr)):
16 Listendistenfd LISTENQ).
17 Signal(SIGCHLD. sig_chld).
1819202122232425262728293031323318192021222324252627282930313233for ( .) {clilen = sizeof(cliaddr).if ( (connfd = accept(listenfd (SA *) &c]iaddr &clilen)) < 0) {for ( .) {clilen = sizeof(cliaddr).if ( (connfd = accept(listenfd (SA *) &c]iaddr &clilen)) < 0) {if (errno == EINTR) continue elseerr_sys("accept error'1))—if ( (childpid = ForkO) == 0) { Closed istenfd).str_echo(connfd), exit(0)}Close(connfd).if (errno == EINTR) continue elseerr_sys("accept error'1))—if ( (childpid = ForkO) == 0) { Closed istenfd).str_echo(connfd), exit(0)}Close(connfd)./* назад в for() *//* назад в for() *//* дочерний процесс *//* закрывается прослушиваемый сокет *//* обработка запроса *//* дочерний процесс *//* закрывается прослушиваемый сокет *//* обработка запроса *//* родитель закрывает прнсое胆ненныВ сокет^*Д/* родитель закрывает прнсое胆ненныВ сокет^*Д
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
for ( .) {
clilen = sizeof(cliaddr).
if ( (connfd = accept(listenfd (SA *) &c]iaddr &clilen)) < 0) {
for ( .) {
clilen = sizeof(cliaddr).
if ( (connfd = accept(listenfd (SA *) &c]iaddr &clilen)) < 0) {
if (errno == EINTR) continue else
err_sys("accept error'1)
)—
if ( (childpid = ForkO) == 0) { Closed istenfd).
str_echo(connfd), exit(0)
}
Close(connfd).
if (errno == EINTR) continue else
err_sys("accept error'1)
)—
if ( (childpid = ForkO) == 0) { Closed istenfd).
str_echo(connfd), exit(0)
}
Close(connfd).
/* назад в for() */
/* назад в for() */
/* дочерний процесс */
/* закрывается прослушиваемый сокет */
/* обработка запроса */
/* дочерний процесс */
/* закрывается прослушиваемый сокет */
/* обработка запроса */
/* родитель закрывает прнсое胆ненныВ сокет^*Д
/* родитель закрывает прнсое胆ненныВ сокет^*Д
Теперь две структуры адреса сокета относятся к типу sockdddr_un.
Для создания потокового доменного сокета Unix первый аргумент функции socket должен иметь значение AF_LOCAL.
11-15 Константа UNIXSTR_PATH определяется в файле unp h как /tmp/unix/str. Сначала мы вызываем функцию unlink, чтобы удалить полное имя в случае, если оно сохранилось после предыдущего запуска сервера, а затем инициализируем структуру адреса сокета перед вызовом функции bind. Ошибка при выполнении функции unlink — это нормальное явление.
Обратите внимание, что этот вызов функции bind отличается от вызова, показанного в листинге 14.2. Здесь мы задаем размер структуры адреса сокета (третий аргумент) как общий размер структуры sockaddr_un, а не просто число байтов, занимающее полное имя. Оба значения длины приемлемы, поскольку полное нмя должно оканчиваться нулем.
Оставта只ся часть функции такая же, как и в листинге 5.9. Используется та же функция str_echo (см. листинг 5.2).
В листинге 14.4 представлен эхо-клиент потокового доменного протокола Unix. Это модификация листинга 5.3.
Листинг 14.4. Эхо-клиент потоково「о доменного протокола Unix umxdomain/umxstrcliOl с
2 int2 int#1nclude "unp.h"
2 int
2 int
продолжение 夕
Листинг 14.4 (продолжение)
rnain(int а где char **argv)
{
int sockfd
struct sockaddr_un servaddr
sockfd » Socket?AF_LOCAL SOCK_STREAM 0)
bzero(&servaddr sizeof(servaddr))
servaddr sun_fann1y - AF_LOCAL
strcpyCservaddr sun_path UNIXSTR_PATH)
Connect(sockfd (SA *) &servaddr sizeof(servaddr))
str_c1i(stdin sockfd) /* выполняет всю работу */
exit(0)
}
Теперь структурой адреса сокета, которая должна содержать адрес сервера, будет структура sockaddr_un.
Первый аргумент функции socket — AF_LOCAL.
МО Код для заполнения структуры адреса сокета идентичен коду, показанному для сервера: инициализация структуры нулем, установка семейства протоколов AF_ LOCAL ii копирование полного имени в элемент sun_path.
12 Функция str_cl 1 — та же, что и раньше (в листинге 6.2 представлена последняя разработанная нами версия).
4.6. Клиент и сервер дейтаграммного доменного протокола Unix
Теперь мы перепишем наши клиент и сервер UDP из разделов 8 3 и 8 5 с использованием сокетов. В листинге 14.5 показан сервер, который является модификацией листинга 8.1.
Листинг 14.5. Эхо-сервер дейтаграммного доменного протокола Unix urn xdomain/um xdgservOl c
include ' unp h"
mt
main(int argc char **argv)
{
int sockfd
struct sockaddr_un servaddr cliaddr
sockfd « Socket(AF_LOCAL SOCK_DGRAM 0).
unlink(UNIXDG_PATH)
bzero(&servaddr sizeof(servaddr))
servaddr sun_family = AF_LOCAL
strcpyCservaddr sun_path UNIXDG_PATH)
Bind(sockfd (SA *) &servaddr sizeof(servaddr))
dg_echo(sockfd (SA *) &cliaddr sizeof(cliaddr)).
}-
Две структуры адреса сокета относятся теперь к тину sockaddr_un.
Для создания дейтаграммного доменного сокета Unix первый аргумент функции socket должен иметь значение AF_LOCAL
8-12 Константа UNIXDG_PATH определяется в заголовочном файле unp hKaK/tmp/umx dg. Сначала мы вызываем функцию unlink, чтобы удалить полное имя в случае, если ино сохранилось после предыдущего запуска сервера, а затем инициализируем структуру адреса сокета перед вызовом функции bind Ошибка при выполнении (Ьункции unlink — это нормальное явление.
1з Используется та же функция dg_echo (см листинг 8 2)
В листинге 14 6 представлен эхо-клиент деитаграммпохо доменного протокола Unix Эго модификация листинга 8 3
Листинг 14.6. Эхо-клиент дейтаграммного доменного протокола Unix unixdomain/unixdgcli01 c
include unp h
int
main(int argc char **argv)
{
int sockfd
struct sockaddr_un cliaddr servaddr
sockfd = Socket(AFJOCAL SOCK_DGRAM 0)
bzeroC&cliaddr sizeof(cliaddr))/* связывание сокета с адресом */
cl 1 addr sun_fann ly = AF_LOCAL
strcpy(cliaddr sun_path tmpnani(NULL))
1j Bind(sockfd (SA *) &cl1addr sizeof(cliaddr))
12 bzero(&servaddr sizeof(servaddr)) /* заполняем структуру адреса сокета сервера */
servaddr sun_family = AF_LOCAL
strcpyCservaddr sun_path UNIXDG_PATH)
dg_cli(stdin sockfd. (SA *) &servaddr sizeof(servaddr))
exit(0)
}
Структурой адреса сокета, содержащей адрес сервера, теперь будет структура sockaddr un Мы также размещаем в памяти одну из этих структур, чтобы она содержала адрес клиента, о чем мы расскажем далее.
Первый аргумент функции socket — эю AF_LOCAL.
8-11 В отличие от клиента UDP, при использовании дейтафаммпого доменного про- jокола Unix требуется явно связать с помощью функции bind полное имя с нашим сокетом, чтобы сервер имел полное имя, на которое он мог бы отправить (вой ответ Мы вызываем функцию tmpnam, чтобы получить уникальное полное имя, с которым мы затем при помощи функции bi nd свяжем наш сокет Вспомните из раздела 14 4, что при отправке дейтаграммы на неприсоединенный дента- граммннй доменный сокет Unix iic происходит неявного связывания полного имени с сокетом. Следовательно, если мы опустим этот шаг, вызов сервером функции recvfrom в функции dg_echo возвращает пустое полное имя, что затем приведет к ошибке, когда сервер вызовет функцию sendto
12-14 Код для заполнения структуры адреса сокета заранее известным полным именем идентичен коду, представленному ранее для сервера.
Функция dg_cl 1 остается той же, что и раньше (см. листинг 8.4).
Передача дескрипторов
Когда нам требуется передать дескриптор от одного процесса другому, обычно мы выбираем одно из двух решений:
Дочерний процесс использует все открытые дескрипторы совместно с родительским процессом после вызова функции fork.
Все дескрипторы обычно остаются открытыми при вызове функции ехес.
В первом случае процесс открывает дескриптор, вызывает функцию fork, а затем родительский процесс закрывает дескриптор, позволяя дочернему процессу с ним работать. При этом открытый дескриптор передается от родительского процесса дочернему. Но нам также хотелось бы, чтобы у дочернего процесса была возможность открывать дескриптор и передавать его обратно родительскому процессу.
Современные системы Unix предоставляют способ передавать любой открытый дескриптор от одного процесса любому другому процессу. При этом вовсе не обязательно, чтобы процессы были родственными, как родительский и дочерний процессы. Эта технология требует, чтобы мы сначала создали между двумя процессами доменный сокет Unix и затем использовали функцию sendmsg для отправки специального сообщения через этот доменный сокет. Ядро обрабатывает это сообщение специальным образом, передавая открытый дескриптор от отправителя получателю.
ПРИМЕЧАНИЕ
Передача ядром 4.4BSD огкрытого дескриптора но домешюму сокету Unix описывается в главе 18 [95].
SVR4 использует другую технологию внутри ядра для передачи открытого дескриптора: команды I SENDFD и I_RECVFD функции ioctl, описанные в разделе 15.5.1 [93]. Но процесс все же имеет возможность доступа к указанному свойству ядра за счет доменного сокета Unix. В этой книге мы описываем применение доменных сокетов Unix для передачи открытых дескрипторов, поскольку это наиболее переносимая технология программирования: она работает как с Беркли-ядрами, так н с SVR4, в то время как команды I_SENDFD н I RECVFD функции ioctl работают только в SVR4.
Технология 4.4BSD позволяет передавать множество дескрипторов с помощью одиночной функции sendmsg, в то время как технология SVR4 передаст за один раз юлько один дескриптор. Во всех наших примерах один дескриптор передается за один раз.
Шаги при передаче дескриптора между процессами будут такими:
Создание доменного сокета Unix или потокового сокета, или дейтаграммного сокета.
Если целью является породить с помощью функции fork дочерний процесс, с тем чтобы дочерний процесс открыл дескриптор и передал его обратно родительскому процессу, родительский процесс может вызвать функцию socketpai г для создания потокового канала, который может использоваться для передачи дескриптора.〃」,‘
Если процессы не являются родственными, сервер должен создать потоковый доменный сокет Unix, связать его при помощи функции bind с полным именем, тем самым позволяя клиенту соединиться с этим сокетом при помощи функции connect. Затем клиент может отправить запрос серверу для открытия некоторого дескриптора, а сервер может передать дескриптор обратно через доменный сокет Unix. Как альтернатива между клиентом и сервером может также использоваться дейтаграммный доменный сокет Unix, однако преимущества этого способа невелики, к тому же существует возможность игнорирования дейтаграммы. Далее в примерах этой главы мы будем использовать потоковый сокет между клиентом и сервером.
Один процесс открывает дескриптор при помощи вызова любой из функций Unix, возвращающей дескриптор, например open, pi ре, mkfi fo, socket или accept. От одного процесса к другому можно передать дескриптор любого типа, поэтому мы называем эту технологию «передачей дескриптора», а ие «передачей дескриптора файла».
Отправляющий процесс строит структуру msghdr (см. раздел 13.5), содержащую дескриптор, который нужно передать. В Posix.lg определено, что дескриптор должен отправляться как вспомогательные данные (элемент msg_control структуры msghdr, см. раздел 13.6), ио более старые реализации используют элемент msg_accri ghts. Отправляющий процесс вызывает функцию sendmsg для отправки дескриптора через доменный сокет Unix, созданный на шаге 1. На этом этапе мы говорим, что дескриптор находится «в полете». Даже если отправляющий процесс закроет дескриптор после вызова функции sendmsg, ио до вызова принимающим процессом функции recvmsg, дескриптор остается открытым для принимающего процесса. Отправка дескриптора увеличивает счетчик ссылок дескриптора на единицу.
Принимающий процесс вызывает функцию recvmsg для получения дескриптора через доменный сокет Unix, созданный на шаге 1. Номер дескриптора в принимающем процессе может отличаться от номера дескриптора в отирав- ляющем процессе. Передача дескриптора — это не передача номера дескриптора. Этот процесс включает создание нового дескриптора в принимающем процессе, который ссылается на ту же запись таблицы файлов в ядре, что и дескриптор, отправленный отправляющим процессом.
Клиент и сервер должны располагать некоторым протоколом уровня приложения, с тем чтобы получатель дескриптора имел информацию о времени его появления. Если получатель вызывает функцию recvmsg, не выделив места в памяти для получения дескриптора, и дескриптор передается как готовый для чтения, то передаваемый дескриптор закрывается [105, с. 518]. Кроме того, нужно избегать установки флага MSG_PEEK в функции recvmsg, если предполагается получение дескриптора, поскольку в этом случае результат непредсказуем.
Пример передачи дескриптора
Теперь мы представим пример передачи дескриптора. Мы напишем программу под названием mycat, которой в качестве аргумента командной строки передается полное имя файла. Эта программа открывает файл и копирует его в стандартный поток вывода. Но вместо вызова обычной функции Unix open мы вызываем нашу
собственную функцию my_open. Эта функция создает потоковый канал и вызывает функции fork и ехес для запуска другой программы, открывающей нужный файл. Эта программа должна затем передать дескриптор обратно родительскому процессу по потоковому каналу.
На рис. 14.1 показан первый шаг: наша программа mycat после создания потокового канала при помощи вызова функции socketpair. Мы обозначили два дескриптора, возвращаемых функцией socketpai г, как [0] и [1].
mycatmycat
mycat
mycat
Рис. 14.1. Программа mycat после создания потокового канала при использовании функции socketpair
Затем процесс взывает функцию fork, и дочерний процесс вызывает функцию ехес для выполнения программы openfi 1 е. Родительский процесс закрывает дескриптор [1], а дочерний процесс закрывает дескриптор [0]. (Нет разницы, на каком конце потокового канала происходит закрытие. Дочерний процесс мог бы закрыть [1], а родительский — [0].) При этом получается схема, показанная на рис. 14.2.
exit (состояние выхода)exit (состояние выхода)openfileopenfileРис. 14.2. Программа mycat после запуска программы openfileРис. 14.2. Программа mycat после запуска программы openfile
exit (состояние выхода)
exit (состояние выхода)
openfile
openfile
Рис. 14.2. Программа mycat после запуска программы openfile
Рис. 14.2. Программа mycat после запуска программы openfile
Родительский процесс должен передать программе openfile три фрагмента информации: полное имя открываемого файла, режим открытия (только чтение, чтение и запись или только запись) и номер дескриптора, соответствующий его концу потокового канала (который мы обозначили [1].) Мы выбрали такой способ передачи этих трех элементов, как ввод аргументов командной строки при вызове функции ехес. Альтернативным способом будет отправка этих элементов в качестве данных по потоковому каналу. Программа отправляет обратно открытый дескриптор по потоковому каналу и завершается. Статус выхода программы сообщает родительскому процессу, смогли файл открыться, и если нет, то какого типа ошибка произошла.
Преимущество выполнения дополнительной программы для открытия файла заключается в том, что за счет приравнивания привилегий пользователя к при-
вилегиям владельца файла мы получаем возможность открывать те файлы, которые мы не имеем права открывать в обычной ситуации. Эта программа позволяет расширить концепцию обычных прав доступа Unix (пользователь, группа и все остальные) н включить любые формы проверки прав доступа.
Мы начнем с нашей программа mycat, показанной в листинге 14.7.
Листинг 14.7. Программа mycat: копирование файла в стандартный поток вывода //urnxdomain/тусat с
1 #include "unp h"
2 mt щу_ореп(const char *, int);
mt
main(int argc. char **argv)
{
intfd. n,
charbuff[BUFFSIZE]:
if (argc !=» 2)
err_quit("usage mycat <pathname>");
if ( (fd ■ my_open(drgv[l]. O_RDONLY)) < 0)
err_sys("cannot open %s”. argv[l]).
while ( (n = Read(fd. buff. BUFFSIZE)) > 0)
W门te(STDOUT_FILENO. buff. n).
exit(0).
}
Если мы заменим вызов функции ту_ореп вызовом функции open, эта простая программа всего лишь скопирует файл в стандартный поток вывода.
Функция my_open, показанная в листинге 14.8, должна выглядеть для вызывающего процесса как обычная функция Unix open. Она получает два аргумента — полное имя и режим открытия (например, O_RDONLY обозначает, что файл доступен только для чтения), открывает фай刀 н возвращает дескриптор.
Листинг 14.8. Функция my_open: открытие файла и возвращение дескриптора //um xdomain/myopen с
1 #include "unp h"
int
my_open(const char *pathname, mt mode)
4「
intfd, sockfd[2], status.
pid_tchildpid,
charc, argsockfd[10], argmode[10].
Socketpair(AF_LOCAL. SOCK_STREAM. 0. sockfd).
if ( (childpid = ForkO) == 0) {/* дочерний процесс */
Close(sockfd[0])
snpnntfCargsockfd. sizeof(argsockfd). м%^. sockfd[l]).
snprintf(argmode. sizeof(argmode) :d" mode).
exec1(" /openfile", "openfile". argsockfd. pathname argmode.
14 (char *) NULL).
Листинг 14.8. (продолжение)
15 err sys('execl error").
16}-
17/* родительский процесс - ожидание завершения дочернего процесса */
18 Close(sockfd[l]) /* закрываем конец, который мы не используем */
19 Wditpid(childpid. &status, 0).
20 if (WIFEXITED(status) == 0)
21err_quit("child did not terminate").
22 if ( (status = WEXITSTATUS(status)) == 0)
23Read_fd(sockfd[0], &c. 1 &fd),
24 else {一
25 errno = status. /* установка значения errno в статус дочернего процесса */
26 fd н <1,
27}
Создание потокового канала
8 Функция socketpai г создает потоковый канал. Возвращаются два дескриптора: sockfd[0] и sockfd[l]. Это состояние, которое мы показали на рис. 14.1.
Функции fork и ехес
9-16 Вызывается функция fork, после чего дочерний процесс закрывает один конец потокового канала. Номер дескриптора другого конца потокового канала помещается в массив argsockfd, а режим открытия помещается в массив argmode. Мы вызываем функцию snprintf, поскольку аргументы функции ехес должны быть символьными строками. Выполняется программа openfile. Функция execl возвращает управление только в том случае, если она встретит ошибку. При удачном выполнении начинает выполняться функция main программы openfile.
Родительский процесс в ожидании завершения дочернего процесса
17-22 Родительский процесс закрывает другой конец потокового канала и вызывает функцию waitpid для ожидания завершения дочернего процесса. Статус завершения дочернего процесса возвращается в переменной status, и сначала мы проверяем, что программа завершилась нормально (то есть не была завершена из-за возникновения какого-либо сигнала). Затем макрос WEXITSTATUS преобразует статус завершения в статус выхода, значение которого должно находиться между О и 255. Мы вскоре увидим, что если при открытии необходимого файла программой openfi 1 е происходит ошибка, то эта программа завершается, причем статус ее завершения равен соответствующему значению переменной errno.
Получение дескриптора
23 Наша функция redd_fd, которую мы показываем в следующем листинге, получает дескриптор потокового канала. Кроме получения дескриптора мы считываем 1 байт данных, но ничего с этими данными не делаем.
ПРИМЕЧАНИЕ
При огправке и получении дескриптора по потоковому каналу мы всегда отправляем как минимум 1 байт данных, даже если получагель никак эти данные не обрабатывает. Иначе получатель не сможет распознать, что значит пулевое возвращаемое значение из функции read_fd: orcyiствие да!111ых (по, возможно, есть дескриптор) или конец(|)айла.
В листинге 14.9 показана функция read_fd, вызывающая функцию recvmsg для получения данных и дескриптора через доменный сокет Unix. Первые три аргумента этой функции те же, что и для функции read, а четвертый (recvfd) является указателем на целое число. После выполнения этой функции recvfd будет указывать на полученный дескриптор.
Листинг 14.9. Функция read_fd: получение данных и дескриптора
//lib/read_fd с
#1 nd ude"unp h"
ssize^t
read_fd(wt fd. void *ptr, size t nbytes. int *recvfd)
{一一
struct msghdr msg,
struct lovec iov[l],
ssize_t n
mtnewfd.
#ifdef HAVE_MSGHDR_MSG„CONTROL
union {
struct cmsghdr cm,
char control[CMSG_SPACE(s1zeof(Int))];
} control_un;-
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_acc「ightslen = sizeof(int).
20 #endif
21msgmsg_name = NULL.
22msgmsg_namelen = 0,
23wv[0] iov_base = ptr,
24 iov[0j iov_len = nbytes:,
25msgmsg_iov = iov,
26msgmsg_iovlen = 1.
27 if ( (n = recvmsg(fd. &msg. 0)) <- 0)
28 return (n).
29 #ifdef HAVE_MSGHDR_MSG_CDNTRDL
30 if ( (cmptr = CMSG_FTRSTHDR(&msg)) != NULL &&
31mptr->cmsg_len == CMSG_LEN(sizeof(mt))){
32if (cmptr->cmsg_level T= SOL_SOCKET)
33 err_quit("control level 片飞"一SOCKET”).
34if (cinptr->crnsg__type〔= SCM_RIGHTS)
35err_quit("control type i= SCM_RIGHTS").
36 *recvfd = *((mt *) CMSG_DATA(cmptr))
37} else~
38 *recvfd = -1./* дескриптор не был передан */
39 #else
40 if (msg msg_accnghtslen == sizeof(int))
41 *recvfd = newfd.
42 else
Листинг 14.9 (продолжение)
*recvfd =・1./* дескриптор не был передан */
#endif
return (n).
}
9-26 Эта функция должна работать с обеими версиями функции reevmsg: с элементом msg_control и с элементом msg^accnghts. Наш заголовочный файл config.h (см. листинг Г.2) определяет константу HAVE_MSGHDR_MSG_CONTROL, если поддерживается версия функции reevmsg с msg_control.
Проверка выравнивания буфера msg_control
10-13 Буфер msg_control должен быть выровнен в соответствии со структурой msghdr. Просто выделить в памяти массив типа char недостаточно. Здесь мы объявляем объединение, состоящее из структуры emsghdr и символьного массива, что гарантирует необходимое выравнивание массива. Возможно и другое решение — вызвать функцию та 11 ос, но это потребует освобождения памяти перед завершением функции.
27-45 Вызывается функция reevmsg. Если возвращаются вспомогательные данные, их формат будет таким, как показано на рис. 13.4. Мы проверяем, что длина, уровень и тип верны, затем получаем вновь созданный дескриптор и возвращаем его через указатель вызывающего процесса reevfd. Макрос CMSG_DATA возвращает указатель на элемент cmsg_data объекта вспомогательных данных как указатель на элемент типа unsigned char. Мы преобразуем его к указателю на элемент типа int и получаем целочисленный дескриптор, на который указывает этот указатель.
Если поддерживается более старый элемент msg_accmghts, то длина должна быть равна размеру целого числа, а вновь созданный дескриптор возвращается через указатель reevfd вызывающего процесса.
В листинге 14.10 показана программа openfile. Она получает три аргумента командной строки, которые должны быть переданы, и вызывает обычную функцию open.
Листинг 14.10. Функция openfile: открытие файла и передача дескриптора обратно
//unixdoniain/openfi 1 е с
include 'unp h"
mt
—(int argc. char **argv)
{
mtfd
ssize_t n
7 if (argc i= 4)
8err_quit('openfile <sockfd#> <fi1ename> <mode>"):
9 if ( (fd = open(argv[2], atoi(argv[3]))) < 0)
10 exit((errno > 0) ? errno 255)
11 if ( (n = wnte_fd(atoi(argv[lj) "“,1. fd)) < 0)
12 exit((errno >~0) 7 errno 255)
exit(0).
I
Аргументы командной строки
7-12 Поскольку два из трех аргументов командной строки были превращены в символьные строки функцией my__open, они преобразуются обратно в целые числа при помощи функции atoi.
Открытие файла
9-10 Файл открывается с помощью функции open. Если встречается ошибка, статус завершения этого процесса содержит значение переменной errno, соответствующее ошибке функции open.
Передача дескриптора обратно
П-12 Дескриптор передается обратно функцией wnte_fd, которую мы покажем в следующем листинге. Затем этот процесс завершается, но ранее в этой главе мы сказали, что отправляющий процесс может закрыть переданный дескриптор (это происходит, когда мы вызываем функцию exit), поскольку ядро знает, что дескриптор находится в состоянии передачи («в полете»), и оставляет его открытым для принимающего процесса.
ПРИМЕЧАНИЕ
Статус выхода должен лежать в пределах от 0 до 255. Максимальное значение переменной еггпо — около 150. Альтернативный способ, при котором не требуется, чтобы значение переменной еггпо было меньше 256, заключается в том, чтобы передать обратно указание на ошибку в виде обычных данных при вызове функции sendmsg.
В листинге 14.11 показана последняя функция, write_fd, вызывающая функцию sendmsg для отправки дескриптора (и, возможно, еще каких-либо данных, которые мы не используем) через доменный сокет Unix.
Листинг 14.11. Функция wntejd: передача дескриптора при помощи вызова < функции sendmsg
//lib/wnte_fd с
#include "unp h"
ssize_t
w门te二fd(int fd void *ptr. size_t nbytes, mt sendfd)
{'—
struct msghdr msg,
struct iovec iov[l],
#ifdef HAVE—MSGHDR—MSG一CONTROL
union {
struct emsghdr cm.
char control[CMSG_SPACE(sizeof(Int))]:
} control_un. 一
struct emsghdr *cmptr,
msg msg_control = control_un control
msg msg_controllen = sizeof(control_un control).
emptr = CMSG_FIRSTHDR(&msg)
cmptr->cmsg_Ten = CMSG_LEN(s1zeof(int)).
cmptr->cmsgjevel = SOL_SOCKET.
Листинг 14.11 (продолжение)
19*((mt *) CMSG_DATA(cmptr)) = sendfd,
20 #else-
21 msg msg^accrights = (caddr^t) & sendfd,
22 msg msg_accnghtslen = sizeof(int),
23 #endif
msg msg_name = NULL.
msg msg^namelen * 0.
wv[0] iov_base - ptr:
iov[0] iov」en = nbytes;
msg msgjov = iov,
msg msg_iovlen = 1.
return ?sendmsg(fd. &msg, 0)).
}
Как и в случае функции read^fg, эта функция обрабатывает либо вспомогательные данные, либо права доступа, которые предшествовали вспомогательным данным в более ранних реализациях. В любом случае инициализируется структура msghdr и затем вызывается функция sendmsg.
В разделе 25.7 мы приводим пример передачи дескриптора, в котором участвуют неродственные (unrelated) процессы, а в разделе 27.9 — пример, где задействованы родственные процессы. В них мы будем использовать функции read_fd и write_fd, которые только что описали.
14.8. Получение информации об отправителе
struct fcred {
uid_t
fc_ruid.
/*
gid^t
fc[rgi“
/*
char
fcjogmCMAXLOGNAME].
/*
uid_t
fc 二 ind.
/*
short
fc_ngroups.
/*
gid_t
fc_groups[NGROUPS].
/*
}. 一
#define
fc_gid fc_groups[0]
/*
На рис. 13.4 мы показали другой тип информации, передаваемой через доменный сокет Unix в виде вспомогательных данных: информацию об отправителе, которая передается с помощью структуры fcred, определяемой путем включения заголовочного файла <sys/ucred.h>.
действующий идентификатор пользователя */ действующий групповой идентификатор */ имя setlogin()*/
идентификатор пользователя */
количество групп */
дополнительные групповые идентификаторы */
групповой идентификатор */
Обычно MAXLONGNAME и NGROUPS имеют значение 16. Значение fc_ngroups равно как минимум 1, а первым элементом массива является идентификатор группы.
ПРИМЕЧАНИЕ
Эту структуру мы фактически определяем как структуру ucred внутри структуры fcred с именами, определенными при помощи директивы #define, с тем чтобы получить вид единой структуры. Мы показываем «логическое» определение этой структуры.
Такая возможность появилась только в BSD/OS 2.1. Мы описываем ее, несмотря па то что она не является широко распространенной, поскольку это важное, хотя и простое дополнение доменных протоколов Unix. Когда клиент и сервер связываются с помощью этих протоколов, серверу часто бывает необходим способ точно узнать, кто является клиентом, чтобы убедиться, что клиент имеет право запрашивать определенный сервис.
Указанная информация всегда доступна через доменный сокет Unix при следующих условиях:
Информация о пользователе отправляется в качестве вспомогательных данных при пересылке через доменный сокет Unix, только если получатель включил параметр сокета LOCAL_CREDS. Значение level для этого параметра (см. раздел 7.2) равно нулю.
' На сокете дейтаграмм идентифицирующие данные сопровождают каждую дейтаграмму. На потоковом сокете они отправляются однократно — при первой отправке данных.
纟 Идентифицирующие данные не могут быть отправлены вместе с дескриптором, то есть с конкретным сообщением можно отправить только один из двух типов вспомогательных данных.
ПРИМЕЧАНИЕ
Это ограничение, присущее реализации BSD/OS. В общем случае у пас должна быть возможность передавать множество типов вспомогательных данных в одном вызове функции send msg, поскольку каждый объект вспомогательных данных имеет свой собственный тин и длину (см. рис. 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 так, чтобы он запрашивал идентифицирующие
данные клиента. В листинге 14.12 показана новая функция, read^cred, аналогич- иая функции read, но возвращающая также структуру fcred, содержащую идентифицирующие данные отправителя.
Листинг 14.12. Функция read_cred: чтение и возвращение идентифицирующих данных отправителя
//umxdomain/readcred с
#include"unp h"
#include<sys/paramh>
#includevsys/ucredh>
ssize_t
readme red (mt fd. void *ptr. size t nbjytes. struct fcred *fcredptr)
{-
struct msghdr msg.
struct iovec wv[l].
ssize_t n.
union-{
struct emsghdr cm. > ,*
char control[CMSG^SPACE($l2epf(struct fcred))];
} control_un.
struct emsghdr *cmptr;
msg msg_control = control^un.control;
msg msg二controller! - si zeof(control^un. control);
17msgmsg_name = NULL.
18msgmsg_namelen = 0.
19iov[0] iov_base « ptr.
20wv[0] iov_len = nbytes;
21msgmsg_iov = iov
22msgmsgjovlen = 1
23 7f ( (n = recvmsg(fd. &msg. 0)) < 0)
24 return (n).
25 if (fcredptг) {
26 if (msg msg control 1 en > sizeofCstruct emsghdr)) {
emptr = CMSG_FIRSTHDR(&msg).
if(cmptr->cmsg_len 『CMSG_LEN(sizeof(struct fcred)))
err_quit("control length = %d" cmptr->cmsg_len).
if(cmpEr->crnsg_level〔= SOL_SOCKET)
err_quit("control level 匸 SOL_SOCKET")
if(emptr->cmsg_type l= SCMJREDS)
err_quit(Hcontrol type 匸 SCM_CREDS").
34memepy(fcredptг. CMSG_DATA(cmptr) sizeof(struct fcred))
35} else
36 bzero(fcredptr sizeof(struct fcred)) /* ничего не возвращается */
37}
38 return (n),
39 }
4-5 Первые три аргумента идентичны аргументам функции read, а четвертый аргумент —это указатель на структуру fcred, которая будет заполнена. Формат возвращаемых вспомогательных данных показан на рис. 13.4.
26-36 Если данные были переданы, проверяются длина, уровень и тип вспомогательных данных, и результирующая структура копируется обратно вызывающему
процессу. Если никаких идентифицирующих данных не было передано, мы обнуляем структуру. Поскольку число групп (fc_ngroups) всегда равно 1 или больше, нулевое значение указывает вызывающему процессу, что ядро не возвратило никаких идентифицирующих данных.
Функция mai п для нашего эхо-сервера (см. листинг 14.3) остается неизменной. В листинге 14.13 показана новая версия функции str_echo, полученная путем модификации листинга 5.2. Эта функция вызывается дочерним процессом после того, как родительский процесс принял новое клиентское соединение и вызвал функцию fork.
Листинг 14,13. Функция str_echo, запрашивающая идентифицирующие данные клиента
//umxdomam/strecho с
1^include"unp h"
include<sys/parani
#include<sys/ucred
ssize_t read_cred(int
void
h>h>void *, sizjt. struct fcred *),h>h>void *, sizjt. struct fcred *),67891011121314151617181920212223242526276789101112131415161718192021222324252627str_echo(int sockfd)
h>
h>
void *, sizjt. struct fcred *),
h>
h>
void *, sizjt. struct fcred *),
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{_ ssize_t n const~int on = 1. charline[MAXLINE],
struct fcred cred.
SetsockoptCsockfd. 0. LOCALJREDS. &on sizeof(on)), if ( (n = read_cred(sockfd. NULL, 0. &cred)) < 0) err_sys("read_cred error'1)
if (cred fc_ngroups = 0) pnntf(M(no credentials returned)\en") else {
printfC'real user ID = Zd\en". cred fc_ruid). prwtfCreal group ID =象Aen” cred fc_rgid). printf(**login name = %-*s\en' MAXLOGNAME cred fcjogln): prwtf("effective user ID = Xd\en" cred fc_uid) printf("effective group ID = %d\en"・ cred fc_gid).
p门ntf("%d supplementary groups ". cred fc_ngroups - 1” for (n - 1 n v cred fc_ngroups, n++) /* [0] is the egid */ p门ntf(“ *d". cred fc_groups[n]).
p门ntf("\en”)
}
28 for (. ) {
29 if ( (n = Readline(sockfd line MAXLINE)) = 0)
30 return/* соединение закрывается удаленньГм концом */
31Wnten(sockfd, line n)
32}
33 }
12 Для присоединенного сокета включается параметр сокета LOCAL一CREDS.
13-14 Наша функция read_cred вызывается впервые. Мы задаем нулевую ”刀!^,乎как нас интересуют только вспомогательные данные.丨 *
17-27 Если идентифицирующие данные возвращаются, они выводятся.
28-32 Оставшаяся часть цикла не меняется. Этот код считывает строки от клиента и затем отправляет их обратно клиенту.
Наш клиент, представленный в листинге 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).
Резюме
Доменные сокеты 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 — другой пример, когда дескриптор передается от родительского процесса дочернему.
Упражнения
Что произойдет, если доменный сервер Unix вызовет функцию unlink после вызова функции bind?
Что произойдет, если доменный сервер Unix при завершении не отсоединит с помощью функции unlink свое известное полное имя, а клиент будет пытаться с помощью функции connect соединиться с сервером через некоторое время после того, как сервер завершит работу?
Скомпилируйте не зависящие от протокола клиент и сервер времени и даты (см. листинги 11.8и 11.11)и запустите их, задав доменный сокет Unix (см. раздел 11.6). Что происходит? Как вы можете исправить эту ситуацию?
Измените листинг 11.3 так, чтобы после того, как будет выведен адрес протокола собеседника, вызывалась бы функция sleep(5), а также чтобы вывести число байтов, возвращаемых функцией read всякий раз, когда она возвращает положительное значение. Измените листинг 11.6 так, чтобы для каждого байта результата, отправляемого клиенту, вызывалась функция write. (Мы обсуждаем подобные изменения в решении упражнения 1.5.) Запустите клиент и сервер на одном узле, используя TCP. Сколько байтов считывает клиент с помощью функции read?
Запустите клиент и сервер на одном узле, используя доменный сокет Unix. Изменилось ли что-нибудь?
Теперь для сервера вместо функции write вызовите функцию send и задайте флаг MSG_EOR (чтобы выполнить это упражнение, вам нужно использовать Беркли-реализацию). Запустите клиент и сервер на одном узле, используя доменный сокет Unix. Изменилось ли что-нибудь?
Напишите программу, определяющую значения, показанные в табл. 4.6. Один из подходов — создать потоковый канал и затем с помощью функции fork разветвить родительский и дочерний процессы. Родительский процесс входит в цикл for, увеличивая на каждом шаге значение backlog от 0 до 14. Каждый раз при прохождении цикла родительский процесс сначала записывает значение backlog в потоковый канал. Дочерний процесс читает это значение, создает прослушиваемый сокет, связанный с адресом закольцовкп, и присваивает backlog считанное значение. Затем дочерний процесс делает запись в потоковый канал просто для того, чтобы сообщить родительскому процессу о своей готовности. Затем родительский процесс пытается установить как можно больше соединений, задав предварительно аргумент функции alarm равным 2 секундам, поскольку при достижении предельного значения backlog вызов функции connect заблокируется, и отправляет еще раз сегмент SYN. Дочерний процесс никогда не вызывает функцию accept, что позволяет ядру установить в очередь все соединения с родительским процессом. Коцца истекает время ожидания родительского процесса (аргумент функции al arm, в данном случае 2 секунды), по счетчику цикла он может определить, какая по счету функция connect соответствует предельному значению backl од. Затем родительский процесс закрывает свои сокеты и пишет следующее новое значение в потоковый канал для дочернего процесса. Когда дочерний процесс считывает новое значение, он закрывает прежний прослушиваемый сокет н создает новый, заново начиная процедуру.
Проверьте, вызывает ли пропуск вызова функции bind в листинге 14.6 ошибку сервера.
*'>j
ГЛАВА 15
Неблокируемый ввод-вы вод
Введение
По умолчанию сокеты блокируют выполнение процесса. Это означает, что когда мы вызываем на сокете функцию, которая не может выполниться немедленно, наш процесс переходит в «спящее» состояние и ждет, когда будет выполнено определенное условие. Мы можем разделить функции сокетов, способные вызвать блокирование, на четыре категории.
Операции ввода: функции read, readv, recv, recvfrom и recvmsg. Если мы вызываем одну из этих функций ввода для блокируемого сокета TCP (а по умолчанию такой сокет является блокируемым) и в приемном буфере сокета отсутствуют данные, то сокет вызывает переход в спящее состояние на то время, пока не придут какие-нибудь данные. Поскольку TCP является протоколом байтового потока, из этого состояния мы выйдем, когда придет «хоть сколько- нибудь» данных: это может быть одиночный байт данных, а может быть и целый сегмент данных TCP. Если мы хотим ждать до тех пор, пока не будет доступно определенное фиксированное количество данных, мы вызываем нашу функцию readn (см. листинг 3.8) или задаем флаг MSG_WAITALL (см. табл. 13.1). Поскольку UDP является протоколом дейтаграмм, то если приемный буфер блокируемого сокета UDP пуст, мы переходим в состояние ожидания и находимся в нем до тех пор, пока не придет дейтафамма UDP.
В случае неблокируемого сокета при невозможности удовлетворить условию операции ввода (как минимум 1 байт данных для сокета TCP или целая дейтаграмма для сокета UDP) возврат происходит немедленно с ошибкой EWOULDBLOCK.
Операции вывода: функции write, writev, send, sendto и sendmsg. В отношении сокета TCP в разделе 2.9 мы сказали, что ядро копирует данные из буфера приложения в буфер отправки сокета. Если для блокируемого сокета недостаточно места в буфере отправки, процесс переходит в состояние ожидания до тех пор, пока место не освободится.
В случае неблокируемого сокета TCP при недостатке места в буфере отправки завершение происходит немедленно с ошибкой EWOULDBLOCK. Если в буфере отправки сокета есть место, возвращаемое значение будет представлять количество байтов, которое ядро смогло скопировать в буфер (зто называется частичным копированием — skort count).
В разделе 2.9 мы также сказали, что на самом деле буфера отправки UDP не существует. Ядро только копирует данные приложения и перемещает их вниз
по стеку, добавляя к данным заголовки UDP и IP. Следовательно, операция вывода па блокируемом сокете UDP (каким он является по умолчанию) никогда не заблокируется.
Прием входящих соединений: функция accept. Если функция accept вызывается для блокируемого сокета и новое соединение недоступно, процесс переводится в состояние ожидания.
Если функция accept вызывается для неблокируемого сокета и новое соединение недоступно, возвращается ошибка EWOULDBLOCK.
Инициирование исходящих соединений: функция 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) определяют эти две ошибки с одним и тем же кодом (iфовсрьте свой системный заголовочный файл <sys/errno.h>), поэтому не важно, какой из них использовать. В нашем тексте мы используем ошибку EWOULDBLOCK, как определяется в Posix. 1g.
В разделе 6.2 мы представили различные модели ввода-вывода и сравнили неблокируемый ввод-вывод с другими моделями. В этой главе мы покажем примеры четырех типов операций и разработаем новый тип клиента, аналогичный web-клиенту, инициирующий одновременно множество соединений TCP при помощи неблокируемой функции connect.
Неблокируемые чтение и запись: функция str_cli (продолжение)
Мы снова возвращаемся к нашей функции str_cl 1, которую мы обсуждали в разделах 5.5 н 6.4. Последняя ее версия, задействущая функцию select, продолжает использовать блокируемый ввод-вывод. Например, если в стандартном устрой-
стве ввода имеется некоторая строка, мы читаем ее с помощью функции fgets и затем отправляем серверу с помощью функции wri ten. Но вызов функции wri ten может вызвать блокирование процесса, если буфер отправки сокета полон. В то время как мы заблокированы в вызове функции wri ten, данные могут быть доступны для чтения из приемного буфера сокета. Аналогично, когда строка ввода доступна из сокета, мы можем заблокироваться в последующем вызове функции fputs, если стандартный поток вывода работает медленнее, чем сеть. Наша цель в данном разделе — создать версию этой функции, использующую неблокируемый ввод-вывод. Блокирование будет предотвращено, благодаря чему в это время мы сможем сделать еще что-то полезное.
К сожалению, добавление неблокируемого ввода-вывода значительно усложняет управление буфером функции, поэтому мы будем представлять функцию частями. Мы также заменим вызовы функций из стандартной библиотеки ввода- вывода на обычные read и write. Это даст возможность отказаться от функций стандартной библиотеки ввода-вывода с неблокируемьши дескрипторами, так как их применение может привести к катастрофическим последствиям.
Мы работаем с двумя буферами: буфер to содержит данные, направляющиеся из стандартного потока ввода к серверу, а буфер fr — данные, приходящие от сервера в стандартный поток вывода. На рис. 15.1 представлена организация буфера to и указателей в буфере.
stdin
[ toiptr&to[MAXLINE]
Уже отправленные данные
Данные, которые нужно отправить серверу
Пространство для считывания данных из стандартного устройства ввода
tooptr ▼ socket
Рис. 15.1- Буфер, содержащий данные из стандартного потока ввода, идущие к сокету
Указатель toi ptr указывает на следующий байт, в который данные могут быть считаны из стандартного потока ввода. Указатель tooptr указывает на следующий байт, который должен быть записан в сокет. Число байтов, которое может
Уже отправленные данные
Данные, которые нужно отправить на стандартное устройство вывода
Пространство для считывания данных из стандартного устройства ввода
socket
•friptr&fг[MAXLINE]
froptr ▼
stdout
Рис. 15.2. Буфер, содержащий данные из сокета, идущие к стандартному \/г,тппйг*ти\/ ouicsnnn
быть считано из стандартного потока ввода, равно &to[MAXLINE] минус toiptr. Как только значение tooptr достигает toiptr, оба указателя переустанавливаются на начало буфера.
На рис. 15.2 показана соответствующая организация буфера fr. В листинге 15.11 представлена первая часть функции.
Листинг 15.1. Функция str_cli: первая часть, инициализация и вызов функции
/ /попЫ ock /st rc 11 nonb с
1 #i nc 1 tide"unp h"
2345678923456789void
23456789
23456789
str_cl】(FILE *fp, int sockfd)
int maxfdpl. val. stdineof: ssize_t n. nwritten, fd_set rset, wset.
char to[MAXLINE], fr[MAXLINE].
char *toiptr, *tooptr, *friptr. *froptr;
val = Fcntl(sockfd, F_GETFL, 0).
Fcnt1(sockfd. F_SETFL? val | O_NONBLOCK):
val = Fcntl(STDIN_FILENO. F_GETFL. 0).
Fcntl(STDINJILENO. F_SETFL? val |。一NONBLOCK):
val = Fcntl(STDOUTJILENO. F_GETFL・ 0).
Fcntl (STDOUTJILENO. F_SETFL. val | O_NONBLOCK):
toiptr = tooptr = to; /* инициализация указателей буфера */
fnptr = froptr = fr.
stdineof = 0
19
maxfdpl = max(max(STDIN__FILENO.
STDOUTJILENO). sockfd) + 1;
20
21
22
23
24
for (..) {
FD_ZERO(&rset).
FD 二 ZERO(&wset).
if (stdineof == 0 && toiptr v FD_SET(STDIN_FILENO. &rset)
&to[MAXLINE])
,/* чтение из стандартного потока ввода */
25
26
27
28
if (friptr < &fr[MAXLINE]) FD_SET(sockfd. &rset).
if (tooptr != toiptr)
FD SET(sockfd. &wset).
/* чтение из сокета */
/* данные для записи в сокет */
29
30
if (froptr •= friptr)
FD_SET(STDOUT_FILENO. &wset),/* данные для записи в стандартный поток
31
Select(maxfdpl. &rset. &wset.
вывода */
NULL. NULL),
Установка неблокируемых дескрипторов
10-15 Все три дескриптора делаются неблокируемыми при помощи функции fcntl: сокет в направлении к серверу и от сервера, стандартный поток ввода и стандартный поток вывода.
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:〃 www.piter.com/download.•
Инициализация указателей буфера
.6-19 Инициализируются указатели в двух буферах и вычисляется максимальный дескриптор. Это значение, увеличенное на единицу, будет использоваться в качестве первого аргумента функции select.
Основной цикл: подготовка к вызову функции select
20 Как и в случае первой версии этой функции, показанной в листинге 6.2, основной цикл функции содержит вызов функции select, за которой следуют отдельные проверки различных интересующих нас условий.
Подготовка интересующих нас дескрипторов
〃一30 Оба набора дескрипторов обнуляются и затем в каждом наборе включается не более двух битов. Если мы еще не прочитали конец файла из стандартного потока ввода и есть место для как минимум 1 байта данных в буфере to, то в наборе флагов чтения включается бит, соответствующий стандартному потоку ввода. Если есть место для как минимум 1 байта данных в буфере fr, то в наборе флагов чтения включается бит, соответствующий сокету. Если есть данные для записи в сокет в буфере to, то в наборе флагов записи включается бит, соответствующий сокету. Наконец, если в буфере fr есть данные для отправки в стандартный поток вывода, то в наборе флагов записи включается бит, соответствующий этому стандартному потоку.
Вызов функции select
31 Вызывается функция sei ect, ожидающая, пока одно из четырех условий не станет истинным. Для этой функции мы не задаем тайм-аута.
Следующая часть нашей функции показана в листинге 15.2. Этот код содержит первые две проверки (из четырех возможных), выполняемые после завершения функции select.
Листинг 15.2. Функция str_cli: вторая часть, чтение из стандартного потока ввода или сокета ~
//nonblock/strcl 1 nonb с
32 if (FD」SSET(STDIN_FILENO, &rset)) {
33 if ( (n = read(S亍DIN—FILENO, toiptr. &to[MAXLINE] - toiptr)) < 0) {
34if (errno != EWOULDBLOCK)
35err_sys("read error on stdin").
36} else if (n == 0) {
37fprintf(stderr, "is EOF on stdin\en". gf_t】me()).
38stdineof = 1./* c stdin все сделано*/
39if (tooptr == toiptr)
40Shutdown(sockfd. SHUT_WR). /* отсылаем FIN */
41} else {
42fpnntf(stderr, "%s. read %d bytes from stdin\en". gf_t】me(). n).
43toiptr += n./* только что полученное из функции read число,*/
44FD SET(sockfd, &wset). /* включаем бит в наборе чтения */
45} ~
46}
47 if (FD_ISSET(sockfd. &rset)) {
48 if ( (п = readCsockfd. friptr. &fr[MAXLINE] - fnptr)) < 0) {
49if (errno != EWOULDBLOCK)
50err_sys("read error on socket"),
51} else if (n == 0) {
52fprintf(stderr, H^s EOF on socket\enh. gf_time()).•
53if (stdineof)~
54return. /* нормальное завершение */
55else
56err_quit("st^cli server terminated prematurely"):
57} else {
58fprintf(stderr, wls read Nd bytes from socket'en"
59gf timeO, n).
60friptr += n. /* только что полученное из функции read число */
61FD_SET(STDOUT_F1LENO, &wset). /* включаем бит в наборе чтения */
62} 一一
63}
Чтение из стандартного потока ввода с помощью функции read
32-33 Если стандартный поток ввода готов для чтения, мы вызываем функцию read. Третий ее аргумент — это количество свободного места в буфере to.
Обработка ошибки
34-35 Если происходит ошибка EWOULDBLOCK, мы ничего нс предпринимаем. Обычно эта ситуация — когда функция select сообщает нам о том, что дескриптор готов для чтения, а функция read возвращает ошибку EWOULDBLOCK — не должна возникать, но тем не менее мы ее обрабатываем.
Возвращение конца файла функцией read
36-40 Если функция read возвращает нуль, мы закончили со стандартным потоком ввода. Флаг stdineof установлен. Если в буфере to больше нет данных для отправки (tooptr равно toiptr), функция shutdown отправляет серверу сегмент FIN. Если в буфере to еще есть данные для отправки, сегмепт FIN не можег быть отправлен до тех пор, пока содержимое буфера не будет записано в сокет.
ПРИМЕЧАНИЕ
Мы выводим в стандартный поток сообщений об ошибках строку, отмечающую конец файла, вместе с текущим временем. Мы покажем, как мы используем этот вывод, после описания функции. Аналогичные вызовы функции fprintf выполняются неоднократно в процессе выполнения нашей функции.
Возвращение данных функцией read
41-45 Когда функция read возвращает данные, мы увеличиваем на единицу toiptr. Мы также включаем бит, соответствующий сокету, в наборе флагов записи, чтобы позже при проверке этого бита в цикле он был включен и тем самым инициировалась бы попытка записи в сокет с помощью функции write.
ПРИМЕЧАНИЕ
Это одно из непростых конструктивных решений, которые приходится принимать при написании кода У пас есть несколько альтернатив Вмесго установка бита в наборе записи мы можем ничего не дсла1Ь, и в этом случае функция select будет проверять возможное! ь записи в сокет, koi да опа будет вы звана в следующим раз Но это гребуст дополнительною прохода цикла и вызова функции select когда мы уже знаем, что у нас ecib данные для записи в сокет Другой вариант — дублировать код, который запись! вас г в сокет, но эю кажется расточитслы 1ым, к тому же это возможный источник ошибки (в случае, если в этой части дублируемо! о кода есть ошибка н мы обнаруживаем и устраняем ее только в одном месте) Наконец, мы можем создать функцию записывающую в сокет, и вызывать эту (функцию вместо дублирования кода, по эта функция должна использовать три локальные переменные совместно с функцией str eh, чго может привести к необходимости сделать)тм переменные глобальными Выбор, сделанный в пашем случае — это резулыаг субъективно! о мнения автора относительно того, какой из описанных трех вариантов предпочгительнсе
Чтение из сокета с помощью функции read
7 63 Эти строки кода аналогичны выражению if, только что описанному для случая, когда стандартный поток ввода готов для чтения Если функция read возвращает ошибку EWOULDBLOCK, ниче! о не происходит Если мы встречаем признак конца файла, присланный сервером, это нормально в том случае, когда мы уже получили признак конца файла в стандартном потоке ввода Но в противном случае это будет ошибкой, означающей преждевременное завершение работы сервера (Server terminated prematurely) Если функция read возвращает некоторые данные, fri pt г увеличивается на единицу и в наборе флагов записи включается бит для стандартного потока вывода, с тем чтобы попытаться записать туда данные в следующей части функции
В листинге 15 3 показана последняя часть нашей функции
Листинг 15.3. функция str_ch третья часть, запись в стандартный поток вывода или сокет
//nonblock/strcl1nonb с
64 if (FD_ISSET(STDOUT_FILENO &wset) && ((n = friptr froptr) > 0)) {
65 if ("(nwritten = write(STDOUT_FILENO froptr n)) < 0) {
66if (errno i= EWOULDBLOCK) 一
67err_sys( write error to stdout )
68} else {
69fpnntf(stderrZs wrote Nd bytes to stdout\en
70gf_t】me() nwritten)
71froptr += nwritten /* только что полученное из функции write число */
72if (froptr == friptr)
73froptr = friptr = fr /* назад к началу буфера */
74}
75}
76 if (FD_ISSET(sockfd Swset) && ((n = toiptr tooptr) > 0)) {
77 if ( (nwritten = write(sockfd tooptr n)) < 0) {
78if (errno i= EWOULDBLOCK)
79err_sys( write error to socket )
80} else {
81fprintf(stderrMs wrote %d bytes to socket\en
gf_t】me() nwritten)
tooptr — nwritten /* только что полученное из функции write число */
if (tooptr == toiptr) {
toiptr = tooptr = to /* назад к началу буфера */
if (stdineof)
Shutdown(sockfd SHUT_WR) /досылаем FIN */
}-
}
}
}
}
Запись в стандартный поток вывода
с помощью функции 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 с
#incTudeunp h
#】nclude<time h>
char *
gf_time(void)
{"
struct timeval tv
static char str[30]
char *ptr
if (gettimeofday(&tv NULL) < 0)
err_sys( gettimeofday error )
ptr = ctime(&tv tv_sec)
strcpy(str &ptr[ll])
/* Fr】Sep 13 00 00 DO 1986\en\e0 */продолжение 按
Листинг 15.4 (продолжение)
/* 0123456789012345678901234 5 */
snprintfCstr + 8 sizeof(str) - 8 ' Z061d' tv tv_usec).
return (str)
}
Эта функция возвращает строку, содержащую текущее время, включая микросекунды, в таком формате:
34 56 123456
Здесь специально используется тот же формат, что и для отметок времени, которые выводятся программой tcpdump. Обратите внимание, что все вызовы функции fprmtf в нашей функции st「_cli записывают данные в стандартный поток сообщений об ошибках, позволяя нам отделить данные стандартного потока вывода (строки, отраженные сервером) от наших диагностических данных. Затем мы можем запустить наш клиент и функцию tcpdump, получить эти диагностические данные вместе с результатом функции tcpdump и отсортировать вместе два вида выходных данных в порядке их получения. Это позволит нам увидеть, что происходит в нашей программе, и соотнести это с действиями TCP.
Например, сначала мы запускаем функцию tcpdump на нашем узле solans, собирая только сегменты TCP, идущие к порту 7 или от него (эхо-сервер), и сохраняем выходные данные в файле, который называется tcdp
solans % tcpdump -w tcpd tcp and port 7
Затем мы запускаем клиент TCP на этом узле и задаем сервер иа узле bsdi: solans Z tcpch02 206.62.226.35 < 2000.lines > out 2> dmg
Стандартный поток ввода — это файл 2000 lines, тот же файл, что мы использовали для листинга 6.2. Стандартный поток вывода перенаправлявiся в файл out, а стандартный поток сообщений об ошибках — в файл d iag По завершении мы запускаем.
Solaris % diff 2000.lines out
чтобы проверить, что отраженные строки идентичны введенным строкам. Наконец, мы прекращаем выполнение функции tcpdump нажатием соответствующей клавиши терминала, после чего выводим записи функции tcpdump, сортируя их по времени получения вместе с данными диагностики, полученными от клиента. В листинге 15.5 показана первая часть этого результата.
Листинг 15.5. Отсортированный вывод функции tcpdump и данных диагностики
solans Z tcpdump ~r tcpd -N | sort diag -10 18 34 486392 solans 33621 > bsdi echo S10 18 34 488278 bsdi echo > solans 33621 S8760 <mss 1460>10 18 34 488490 solans 33621 > bsdi echo10 18 34 491482 read 4096 bytes from stdin10 18 34 518663 solans 33621 > bsd】 echo P10 18 34 519016 wrote 4096 bytes to socket10 18 34 528529 bsdi echo > solans 33621 P10 18 34 528785 solans 33621 > bsdi echo10 18 34 528900 solans 33621 > bsdi echo P10 18 34 528958 solans 33621 > bsdi echo10 18 34 536193 bsdi echo > solans 33621solans Z tcpdump ~r tcpd -N | sort diag -10 18 34 486392 solans 33621 > bsdi echo S10 18 34 488278 bsdi echo > solans 33621 S8760 <mss 1460>10 18 34 488490 solans 33621 > bsdi echo10 18 34 491482 read 4096 bytes from stdin10 18 34 518663 solans 33621 > bsd】 echo P10 18 34 519016 wrote 4096 bytes to socket10 18 34 528529 bsdi echo > solans 33621 P10 18 34 528785 solans 33621 > bsdi echo10 18 34 528900 solans 33621 > bsdi echo P10 18 34 528958 solans 33621 > bsdi echo10 18 34 536193 bsdi echo > solans 336211802738644 1802738644(0) win 8760 <mss 1460> 3212986316 3212986316(0) ack 1802738645 winack 1 win 87601 1461(1460) ack 1 win 87601 1461(1460) ack 1461 win 87601461 2921(1460) ack 1461 win 87602921 4097(1176) ack 1461 win 8760ack 1461 win 87601461 2921(1460) ack 4097 win 87601802738644 1802738644(0) win 8760 <mss 1460> 3212986316 3212986316(0) ack 1802738645 winack 1 win 87601 1461(1460) ack 1 win 87601 1461(1460) ack 1461 win 87601461 2921(1460) ack 1461 win 87602921 4097(1176) ack 1461 win 8760ack 1461 win 87601461 2921(1460) ack 4097 win 8760
solans Z tcpdump ~r tcpd -N | sort diag -
10 18 34 486392 solans 33621 > bsdi echo S
10 18 34 488278 bsdi echo > solans 33621 S
8760 <mss 1460>
10 18 34 488490 solans 33621 > bsdi echo
10 18 34 491482 read 4096 bytes from stdin
10 18 34 518663 solans 33621 > bsd】 echo P
10 18 34 519016 wrote 4096 bytes to socket
10 18 34 528529 bsdi echo > solans 33621 P
10 18 34 528785 solans 33621 > bsdi echo
10 18 34 528900 solans 33621 > bsdi echo P
10 18 34 528958 solans 33621 > bsdi echo
10 18 34 536193 bsdi echo > solans 33621
solans Z tcpdump ~r tcpd -N | sort diag -
10 18 34 486392 solans 33621 > bsdi echo S
10 18 34 488278 bsdi echo > solans 33621 S
8760 <mss 1460>
10 18 34 488490 solans 33621 > bsdi echo
10 18 34 491482 read 4096 bytes from stdin
10 18 34 518663 solans 33621 > bsd】 echo P
10 18 34 519016 wrote 4096 bytes to socket
10 18 34 528529 bsdi echo > solans 33621 P
10 18 34 528785 solans 33621 > bsdi echo
10 18 34 528900 solans 33621 > bsdi echo P
10 18 34 528958 solans 33621 > bsdi echo
10 18 34 536193 bsdi echo > solans 33621
1802738644 1802738644(0) win 8760 <mss 1460> 3212986316 3212986316(0) ack 1802738645 win
ack 1 win 8760
1 1461(1460) ack 1 win 8760
1 1461(1460) ack 1461 win 8760
1461 2921(1460) ack 1461 win 8760
2921 4097(1176) ack 1461 win 8760
ack 1461 win 8760
1461 2921(1460) ack 4097 win 8760
1802738644 1802738644(0) win 8760 <mss 1460> 3212986316 3212986316(0) ack 1802738645 win
ack 1 win 8760
1 1461(1460) ack 1 win 8760
1 1461(1460) ack 1461 win 8760
1461 2921(1460) ack 1461 win 8760
2921 4097(1176) ack 1461 win 8760
ack 1461 win 8760
1461 2921(1460) ack 4097 win 8760
1010101010101010101010101010101010101010101010101010101010101010101010101010101018 34 53669718 34 54463618 34 56850518 34 58037318 34 58224418 34 59335418 34 61727218 34 61761018 34 61790818 34 61806218 34 62331018 34 62612918 34 62633918 34 62661118 34 62839618 34 64352418 34 66730518 34 67032418 34 67222118 34 69103918 34 53669718 34 54463618 34 56850518 34 58037318 34 58224418 34 59335418 34 61727218 34 61761018 34 61790818 34 61806218 34 62331018 34 62612918 34 62633918 34 62661118 34 62839618 34 64352418 34 66730518 34 67032418 34 67222118 34 691039bsdi echo > solans 33621 Р 2921 3509(588) ack 4097 win 8760 read 4096 bytes from stdin
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
18 34 536697
18 34 544636
18 34 568505
18 34 580373
18 34 582244
18 34 593354
18 34 617272
18 34 617610
18 34 617908
18 34 618062
18 34 623310
18 34 626129
18 34 626339
18 34 626611
18 34 628396
18 34 643524
18 34 667305
18 34 670324
18 34 672221
18 34 691039
18 34 536697
18 34 544636
18 34 568505
18 34 580373
18 34 582244
18 34 593354
18 34 617272
18 34 617610
18 34 617908
18 34 618062
18 34 623310
18 34 626129
18 34 626339
18 34 626611
18 34 628396
18 34 643524
18 34 667305
18 34 670324
18 34 672221
18 34 691039
read 3508 bytes from socket
solans 33621 > bsdi echo ack 3509 win 8760
bsdi echo > solans 33621 P wrote 3508 bytes to stdout solans 33621 > bsdi echo P solans 33621 > bsdi echo P solans 33621 > bsdi echo P wrote 4096 bytes to socket
bsdi echo > solans 33621 bsdi echo > solans 33621 solans 33621 > bsdi echo bsdi echo > solans 33621 P bsdi echo > solans 33621 read 4096 bytes from stdin read 2636 bytes from socket solans 33621 > bsdi echo
3509 4097(588) ack 4097 win 87604097 5557(1460) ack 4097 win 8760 5557 7017(1460) ack 4097 win 8760 7017 8193(1176) ack 4097 win 8760ack 8193 win 87604097 5557(1460) ack 8193 win 8760 ack 5557 win 87605557 6145(588) ack 8193 win 87606145 7605(1460) ack 8193 win 87603509 4097(588) ack 4097 win 87604097 5557(1460) ack 4097 win 8760 5557 7017(1460) ack 4097 win 8760 7017 8193(1176) ack 4097 win 8760ack 8193 win 87604097 5557(1460) ack 8193 win 8760 ack 5557 win 87605557 6145(588) ack 8193 win 87606145 7605(1460) ack 8193 win 8760ack 7605 win 8760ack 7605 win 8760bsdi echo > solans 33621 P 7605 8193(588) ack 8193 win 8760 wrote 2636 bytes to stdout
3509 4097(588) ack 4097 win 8760
4097 5557(1460) ack 4097 win 8760 5557 7017(1460) ack 4097 win 8760 7017 8193(1176) ack 4097 win 8760
ack 8193 win 8760
4097 5557(1460) ack 8193 win 8760 ack 5557 win 8760
5557 6145(588) ack 8193 win 8760
6145 7605(1460) ack 8193 win 8760
3509 4097(588) ack 4097 win 8760
4097 5557(1460) ack 4097 win 8760 5557 7017(1460) ack 4097 win 8760 7017 8193(1176) ack 4097 win 8760
ack 8193 win 8760
4097 5557(1460) ack 8193 win 8760 ack 5557 win 8760
5557 6145(588) ack 8193 win 8760
6145 7605(1460) ack 8193 win 8760
ack 7605 win 8760
ack 7605 win 8760
Мы разбили длинные строки на части, а также удалили записи (DF) из сегментов Solans, означающие, что устанавливается бит DF (он используется для определения величины транспортной MTU). Параметр -N функции tcpdump указывает, что нужно вывести только часть полного доменного имени (sol aris kohal a com), относящуюся к узлу (sol ari s).
Используя этот вывод, мы можем нарисовать временную диаграмму происходящих событий (рис. 15.3). На этой диаграмме представлены события в различные моменты времени, причем ориентация диаграммы такова, что более поздние события расположены ниже на странице.
На этом рисунке мы не показываем сегменты АСК. Также помните, что если программа выводит сообщение wrote N bytes to stdout (записано ^бaйт в стандартное устройство вывода), это означает, что выполнилась функция 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 строками начальной версии, работающей в режиме остановки и ожидания (см. лис-
СтандартноеустройствовводаСтандартноеустройствовводаБуферБуферtoотправкиклиентасокетаБуферБуферtoотправкиклиентасокетаПринимающийбуферсокетаПринимающийбуферсокетаБуферfrклиентаБуферfrклиентаСегменты~ТСР^Сегменты~ТСР^СегментыСегменты4096 ] (чтение)4096 ] (чтение)4096 > (запись)4096 > (запись)146014601460 »1176 a1460 »1176 a
Стандартноеустройствоввода
Стандартноеустройствоввода
БуферБуфер
toотправки
клиентасокета
БуферБуфер
toотправки
клиентасокета
Принимающийбуферсокета
Принимающийбуферсокета
Буферfrклиента
Буферfrклиента
Сегменты~ТСР^
Сегменты~ТСР^
Сегменты
Сегменты
4096 ] (чтение)
4096 ] (чтение)
4096 > (запись)
4096 > (запись)
1460
1460
1460 »
1176 a
1460 »
1176 a
3508(чтение)
Стандартное устройство вводаСтандартное устройство вводаСтандартное устройство вводаСтандартное устройство ввода4096409614601460146014602636 ►2636 ►3508 、Стандартное 、А устройство (запись) ввода3508 、Стандартное 、А устройство (запись) ввода2636 j Стандартное—~―А устройствоввода2636 j Стандартное—~―А устройствоввода
Стандартное устройство ввода
Стандартное устройство ввода
Стандартное устройство ввода
Стандартное устройство ввода
4096
4096
1460
1460
1460
1460
2636 ►
2636 ►
3508 、Стандартное 、А устройство (запись) ввода
3508 、Стандартное 、А устройство (запись) ввода
2636 j Стандартное
—~―А устройствоввода
2636 j Стандартное
—~―А устройствоввода
Рис. 15.3. Временная диаграмма событий для примера неблокируемого ввода
тинг 5 4) Мы знаем, что эффект от удлинения кода в два раза, с 20 до 40 строк, оправдывает затраченные усилия, поскольку в пакетном режиме скорость возрастает почти в 30 раз, а применение функции select с блокируемыми дескрипторами осуществляется не слишком сложно Но будут ли оправданы затраченные усилия при написании приложения, использующего неблокируемый ввод-вывод, с учетом усложнения итогового кода? Нет, ответим мы Если нам необходимо использовать неблокируемый ввод-вывод, обычно бывает проще разделить приложение либо на процессы (при помощи функции fork), либо на потоки (см главу 23)
В листинге 15 6 показана еще одна версия нашей функции str_ch, разделяемая на два процесса при помощи функции fork
Эта функция сразу же вызывает функцию fork для разделения на родительский и дочерний процессы Дочерний процесс копирует строки от сервера в стандартный поток вывода, а родительский процесс — из стандартного потока ввода серверу, как показано на рис 15 4
Стандартное устройство вводаСтандартное устройство вводаСтандартное устройство выводаСтандартное устройство выводаРис. 15.4. Функция str^cli, использующая два процессаРис. 15.4. Функция str^cli, использующая два процесса
Стандартное устройство ввода
Стандартное устройство ввода
Стандартное устройство вывода
Стандартное устройство вывода
Рис. 15.4. Функция str^cli, использующая два процесса
Рис. 15.4. Функция str^cli, использующая два процесса
Мы явно отмечаем, что соединения TCP являются двусторонними и что родительский и дочерний процессы совместно используют один и тот же дескриптор сокета родительский процесс записывает в сокет, а дочерний процесс читает из сокета Есть только один сокет, один буфер приема сокета и один буфер отправки, но на этот сокет ссылаются два дескриптора один в родительском процессе и один в дочернем
Листинг 15.6. Версия функции str^cli, использующая функцию fork //nonblock/strcl1 fork с
include unp h
void
str_cli(FILE *fp mt sockfd)
{-
pid_t pid
charsendline[MAXLINE] recvline[MAXLINE]
if ( (pid = ForkO) -- 0) { /* дочернин процессs c冊0P -<承®ut */
while (Readline(sockfd recvline MAXLINE) > 0)”
Fputs(recvline stdout)
kilKgetppidO SIGTERM) /* в случае если родительский процесс все е|це выполняется */
exit(0)
}
/* родитель stdin > сервер */
while (Fgets(sendline MAXLINE fp) i= NULL)
Writer)(sockfd sendline strlen(sendline))
Shutdown(sockfd SHUT_WR) /* конец файла на stdin, посылаем FIRt/
pause 0-
return
}
Нам нужно снова вспомнить о последовательности завершения соединения Обычное завершение происходит, когда в стандартном потоке ввода встречается конец файла Родительский процесс считывает конец файла и вызывает функ-
цию shutdown для отправки сегмента FIN (Родительский процесс не может вызвать функцию cl ose, см упражнение 15 1) Но когда это происходит, дочерний процесс должен продолжать копировать от сервера в стандартный поток вывода, пока он не получит признак конца файла на сокете
Также возможно, что процесс сервера завершится преждевременно (см раздел 5 12), и если это происходит, дочерний процесс считывает признак конца файла на сокете В таком случае дочерний процесс должен сообщить родительскому, что нужно прекратить копирование из стандартного потока ввода в сокет (см упражнение 15 2) В листинге 15 6 дочерний процесс отправляет родительскому процессу сигнал SIGTERM, в случае, если родительский процесс еще выполняется (см упражнение 15 3) Другим способом обработки этой ситуации было бы завершение дочернего процесса, и если родительский процесс все еще выполнялся бы к этому моменту, он получил бы сигнал SIGCHLD
Родительский процесс вызывает функцию pause, когда заканчивает копирование, что переводит его в состояние ожидания того момента, когда будет получен сигнал Даже если родительский процесс не перехватывает никаких сигналов, он все равно переходит в состояние ожидания до получения сигнала SIGTERM от дочернего процесса По умолчанию действие этого сигнала — завершение процесса, что вполне устраивает нас в этом примере Родительский процесс ждет завершения дочернего процесса, чтобы измерить точное время для этой версии функции st r_cli Обычно дочерний процесс завершается после родительского, но поскольку мы измеряем время, используя команду оболочки time, измерение заканчивается, когда завершается родительский процесс
Отметим простоту этой версии по сравнению с неблокируемым вводом-выво> дом, представленным ранее в этом разделе Наша неблокируемая версия управ~ ляла четырьмя различными потоками ввода-вывода одновременно, и поскольку все четыре были неблокируемыми, нам пришлось иметь дело с частичным чтением и частичной записью для всех четырех потоков Но в версии с функцией fork каждый процесс обрабатывает только два потока ввода-вывода, копируя один в другой В применении неблокируемого ввода-вывода не возникает необходимости, поскольку если есть данные для чтения из потока ввода, то в соответствующий поток вывода записывать нечего
Сравнение времени выполнения различных версий функции str_cli
Итак, мы продемонстрировали четыре различных версии функции str_cl i Для каждой версии мы покажем время, которое потребовалось для ее выполнения, в том числе и для версии, использующей программные потоки (см листинг 23 1) В каждом случае было скопировано 2000 строк от клиента Solans 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)
Наша версия с неблокируемым вводом-выводом почти вдвое быстрее версии, использующей блокируемый ввод-вывод с функцией select Наша простая вер-
сия с применением функции fork медленнее версии с неблокируемым вводом- выводом Тем не менее, учитывая сложность кода неблокируемого ввода-вывода по сравнению с кодом функции fork, мы рекомендуем более простой подход
Неблокируемая функция connect
Когда сокет TCP устанавливается как неблокируемый, а затем вызывается функция connect, она немедленно возвращает ошибку EINPROGRESS, однако трехэтапное рукопожатие TCP продолжается Далее мы с помощью функции select проверяем, успешно или нет завершилось установление соединения Неблокируемая функция connect находит применение в трех случаях
Трехэтапное рукопожатие может наложиться на какои-либо другой процесс Для выполнения функции connect требуется один период обращения RTT (см раздел 2 5), и это может занять от нескольких миллисекунд в локальной сети до сотен миллисекунд или нескольких секунд в глобальной сети Это время мы можем провести с пользой, выполняя какой-либо другой процесс
Мы можем установить множество соединений одновременно, используя эту технологию Этот способ уже стал популярен в применении к web-браузерам, и такой пример мы приводим в разделе 15 5
Поскольку мы ждем завершения установления соединения с помощью функции select, мы можем задать предел времени для функции select, что позволит нам сократить тайм-аут для функции connect Во многих реализациях таймаут функции connect лежит в пределах от 75 секунд до нескольких минут Бывают случаи, когда приложению нужен более короткий тайм-аут, и одним из решений может стать использование неблокируемой функции connect В разделе 13 2 рассматриваются другие способы помещения тайм-аута в операции с сокетами
Как бы просто ни выглядела неблокируемая функция connect, есть ряд моментов, которые следует учитывать
。 Даже если сокет является неблокируемым, то когда сервер, с которым мы соединяемся, находится на том же узле, обычно установление соединения происходит немедленно при вызове функции connect
В Беркли-реализациях (а также Posix 1g) имеются два следующих правила, относящихся к функции sei ect и неблокируемой функции connect во-первых, когда соединение устанавливается успешно, дескриптор становится готовым для записи [105, с 531], и во-вторых, когда при установлении соединения встречается ошибка, дескриптор становится готовым как для чтения, так и для записи [105, с 530]
ПРИМЕЧАНИЕ
Эти два правила в отношении функции select выпадают из общего ряда наших правил из раздела 6 3 относительно условий, при которых дескриптор становится готовым для чтения или записи В сокет TCP можно записывать если достаточно места в буфере отправки (что всегда будет выполнено в случае присоеди ненно! о сокета, поскольку мы еще ничего не записали в сокет) и сокет является присоединенным (что выполняется, только когда завершено трехэтапное рукопожатие) При наличии ошибки, ожидающей обработки, появляется возможность читать из сокета и записывать в сокет
С неблокируемыми функциями connect связано множество проблем переносимости, которые мы отметим в последующих примерах.
Неблокируемая функция connect: клиент времени и даты
В листинге 15.7 показана наша функция connect_nonb, вызывающая неблокируемую функцию connect. Мы заменяем вызов функции connect, имеющийся в листинге 1.1, следующим фрагментом кода:
if (connect_nonb(sockfd. (SA *) &se「vaddr. sizeof(se「vaddr). 0) < 0)
err_sys?"connect error").
Первые три аргумента являются обычными аргументами функции connect, а четвертый аргумент — это число секунд, в течение которых мы ждем завершения установления соединения. Нулевое значение подразумевает отсутствие таймаута для функции sei ect; следовательно, для установления соединения TCP ядро будет использовать свой обычный тайм-аут.
Листинг 157 Неблокируемая функция connect
//lib/connect_nonb с
1 include "unp h"'
2 3 4 5 6 7 82 3 4 5 6 7 8int
2 3 4 5 6 7 8
2 3 4 5 6 7 8
connect_nonb(mt sockfd const SA *saptr, qqcklen t salen, int nsec)
{ "~
int flags, n. error
socklen_t len.
fd_set rset. wset丨 •
struct tirneval tval.
9 flags = FcntKsockfd. F_GETFL. 0).
10 FcntKsockfd. F_SETFL. flags | O_NONBLOCK).
И error = 0.
12 if ( (n = connect(sockfd. saptr. salen)) < 0)
13 if (errno •= EINPROGRESS)
14 return (-1),
16 if (п == 0)17 goto done16 if (п == 0)17 goto done15/* Пока соединение устанавливается мы можем заняться чем-то другим */
16 if (п == 0)
17 goto done
16 if (п == 0)
17 goto done
/* функция connect завершилась немедленно */
18
FD_ZERO(&rset).
19
FD_SET(sockfd &rset):
20
wset = rset
21
tval tv_sec = nsec.
22
tval tv_usec = 0.
23
if ( (n = Select(sockfd + 1. &rset. &wset. NULL.
24
nsec ? &tval NULL)) == 0) {
25
close(sockfd)./* тайм-аут */
26
errno н ETIMEDOUT.
27
return (-I),
28
if (FD__ISSET(sockfd. &rset) || FD_ISSET(sockfd. &wset)) {
len = sizeof(error).
if (getsockopt(sockfd. S0L_S0CKET. SD_ERROR. &error, &len) < 0)
return (-1)./*в Solans ошибка. ожидающая обработки */
} else
err_quit("select error sockfd not set")
done
Fcntl(sockfd, F—SETFL. flags). /* восстанавливаем флаги, задающие статус файма */
if (error) {
close(sockfd)./* на всякий случай*/
errno = error.
return (-1)
}
return (0).,
}
Задание неблокируемого сокета
9-10 Мы вызываем функцию fcntl, которая делает сокет неблокируемым.
11-14 Мы инициируем неблокируемую функцию connect. Ошибка, которую мы ожидаем (EINPROGRESS), указывает на то, что установление соединения началось, но еще не завершилось [105, с. 466]. Любая другая ошибка возвращается вызывающему процессу.
Выполнение других процессов во время установления соединения
15 На этом этапе мы можем делать все. что захотим, ожидая завершения установления соединения.
Проверка немедленного завершения
16-17 Если неблокируемая функция connect возвратила нуль, установление соединения завершилось. Как мы сказали, это может произойти, когда сервер находится на том же узле, что и клиент.
Вызов функции select
18-24 Мы вызываем функцию select и ждем, когда сокет будет готов либо для чтения, либо для записи. Мы обнуляем rset, включаем бит, соответствующий sockfd в этом наборе дескрипторов, и затем копируем rset в wset. Это присваивание, возможно, является структурным присваиванием, поскольку обычно наборы дескрипторов представляются как структуры. Далее мы инициализируем структуру timeval и затем вызываем функцию sei ect. Если вызывающий процесс задает четвертый аргумент нулевым (что соответствует использованию тайм-аута по умолчанию), следует задать в качестве последнего аргумента функции sei ect пустой указатель, а не структуру timeval с нулевым значением (означающим, что мы не ждем вообще).
Обработка тайм-аутов
25-28 Если функция select возвращает нуль, это означает, что время таймера истекло, и мы возвращаем вызывающему процессу ошибку ETIMEDOUT. Мы также закрываем сокет, чтобы трехэтапное рукопожатие не продолжалось.
Проверка возможности чтения или записи
: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:
Вызвать функцию getpeername вместо функции getsockopt. Если этот вызов окажется неудачным и возвратится ошибка ENOTCONN, значит, соединение не было установлено, и чтобы получить ошибку, ожидающую обработки, следует вызвать для сокета функцию getsockopt с SO ERROR.
Вызвать функцию read с нулевым значением аргумента 1 ength. Если выполне- {, ние функции read окажется неудачным, функция connect выполнилась неудачно, , и переменная errno из функции read при этом указывает на причину неудачной попытки установления соединения. Если соединение успешно установлено, функция read возвращает нуль.
Снова вызвать функцию connect. Этот вызов окажется неудачным, и если ошибка —EISCONN, сокет уже присоединен, а значит, первое соединение завершилось успешно.
К сожалению, неблокируемая функция connect — это одна из самых сложных областей сетевого программирования с точки зрения переносимости. Будьте готовы к проблемам совместимости, особенно с более ранними реализациями. Бо-
лее простой технологией является создание потока (см. главу 23) для обработки соединения.
Прерванная функция connect
Что происходит, если наш вызов функции connect на обычном блокируемом сокете прерывается, скажем, перехваченным сигналом, прежде чем завершится трехэтапное рукопожатие TCP? Если предположить, что функция connect не перезапускается автоматически, то она возвращает ошибку EINTR. Но мы не можем снова вызвать функцию connect, чтобы добиться завершения установления соединения. Это приведет к ошибке EADDRINUSE.
Все, что требуется сделать в этом сценарии, — это вызвать функцию select, так, как мы делали в этом разделе для неблокируемой функции connect. Тогда функция select завершится в том случае, если соединение успешно устанавливается (делая сокет доступным для записи) или если попытка соединения неудачна (сокет становится доступен для чтения и для записи).
ПРИМЕЧАНИЕ
В Posix.lg явно определено, что происходит, когда вызов функции XTI reconnect пре* рывается перехваченным сигналом, но ничего не говорится о том, как подобная ситуация обрабатывается в том случае, когда была вызвана функция connect. То, что мы описали, соответствует обработке этого сценария Беркли-ядрами.
Неблокируемая функция 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 единице, то есть все равно меньше, чем в последовательном сценарии.
Последовательное .установление трех соединенийПоследовательное .установление трех соединенийПараллельное установление трех соединений;одновременно устанавливается не более двух соединенийПараллельное установление трех соединений;одновременно устанавливается не более двух соединенийПараллельное установление трех соединений;одновременно устанавливается не более трех соединенийПараллельное установление трех соединений;одновременно устанавливается не более трех соединений
Последовательное .установление трех соединений
Последовательное .установление трех соединений
Параллельное установление трех соединений;
одновременно устанавливается не более двух соединений
Параллельное установление трех соединений;
одновременно устанавливается не более двух соединений
Параллельное установление трех соединений;
одновременно устанавливается не более трех соединений
Параллельное установление трех соединений;
одновременно устанавливается не более трех соединений
Рис. 15.5. Установление множества параллельных соединений
В третьем сценарии мы выполняем три параллельных соединения и снова считаем, что эти три соединения не мешают друг другу (идеальный случай). Но общее время такое (15 единиц), как и во втором сценарии.
При работе с web-клиентами первое соединение устанавливается само по себе, за ним следуют соединения по ссылкам, обнаруженным в данных от первого соединения. Мы показываем это на рис. 15.6.
Рис. 15.6- Установление первого соединения, азатем • множества параллельных соединений
Для дальнейшей оптимизации клиент может начать обработку данных, возвращаемых по первому соединению, до того, как установление первого соединения завершится, и инициировать дополнительные соединения, как только ему станет известно, что они нужны.
Поскольку мы выполняем несколько неблокируемых функций connect одновременно, мы не можем использовать нашу функцию connect_nonb, показанную в листинге 15.7, так как она не завершается, пока соединение не установлено. Вместо этого мы отслеживаем множество соединений самостоятельно.
Наша программа считывает около 20 строк с web-сервера. Мы задаем в качестве аргументов командной строки максимальное число параллельных соединений, имя узла сервера, а затем каждое из имен файлов, получаемых с сервера. Типичное выполнение нашей программы выглядит так:
solans % web 3 ww.fbobar.can / imagel.gif image2.gif \ image3.gif image4.gif 1mage5.gif \ image6 gif image7 gif
Аргументы командной строки задают три одновременных соединения, имя узла сервера, имя файла домашней страницы (/ обозначает корневой каталог сервера) и семь файлов, которые затем нужно прочитать (в нашем примере это файлы с изображениями в формате GIF). Обычно на эти семь файлов имеются ссылки с домашней страницы, и чтобы получить их имена, web-клиент читает домашнюю страницу и обрабатывает код HTML. Чтобы не усложнять этот пример разбором кода HTML, мы просто задаем имена файлов в командной строке.
Это большой пример, поэтому мы будем показывать его частями. В листинге 15.8 представлен наш заголовочный файл web h, который включен во все файлы.
struct file {char *f_name:/*имя файла */char *f_host./*имя узла или адрес IPv4/IPv6 */mt f_fd./★дескриптор */int f flags;/★F_xxx определены ниже */'} file[MAXFILES];#define FJONNECTING 1/★connectО в процессе выполнения */define 匸READING2/★соединение установлено .叩оисходит считывание */#define F_DONE4/*все сделано */Refine GET CMD "GET 纪HTTP/10\er\en\er\en"struct file {char *f_name:/*имя файла */char *f_host./*имя узла или адрес IPv4/IPv6 */mt f_fd./★дескриптор */int f flags;/★F_xxx определены ниже */'} file[MAXFILES];#define FJONNECTING 1/★connectО в процессе выполнения */define 匸READING2/★соединение установлено .叩оисходит считывание */#define F_DONE4/*все сделано */Refine GET CMD "GET 纪HTTP/10\er\en\er\en"14/* глобальные переменные */15 mt nconn, nfiles. nlefttoconn nlefttoread, maxfd:16 fd_set rset. wset.14/* глобальные переменные */15 mt nconn, nfiles. nlefttoconn nlefttoread, maxfd:16 fd_set rset. wset.Листинг 15.8. Заголовок web. h//nonblock/web h1 include "unp h"2 #define MAXFILES 203 define SERV "80"/* номер порта или имя Службы */Листинг 15.8. Заголовок web. h//nonblock/web h1 include "unp h"2 #define MAXFILES 203 define SERV "80"/* номер порта или имя Службы */4 5 6 7 8 94 5 6 7 8 933
struct file {
char *f_name:
/*
имя файла */
char *f_host.
/*
имя узла или адрес IPv4/IPv6 */
mt f_fd.
/★
дескриптор */
int f flags;
/★
F_xxx определены ниже */'
} file[MAXFILES];
#define FJONNECTING 1
/★
connectО в процессе выполнения */
define 匸READING2
/★
соединение установлено .叩оисходит считывание */
#define F_DONE4
/*
все сделано */
Refine GET CMD "GET 纪
HTTP/1
0\er\en\er\en"
struct file {
char *f_name:
/*
имя файла */
char *f_host.
/*
имя узла или адрес IPv4/IPv6 */
mt f_fd.
/★
дескриптор */
int f flags;
/★
F_xxx определены ниже */'
} file[MAXFILES];
#define FJONNECTING 1
/★
connectО в процессе выполнения */
define 匸READING2
/★
соединение установлено .叩оисходит считывание */
#define F_DONE4
/*
все сделано */
Refine GET CMD "GET 纪
HTTP/1
0\er\en\er\en"
14/* глобальные переменные */
15 mt nconn, nfiles. nlefttoconn nlefttoread, maxfd:
16 fd_set rset. wset.
14/* глобальные переменные */
15 mt nconn, nfiles. nlefttoconn nlefttoread, maxfd:
16 fd_set rset. wset.
Листинг 15.8. Заголовок web. h
//nonblock/web h
1 include "unp h"
2 #define MAXFILES 20
3 define SERV "80"/* номер порта или имя Службы */
Листинг 15.8. Заголовок web. h
//nonblock/web h
1 include "unp h"
2 #define MAXFILES 20
3 define SERV "80"/* номер порта или имя Службы */
4 5 6 7 8 9
4 5 6 7 8 9
3
3
17/* прототипы функций */
18 void home_psge(const char *. const char *).
19 voidstart_connect(struct file *).
20 voidwr11e_get_cmd(struct file *).
Задание структуры file
2-13 Программа считывает некоторое количество (не более MAXFILES) файлов с web- сервера. Структура fi 1 е содержит информацию о каждом файле: его имя (копируется из аргумента командной строки), имя узла или IP-адрес сервера, с которого читается файл, дескриптор сокета, используемый для этого файла, и набор флагов, которые указывают, что мы делаем с этим файлом (устанавливаем соединение для получения файла или считываем файл). Флаг FJDONE указывает, что работа с данным файлом закончена.、,
Определение глобальных переменных и прототипов функций
14-20 Мы определяем глобальные переменные и прототипы для наших функций, которые мы вскоре опишем.
Листинг 15.9, Первая часть программы одновременного выполнения функций connect: глобальные переменные и начало функции main
//nonblock/web.с
#include "web h"
2 3 4 5 6 72 3 4 5 6 7int
2 3 4 5 6 7
2 3 4 5 6 7
mann(int argc, char **argv)
{
mt i. fd. n. maxnconn. flags, error: charbuf[MAXLINE].
fd set rs. ws.
8 if (argc < 5),
9 err_quit("usage, web <#conns> <hostnsme> <homepsge> <filel> ..."):
10 maxnconn = atoi(argv[l]);
nfiles = nnnQrgc - 4. MAXFILES),
for (i = 0. i < nfiles; i++) {
file[i] f_name = argv[i + 4]:
file[i] f_host = argv[2].
filed] Olsgs = 0,
} 一
pnntf(wnfiles = Zd\en". nfiles):
18 home_page(argv[2]. d「gv[3]).
19 FD_ZERO(&rset).
20 FD~ZER0(&wset),
21 maxfd = -1,
nlefttoread = nlefttoconn = nfiles.
nconn = 0.
Обработка аргументов командной строки
1Ы7 Структуры fi 1 е заполняются соответствующей информаций из аргументов командной строки.
Чтение домашней страницы
18 Функция home_pdge, которую мы показываем в следующем листинге, создает соединение TCP, посылает команду серверу и затем читает домашнюю страницу. Это первое соединение, которое выполняется самостоятельно, до того как мы начнем устанавливать параллельные соединения.
Инициализация глобальных переменных
19-23 Инициализируются два набора дескрипторов, по одному для чтения и для записи. maxfd — это максимальный дескриптор для функции select (который мы инициализируем значением -1, поскольку дескрипторы неотрицательны), nl efttoread — число файлов, которые осталось прочитать (когда это значение становится нулевым, чтение заканчивается), nlefttoconn — это количество файлов,
для которых пока еще требуется соединение TCP, а псопп — это число соединений, открытых в настоять访 момент (оно никогда не может превышать первый аргумент командной строки).
В листинге 15.10 показана функция home_page, вызываемая один раз, когда начинается выполнение функции mam.
Листинг 15.10- Функция home_page
//nonblock/home_page с
include "web hw
void
home_psge(const char *host. const char *fname
{-
mtfd, n.
charhnefMAXLINE].
fd = Tcp_connect(host. SERV). /* блокируемая функция connect() */
n = snpnntffline. sizeof(line). GET_CMD. fname).
Writen(fd. line, n),
for (,,) {
if ( (n = Read(fd, line. MAXLINE)) == 0)
break:/* сервер закрыл соединение */
printfCread Zd bytes of home psge\en". n).
/* обрабатываем полученные данные */
}
pnntf("end-of-f11e on home psge\en"):
Close(fd);
}
Установление соединения с сервером
Наша функция tcp_connect устанавливает соединение с сервером.
Отправка команды HTTP серверу, чтение ответа
8-17 Запускается команда HTTP GET для домашней страницы (часто обозначается символом /). Читается ответ (с ответом мы в данном случае ничего не делаем), и соединение закрывается.
Следующая функция, stareconnect, показанная в листинге 15.11, инициирует вызов неблокируемой функции connect.
Листинг 15.11. Инициирование неблокируемой функции connect
//nonbl ock/stareconnect c
include "web h”
void
sts reconnect (struct file *fptr)
{_
mt fd. flags, n.
struct addrinfo *si.
si = Host_serv(fptr->f_host, SERV. 0, SOCK_STREAM).
*8fd = Socket(ai->ai_fsniily. ai->ai_socktype. ai->ai_protocol):продолжение 步
Листинг 15.11 (продолжение)
fptr->f_fd = fd;
printf(Hstart_connect for ^s. fd ^d\en". fptr->f_name. fd):
/* отключаем блокирование сокета */
flags = FcntUfd, F_GETFL. 0).
FcntKfd, F—SETFL, flags | 0_N0NBL0CK).
/* инициируем неблокируемое соединение с сервером */
if ( (n = connect(fd, ai->ai_addr. ai->si_addrlen)) < 0) {
if (errno != EINPROGRESS) ~~
err_sys("nonblocking connect error").
fptr->f_flags = F_CONNECTING.
FD_SET(fd. Srset). /* включаем дескриптор сокета в наборе чтения и записи */
FD 二 SET(fd. &wset):
if (fd > mdxfd)
maxfd = fd.
23} else if (n >= 0)/* соединение уже установлено */
24write_get_cmd(fptr): /* отправляем команду GET серверу */
25 } 一 —
Создание сокета, отключение блокировки сокета
-13 Мы вызываем нашу функцию host_serv для поиска и преобразования имени узла и имени службы. Она возвращает указатель на массив структур addri nfo. Мы используем только первую структуру. Создается сокет TCP, и он становится неблокируемым.
Вызов неблокируемой функции connect
1-22 Вызывается неблокируемая функция connect, и флагу файла присваивается значение F_CONNECTING. Включается дескриптор сокета и в наборе чтения, и в наборе записи, поскольку функция select будет ожидать любого из этих условий как указания на то, что установление соединения завершилось. При необходимости мы также обновляем значение maxfd.
Обработка завершения установления соединения
-24 Если функция connect успешно завершается, значит, соединение уже установлено, и функция wnte_get_cmd (она показана в следующем листинге) посылает команду серверу.
Мы делаем сокет неблокируемым для функции connect, но никогда не переустанавливаем его в блокируемый режим, заданный по умолчанию. Это нормально, поскольку мы записываем в сокет только небольшое количество данных (команда GET следующей функции) и считаем, что эти данные занимают значительно меньше места, чем имеется в буфере отправки сокета. Даже если из-за установленного флага отсутствия блокировки при вызове функции write происходит частичное копирование, наша функция writen обрабатывает эту ситуацию. Если оставить сокет неблокируемым, это не повлияет на последующее выполнение функций read, потому что мы всегда вызываем функцию select для определения того момента, когда сокет станет готов для чтения.
В листинге 15.12 показана функция wri te_get_cmd, посылающая серверу команду HTTP GET.「-,,,一、•一
Листинг 15.12. Отправка команды HTTP GET серверу
//nonbl ock /wr 1 te_get_cmd. c
find ude"web'h"
void
write_get_cmd(struct file *fptr)
{- 一
intn.
charlineLMAXLINE],
n = snprmtfdine, sizeof(line), GET_CMD. fptr->f_name);
Writen(fptr->f_fd. line, n),
pnntf("wrote Ed bytes for ^s\en", n, fptr->f_nanie)
fptr->f_fl3gs = F_READING. /* сброс F_CONNECTING */
FD_SET(fptr->f_fd. &rset): /* прочитаем ответ сервера */
if (fptr->f_fd^> mdxfd)
maxfd ^ fptr->f_fd.
14 }一
Создание команды и ее отправка
7-9 Команда создается и пишется в сокет.
Установка флагов
10-13 Устанавливается флаг F_READING, при этом также сбрасывается флаг F_CONNECTING (если он установлен). Это указывает основному циклу, что данный дескриптор готов для ввода. Также включается дескриптор в наборе чтения, и при необходимости обновляется значение maxfd.
Теперь мы возвращаемся в функцию mai п, показанную в листинге 15.13, начиная с того места, где закончили в листинге 15.9. Это основной цикл программы: пока имеется ненулевое количество файлов для обработки (значение nl efttoresd больше нуля), устанавливается, если это возможно, другое соединение и затем вызывается функция select для всех активных дескрипторов, обрабатывающая как завершение неблокируемых соединений, так и прием данных.
Можем ли мы инициировать другое соединение?
24-35 Если мы не дошли до заданного предела одновременных соединений и есть дополнительные соединения, которые нужно установить, мы ищем еще не обработанный файл (на него указывает нулевое значение f_fl ags) и вызываем функцию sta reconnect для инициирования соединения. Число активных соединений увеличивается на единицу (псопп), а число соединений, которые нужно установить, на единицу уменьшается (nl efttoconn).<
Функция select: ожидание событий1
36-37 Функция select ожидает готовности сокета либо для чтения, либо для записи. Дескрипторы, для которых в настоящий момент происходит установление соединения (неблокируемая функция connect находится в процессе выполнения), будут включены в обоих наборах, в то время как дескрипторы с завершенным соединением, ожидающие данных от сервера, будут включены только в наборе
чтения. ,)л,厶〃・/ ,‘чтения. ,)л,厶〃・/ ,‘
чтения. ,)л,厶〃・/ ,‘
чтения. ,)л,厶〃・/ ,‘
Листинг 15.13. Основной цикл функции main
//nonblock/web с//nonblock/web с
//nonblock/web с
//nonblock/web с
242526272829303132333435242526272829303132333435while (nlefttoresd > 0) {
24
25
26
27
28
29
30
31
32
33
34
35
24
25
26
27
28
29
30
31
32
33
34
35
while (nconn v maxnconn && nlefttoconn > 0) {
/* 4find a file to read */
for (i = 0. 1 v nfiles. i++)
if (file[i] f_flags == 0) break.
if (i == nfiles)
err_qu11(иn1efttoconn = Md but nothing found". nlefttoconn), start_connect(&fi le[i]):
ПСОПП++.
nlefttoconn-
36rs = rset,
37ws = wset
38 n = Select(msxfd + 1. &rs. &ws, NULL, NULL):
39 for (1 = 0 i < nfiles. i++) {
40 fl ags = file[i] f_flags.
41 if (flags == 0 |「flags & F_D0NE)
42continue,
43fd = f】le[i] f_fd.
44if (flags & F_tONNECTING &&
45(FDJSSET(fd. &rs)||FD」SSET(fd.&ws))){
46n = sizeof(error),
47if (getsockopt(fdS0L_S0CKET.S0_ERR0R,&error. &n) < 0 ||
48error != 0) {
49err_ret(**nonblocking connect failed
50for Zs“. file[i] f name).
51}-
52/* соединение установлено */
53printf(''connection established for ^s\enM, file[i] f^name);
54FD_CLR(fd. &wset). /* отключаем запись в этот сокет */
55wri te_get_cmd(&f 1 le[i]),/* write О команду GET */
56} else if (flags & F_READING && FDJSSEKfd. &”)){
57if ( (n = ResdCfd. buf. sizeof(buf))) == 0) {
58prmtfC'end-of-file on ^s\en". file[i] f_nsnie).
59Close(fd),一
60file[i] f_f1ags = F DONE. /* сбрасывает флаг F_READWiG */
61FD__CLR(fd7 &rset).-
62nconn--,
63nlefttoread--.
64} else {
65printf("read 紀bytes from総\en",n. file[i] f_name).
66}一
67}
6B }
69}
70 exit(0).
71 }
Обработка всех готовых дескрипторов
55 Теперь мы обрабатываем каждый элемент массива структур file, чтобы определить, какие дескрипторы нужно обрабатывать. Если установлен флаг F_CON-
NECTING и дескриптор включен либо в наборе чтения, либо в наборе записи, неблокируемая функция connect завершается. Как мы говорили при описании листинга 15.7, мы вызываем функцию getsockopt, чтобы получить ошибку для сокета, ожидающую обработки. Если это значение равно нулю, соединение успешно завершилось. В этом случае мы выключаем дескриптор в наборе флагов записи и вызываем функцию wri te_get_cmd для отправки запроса 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
Максимальное увеличение эффективности происходит при трех одновременных соединениях (время уменьшается вдвое), а при четырех и более одновременных соединениях прирост производительности значительно меньше.
ПРИМЕЧАНИЕ
Мы показали пример использования одновременных соединений, поскольку он служит хорошей иллюстрацией применения неблокируемого ввода-вывода, а также потому, что в данном случае эффективность применения одновременных соединеггий может быть измерена. Это свойство также используется в популярном приложении — * • web-браузере Netscape. В этой технологии могут появиться некоторые «подводные камни», если сеть перегружена. В главе 21 [94] подробно описываются алгоритмы TCP, называемые алгоритмами медленного старта (slow start), и предотвращения перегрузки сети (congestion avoidance). Когда от клиента к серверу устанавливается множество соединений, то взаимодействие между соединениями на уровне TCP отсутствует, то есть если на одном из соединений происходит потеря пакета, другие соединения с тем же сервером не получают соответствующего уведомления, и вполне возможно, чтодру- :гие соединения вскоре также столкнутся с потерей пакетов, пока не замедлятся. По этим дополнительным соединениям будет продолжаться отправка слишком большого количества пакетов в уже перегружен ную сеть. Эта технология также увеличивает нагрузку на сервер.
Неблокируемая функция accept
Как было сказано в главе 6, функция select сообщает, что прослушиваемый сокет готов для чтения, когда установленное соединение готово к обработке функцией accept. Следовательно, если мы используем функцию select для определения готовности входящих соединений, то нам не нужно делать прослушиваемый сокет неблокируемым, потому что когда функция select сообщает нам, что соединение установлено, функция accept обычно не является блокируемой.
К сожалению, существует определенная проблема, связанная с временем, способная запутать нас [31]. Чтобы увидеть эту проблему, изменим код нашего эхо- клиента TCP (см. листинг 5.3) таким образом, чтобы после установления соединения серверу отсылался сегмент RST. В листинге 15.14 представлена новая версия.
Листинг 15.14. Эхо-клиент TCR устанавливающий соединение и посылающий серверу сегмент RST
//nonblock/tcpcl103 с
intmain(int{intstructstructintmain(int{intstructstruct#include "unp h”
int
main(int
{
int
struct
struct
int
main(int
{
int
struct
struct
argc, char **argv)
sockfd.
1inger ling.
sockaddr_in servaddr:
89891010tcpcli <IPaddress>M).tcpcli <IPaddress>M).if (argc i= 2) err^quitC'ussge
8
9
8
9
10
10
tcpcli <IPaddress>M).
tcpcli <IPaddress>M).
sockfd = Socket(AFJNET SOCK_STREAM, 0).
bzero(&servaddr. sizeof(servaddr)).
servaddr sin_fdnnly = AF_INET.
servaddr sin_port = htons(SERV_PORT).
Inet_pton(AF_INET. argv[l]. &servaddrsin_addr).
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)).
ling l onoff = 1,/* для отправки сегмента RST при зак(5ытмМ^соединёйия */ ■
ling l_hnger = 0.
SetsockoptCsockfd, S0L_S0CKET. S0_LINGER. &lmg. sizeof(ling)).
Close(sockfd).
exit(0).
}
Установка параметра сокета SO_LINGER
16-19 Как только соединение устанавливается, мы задаем параметр сокета SO_LINGER, устанавливая флаг l_onoff в единицу и обнуляя время 1_1 inger. Как утверждалось в разделе 7.5, это вызывает отправку RST на сокете TCP при закрытии соединения. Затем с помощью функции cl ose мы закрываем сокет.
Затем мы изменяем наш сервер TCP, приведенный в листингах 6.3 и 6.4, с тем чтобы после сообщения функции select о готовности прослушиваемого сокета для чтения, но перед вызовом функции accept наступала пауза. В следующем коде, взятом из начала листинга 6.4, две добавленные строки помечены знаком +.
if (FD_ISSET(listenfd. &rset)) {/* новое соединение */
+pnntf(''listening socket readable\enM).
+sleep(5).
clilen = sizeof(cliaddr).
connfd = Acceptdistenfd, (SA *) &cl】addr. &cl】len).、
Здесь мы имитируем занятый сервер, который не может вызвать функцию accept сразу же, как только функция select сообщит, что прослушиваемый сокет готов для чтения. Обычно подобное замедление со стороны сервера не вызывает проблем (на самом деле именно для этих ситуаций предусмотрена очередь полностью установленных соединений). Но поскольку после установления соединения от клиента прибыл сегмент RST, у нас возникает проблема.
В разделе 5.11 мы отмечали, что когда клиент разрывает соединение до того, как сервер вызывает функцию accept, в Беркли-реализациях прерванное соединение не возвращается серверу, в то время как другие реализации должны возвращать ошибку ECONNABORTED, но часто вместо нее возвращают ошибку EPROTO. Рассмотрим Беркли-реализацию.
八 Клиент устанавливает соединение и затем прерывает его, как показано в листинге 15.14.
嫉 Функция sei ect сообщает процессу сервера, что дескриптор готов для чтения, но у сервера вызов функции accept занимает некоторое, хотя и непродолжительное, время.
變 После того как сервер получил сообщение от функции sei ect и прежде, чем была вызвана функция accept, прибыл сегмент RST от клиента.
鬱 Установленное соединение удаляется из очереди, и мы предполагаем, что не существует никаких других установленных соединений.< *
Сервер вызывает функцию accept, но поскольку установленных соединений нет, он оказывается заблокирован.
Сервер останется блокированным в вызове функции accept до тех пор, пока какой-нибудь другой клиент не установит с ним соединение. Но если сервер аналогичен показанному в листинге 6 4, в это время он заблокирован в вызове функции accept и не может обрабатывать никакие другие готовые дескрипторы
ПРИМЕЧАНИЕ
Проблема в некоторой степени аналогична проблеме, называемой атакой типа «отказ в обслуживании», описанной в разделе 6 8 Однако в данном случае сервер выходит из состояния блокировки, как только другой клиент установит соединение
Чтобы решить эту проблему, нужно соблюдать два следующих правила:
Всегда делать прослушиваемый сокет неблокируемым, если мы используем функцию select для определения того, готово ли соединение к обработке функцией accept.
Игнорировать следующие ошибки, возникающие при повторном вызове функции accept: EWOULDBLOCK (для Беркли-реализаций, когда клиент разрывает соединение), ECONNABORTED (для реализаций Posix 1g, когда клиент разрывает соединение), EPROTO (для реализаций SVR4, когда клиент разрывает соединение) и EINTR (если перехватываются сигналы).
Резюме
В примере неблокируемого чтения и записи в разделе 15.2 использовался наш клиент str_cli, который мы изменили для применения неблокируемого ввода- вывода на соединении TCP с сервером. Функция select обычно используется с неблокируемым вводом-выводом для определения того момента, когда дескриптор станет готов для чтения или записи. Эта версия нашего клиента является самой быстродействующей из всех показанных версий, хотя требует нетривиального изменения кода. Затем мы показали, что проще разделить процесс клиента на две части при помощи функции fork. Мы используем ту же технологию при создании потоков в листинге 23.1.
Неблокируемая функция connect позволяет нам во время трехэтапного рукопожатия TCP выполнять другие задачи вместо блокирования в вызове функции connect. К сожалению, с этими функциями также связана проблема совместимости, так как различные реализации по-разному указывают, успешно ли установлено соединение или произошла ошибка. Мы использовали неблокируемые соединения для создания нового клиента, аналогичного web-клиенту, открывающему одновременно множество соединений TCP для уменьшения затрат времени при получении нескольких файлов от сервера Подобное иницииирование множества соединений может сократить временные затраты, но также является «недружественным по отношению к сети», поскольку не позволяет воспользоваться алгоритмом TCP, предназначенным для предотвращения перегрузки (congestion avoidance).
Упражнения
Обсуждая листинг 15 6, мы отметили, что родительский процесс должен вызвать функцию shutdown, а не функцию close Понему?
Что произойдет в листинге 15.6, если процесс сервера завершится преждевременно и дочерний процесс получит признак конца файла, но не уведомит об этом родительский процесс?
Что произойдет в листинге 15 6, если родительский процесс непредвиденно завершится до завершения дочернего процесса, и дочерний процесс затем считает конец файла на сокете?
Что произойдет в листинге 15.7, если мы удалим следующие две строки*
if (п == 0)
goto done /* функция connect завершилась немедленно */
В разделе 15.3 мы сказали, что возможна ситуация, когда данные для сокета придут раньше, чем завершится функция connect. Когда это может случиться?
ГЛАВА 16
Операции функции ioctl
Введение
Функция ioctl традиционно являлась системным интерфейсом, используемым для всего, что не входило в какую-либо другую четко определенную категорию. Posix постепенно избавляется от функции ioctl, создавая заменяющие ее функции-обертки и стандартизуя их функциональность. Например, доступ к интерфейсу терминала Unix традиционно осуществлялся с помощью функции ioctl, но в Posix. 1 были созданы 12 новых функций для терминалов: tcgetattr для получения атрибутов терминала, tcfl ush для сброса незавершенного ввода или вывода, и т. д. Аналогичным образом Posix. 1g заменяет одну функцию ioctl: новая функция sockatmark (см. раздел 21.3) заменяет SIOCATMARK ioctl. Тем не менее различные функции ioctl остаются для зависящих от реализации свойств, относящихся к сетевому программированию, например для получения информации об интерфейсе и обращения к таблице маршрутизации и кэшу ARP (Address Resolution Protocol — протокол разрешения адресов).
В этой главе представлен обзор вызовов функции ioctl, имеющих отношение к сетевому программированию, многие из которых зависят от реализации. Кроме того, более новые Беркли-реализации используют сокеты семейства AF_ROUTE (маршрутизирующие сокеты) для выполнения многих из этих операций. Маршрутизирующие сокеты мы рассматриваем в главе 17.
Обычно сетевые программы (как правило, серверы) используют функцию 】octi для получения информации обо всех интерфейсах узла при запуске программы, с тем чтобы узнать адрес интерфейса, выяснить, поддерживает ли интерфейс широковещательную передачу, многоадресную передачу и т. д. Для возвращения этой информации мы разработали нашу собственную функцию. В этой главе мы представляем ее реализацию с применением функции ioctl, а в главе 17 — другую реализацию, использующую маршрутизирующие сокеты.
Функция ioctl
Эта функция работает с открытым файлом, на который делается ссылка при помощи аргумента fd.
#mclude <umstd h>
mt ioctl(int fd. int request /* void *arg */ ).
CcC 一f C...C С Л C ci,,.,rc . ni ■ л г\ п ■ >zi ■ *■ л Л 1 r> CnmiAC CUIxKuiA
Третий аргумент всегда является указателем, но тип указателя зависит от аргумента request.
ПРИМЕЧАНИЕ
В 4.4BSD второй аргумент имеет тип unsigned long вместо int, но это не вызывает проблем, поскольку в заголовочных файлах определены константы, используемые для данного аргумента.
Некоторые реализации определяют третий аргумент как пустой указатель (void*), а не так, как он определен в ANSI С.
Не существует единого стандарта заголовочного файла, определяющего прототип функции для ioctl, поскольку он не стандартизован в Posix. Многие системы определяют этот прототип в файле <unistd.h>, как это показываем мы, но традиционные системы BSD определяют его в заголовочном файле <sys/ioctl.h>.
Мы можем разделить аргументы requests, имеющие отношение к сети, на шесть категорий:
операции с сокетами;
¥ операции с файлами; •
觀 операции с интерфейсами;
綴 операции с кэшем ARP;
攀 операции с таблицей маршрутизации;
綿 операции с потоками (см. главу 33).,
Помимо того что, как показывает табл. 7.5, некоторые операции ioctl перекрывают часть операций fcntl (например, установка неблокируемого сокета), существуют также некоторые операции, которые с помощью функции 】octi можно задать более чем одним способом (например, смена групповой принадлежности сокета).
В табл. 16.1 перечислены аргументы requests вместе с типами данных, на которые должен указывать адрес агд. В последующих разделах эти вызовы рассматриваются более подробно.
Таблица 16.1. Обзор сетевых вызовов ioctl
Категория
request
Описание
Тип данных
Сокет
SIOCATMARK
Находится ли указатель чтения сокета па отметке внеполосных данных
mt
SIOCSPGRP
Установка идентификатора процесса или идентификатора группы процессов для сокета
int
SIOCGPGRP
Получение идентификатора процесса или идентификатора группы процессов для сокета
mt
Фййл
FIONBIO
Установка/сброс флага отсутствия
mt
блокировки
FIOASYNC
Установка/сброс флага асинхронного ввода-вывода
mt
FIONREAD
Получение количества байтов в приемном буфере
int
Таблица 16.1 (продолжение)
Категория
request
Описание
Тип данных
Интерфейс
ARP
Маршрутизация
Потоки
FIOSETOWN
FIOGETOWN
SIOCGIFCONF SIOCSIFADDR SIOCGIFADDR SIOCSIFFLAGS SIOCGIFFLAGS SIOCSIFDSTADDR SIOCGIFDSTADDR SIOCGIFBRDADDR SIOCSIFBRDADDR SIOCGIFNETMASK SIOCSIFNETMASK SIOCGIFMETRIC SIOCSIFMETRIC SIOCxxx
SIOCSARP SIOCGARP SIOCDARP SIOCADDRT
SIOCDELRT
I _xxx
Установка идентификатора процесса или идентификатора группы процессов для файла
Получение идентификатора процесса или идентификатора группы процессов для файла
Получение списка всех интерфейсов
Установка адреса интерфейса
Получение адреса интерфейса
Установка флагов интерфейса Получение флагов интерфейса
Установка адреса типа «точка-точка» Получение адреса типа «точка-точка» Получение широковещательного адреса Установка широковещательного адреса Получение маски подсети
Установка маски подсети
Получение метрики интерфейса
Установка метрики интерфейса (Множество вариантов в зависимости от реализации)
Создание/модификация элемента ARP
Получение элемента ARP
Удаление элемента ARP
Добавление маршрута
Удаление маршрута
(См. раздел 33.5)
int
int
struct ifeonf struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq struct ifreq
struct arpreq struct arpreq struct arpreq struct rtentry
struct rtentry
Операции с сокетами
Существует три типа вызова, или запроса (в зависимости от значения аргумента request), функции wet 1, предназначенные явным образом для сокетов [105, с. 551553]. Все они требуют, чтобы третий аргумент функции loctl был указателем на целое число.
鑫 SIOCATMARK. Возвращает указатель на ненулевое значение в качестве третьего аргумента (его тип, как только что было сказано, — указатель на целое число), если указатель чтения сокета в настоящий момент находится на отметке внеполосных данных (out-of-band mark), или указатель на нулевое значение, если указатель чтения сокета не находится на этой отметке. Более подробно внеполосные данные (out-of-band data) рассматриваются в главе 21. Posix.lg заменяет этот вызов функцией sockatmark, и мы рассматриваем реализацию этой новой функции с использованием функции loctl в разделе 21.3.
餐 SIOCGRP. Возвращает в качестве третьего аргумента указатель на целое число — идентификатор процесса или группы процессов, которым будут посылаться
сигналы SIGI0 или SIGURG по окончании выполнения асинхронной операции или при появлении срочных данных. Этот вызов идентичен вызову F GETOWN функции fcntl, и в табл. 7.5 мы отмечали, что Posix.lg стандартизирует функцию fcntl.
SIOCSPGRP. Задает идентификатор процесса или группы процессов для отсылки им сигналов SIGIO или SIGURG как целое число, на которое указывает третий аргумент. Этот вызов идентичен вызову F_SETOWN функции fcntl, и в табл. 7.5 мы отмечали, что Posix.lgстандартизирует функцию fcntl.
Операции с файлами
Следующая группа вызовов начинается с FI0 и может применяться к определенным типам файлов в дополнение к сокетам. Мы рассматриваем только вызовы, применимые к сокетам [105, с. 553].
Следующие пять вызовов требуют, чтобы третий аргумент функции т octi указывал на целое число.
ж FIONBIO. Флаг отключения блокировки при выполнении операций ввода-вывода сбрасывается или устанавливается в зависимости от третьего аргумента функции 】octi. Если этот аргумент является пустым указателем, то флаг сбрасывается (блокировка разрешена). Если же третий аргумент является указателем на единицу, то включается неблокируемый ввод-вывод. Этот вызов об-
• ладает таким же действием, что и команда F_SETFL функции fcntl, которая позволяет установить или сбросить флаг O_NONBLOCK, задающий статус файла.
沁 FIOASYNC. Флаг, управляющий получением сигналов асинхронного ввода-вывода (SIGIO), устанавливается или сбрасывается для сокета в зависимости от того, является ли третий аргумент функции wctl пустым указателем. Этот флаг имеет то же действие, что и флаг статуса файла O_ASYNC, который можно установить и сбросить с помощью команды F_SETFL функции wctl.
/ FIONREAD. Возвращает число байтов, в настоящий момент находящихся в приемном буфере сокета, как целое число, на которое указывает третий аргумент функции loctl. Это свойство работает также для файлов, каналов и терминалов. Более подробно об этом вызове мы рассказывали в разделе 13.7.
FIOSETOWN. Эквивалент SIOCSPGRP для сокета.
> FIOGETOWN. Эквивалент SIOCGPGRP для сокета.
Конфигурация интерфейса
Один из шагов, выполняемых многими программами, работающими с сетевыми интерфейсами системы, — это получение от ядра всех интерфейсов, сконфигурированных в системе. Это делается с помощью вызова SIOCGIFCONF, использующего структуру if conf, которая, в свою очередь, использует структуру ifreq. Обе эти структуры показаны в листинге 16.1L
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter com /download.一
Листинг 16.1. Структуры ifconf и if req, используемые в различных вызовах функции loctl, относящихся к интерфейсам
<net/if h>
struct 1fconf {
int ifcjlen/* размер буфера «значение-результат» */
union {
caddr_t ifcu_buf/* ввод от пользователя к ядру */
struct ifreq *ifcu_req/* ядро возвращает пользователю */
} ifc_ifcu
}~
#define ifc_buf i fc_ifeu ifcu_buf /* адрес буфера */
#define ifc]req ifc_ifcu ifcu_req /* массив возвращенных структур */
#de目ne IFNAMSIZ 16
struct ifreq {
chari fr_name[IFNAMSIZ]/* имя интерфейса например 'le(T 步/
union {'
struct sockaddr if「u_addr
struct sockaddr ifru_dstaddr
struct sockaddr ifru_broadaddr.
j short ifru_flags'
int ifru_metnc caddr_t ifru_data } ifr_ifru
}
#define
ifr_addr
ifjifru
ifru_addr
/* адрес */
#define
ifr_dstaddr
ifr_ifru
ifru_dstaddr
/* другой конец линии передачи называемый
«точка-точка» */
#define
ifr_broadaddr
ifr_if「u
1fru_broadaddr
/* широковещательный адрес */
#define
if匸f1ags
ifr_ifru
ifruflags
/* флаги */
#define
ifr_metric
】fr_if「u
ifru_metric
/* метрика */
#define
ifr_data
ifr_ifru
ifru_data
/* с использованием интерфейса */
Прежде чем вызвать функцию loctl, мы выделяем в памяти место для буфера и для структуры 1 fconf, а затем инициализируем эту структуру. Мы показываем это на рис. 16.1, предполагая, что наш буфер имеет размер 1024 байта Третий аргумент функции loctl 一 это указатель на нашу структуру ifconf.
ifconf{}ifconf{}Рис. 16.1, Инициализация структуры ifconf перед вызовом SIOCGIFCONFРис. 16.1, Инициализация структуры ifconf перед вызовом SIOCGIFCONF
ifconf{}
ifconf{}
Рис. 16.1, Инициализация структуры ifconf перед вызовом SIOCGIFCONF
Рис. 16.1, Инициализация структуры ifconf перед вызовом SIOCGIFCONF
Если мы предположим, что ядро возвращает две структуры ifreq, то при завершении функции тосМ мы можем получить ситуацию, представленную на рис. 16 2. Затененные области были изменены функцией loctl. Буфер заполняет-
ся двумя структурами, и элемент i fс_1 еп структуры i fconf обновляется, с тем чтобы соответствовать количеству информации, хранимой в буфере. Предполагается, что на этом рисунке каждая структура ?freq занимает 32 байта.
ifconf{}ifconf{}Рис. 16.2- Значения, возвращаемые в результате вызова SIOCGIFCONFРис. 16.2- Значения, возвращаемые в результате вызова SIOCGIFCONF
ifconf{}
ifconf{}
Рис. 16.2- Значения, возвращаемые в результате вызова SIOCGIFCONF
Рис. 16.2- Значения, возвращаемые в результате вызова SIOCGIFCONF
Указатель на структуру 】freq также используется в качестве аргумента оставшихся функций loctl интерфейса, показанных в табл. 16.1, которые мы описываем в разделе 16 7. Отметим, что каждая структура ifreq содержит объединение (urn on), а директивы компилятора #def i ne позволяют непосредственно обращаться к полям объединения по их именам. Помните о том, что в некоторых системах в объединение i fr_i fru добавлено много зависящих от реализации элементов
Функция getjfijnfo
Поскольку многим программам нужно знать обо всех интерфейсах системы, мы разработаем нашу собственную функцию get_】f】_】nfo, возвращающую связный список структур — по одной для каждого активного в настоящий момент интерфейса. В этом разделе мы покажем, как эта функция реализуется с помощью вызова SIOCGIFCONF функции ioctl, а в главе 17 мы создадим ее другую версию, использующую маршрутизирующие сокеты.
ПРИМЕЧАНИЕ
BSD/OS предоставляет функцию getifaddrs, имеющую аналогичную функциональ- , ность
- Поиск во всему дереву исходного кода BSD/OS 2 1 показывает, что 12 программ вы- I полняют вызов SIOCGIFCONF функции ioctl для определения присутствующих интерфейсов
Сначала мы определяем структуру 】fi_】nfo в новом заголовочном файле, который называется unpifi h, показанном в листинге 16.2.
Листинг 16.2. Заголовочный файл unpifi.h
Z/iocti/unpifi h
/* Ha川 собственный заголовочный файл для программ которым требуется информация о конфигурации интерфейса
Включаем этот файл вместо файла "unp h" */
#ifndef —unp_i fi_h
#define —unp_ifi_h
#】nclude"unp h"
#include<net/if h>
10
char
ifi_name[IFIJAME],
11
u char
ifi haddHIFI HADDR].
12
u short ifi hlen.
13
short
ifi_flags.
14
short
ifi^myflags.
15
struct
sockaddr *ifi_add「.
16
struct
sockaddr *ifi_brdaddr.
17
struct
sockaddr *ifi_dstaddr.
18
struct
ifi_info *ifi_next.
19
}•
7 #define IFI_NAME168 #define IFIJADDR87 #define IFI_NAME168 #define IFIJADDR89 struct ifi_inf。 {9 struct ifi_inf。 {20 #define IFI_ALIAS 120 #define IFI_ALIAS 1/* то же. что и IFNAMSIZ в заголовочном файле
7 #define IFI_NAME16
8 #define IFIJADDR8
7 #define IFI_NAME16
8 #define IFIJADDR8
9 struct ifi_inf。 {
9 struct ifi_inf。 {
20 #define IFI_ALIAS 1
20 #define IFI_ALIAS 1
<net/if h> */
/* с учетом 64-битового интерфейса EUI-64 в будущем */
/* имя интерфейса. заканчивается символом конца строки */
/* аппаратный адрес */
/* количество байтов в аппаратном адресе 0. 6. 8 */
/* константы IFF ххх из заголовочного файла
<net/if h> */
/* наши флаги IFI_xxx */
/* первичный адрес */
/* широковещательный адрес */
/* адрес получателя */
/* следующая из этих структур */
/* ifi addr- это псевдоним */
/* прототипы функций */
struct ifi_info *get_ifi_)nfo(int. int).
struct 1 fi_inf。*Get_i fi_info(mt int).
void free_ifi_mfo(struct ifi_mfo *).
#endif /* —unp_ifi_h */
9-19 Связный список этих структур возвращается нашей функцией. Элемент 】f 】_next каждой структуры указывает на следующую структуру. Мы возвращаем в этой структуре информацию, которая может быть востребована в типичном приложении: имя интерфейса, аппаратный адрес (например, адрес Ethernet), флаги интерфейса (чтобы позволить приложению определить, поддерживает ли приложение широковещательную или многоадресную передачу и относится ли этот интерфейс к типу «точка-точка»), адрес интерфейса, широковещательный адрес, адрес получателя для связи «точка-точка». Вся память, используемая для хранения структур 1 fi_info вместе со структурами адреса сокета, содержащимися в них, выделяется динамически. Следовательно, мы также предоставляем функцию free_i fi_i nfo для освобождения всей этой памяти.
Перед тем как представить реализацию нашей функции 】f】_】 nfo, мы покажем простую программу, которая вызывает эту функцию и затем выводит информа-
цию. Эта программа, представленная в листинге 16.3, является уменьшенной версией программы if config.
ПРИМЕЧАНИЕ
Большинство аппаратных адресов на сегодня являются 48-разрядными МАС-адресами (например, Ethernet, Token Ring и т. д.). Однако существует тенденция использования 64-разрядных идентификатеров, называемых EUI-64 [42]. Адреса IPv6 содержат значение EUI-64 в младших 64 битах (см. раздел А.5), и можно простым способом инкапсулировать 48-разрядный МАС-адрес в 64-разрядный EUL Поэтому мы выделяем достаточно места в нашей структуре ifi info для 64-разрядного идентификатора и храним длину аппаратного адреса.
Листинг 16.3. Программа prifinfo, вызывающая нашу функцию ifijnfo
//1 octi/рп flnfo с
1 include "unpifi h“
2 3 4 5 6 7 82 3 4 5 6 7 8mt
2 3 4 5 6 7 8
2 3 4 5 6 7 8
md】n(int argc. char **argv)
{
struct 1f1_jnfo *1fi. *1fihead;
struct sockaddr *sa.
u_cha「 *ptr
int i. family doaliases
9
10
if (argc '= 3)
err_quit("usage prifinfo <inet4[inet6> <doaliases>rt):
11
12
13
14
15
16
17
18
19
20
21
22
23
if (strcmp(argv[lj. ,,^net4^,) == 0) family = AF_INET
#ifdef IPV6
else if (strcmp(argv[l]. "inet6") == 0) family = AF_INET6
#endif
else
err_quit("invalid〈address-fann 1 y>") doaliases = atoi(argv[2J).
for (1fihead = ifi = Get_ifi_info(family, ifi i= NULL ifi = ifi->ifi_next) { pri ntf("%s <M ifi->ifi_name).
if (ifi->ifi flags & IFF UP)
doaliases).
pnntfd'UP ”).
24
if (ifi->if匸flags & IFF二BROADCAST)
printf(*'BCAST
").
25
if (if?->if匸flags & IFF二MULTICAST)
pnntfCMCAST
26
if (if】->if匸flags & IFF二LOOPBACK)
pEtf(” LOOP "
)
27
28
29
30
31
if (if”>if匸flags & IFF二POINTOPOINT) printf(n>\enr),—
if ( (i = ifi->ifi_hlen) > 0) {
ptr 38 ifi->ifi haddr.
do {—
pnntf(“P2P “)
32
33
34
35
36
37
printfC^s^x". (i = ifi->ifi_hlen) } while (--i > 0).
printf("\en").
}
'"if ( (sa = ifi->ifi_addr) ,= NULL)
'卩pnntf(” IP ddd「:総\en“.
7 itиtt к
*ptr++)
Листинг 16.3 (продолжение)
Sock_ntop_host(sa sizeof(*sa)))
if ( (sa = Tfi >ifi_brdaddr)【=NULL)
prmtf( broadcast addr 犯\en
Sock_ntop_host(sa sizeof(*sa)))
if ( (sa =~ifi >ifi_dstaddr) 「 NULL)
printf( destination addr *s\en
Sock ntop host(sa sizeof(*sa)))
}一 -
free_ifi _info(ifihead)
exitTO)-
}
20-45 Программа представляет собой цикл for, в котором один раз вызывается функция get_i fi _i nfo,a затем последовательно перебираются все возвращаемые структуры ifi_info
22-35 Выводятся все имена интерфейсов и флаги Если длина аппаратного адреса больше нуля, он выводится в виде шестнадцатеричного числа (наша функция get_if】_info возвращает нулевую длину ifi_hlen, если адрес недоступен)
36-44 Выводятся три IP-адреса, если они возвращаются
Если мы запустим эту программу на нашем узле Solans (см рис 1 7), то получим следующий результат solans 案 pnfinfo inet4 0 loO <UP MCAST LOOP >
IP addr 127 0 0 1 leO <UP BCAST MCAST >
IP addr 206 62 226 33
broadcast addr 206 62 226 63
Первый аргумент командной строки inet4 задает адрес IPv4, а второй, нулевой аргумент указывает, что не должно возвращаться никаких псевдонимов или альтернативных имен (альтернативные имена IP-адресов мы описываем в разделе А 4) Обратите внимание, что в Solans аппаратный адрес интерфейса Ethernet недоступен
Если мы добавим к интерфейсу Ethernet (1е0) три альтернативных имени адреса с идентификаторами узла 44,45 и 46 и изменим второй аргумент командной строки на 1, то получим solans % pnfinfo 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_if】_info, приведенную в листинге 17.9 (которая может легко получиты аппаратный адрес), то получим
bsdi Z prifinfo inet4 1
weO <UP BCAST MCAST >
О 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 (efO) с идентификатором узла 50
ПРИМЕЧАНИЕ
Получаемый в данном случае результат зависит от того, как задаются адреса с альтернативными именами интерфейсов В данном примере адрес с альтернативным именем задан интерфейсу Ethernet efO, и это обычная технология для BSD/OS 2 1 Но в BSD /OS 3 0 рекомендуется задавать адреса с альтернативными именами интерфейсу 1о0 (интерфейсу закольцовки)
Теперь мы покажем нашу реализацию функции get_】f】_】nfo, использующую вызов SIOCGIFCONF функции 】octi В листинге 164 показана первая часть этой функции, получающая от ядра конфигурацию интерфейса
Листинг 16.4. Выполнение вызова SIOCGIFCONF для получения конфигурации
интерфейса
//lib/get_ifi_info с
1
2
3
4
5
6
7
8
9
10
#includeunpifi h
struct ifi_info *
get ifi info(int family int doaliases) {""
struct ifi_info *ifi *1fihead **ifipnext int sockfd len lastlen flags myflags char *ptr *buf lastname[IFNAMSIZ] *cptr struct ifconf ifc
struct ifreq *ifr ifrcopy
struct sockdddr一in *sinptr
И
sockfd = Socket(AF_INET SOCK_DGRAM 0)
12
13
14
15
16
17
18
19
20
21
lastlen = 0
len = 100 * sizeof(struct ifreq) /* исходное предположение 6 размере буфера */
for ( ) {
buf = Malloc(len)
ifc ifcjlen = len
ifc ifc_buf = buf
if (ioctl(sockfd SIOCGIFCONF &ifc) < 0) {
if (errno i= EINVAL || lastlen '= 0) err_sys( ioctl error )
} else {продолжение 身
Листинг 16.4 (продолжение)
if (ifc ifc_len == lastlen)
break. /* значение len не изменилось */
lastlen = ifc ifc_len.
}一
len += 10 * sizeof(struct ifreq),/* увеличиваем значение len */
free(buf).
}
iflhead = NULL.
30 ifipnext = &ifihead;
31 lastname[0] = 0.
Создание сокета Интернета
11 Мы создаем сокет UDP, который будет использоваться с функциями toctl. Может применяться как сокет TCP, так и сокет UDP [105, с. 163].
Выполнение вызова SIOCGIFCONF в цикле
12-28 Фундаментальной проблемой, связанной с вызовом SIOCGIFCONF, является то, что некоторые реализации не возвращают ошибку, если буфер слишком мал для хранения полученного результата [105, с. 118-119]. В этом случае результат просто обрезается, так чтобы поместиться в буфер, и функция ioctl возвращает нулевое значение, что соответствует успешному выполнению. Это означает, что единственный способ узнать, достаточно ли велик наш буфер, — сделать вызов, сохранить возвращенную длину, снова сделать вызов с большим размером буфера и сравнить полученную длину со значением, сохраненным из предыдущего вызова. Только если эти две длины одинаковы, наш буфер можно считать достаточно большим.
ПРИМЕЧАНИЕ
Беркли-реализации не возвращают ошибку, если буфер слишком мал [ 105, с. 118-199], и результат просто обрезается так, чтобы поместиться в существующий буфер. Solaris 2.5 возвращает ошибку EINVAL, если возвращаемая длина больше или равна длине буфера. Но мы не можем считать вызов успешным, если возвращаемая длина меньше раз~ мера буфера, поскольку Беркли-реализации могут возвращать значение, меньшее раз- мера буфера, если часть структуры в него не помещается.
В некоторых реализациях предоставляется вызов SIOCGIFNUM, который возвращает число интерфейсов. Это позволяет приложению перед выполнением вызова SIOCGIFCONF выделить в памяти место для буфера достаточного размера, но такой подход не является широко распространенным.
Выделение в памяти места под буфер фиксированного размера для результата вызова SIOCGIFCONF стало проблемой с ростом Сети, поскольку большие web-серверы ис- пользуют много альтернативных адресов для одного интерфейса. Например, в Solaris 2.5 был предел в 256 альтернативных адресов для интерфейса, по в версии 2.6 этот предел вырос до 8192. Обнаружилось, что на сайтах с большим числом альтернативных адресов перестают работать программы с буферами фиксированного размера для размещения информации об интерфейсе. Хотя Solaris возвращает ошибку, если буфер слишком мал, эти программы размещают в памяти буфер фиксированного размера, запускают функцию ioctl, по затем перестают работать при возвращении ошибки.
12-15 Мы динамически размещаем в памяти буфер, начиная с размера, достаточного для 100 структур 1 freq. Мы также отслеживаем длину, возвращаемую последним вызовом SIOCGIFCONF в lastlen, и инициализируем ее нулем.
19-20 Если функция wctl возвращает ошибку EINVAL п функция еще нс возвращалась успешно (то есть lastlen все еще равно нулю), значит, мы еще не выделили буфера достаточного размера, поэтому мы продолжаем выполнять цикл.
22-23 Если функция ioctl завершается успешно и возвращаемая длина равна lastlen, значит, длина не изменилась (наш буфер имеет достаточный размер) и мы с помощью функции break выходим из цикла, так как у нас имеется вся информация.
26-27 В каждом проходе цикла мы увеличиваем размер буфера для хранения еще 10 структур ifreq.
Инициализация указателей связного списка
?9-31 Поскольку мы будем возвращать указатель на начало связного списка структур 1 f ч_ч nfo, мы используем две переменные» i fihead и i fi pnext, для хранения указателей на список по мере его создания. Это та же технология, которую мы описывали применительно к листингу 11.24.
Следующая часть нашей функции get_i fi_i nfo, содержащая начало основного цикла, показана в листинге 16.5.
Листинг 16.5. Конфигурация интерфейса процесса
//Tib/get^fi info с
32 for (ptr = buf, ptr < buf + ifc ifc len ) {
33 ifr = (struct ifreq *) ptr
1
34 #ifdef HAVE_SOCKADDR_SA_LEN
35 len = max(sizeof(struct sockaddr) ifr->ifr_dddr.sa_】Du.
36 #else
37switch (ifr->ifr_addr sa_family) {
38 #ifdef IPV6 —"
39case AF_INET6.
40len =~sizeof(struct sockaddr_in6).
41break
42 #endif>.
43 case AFJNET
44default-
45len = sizeof(struct sockaddr),
46break.
47}
J 48 #endif /* HAVE_SOCKADDR_SA_LEN */
I 49 ptr += sizeof(ifr->ifr_name) -»- len, /* для следующей в буфере */
К 50 ' if (ifr->ifr_addr sa_fannly 『family)
51 continue, /* игнорируем если зто не искомое семейство адресов */
52 ' ” myf1ags = 0.
53 if ( (cptr = strchr(ifr->ifr_name, 1 '))』NULL)
*cptr = 0./* заменяем двоеточие ча нуль */
if (strncmp(lastname, ifr->ifr_name. IFNAMSIZ) == 0) {
if (doaliases == 0)~
57 < / continue. /* этот интерфейс уже обработан */
58 ^п< щуflags ^ IFI_ALIAS,
59-
Е 'продолжение 按
Листинг 16.5 (продолжение)
60memcpy(lastname, ifr->ifrename.IFNAMSIZ).
611frcopy = *ifr,
62Ioctl(sockfd, SIOCGIFFLAGS.&ifrcopy),
63flags * 1frcopy.ifr_flags.
64 if ( (flags & IFF.UP) ^^ 0)
65 continue./* игнорируем, если этот интерфейс неактивен */
6667686970717266676869707172if】* CallocCl, sizeof(struct ifi info))if】* CallocCl, sizeof(struct ifi info))*】fipnext ^ ifi.ifipnext « &ifi->ifi_next.*】fipnext ^ ifi.ifipnext « &ifi->ifi_next.ifv>ifi_fl ags * fl ags.тfi、>ifi_myflags * myflags.ifv>ifi_fl ags * fl ags.тfi、>ifi_myflags * myflags./* /*/* /*prev указывает на созданную структуру */сюда указывает указатель на следук)1цую структуруprev указывает на созданную структуру */сюда указывает указатель на следук)1цую структуру/* значения IFF_xxx */ /* значения IFI_xxx *//* значения IFF_xxx */ /* значения IFI_xxx */memcpy (i fi ->i fi_r)ame ifr->i fr_name, IFI_NAME). if1'>if1_name[IF1_NAME - 1] = '\e0'.memcpy (i fi ->i fi_r)ame ifr->i fr_name, IFI_NAME). if1'>if1_name[IF1_NAME - 1] = '\e0'.*/*/
66
67
68
69
70
71
72
66
67
68
69
70
71
72
if】* CallocCl, sizeof(struct ifi info))
if】* CallocCl, sizeof(struct ifi info))
*】fipnext ^ ifi.
ifipnext « &ifi->ifi_next.
*】fipnext ^ ifi.
ifipnext « &ifi->ifi_next.
ifv>ifi_fl ags * fl ags.
тfi、>ifi_myflags * myflags.
ifv>ifi_fl ags * fl ags.
тfi、>ifi_myflags * myflags.
/* /*
/* /*
prev указывает на созданную структуру */
сюда указывает указатель на следук)1цую структуру
prev указывает на созданную структуру */
сюда указывает указатель на следук)1цую структуру
/* значения IFF_xxx */ /* значения IFI_xxx */
/* значения IFF_xxx */ /* значения IFI_xxx */
memcpy (i fi ->i fi_r)ame ifr->i fr_name, IFI_NAME). if1'>if1_name[IF1_NAME - 1] = '\e0'.
memcpy (i fi ->i fi_r)ame ifr->i fr_name, IFI_NAME). if1'>if1_name[IF1_NAME - 1] = '\e0'.
*/
*/
Переход к следующей структуре адреса сокета
32-49 При последовательном просмотре всех структур ifreq ifr указывает на текущую структуру, а мы увеличиваем ptr на единицу, чтобы он указывал на следующую. Необходимо предусмотреть особенность более новых систем, предоставляющих поле длины для структур адреса сокета, и вместе с тем учесть, что более старые системы этого поля не предоставляют. Хотя в листинге 16.1 структура адреса сокета, содержащаяся в структуре ч freq, объявляется как общая структура адреса сокета, в новых системах она может относиться к произвольному типу. Действительно, в 4.4BSD структура адреса сокета канального уровня также возвращается для каждого интерфейса [105, с. 118]. Следовательно, если поддерживается элемент длины, то мы должны использовать его значение для переустановки нашего указателя на следующую структуру адреса сокета. В противном случае мы определяем длину, исходя из семейства адресов, используя размер общей структуры адреса сокета (16 байт) в качестве значения по умолчанию.
ПРИМЕЧАНИЕ
В системах, поддерживающих IPv6, не оговаривается, возвращаеася ли адрес IPv6 вызовом SI OCG1FCONF. Для более новых систем мм вводим оператор switch, в ко юром предусмотрена возможность возвращения адресов IPv6. Проблема состоит в том, что объединение в структуре ifreq определяет возвращаемые адреса как общие 16-байтовые структуры sockadd г, подходящие для 16-байтовых структур sockaddr_in IPv4, но для 24-байтовых структур sockaddr_in6 IPv6 они слишком малы. В случае возвращения адресов IPv6 возможно некорректное поведение существующего кода, созданного в пред пол олсс! гм и, что в каждой структуре ifreq содержится структура sockadd г фиксированного размера.
50-51 Мы игнорируем все адреса из семейств, отличных от указанного, вызывающим процессом в аргументе функции get_mi_info.
Обработка альтернативных имен
52-60 Нам нужно обнаружить все альтернативные имена (псевдонимы), которые могут существовать для интерфейса, то есть присвоенные этому интерфейсу дополнительные адреса. Обратите внимание в наших примерах, следующих за листан-
гом 16.3, что в Solaris псевдоним содержит двоеточие, в то время как в 4.4BSD имя интерфейса в псевдониме не изменяется. Чтобы обработать оба случая, мы сохраняем последнее имя интерфейса в lastname и сравниваем его только до двоеточия, если оно присутствует. Если двоеточия нет, мы игнорируем этот интерфейс в том случае, когда имя эквивалентно последнему обработанному интерфейсу.
Получение флагов интерфейса
61-65 Мы выполняем вызов SIOCGIFFLAGS функции icctl (см. раздел 16.5), чтобы получить флаги интерфейса. Третий аргумент функции ioctl — это указатель на структуру ifreq, содержащую имя интерфейса, для которого мы хотим получить флаги. Мы создаем копию структуры i f「eq, перед тем как запустить функцию ioctl, поскольку в противном случае этот вызов перезаписал бы IP-адрес интерфейса, потому что оба они являются элементами одного и того же объединения из листинга 16.1. Если интерфейс не активен, мы игнорируем его.
Выделение памяти и инициализация структуры ifijnfo
66-72 На этом этапе мы знаем, что возвратим данный интерфейс вызывающему процессу. Мы выделяем память для нашей структуры ifijnfo и добавляем ее в конец связного списка, который мы создаем. Мы копируем флаги и имя интерфейса в эту структуру. Далее мы проверяем, что имя интерфейса заканчивается нулем, и поскольку функция cal 1 ос инициализируег выделенную в памяти область нулями, мы знаем, что ifi_hlen инициализируется нулем, a ifi„next — пустым указателем.
Листинг 16.6. Получение и возвращение адресов интерфейса
//ioctl/get_ifi_info с
switch Tifr->1 fr_addr sa_farmly) {
case AF—INET
sinptr = (struct sockaddr^in *) &ifr->ifr_addr.
if (ifi->ifi_addr == NULU {
ifi->ifi_addr = CallocCl sizeof(struct sockaddr_in)):
men)cpy( ifi ->i fi_addr. swptr sizeof(struct sockaddr__7n));
#ifdef SIOCGIFBRDADDR
if (flags & IFF_BROADCAST) {
Ioct1(sockfd SIOCGIFBROAOOR &ifrcopy)
sinptr = (struct sockaddr_in *) Л-1 frcopy ifr_broadaddr.
ifi->ifi_brdaddr = Calloc(l sizeofCstruct sockaddr_in)).
memcpyC^fi->ifi_brdaddr, sinptr sizeof(struct sockaddr_in)).
}——
#endif
87 #ifdef888990919293 >94 妬xli f95 "}87 #ifdef888990919293 >94 妬xli f95 "}SIOCGIFDSTADDR
87 #ifdef
88
89
90
91
92
93 >
94 妬xli f
95 "}
87 #ifdef
88
89
90
91
92
93 >
94 妬xli f
95 "}
if (flags & IFF_P0imP0lN7) {
Ioctl(sockfd "SIOCGIFDSTADDR &ifrcopy).
sinptr = (struct sockaddr_in *) &ifrcopy ifr_dstaddr. ifi->ifi_dstaddr = Callocfl sizeof(struct sockadd「一in)).
memcpy(7fi->ifi_dstaddr. sinptr size。"struct sockaddr_in));
Листинг 16.6 (продолжение)
96break
97default
98break,
99}
100}
101 free(buf)
102 return (ifihead)/* указатель на первую структуру в связном списке */,
103 }'
73-78 Мы копируем IP-адрес, возвращенный из нашего начального вызова SIOCGIFCONF функции ioctl, в структуру, коюрую мы создаем.
79-96 Если интерфейс поддерживает широковещательную передачу, мы получаем широковещательный адрес с помощью вызова SIOCGIFBRDADDR функции чocti. Мы выделяем память для структуры адреса сокета, содержаще!! этот адрес, и добавляем ее к структуре if】 info, которую мы создаем. Аналогично, если интерфейс является интерфейсом типа «точка-точка», вызов SIOCGIFBRDADDR возвращает IP- адрес другого конца связи.
ПРИМЕЧАНИЕ
Здесь отсутствует case для AF_INET6, поскольку, как мы О1мсчали рапсе, псизвсс ню, возвратит ли реализация IPv6 при вызове SIOCGIFCONF именно IPv6-aapcc
В листинге 16.7 показана функция free_i fч_ч nfo, которой передается указатель, возвращенный функцией get_ifi_info. Эта функция освобождает всю динамически выделенную память.
Листинг 16.7. Функция freejfijnfo: освобождение памяти, которая была динамически выделёнафункцией getjfijnfo
//ioctl/get_if1_7nfо с
104 void
105 free_ifi_info(struct ifi_info *ifihead)
106一
107 struct ifi_inf。 *ifi *ifinext.
108109110111112113114115116117118108109110111112113114115116117118for (ifi ^ 1fihead ifi i= NULL if (ifi->ifi_addr i= NULL) free(ifi->ifi_addr)if (ifi->ifi_brdaddr i= NULL) free(ifi->ifi_brdaddr)if (ifi->ifi_dstaddr 』NULL) free(ifi->ifi_dstaddr).ifinext = ifi->ifi_next./*free(ifi)/*for (ifi ^ 1fihead ifi i= NULL if (ifi->ifi_addr i= NULL) free(ifi->ifi_addr)if (ifi->ifi_brdaddr i= NULL) free(ifi->ifi_brdaddr)if (ifi->ifi_dstaddr 』NULL) free(ifi->ifi_dstaddr).ifinext = ifi->ifi_next./*free(ifi)/*ifi = ifinext) {ifi = ifinext) {невозможно получить ifi_next. после того какфункция freeО */освободили память, которая была занята структурой lfl_mfo{} */невозможно получить ifi_next. после того какфункция freeО */освободили память, которая была занята структурой lfl_mfo{} */
108
109
110
111
112
113
114
115
116
117
118
108
109
110
111
112
113
114
115
116
117
118
for (ifi ^ 1fihead ifi i= NULL if (ifi->ifi_addr i= NULL) free(ifi->ifi_addr)
if (ifi->ifi_brdaddr i= NULL) free(ifi->ifi_brdaddr)
if (ifi->ifi_dstaddr 』NULL) free(ifi->ifi_dstaddr).
ifinext = ifi->ifi_next./*
free(ifi)/*
for (ifi ^ 1fihead ifi i= NULL if (ifi->ifi_addr i= NULL) free(ifi->ifi_addr)
if (ifi->ifi_brdaddr i= NULL) free(ifi->ifi_brdaddr)
if (ifi->ifi_dstaddr 』NULL) free(ifi->ifi_dstaddr).
ifinext = ifi->ifi_next./*
free(ifi)/*
ifi = ifinext) {
ifi = ifinext) {
невозможно получить ifi_next. после того как
функция freeО */
освободили память, которая была занята структурой lfl_mfo{} */
невозможно получить ifi_next. после того как
функция freeО */
освободили память, которая была занята структурой lfl_mfo{} */
Операции с интерфейсами
Как мы показали в предыдущем разделе, запрос SIOCGIFCONF возвращает имя и структуру адреса сокета для каждого сконфигурированного интерфейса. Существует множество других вызовов, позволяющих установить или получить все
остальные характеристики интерфейса. Версия get этих вызовов (SIOCGxxx) часто запускается npoiраммой netstat, а версия set (SIOCSxxx) — программой ifconfig. Любой пользователь может получить ипформаиию об интерфейсе, в то время как ус1ановка этой информации требует прав привилегированного пользователя.
Эти вызовы получают или возвращают структуру i freq, адрес которой задается в качестве трегьего аргумента функции wctl. Интерфейс всегда идентифицируется по имени: 1е0,1о0, рррО, — то есть по имени, заданному в элементе т fr_name структуры ч freq.
Многие из этих запросов используют структуру адреса сокета, для того чтобы задать или возвратить IP-адрес или маску адреса. Для IPv4 адрес или маска содержится в элементе si n_addr из структуры адреса сокета Интернета.
SIOCGIFADDR. Возвращаег адрес направленной передачи в элементе ifr_addr.
SIOCSIFADDR. Устанавливает адрес интер(|)ейса из элемент а ч fr_addr. Также вызывается функция инициализации для интерфейса.
l SIOCGIFFLAGS. Возвращаег флаги интерфейса в элементе 7 fr_flags. Имена различных (флагов определяются в виде IFF.xxx в заголовочном фа11ле <net/if h>. Флаги указывают, например, включен ли интерфейс (IFFJJP), является ли он интерфейсом типа «точка-точка^ (I FF_PO INTOPOI NT), поддерживает ли широковещательную передачу (IFF_BROADCAST) и т. д.
SIOCSIFFLAGS. Устанавливает флаги из элемента i fr_fl ags.
'SIOCGIFDSTADDR. Возвращает адрес типа «точка-точка» в элементе ifr^dstaddr.
“ SIOCSIFDSTADDR. Устанавливает адрес типа «точка-точка» из элемента т f r_dstaddr. SIOCGIFBRDADDR. Возвращает широковещательный адрес в элементе ч fr_broadaddr. Приложение сначала должно получить флаги интерфейса, а зат ем сделать корректный вызов: SIOCGIFBRDADDR для широковещательного интерфейса или SIOCGIFDSTADDR 一 для интерфейса типа «точка- точка».
$ SIOCSIFBRDADDR. Устанавливает широковеща!ельный адрес из элемента ifr_ broadaddr.
SIOCGIFNETMASK. Возвращает маску подсети в элементе ifr_addr.
, SIOCSIFNETMASK. Устанавливает маску подсети из элемента т fr_addr.
“ SIOCGIFMETRIC. Возвращает метрику интерфейса в элементе i fr_metnc. Метрика поддерживается ядром для каждого интерфейса, но используется демоном маршрутизации routed. Метрика интерфейса добавляется к счетчику количества переходов.
SIOCSIFMETRIC. Устанавливает метрику интерфейса из элемента 7fr_metnc.
В этом разделе мы описали наиболее типичные операции интерфейсов. Во многих реализациях появились дополпич ельные операции.
Операции с кэшем ARP
Операции с кэшем ARP также осуществляются с помощью функции ч octi. В этих запросах используется структура arpreq, показанная в листинге 16.8 и определяемая в заголовочном файле <net/if_arp h>.
адрес протокола */ аппаратный адрес */ флаги */адрес протокола */ аппаратный адрес */ флаги */struct sockaddr arp_pa. struct sockaddr arp_ha./* /* /*int }.arp_flags.#defineATF_INUSE0x01/*#defmeATF_COM0x02/*#defineATF二PERM0x04/*#defmeATF二PUBL0x08/*struct sockaddr arp_pa. struct sockaddr arp_ha./* /* /*int }.arp_flags.#defineATF_INUSE0x01/*#defmeATF_COM0x02/*#defineATF二PERM0x04/*#defmeATF二PUBL0x08/*запись, которую нужно использовать */завершенная запись */постоянная запись */опубликованная запись (отсылается другим узлам) */запись, которую нужно использовать */завершенная запись */постоянная запись */опубликованная запись (отсылается другим узлам) */Листинг 16.8. Структура arpreq, используемая с вызовами ioctl для кэша ARP struct arpreq {
адрес протокола */ аппаратный адрес */ флаги */
адрес протокола */ аппаратный адрес */ флаги */
struct sockaddr arp_pa. struct sockaddr arp_ha.
/* /* /*
int }.
arp_flags.
#define
ATF_INUSE
0x01
/*
#defme
ATF_COM
0x02
/*
#define
ATF二PERM
0x04
/*
#defme
ATF二PUBL
0x08
/*
struct sockaddr arp_pa. struct sockaddr arp_ha.
/* /* /*
int }.
arp_flags.
#define
ATF_INUSE
0x01
/*
#defme
ATF_COM
0x02
/*
#define
ATF二PERM
0x04
/*
#defme
ATF二PUBL
0x08
/*
запись, которую нужно использовать */
завершенная запись */
постоянная запись */
опубликованная запись (отсылается другим узлам) */
запись, которую нужно использовать */
завершенная запись */
постоянная запись */
опубликованная запись (отсылается другим узлам) */
Третий аргумент функции ioctl должен указывать на одну из этих структур. Поддерживаются три следующих вызова:
* SIOCSARP. Добавляет новую запись в кэш ARP или изменяет существующую запись, агр рз — это структура адреса сокета Интернета, содержащая IP-адрес, a arp_ha — это общая структура адреса сокета с элементом sa_fami ly, равным AF_UNSPEC, и элементом sa_data, содержащим аппаратный адрес (например, 6-байтовый адрес Ethernet). Два флага, ATF PERM н ATF_PUBL, могут быть заданы приложением. Два других флага, ATF INUSE и ATF COM, устанавливаются ядром.
鼻 SIOCDARP. Удаляет запись из кэша ARP. Вызывающий процесс задает интернет-адрес удаляемой записи.
/ SIOCGARP. Получает запись из кэша ARP. Вызывающий процесс задает интернет-адрес, и соответствующий адрес Ethernet возвращается вместе с флагами. Добавлять или удалять записи может только привилегированный пользователь. Эти три вызова обычно делает программа а гр.
ПРИМЕЧАНИЕ
Запросы функции ioctl, связанные с ARP, не поддерживаются в некоторых более новых системах, использующих для описанных операций ARP маршрутизирующие сокеты.
Обратите внимание, что невозможно с помощью функции ioctl перечислить все записи кэша ARP. Большинство версий команды а гр при использовании флага -а (перечисление всех записей кэша ARP) считывают память ядра (/dev/kmem), чтобы получить текущее содержимое кэша ARR Мы увидим более простой (и предпочтительный) способ, основанный на применении функции sysctl, описанной в разделе 17.4.
Пример: вывод аппаратного адреса узла
Теперь мы используем нашу функцию my_addrs, приведенную в листинге 9.3, для того, чтобы возвратить все IP-адреса узла. Затем для каждого IP-адреса мы делаем вызов SIOCGARP функции ioctl, чтобы получить п вывести аппаратные адреса. Наша программа показана в листинге 16.9.
Листинг 16.9. Вывод аппаратного адреса узла
//ioctl/ргтас с
#include"unp h"
#indude<net/if_arp.h>
int
mainCint argc. char **argv)
{
int family, sockfd.
char str[INET6_ADDRSTRLEN];
char **pptr.
unsigned char*ptr.
struct arpreqarpreq.
struct sockaddr_in *sin.
pptr = m.y_addrs(&faiTnly).
for (; *pptr != NULL. pptr++) {
printfC^s ". Inet_ntop(family,*pptr. str. sizeof(str)));
switch (family) {
case AF—INET
sockfd = Socket(AF_INET. SOCK_DGRAM. 0),
sin = (struct sockaddrjn *) &arpreq arp_pa,
bzero(sin. sizeof(struct sockaddrjn))
20sin->sin_fannly = AFJNET.
21memcpy(&sin->sin_addr *pptr, sizeof(struct in_addr)).
22loctl(sockfd, SIOCGARP, &arpreq)
23ptr = &arpreq arp_ha sa_data[0],
24 pEntf(«x Zx %x 2x %x ix\en", *ptr, *(ptr -*- 1),
25*(ptr + 2) *(ptr 十 3) *(ptr + 4). *(ptr + 5)).
26break,
27default
28err_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_dddrs. У нас предусмотрен только один вариант (case) для адресов IPv4, так как производители, вероятно, не будут поддерживать адреса IPv6 с запросом SIOCGARP.*
Вызов функции ioctl и вывод аппаратного адреса
18-26 Мы заполняем структуру агр_ра как структуру адреса сокета IPv4, содержащую адрес IPv4. Вызывается функция ioctl и выводится полученный аппаратный адрес.
При запуске этой программы па нашем узле solans мы получаем:
solans Z prmac
206 62 226 33 8 0 20 78 еЗ еЗ
Операции с таблицей маршрутизации
Для работы с таблицей маршрутизации предназначены два вызова функции т octi. Эти два вызова требуют, чтобы третий аргумент функции wctl был указателем на структуру rtentry, которая определяется в заголовочном файле <net/route h>. Обычно эти вызовы исходят от программы route. Их может делать только привилегированный пользователь.
婚 SIOCADDRT. Добави! ь запись в таблицу маршрутизации.
& SIOCDELRT. Удалить запись из таблицы маршрутизации.
Нет способа с помощью функции wctl перечислить все записи таблицы маршрутизации. Эту операцию обычно выполняет программа netstat с флагом -г. Программа получает таблицу маршрутизации, считывая память ядра (/dev/кшеш). Как и в случае с просмотром кэша ARP. в разделе 17.4 мы увидим более простой (и предпочтительный) способ, предоставляемый функцией sysctl.
Резюме
Команды функции wctl, используемые в сетевых приложениях, можно разделить на шесть категорий:
Операции с сокетами (находимся ли мы на отметке внеполосных данных?). 2. Операции с файлами (установить или сбросить флаг отсутствия блокировки).
Операции с интерфейсами (возвратить список интерфейсов, получить широковещательный адрес).
Операции с кэшем ARP (создать, изменить, получить, удалить).
Операции с таблицей маршрутизации (добавить или удалить).
Операции с потоками (см. главу 33).f
Мы будем использовать операции с сокетами и файлами, а получение списка интерфейсов — это настолько типичная операция, что для этой цели мы разработали собственную функцию. Мы будем применять се много раз в оставшейся части книги. Вызовы функции ioctl с кэшем ARP и таблицей маршрутизации используются лишь несколькими специализированными программами.
Упражнения
В разделе 16.7 мы сказали, что широковещательный адрес, возвращаемый запросом SIOCGIFBRDADDR, возвращается в элементе i fr_broadaddr. Но на с. 173 [105] сказано, что он возвращается в элементе i f r_dstaddr. Имеет ли это значение?
Измените программу get_i fi_mfo так, чтобы она делала первый вызов SI0- CGIFCONF для одной структуры i freq, а затем каждый раз в цикле увеличивайте длину на размер одной из этих структур. Затем поместите в цикл операторы, которые выводили бы размер буфера при каждом вызове независимо от того, возвращает функция wctl ошибку или нет, и при успешном выполнении вы-
ведите возвращаемую длину буфера. Запустите программу р门 finfo и посмотрите, как ваша система обрабатывает вызов, когда размер буфера слишком мал. Выведите также семейство адресов для всех возвращаемых структур, семейство адресов которых не совпадает с указанным в первом аргументе функции get」f и nf。,чтобы увидеть, какие еще структуры возвращает ваша система.
Измените функцию get_ifi_mfo так, чтобы она возвращала информацию об адресе с альтернат явным именем, если дополнительный адрес находится не в той подсети, в которой находится предыдущий адрес для данного интерфе;!- са. Таким образом, паша версия из раздела 16.6 будет игнорировать альтернативные имена в диапазоне от 206.62.226.44 до 206.62.226.46, и это вполне нормально, поскольку они находятся в той же подсети, что и первичный адрес интерфейса 206.62.226.33. Но если альтернативное имя находится в другой подсети, допустим 192.3.4.5, возвратите структуру ч f и nfo с информацией о дополнительном адресе.
Если ваша система поддерживает вызов SIOCGIGNUM функции loctl, измените листинг 16.4 так, чтобы запустить этот вызов, и используйте возвращаемое значение как начальный размер буфера.
ГЛАВА 17
Маршрутизирующие сокеты
Введение
Традиционно доступ к таблице маршрутизации Unix внутри ядра осуществлялся с помощью команд функции ioctl В разделе 16 9 мы описали две операции SIOCADDRT и SIOCDELRT, предназначенные для добавления и удаления маршрута Мы также отметили, что не существуег операции чтения всей таблицы маршрутизации —вместо этого npoi раммы такие как netstat, считывают память ядра, для того чтобы получить содержимое таблицы маршрутизации И еще одно добавление Демонам маршрутизации, таким как gated, необходимо огслеживагь сообщения ICMP (Internet Control Message Protocol — npoioi ол управ^тяющих сообщений Интернета) об изменении маршрутов, получаемых ядром н для этого они часто создают символьный (неструктурированный) сокет ICMP (см главу 25), а затем прослушивают на этом сокете все получаемые сообщения ICMP
В 4 3BSD Reno был упрощен интерфейс подсистемы маршрут изации ядра за счет создания семейства адресов (домена) AF.ROUTE Единственный тип сокетов, поддерживаемый для этого семейства, — эго символьный сокет Маршрутизиру ющие сокеты поддерживают три типа операции
Процесс может отправить ядру сообщение, записав его в маршрутизирующий сокет Таким образом добавляются н удаляются маршруты
Процесс может прочитать сообщение от ядра через маршрутизирующий сокет Так ядро уведомпяет процесс о том что сообщение ICMP об изменении маршрутизации было получено и обработано
Некоторые операции включают оба шага например, процесс отправляет ядру сообщение через маршрутизирующий сокет, запрашивая всю информацию по данному маршруту, после чего через маршрутизирующим сокет считывает ответ ядра
Процесс может использовать функцию sysctl (см раздел 17 4) либо для просмотра таблицы маршрутизации, либо для перечисления всех сконфигурированных интерфейсов
Первые две операции требуют прав привилегированноi о пользователя, а тре-
ПРИМЕЧАНИЕ
Технически третья операция выполняется при помощи общей функции sysctl а не мар тру гизирующего сокета Но мы увнднм, что среди ес входных параметров есть семей ство адресов (для описываемых в этой главе операции используется семейство AFROUTE) а результат она возвращает в том же форма1е ко юры и используется ядром для маршрутизирующего сокета Действительно в ядре 4 4BSD обработ кафупк ции sysctl для семейства AF ROUTE является частью кода маршрутизирующею со кета 1105, с 632-643]—'
Утилита sysctl появилась в 4 4BSD К сожалению, не все реализации поддерживаю щи с маршру гизиру Ю7цис сокет ы предоставляют ее Например Л1Х 4 2 Digital Unix 4 0 и Solans 2 6 поддерживают маршрутизирующие сокеты но пп одна)з этих систем ис поддерживает утилиту sysctl
Структура адреса сокета канального уровня
Структуры адреса сокета канального уровня будут встречаться нам как значения, содержащиеся в некоторых сообщениях, возвращаемых на маршру 1 изирую- щем сокете В листинге 17 I1 показано определение структуры, задаваемой в заголовочном файле <net/if_dl h>
Листинг 17.1. Структура адреса сокета канального уровня
struct sockaddr_dl {
uint8_t
sdl len
sa_fami ly_t
sdl
_f ann ly
/*
AF_lINK */
umtl6_t
sdl.
j ndex
/★
индекс интерфейса присвоенным системой если > 0 */
uint8_t
sdl
[type
/★
тип интерфейса из <net/if_types h> IFT_ETHER и т д */
umt8_t
sd〔
nlen
/*
длина имени начинается с sdl_data[O] *7
uint8_t
sdl
_alen
/★
длина адреса канального уровня */
uwt8_t
sdT
slen
/*
адрес селектора канального уровня 夫/
char
sdl]
[data[12]
/*
минимальная рабочая область может быть больше */
содержит имя интерфейса и адрес канального уровня */
У каждого интерфейса имеется уникальный положительный индекс Далее в этой таве мы увидим, каким образом он возв]эащается функциями т f_nametoindex и if_nameindex В главе 19 при обсуждении параметров сокета многоадресной передачи также рассматривается получение индекса интерфейса с помощью функции 1f_nametoindex
Элемент sdl_data содержит и имя, и адрес канального уровня (например, 48-раз- рядный МАС-адрес интерфейса Ethernet) Имя начинается с sdl_data[O] и не заканчивается нулем Начало адреса канального уровня смещено на sdl_nlen байтов относительно начала имени В этом заголовочном файле для возвращения указателя на адрес канального уровня задается следующий макрос
#define LLADDR(s) ((caddr_t)((s) >sdl_data + (s) >sdl__nlen))
Эти структуры адреса сокета имеют переменную длину [105, с 89] Если адрес канального уровня и имя превышают 12 байт, размер структуры будет боль-
Все исходные коды программ опубликованные в этой книге вы можеи паити по адресу http //
ше 20 байт. В 32-разрядных системах размер обычно округляется в большую сторону, до следующего числа, кратного 4 байтам. Мы также увидим на рис. 20.1 ,что когда одна из этих структур возвращается параметром сокета IP RECVIF, все три длины становятся пулевыми, а элемента sdl_data не существует.
Чтение и запись
Создав маршрутизирующий сокет, процесс может отправлять ядру команды путем записи в этот сокет и считывать из него информацию от ядра. Существует 12 различных команд маршрутизации, 5 из которых может запустить процесс. Они определяются в заголовочном файле <net/route. h> и показаны в табл. 17.1.
Таблица 17.1. Типы сообщений, проходящих по маршрутизирующему сокету
{••“■ ОЧЗИМ^:. ьз” '.■KvWj«<”yi<5№«3teg^
Тип сообщения
К ядру?
От ядра?
Описание
««•、<c««w»«< 叫我 >••■ « ♦爪 «^чмиметочме
Тип структуры
RTM_ADD
•
Добавить маршрут
rt_n)sghclr
RTM_CHANGE
•
Поменять шлюз, метрику
rt nisghdr
или флаги
RTM_DELADDR
Адрес был удален из интерфейса
ifa_msghdr
RTM_DELETE
•
Удалить маршрут
rjmsghdr
RTM_GET
•
Сообщить о метрике и других
rt_msghdr
харак геристиках маршру【4
RTMJFINFO
Находится ли интерфейс
if_nisghdr
в акпшном состоянии
RTA1—LOCK
•
Блокировка указанно/! метрики
rt_m、ghdr
RTM_LOSING
Возможно, неправильны it маршрут rlinsghdr
RTMjmSS
Поиск этого адреса заверинiлея
rt_irisghd!
неудачно
RTM_NEWSDDR
Адрес добавлен к шпсрфсису
itainsghdi
RTM—REDIRECT
Ядро получило указание
rtjnsghdr
использовать другой маршрут
RTM—RESOLVE
•
Запрос на определение адреса
ri_in、ghdi
канального уровня по а“рссу
получателя
На маршрутизирующем сокете происходит обмен тремя различными структурами, как показано в последнем столбце таблицы: rt^msghdr, if_ msghdr и ifa_ msghdr. Эти структуры представлены в листинге 17.2.
Листинг 17.2. Три структуры, возвращаемые с маршрутизирующими сообщениями
struct rt_msghdr {
/★
из <net/route h> */
u_short
rtm_msglen.
/★
чтобы пропускать нераспознанные сообщения */
u_char
rtrn_version.
/*
для обеспечения двоичной совместимости в будущем */
u_chdr
rtm_type.
/*
тип сообщения */
sd 0.5v
u_short
rtrn_mdex.
/★
индекс интерфейса. с которым связан адрес */
int
「tm_flags.
/★
флаги */,
mt
rtrrFaddrs.
/*
битовая маска идентифицирующая bockaddr (структуру адреса
сокета) в msg */
p】d_t
rtrn_pid.
/★
идентификация отправителя */
int
rtm_seq,
/★
для идентификации действия отправителем */
int
rtm_errno
/*
причина неудачного выполнения */
int
rtm_use
/★
из rtentry */
u_long
rtm_inits.
/夫
какую метрику мы инициализируем */
struct
rt_metncs rtm_
_rmx
./*
сами метрики */
}.
struct if msghdr {
/*
из <net/if h> */
u_short
ifmjnsglen.
/★
чтобы пропускать непонятые сообщения 夫/
u_chdr
ifm_version
/*
для обеспечения двоичной совместимости в будущем
u_chdr
ifm_type.
/★
тип сообщения */
int
ifm_addrs.
/★
как rtm_addrs */
int
ifm_flags.
/★
значение if__flags */
u_short
ifm_index
/*
индекс интерфейса, с которым связан адрес */
struct
if_data ifm_data:
/*
статистические и другие сведения */
}
struct ifa msghdr {
/*
из <net/if h> */
u_short
ifam_msgl en.
/★
чтобы пропускать непонятые сообщения */
u_char
ifam_version.
/*
для обеспечения двоичной совместимости в будущем
u_char
ifam_type
/*
тип сообщения */
int
■jfam_addrs.
/*
как rtm_addrs */
1 mt
ifam_flags
/★
значение ifa_flags */
u_short
ifam_index
/★
индекс интерфейса. с которым связан адрес */
mt
ifam_metric
/*
значение ifa_metnc */
}.
Первые три элемента каждой структуры одни и те же: длина, версия и тип сообщения. Тип — это одна из констант из первого столбца табл. 17.1. Элемент длины xxx_msglen позволяет приложению пропускать типы сообщений, которые оно не распознает.
Элементы rtm_dddrs, ifm_dddrs и ifdm_dddrs являются битовыми масками, указывающими, какая из возможных восьми структур адреса сокета следует за сообщением. В табл. 17.2 показаны константы и значения для битовой маски, определяемые в заголовочном файле <net/route h>.
Таблица 17.2. Константы, используемые для ссылки на структуры адреса сокета в маршрутизирующих сообщениях
Битовая маска, Битовая мае- Индекс массива. Индекс массива,
Структра адреса сокета содержит
константа
ка,значение
константа
значение
RTA_DST
0x01
RTAX_DST
0
Адрес получателя
RTA_GATEWAY
0x02
RTAX_GATEWAY
1
Адрес иипоза
RTA—NETMASK
0x04
RTAX_NETMASK
2
Маска се 1 и
RTA—GENMASK
0x08
RTAX_GENMASK
3
Маска
клонирования
rta_ifp
0x10
RTAXJFP
4
Имя пшерфейса
rta_ifa
0x20
RTAX_IFA
5
Адрес ин герфеиса
rta_author
0x40
RTAX—AUTHOR
6
Отправитель запроса
на перенаправление
rta_brd
0x80
RTAX—BRD
7
Адрес по 1уча1еля
типа «точка-точка»
или шпроковеща-
ГСЛЫ1ЫЙ
RTAX_MAX
8
Максимальное коли-
чсство элементов
В том случае, когда имеется множество структур адреса сокета, они всегда располагаются в порядке, показанном в таблице.
Пример: получение и вывод записи из таблицы маршрутизации
Теперь мы покажем пример использования маршрутизирующих сокетов. Наша программа получает аргумент командной строки, состоящий из адреса IPv4 в точечно-десятичной записи, и отправляет ядру сообщение RTM.GET для получения этого адреса. Ядро ищет адрес в своей таблице маршрутизации IPv4 и возвращает сообщение RTM_GET с информацией о соответствующей записи из таблицы мар- шругизации. Например, если мы выполним на пашем узле bsdi такой код: bsdi # getrt 4.5.6.7 dest 0000 gateway 206 62 226 62 netmask 0 0 0 0
мы увидим, что этот адрес получателя используется маршрутом по умолчанию (который хранится в таблице маршрутизации с 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 i ndex=2 netmask 255 255 255 224
задав в качестве получателя главную сеть Ethernet, получателем будет сама сеть. Теперь шлюзом является исходящий интерфейс, возвращаемый в качестве структуры sockaddr_dl с индексом интерфейса 2.
Перед тем как представить исходный код, мы показываем иа рис. 17.1, что именно мы пишем в маршрутизирующий сокет и что возвращает ядро.
Мы создаем буфер, содержащий структуру rt_msghdr, за которой следует структура адреса сокета, содержащая адрес получателя, информацию о котором должно найти ядро. Тип сообщения (rtm_type) — RTM^GET, а битовая маска (trm^addrs)— RTA_DST (вспомните табл. 17.2). Эти значения указывают, что структура адреса сокета, следующая за структурой rt_msghdr, — это структура, содержащая адрес получателя. Эта команда может использоваться с любым семейством протоколов (предоставляющим таблицу маршрутизации), поскольку семейство адресов, в которое входит искомый адрес, указано в структуре адреса сокета.
После отправки сообщения ядру мы с помошыо функции read читаем ответ, формат которого показан на рис. 17.1 справа: структура rt_msghdr, за которой следует до четырех структур адреса сокета. Какая из четырех структур адреса сокета возвращается, зависит от записи в таблице маршрутизации. Мы сможем идентифицировать возвращаемую структуру адреса сокета по значению элемента rtm/ddrs возвращаемой структуры rt_msghdr. Семейство каждой структуры адреса сокета указано в элементе sa_famiTy, к как мы видели в наших предыдущих примерах, первый раз сообщение RST GET содержало информацию о том, что адрес шлюза является структурой адреса сокета IPv4, а второй раз это была структура адреса сокета канального уровня.
В листинге 17.3 показана первая часть нашей программы.
Буфер,отправленный ядру
rt^nsghdx {}
rtm_type= RTM_GET
Структура адреса сокета, содержащая адрес получателя
RTA_DST
rt_jnsghdr {}
rtm_type=
RTM_GET
Структура
адреса
сокета,
RTA DST
содержащая
адрес
получателя
Структура
адреса
сокета,
RTA_GATEWAY
содержащая
адрес шлюза
Структура
адреса
сокета,
RTA_NETMASK
содержащая
маску сети
Структура
адреса
сокета,
RTA GANMASK
содержащая
маску
клонирования
Буфер, возвращенный ядру
Рис, 17.1. Обмен данными с ядром на маршрутизирующем сокете для команды RTM_GET
Листинг 17.3. Первая часть программы, запускающая команду RTM_G可 на маршрутизирующем сокете
//route/getrt с
#include"unproute h"
#define BUFLEN (sizeof(struct rt_msghdr) +512)
/* si zeof( struct sock add r_m6) * 8 = 192 */
#defwe SEQ 9999“
5 6 7 в 9
0 12 3 4
1- 1Л 1 1x 1.
mt
main(int агде, char **argv)
{
mt sockfd.
char *buf.
pid_t pick
ssize t n.
struct rt^msghdr *rtm
struct sockaddr *sa. *rti_mfo[RTAX_MAX]; struct sockaddr in *sm.
15 if (argc『2)
16 err_quit( 'usage getrt <IP3ddress>").
Листинг 17.3 (продолжение)
sockfd = Socket(AF ROUTE SOCK_RAW, 0). /* необходимы права привилегированного пользователя */
ouf = Cal1ос(1 BUFLEN),/* инициализируется нулем */
rtm = (struct rt_msghdr *) buf
rtm->rtm_msglen = sizeof(struct rt_msghdr) + sizeof(struct sockaddr_in).
rtm->rtm_version = RTM_VERSION.
rtm->rtm_type ^ RTM_GET,
rtm->rtm_addrs= RTA_DST.
rtm->rtm_pid=pid = getpidO
rtm->rtm_seq=SEO.
sin = (struct sockaddr_in *) (rtm + 1).
sin->sin_〕en=sizeof(struct sockaddr_in):
sm->sin_farnily = AF_INET
Inet_pton(AF_INET.argvCl]&sin->sin_addr).
Wnte(sockfd. rtm.rtm->rtm_msglen),
do {
n = Read(sockfd rtm. BUFLEN).
} while (rtm->rtm_type l= RTM_GET || rtm->rtm_seq 〔= SEQ ||
rtm->rtm_pid l= pid).
-3 Наш заголовочный файл unproute h подключает некоторые необходимые файлы, а затем включает наш файл unp h. Константа BUFLEN — эго размер буфера, который мы размещаем в памяти для хранения нашего сообщения ядру вместе с ответом ядра. Нам необходимо место для одной структуры rt_msghdr и. возможно, восьми структур адреса сокета (максимальное число, которое может возвратиться через маршрутизирующий сокет). Поскольку структура адреса сокета IPv6 имеет размер 24 баИта, то значения 512 нам более чем достаточно.
Создание маршрутизирующего сокета
17 Мы создаем символьный сокет в домене AF__ROUTE, что, как мы отмечали ранее, требует прав привилегированного пользователя. Буфер размещается в памяти и инициализируется нулем.
Заполнение структуры rt_msghdr
”25 Мы заполняем структуру rt_msghdr данными нашего запроса. В этой структуре хранится идентификатор процесса и порядковый номер, который мы выбираем. Мы сравним эти значения, когда будем искать правильный ответ.
Заполнение структуры адреса сокета адресом получателя
)-29 Следом за структурой rt_msghdr мы создаем структуру sockaddr i п, содержащую IPv4-anpec получателя, поиск которого будет проведен ядром в таблице маршрутизации. Все, что мы задаем — это длина адреса, семейство адреса и адрес.
Запись сообщения ядру (функция write) и чтение ответа (функция read)
)-34 Мы пишем сообщение ядру с помощью (функции write, и с помощью функции read читаем ответ. Поскольку у других процессов могут быть открытые маршру-
тизирующие сокеты, а ядро передает копию всех маршрутизирующих сообщений всем маршрутизирующим сокетам, мы должны проверить тип сообщения, порядковый номер и идентификатор процесса, чтобы узнать, что полученное сообщение —это ожидаемое нами сообщение.
Вторая часть этой программы показана в листинге 17.4. Она обрабатывает ответ.
Листинг 17.4. Вторая часть программы, запускающая команду RTM_GET на маршрутизирующем сокете~
//route/getrt с
35 rtm = (struct rt_msghdr *) buf
36 sa 二(struct sockaddr *) (rtm + 1)
37get_rtaddrs(rtm->rtm_addrs, sa rti_inf。).
38 if T (sa = rti_mfo[RTAX_DST]) »= NULL)
39 pmntf(Mdest %s\en" Sock_ntop_host(sa sa->sajen)).
40 if ( (sa = rti_wfo[RTAX_GATEWAY]) >= NULL)
41pnntf( "gateway %s\en". Sock_ntop_host(sa sa->sajen)):
42 if ( (sa = rti_wfo[RTAX_NETMASK])『NULL)
43pnntf("netmask %s\en" Sock_masktop(sa, sa->sa_len)).
44 if ( (sa = rti_info[RTAX_GENMASK]) *= NULL)
45pnntf( "genmask ^s\en". Sock_masktop(sa sa->sa_len)).
exit(0).
}
34-35 Указатель rtm указывает на структуру rt_msghdr, а указатель sa — на первую следующую за ней структуру адреса сокета.
rtm_dddrs — это битовая маска той из возможных восьми структур адреса сокета, которая следует за структурой rt_msghdr. Наша функция get_rtaddrs (она показана в следующем листинге), получив эту маску и указатель на первую структуру адреса сокета (sa), заполняет массив rti_info указателями на соответствующие структуры адреса сокета. В предположен ии, что ядро возвращает все четыре структуры адреса сокета, показанные на рис. 17Л, полученный в результате массив rti_info будет таким, как показано на рис. 172
Затем наша программа проходит массив rti_info, делая все, что ей нужно, с непустыми указателями массива.
37-44 Каждый из присутствующих четырех возможных адресов выводится. Мы вызываем нашу функцию sock_ntop_host для вывода адреса получателя н адреса шлюза, но для вывода двух масок подсети вызываем нашу функцию sock_masktop. Эту новую функцию мы покажем ниже.
В листинге 17.5 показана наша функция get_rtaddrs, которую мы вызывали в листинге 17.4.
Листинг 17.5. Создание массива указателей на структуры адреса сокета в маршрутизирующем сообщении
//1ibroute/get_rtaddrs с
#1nclude"unproute h"
/*
* Округляем 'а' до следующего значения кратного 'size'
*/продолжение 按
Буфер,Буфер,возвращенный ядрувозвращенный ядруrti_info rti_info rti_info rti_info rti__info rti_info rti_info rti_inforti_info rti_info rti_info rti_info rti__info rti_info rti_info rti_info[RTAX—DST] [RTAX_GATEWAY] [RTAX__NETMASK] [RTAX_GENMASK] [RTAX_IFP] [RTAX—IFP] [RTAX_AUTHOR] [RTAX—BRD]
Буфер,
Буфер,
возвращенный ядру
возвращенный ядру
rti_info rti_info rti_info rti_info rti__info rti_info rti_info rti_info
rti_info rti_info rti_info rti_info rti__info rti_info rti_info rti_info
Рис. 17.2. Структура rtijnfo, заполненная с помощью нашей функции get_rtaddrs
Листинг 17.5 (продолжение)
5 #define ROUNDUP(a size) (((а) & ((size)」))9 (1 + ((а) | ((size)-l)))(a))
/* Переходим к следующей структуре адреса сокета
* Если sa_len равно 0 это значит что
* размер выражен числом типа u」ong)
*/一
#define NEXT_SA(ap) ар = (SA *) \e
((caddr_t)-ap + (ap->sd_7en ? ROUNDUP(dp->sd_len. sizeof (ujong)) \e
- sizeof(u_long5))一
void
get__rtaddrs(int addrs SA *sa SA **rti_info)
{一一
int i.
Цикл по восьми возможным указателям
Значение RTAX_MAX — максимальное число структур адреса сокета, возвращаемых от ядра в сообщении через маршрутизирующий сокет, — равно 8. В цикле функции ведется поиск по каждой из восьми констант битовой маски RT/^xxx (см табл. 17.2), которые могут быть присвоены элементам rtm_addrs, ifm_addrs и i fam_ addrs структур, показанных в листинге 17.2. Если бит установлен, соответствующий элемент в массиве rti_info становится указателем на структуру адреса сокета; иначе элемент массива становится пустым указателем.
Переход к следующей структуре адреса сокета
2-12 Структуры адреса сокета имеют переменную длину, но в этом коде считается, что у каждой из них имеется поле Sd_] еп, задающее длину структуры. Есть две сложности, с которыми придется столкнуться. Во-первых, маска подсети и маска клонирования могут возвращаться в структуре адреса сокета с нулевым значением поля sa_len, по на самом деле они занимают размер, представленный числом типа unsigned ] ong (В главе 19 [105] обсуждается свойство клонирования таблицы маршрутизации 4.4BSD.) Эго значение соответствует маске, состоящей только из нулевых битов, что мы видели в одном из приведенных выше примеров, когда для заданного по умолчанию маршрута маска подсети имела вид 0.0.0.0. Во-вторых, каждая структура адреса сокета может быть заполнена в конце таким образом, что следующая начнется на определенной границе, которая в данном случае соответствует значению типа unsi gned 1 ong (например, 4-байтовая граница для 32-разрядной архитектуры). Хотя структуры sockaddr_i п занимают 16 байт, что не требует заполнения, маски часто имеют в конце заполнение.
Последняя функция, которую мы покажем в примере пашей программы,— это функция sockjnasktop, представленная в листинге 17 6, возвращающая строку для одного из двух возможных значений масок. Маски хранятся в структурах адреса сокета. Элемент sa_fami 1у не задан, но имеется элемент Sd_len, принимающий значения 0, 5, 6, 7 или 8 для 32-битовых масок IPv4. Когда длина больше нуля, действительная маска начинается с того же смещения от начала структуры, что и адрес IPv4 в структуре sockaddr_m: 4 байта от начала структуры (как показано на рис. 18.21, с. 577 [ 105]), что соответствует элементу sa_data[2] общей структуры адреса сокета.
Листинг 17.6. Преобразование значения маски к формату представления
//11broute/sock_masktop с
#mclude "unproute hw
char *
sock_masktop(SA *sa socklen_t salen)
{一一
static char str[INET6^_ADDRSTRLEN]
unsigned char *ptr = &sa->sa_data[2]
if (sa->sa_len == 0)
return (% 0 0 0")
else if (sa->sa_len = 5)
snprintf(str ~sizeof(str) ' %d 0 0 0" *ptr),
else if (sa->sa_len == 6)
snprmtf(str sizeof(str) "Md %d 0 O' *ptr *(ptr + 1))
Листинг 17.6 (продолжение)
else if (sa >sa_len == 7)
14snpnntf(str sizeof(str) 2 *d *d 0 *ptr *(ptr + 1) *(ptr + 2))
15 else if (sa >sa_len == 8)
16snpnntf(str sizeof(str) Md Zd 紀 *d
17*ptr *(ptr + 1) *(ptr + 2) *(ptr + 3))
18else
19 snpnr)tf(str sizeof(str) (unknown mask len - Zd family = %d)
20sa >sa_len sa >sa_fannly)
21 return (str)
22 }
21 Если дчина равна нулю, то подразумевае1 ся маска 0 0 0 0 Если длина равна 5, хранится только первый байт 32-разрядпой маски, ад чя оставшихся трех байтов подразумевается нулевое значение Когда длина равна 8, хранятся все 4 байта маски
В эюм примере мы хогим прочитать ответ ядра поскольку он содержит ин- формапию, которую мы ищем Но в общем случае возвращаемое значение нашен функции write на маршрутизируюи(ем сокете сообщает нам, успешно ли была выполнена команда Если это вся необходимая нам информация, мы вызываем функцию shutdown со вторым аргументом SHU7_RD чтобы предотвратить отправку ответа Например если мы удаляем маршрут то возвращение нуля функцией write означает успешное выполнение, а если удали гь маршрут не удалось, возвращается ошибка ESRCH [105, с 608] Аналогично при добавлении маршрута возвращение ошибки EEXIST при выполнении функции write означает, что запись уже существует В нашем примере из листинга 17 3 функция write возвращает ошибку ESRCH если записи в таблице маршрутизации ис существует (допустим, у нашего уз“а пет заданного по умолчанию маршрута)
.4. Операции функции sysctl
Маршрутизирующие сокеты нужны нам i лавным образом для проверки таблицы \1аршрутизацпи и списка интерфейсов при помощи функции sysctl В то время как создание маршрутизирующего соке га (символьною соке? а в домене AF_ ROUTE) требует прав привилегированно! о пользователя, проверить таблицу марш- ру 1 изации и список нн герфейсов с помошыо функции syset 1 может любой пропесс #inciude <sys/param h> include <sys/sysctl h>
mt sysctl(mt *name u_mt namelen void *о?ф size_t *oldlenp void 切冊 s】ze_t Tnewlen)
Возвращает 0 в случае успешного выполнения
Эта функция испоттьзуст имена похожие на имена базы управляющей информации (Management Information Base, MIB) простого протокола управления сетью (Simple Network Management Protocol, SNMP) В главе 25 [94] подробно опи сываются SNMP и его MIB Эти имена являются иерархическими
Аргумент name — это массив целых чисел, задающий имя, a namelen задает число элементов массива Первый элемент массива оиределяег, какой подсистеме ядра направлен запрос Второй элемент определяет некую часть этой подсистемы, и т д На рис 17 3 показана иерархическая организация с некоторыми константами, используемым и на первых трех уровнях
工PPROTO—工CMP IPPROTO_IGMP 工PPROTO_工P IPPROTO_TCP IPPROTO_UDP工PPROTO—工CMP IPPROTO_IGMP 工PPROTO_工P IPPROTO_TCP IPPROTO_UDPРис. 17.3. Иерархическая организация имен функции sysctlРис. 17.3. Иерархическая организация имен функции sysctl
工PPROTO—工CMP IPPROTO_IGMP 工PPROTO_工P IPPROTO_TCP IPPROTO_UDP
工PPROTO—工CMP IPPROTO_IGMP 工PPROTO_工P IPPROTO_TCP IPPROTO_UDP
Рис. 17.3. Иерархическая организация имен функции sysctl
Рис. 17.3. Иерархическая организация имен функции sysctl
Для получения значении используется ар1уменг oldp Он указывает на буфер в котором ядро сохраняет значение Аргумент ol denp имеет тип «значение-резуль тат» когда функция вызывается, значение, на которое указывает oldenp, задает размер этого буфера, а по завершении функции значением этого аргумента становится количество данных сохраненных ядром в буфере Если размера буфера недостаточно, возвращается ошибка ENOMEM В специальном случае с】 dp может быть пустым указателем, a ol denp ~ непустым указателем, и тогда ядро определяет, сколько данных возвратилось бы при вызове, сообщая это значение через oldenp
Чтобы установить новое значение, используется аргумент newp, указывающий на 6)фер размера newl еп Если новое значение не задается newp должен быть пус тым указателем a newl еп должен быть равен нулю
В руководстве по применению функции sysctl подробно описывается различ ная системная информация которую можно получить с помощью этой функции информация о файловых системах, внрт) альной памяти, ограничениях ядра, ап паратых характерисгиках и т д Нас интересует сетевая подсистема, на которую указывает первый элемент массива name, равный CTL NET (константы CTL_хгг определяются в заголовочном файле <sys/sysctl h>) Тогда второй элемент может быть одним из перечисленных ниже
AF_INET Получение или установка переменных, влияющих на протоколы Ин тернета Следующий уровень с помощью одной из коне i ант IPR0T0_txx задает протокол BSD/OS 3 0 предоставляет на это\1 уровне около 30 переменных управляющих такими свойствами как генерация ядром переадресации ICMP, использование параметров TCP из RFC 1323 отправка контрольных сумм UDP ит д Пример подобно! о применения функции sysctl мы покажем в кон це этого раздела
AF L1NK Получение и ш установка информации канального уровня, такой как число интерфейсов РРР
AF_ROUTE Возвращение информации либо о таблице маршругизации, либо о списке интерфейсов Мы вскоре опишем)ту информацию
AF_UNSPEC Получение или установка некоторых переменных уровня сокета, таких как максимальный размер буфера отправки или приема сокет а
Когда втормм элементом массива name является AF ROUTE третий элемент (номер npoi окола) всегда нулевой (поскольку протоколы внутри семейства AF ROUTE отличаются от протоколов, например, в семействе AF INET), четвертый элемент —
это семейство адресов, а пятый и шестой элементы задают выполняемые действия. Вся эта информация обобщается в табл. 17.3.
Таблица 17.3. Информация функции sysctl» возвращаемая для маршрутизирующего домена
name[]
Возвращает таблицу
Возвращает кэш APR маршрутизации
Возвращает список интерфейсов
0
CTL—NET
CTL_NET
CTL_NET
1
AF_ROUTE
AFROUTE
AF_ROUTE
2
0
0
0
3
AF_INET
AFJNET
AF_INET
4
NET_RT_DUMP
NET_RT_FLAGS
NET_RT_IFLIST
5
0
RTF_LLINFO
0
Поддерживаются три операции, задаваемые элементом пате[4]. (Константы NET_RT_хххопределяются в заголовочном файле <sys/socket h>.) Информация возвращается через указатель oldp при вызове функции sysctl. Этот буфер содержит переменное число сообщений RTMjcxx (см. табл. 17.1).
Операция NE匸RT_DUMP возвращает таблицу маршрутизации для семейства адресов, заданного элементом пате[3]. Если задано нулевое семейство адресов, возвращаются таблицы маршрутизации для всех семейств адресов.
Таблица маршрутизации возвращается как переменное число сообщений RTM_GET, причем за каждым сообщением следует до четырех структур адреса сокета: получатель, шлюз, маска сети и маска клонирования записи в таблице маршрутизации. Пример такого сообщения мы показали в правой части рис. 17.1, а в нашем коде в листинге 17.4 проводится анализ одного из сообщений. В результате применения этой операции функции sysctl ядром возвращается одно или несколько таких сообщений.
Операция NET_RT_FLAGS возвращает таблицу маршрутизации для семейства адресов, заданного элементом паше[3], но учитываются только те записи таблицы маршрутизации, для которых значение флага RTF_ххх равно указанному в элементе name]5]. У всех записей кэша ARP в таблице маршрутизации установлен бит флага RTF_LLINFO.
Информация возвращается в том же формате, что и в предыдущем пункте.
Операция NET_RT」FLIST возвращает информацию обо всех сконфигурированных интерфейсах. Если элемент name[5] ненулевой, это номер индекса интерфейса н возвращается информация только об этом интерфейсе. (Более подробно об индексах интерфейсов мы поговорим в разделе 17.6.) Все адреса, присвоенные каждому интерфейсу, также возвращаются, и если элемент name[3] ненулевой, возвращаются только адреса для семейства адресов, указанного в этом элементе.
Для каждого интерфейса возвращается по одному сообщению RTM_IFINFO, за которым следует одно сообщение RTM_NEWADDR для каждого адреса, заданного для интерфейса. За сообщением RTM_IFINFO следует по одной структуре адреса сокета канального уровня, а за каждым сообщением RTM_ NE WADDR — до трех структур адреса сокета: адрес интерфейса, маска сети н широковещательный адрес. Эти два сообщения представлены на рис. 17.4.
Буфер, возвращенный ядру
if_msghdr{}
ifm_type= rtm_jfinfo
Структураадресасокета,содержащаяадресполучателя
if_rnsghdr {}
ifm_type=RTM_JFINFO
Структураадресасокета,содержащаямаскусети
Структураадресасокета,содержащаяадрес направленнойпередачиСтруктураадресасокета,содержащаяшироковещательныйадрес
По одному для каждого интерфейса имя интерфейса, индекс и аппаратный адресПо одному для каждого интерфейса имя интерфейса, индекс и аппаратный адресПо одному на адрес, сконфигурированный для интерфейсаПо одному на адрес, сконфигурированный для интерфейсаРис. 17.4. Информация, возвращаемая функцией sysctl для команд CTL^NET и NET_RT_IFUST
По одному для каждого интерфейса имя интерфейса, индекс и аппаратный адрес
По одному для каждого интерфейса имя интерфейса, индекс и аппаратный адрес
По одному на адрес, сконфигурированный для интерфейса
По одному на адрес, сконфигурированный для интерфейса
ПРИМЕЧАНИЕ
Поскольку к ядрам 4.4BSD добавлена поддержка IPv6, должно поддерживаться значение IPv6 для элемента name| 11 (чтобы устанавливать и получать характерные для IPv6 переменные), а также значение AF_INET6 для элемента name|3] из табл. 17.3 (чтобы получить таблицу маршрутизации IPv6 и кэш соседнего узла или возвращать адреса интерфейсов IPv6).
Пример: определяем, включены ли контрольные суммы UDP
Теперь мы приведем простой пример использования функции sysctl с протоколами Интернета для проверки, включены ли контрольные суммы UDP. Некоторые приложения UDP (например, BIND) проверяют при запуске, включены ли
контрольные суммы UDP, и если нет, пытаются включить их. Для того чтобы включить подобное свойство, требуются права привилегированного пользователя, ио мы сейчас просто проверим, включено это свойство или нет. В листинге 17.7 представлена наша программа.
Листинг 17.7. Проверка включения контрольных сумм
//route/checkudpsum с
#include"unproute h"
#include<netinet/udp h>
#include<netinet/ip_varh>
#】nclude<netinet/udp_varh> /* для констант UDPCTL_xxx */
int
mam(int argc. char **argv)
{
intmib[4]. val•
size_t len.
nnb[0]=CTL—NET.
nnb[l]=af_Tnet
mib[2]=IPPROTO_UDP-
nnb[3]=UDPCTLJHECKSUM.
len = sizeof(val).
Sysctl(mib 4 &val &len. NULL 0)
printf(,,udp checksum flag ^d\er^,^. val):
exit(0)
}
Включение системных заголовков
4 Следует включить заголовочный файл <netinet/udp_var h>, чтобы получить определение констант UDP функции sysctl. Для него требуются два других заголовка.
Вызов функции sysctl
-16 Мы размещаем в памяти массив целых чисел с четырьмя элементами и храним константы, соответствующие иерархии, показанной на рис. 17.3. Поскольку мы только получаем переменную и не присваиваем ей значение, аргумент newp 力унк- цип sysctl мы задаем как пустой указатель, и поэтому аргумент newp этой функции имеет нулевое значение, oldp указывает на нашу целочисленную переменную, в которую сохраняется результат, a ol denp указывает на переменную типа 〈значение-результат», хранящую размер этого целого числа. Мы выводим либо 0 (отключено), либо 1 (включено).
7.5. Функция get_ifi_info
Вернемся к примеру из раздела 16.6 — возвращение всех активных интерфейсов в виде связного списка структур ifi_wfo (см. лпстииг 16.2). Программа prifinfo остается без изменений (см. лисгинг 16.3), но теперь мы покажем версию функции get i f и nfo, использующую функцию sysctl вместо вызова SIOCGIFCONF функции noctl в листинге 16.4.
Сначала в листинге 17.8 мы представим функцию net_rt_i f ] i st. Эта функция вызывает функцию sysctl с командой NET_RT_IFLIST, чтобы возвратить список интерфейсов для заданного семейства адресов.
Листинг 17.8. Вызов фукнции sysctl для возвращения списка интерфейсов //li broute/ net_rt_i fl i st c
1 #indude - "unproute h"
2 char *
3 net_rt_iflist(mt family mt flags. size_t *1enp)
4 { 一』
5 mt mib[6].
6 char *buf
7
mib[0]=
CTL—NET.
8
nnb[l]=
AF ROUTE,
9
mib[2]=
0."
10
mib[3]=
family./*
только адреса этого семейства */
11
nnb[4]=
net_rt_iflist.
12
mb[5]=
flags 一/*
индекс интерфейса или 0 */
13
if (sysctl(nnb 6 NULL
lenp. NULL 0) < 0)
14
return
(NULL).
if ( (buf = malloc(*lenp)) == NULL)
return (NULL).
if (sysctl(mb. 6. buf. lenp. NULL. 0) < 0) {
free(buf).
return (NULL).
}
return (buf).
}
7-14 Инициализируется массив mi b, как показано в табл. 17.3, для возвращения списка интерфейсов и всех сконфигурированных адресов заданного семейства. Затем функция sysctl вызывается дважды. В первом вызове функции третий аргумент нулевой, в результате чего в переменной, на которую указьгвает〕епр, возвращается размер буфера, требуемый для хранения всей информации об интерфейсе.
15-21 Затем в памяти выделяется место для буфера, и функция sysctl вызывается снова, на этот раз с ненулевым третьим аргументом. При этом переменная, на которую указывает 1 епр, содержит при завершении функции число, равное количеству информации, хранимой в буфере, и эта переменная размешается в памя гп вызывающим процессом. Указатель на буфер также возвращается вызывающему
процессу.
ПРИМЕЧАНИЕ
Поскольку размер таблицы маршрутизации или число интерфейсов может изменяться между двумя вызовами функции sysctl, значение, возвращаемое при первом вызове, содержит поправочный множитель 10% [105, с. 639-6401
В листинге 17.9 показана первая половина функции get_ifi_info.
Листинг 17.9. Функция getjfijnfo, первая половина //route/get_if3_i nfo с
struct 彳fi_7nfo *
get_ifi_3nfo(3nt family, mt doaliases)
Листинг 17.9 (продолжение)
{
mtflags.
char *buf. *next. *lim.
size_t len.
structif_msghdr *ifm.
structifa_msghdr *ifam.
structsockaddr *sa. *rti_wfo[RTAX_MAX].
structsockaddr_dl *sdl.
structifi_mfo"*ifi. *ifisave.*ifihead. **ifipnext.
buf = Net_rt_iflist(fami 1у. 0. &len).
ifl head = NULL.
ifipnext = &ifihead.
lim = buf + len.
for (next = buf. next < lim. next += ifm->ifm_msglen) {
ifm = (struct if_msghdr *) next.
if (ifm->ifm_type == RTM_IFINFO) {
if ( ((flags = ifm->ifm_flags) & IFF_UP) == 0)
continue. /* игнорируем, если интерфейс неактивен */
sa = (struct sockaddr *) (ifm + 1).
get_rtaddrs(-ifm->ifm_addrs. sa. rtijnfo).
if ? (sa = rti_mfo[RTAX_IFP]) <= NULL) {
ifi = Callocfl. sizeoflstruct ifi_info)).
*ifipnext = ifi. /* предыдущий указатель указывал на эту структуру */
1 fipnext = &ifi->ifi_next. /* указатель на следующую структуру */
1fi->ifi_flags = flags:
if (sa->sa_family == AF_LINK) {
sdl = (struct sockaddr一dl *) sa,
if (sdl->sdl_nlen > 0)
snprintf(ifi->ifi_name. IFI_NAME."計s".
sdl->sdl_nlen. &sdl->sdl_data[0]).
else
,snprintf(ifi->ifi_name. IFI_NAME, "index Xd'*.
sdl->sdl_index).
if ( (ifi->ifi_hlen = sdl->sdl_alen) > 0)
memcpyCifi ->ifi_haddr. LLADDR(sdl)
min(IFI_HADDR. sdl->sdl alen)).
}—
}
14 Мы объявляем локальные переменные и затем вызываем нашу функцию net_rt_ 1 fl 1st.
19 Цикл for — это цикл по всем сообщениям маршрутизации, попадающим в буфер в результате выполнения функции sysctl. Мы предполагаем, что сообщение — это структура т fjnsghdr, и рассматриваем поле т fm type (вспомните, что первые три элемента трех структур идентичны, поэтому все равно, какую из трех структур мы используем для просмотра типа элемента).
Проверка, включен ли интерфейс
22 Для каждого интерфейса возвращается структура RTM_IFINFO. Если интерфейс не активен, он игнорируется.
Определение, какие структуры адреса сокета присутствуют
23-24 sa указывает на первую структуру адреса сокета, следующую за структурой if_msghd「. Наша функция get_rtaddrs инициализирует массив rti_info в зависимости от того, какие структуры адреса сокета присутствуют.
Обработка имени интерфейса
25-42 Если присутствует структура адреса сокета с именем интерфейса, в памяти размещается структура 1 fi_i nfo и хранятся флаги интерфейса. Предполагаемым семейством этой структуры адреса сокета является AF_LINK, что означает структуру адреса сокета канального уровня. Если элемент sdl_nlen ненулевой, имя интерфейса копируется в структуру ifi_info. В противном случае в качестве имени хранится строка, содержащая индекс интерфейса. Если элемент sdl_alen ненулевой, аппаратный адрес (например, адрес Ethernet) копируется в структуру т fi_i nfo, а его длина также возвращается как i f i_hl en.
В листинге 17.10 показана вторая часть нашей функции get_i fi_i nfo, которая возвращает IP-адреса для интерфейса.
Листинг 17.10. Функция getjfijnfo, вторая часть//route/get_if】_infо.сЛистинг 17.10. Функция getjfijnfo, вторая часть//route/get_if】_infо.с43444546} else 7f (ifm->ifm_type = RTM_NEWADDR) {if (ifi->ifi_addr) {/* уже имеется IP-адрес для интерфейса */if (doaliases == 0)continue:47484950515253545556575859/* у нас имеется новый IP-адрес для существующего интерфейса */ 1fi save = ifi.ifi = Callocd. sizeof(struct ifi_info)).*ifipnext = ifi. /* предыдущий указатель указывал на зту структуру */ ifipnext = &ifi->ifi_next. /* указатель на следующую структуру */ ifi->ifi_flags = ifisave->ifi__flags.ifi->ifi_hlen = ifisave->ifi_hlen,memcpy(ifi->ifi_name. ifisave->ifi_name, IFI_NAME).memcpy(ifi->ifi haddr. ifisave->ifi haddr. lFl_HADDR).}-一 一ifam = (struct ifa_msghdr *) next.sa = (struct sockaddr *) (ifam + 1).get_rtaddrs(ifam->ifam_addrs. sa. rti^info).606162636465666768697071727374if ( (sa = rti_info[RTAX_IFA]) != NULL) { ifi->ifi_addr = CallocU. sa->sa_len): memcpy(1fi->ifi addr. sa. sa->sa~len):}--if ( (flags & IFF BROADCAST) &&(sa = rti_mfo[RTAX_BRD]) «= NULL) { ifi->ifi_brdaddr = Callocd, sa->sa_len); memcpydfi->ifi_brdaddr. sa. sa->sa len).}--if ( (flags & IFF—POINTOPOINT) &&(sa = rti_info[RTAX_BRD]) ,= NULL) { ifi->ifi_dstaddr = Callocd. sa->sa_len). memcpy(ifi->ifi dstaddr, sa. sa->sa~len).}--43444546} else 7f (ifm->ifm_type = RTM_NEWADDR) {if (ifi->ifi_addr) {/* уже имеется IP-адрес для интерфейса */if (doaliases == 0)continue:47484950515253545556575859/* у нас имеется новый IP-адрес для существующего интерфейса */ 1fi save = ifi.ifi = Callocd. sizeof(struct ifi_info)).*ifipnext = ifi. /* предыдущий указатель указывал на зту структуру */ ifipnext = &ifi->ifi_next. /* указатель на следующую структуру */ ifi->ifi_flags = ifisave->ifi__flags.ifi->ifi_hlen = ifisave->ifi_hlen,memcpy(ifi->ifi_name. ifisave->ifi_name, IFI_NAME).memcpy(ifi->ifi haddr. ifisave->ifi haddr. lFl_HADDR).}-一 一ifam = (struct ifa_msghdr *) next.sa = (struct sockaddr *) (ifam + 1).get_rtaddrs(ifam->ifam_addrs. sa. rti^info).606162636465666768697071727374if ( (sa = rti_info[RTAX_IFA]) != NULL) { ifi->ifi_addr = CallocU. sa->sa_len): memcpy(1fi->ifi addr. sa. sa->sa~len):}--if ( (flags & IFF BROADCAST) &&(sa = rti_mfo[RTAX_BRD]) «= NULL) { ifi->ifi_brdaddr = Callocd, sa->sa_len); memcpydfi->ifi_brdaddr. sa. sa->sa len).}--if ( (flags & IFF—POINTOPOINT) &&(sa = rti_info[RTAX_BRD]) ,= NULL) { ifi->ifi_dstaddr = Callocd. sa->sa_len). memcpy(ifi->ifi dstaddr, sa. sa->sa~len).}--
Листинг 17.10. Функция getjfijnfo, вторая часть
//route/get_if】_infо.с
Листинг 17.10. Функция getjfijnfo, вторая часть
//route/get_if】_infо.с
43
44
45
46
} else 7f (ifm->ifm_type = RTM_NEWADDR) {
if (ifi->ifi_addr) {/* уже имеется IP-адрес для интерфейса */
if (doaliases == 0)
continue:
47
48
49
50
51
52
53
54
55
56
57
58
59
/* у нас имеется новый IP-адрес для существующего интерфейса */ 1fi save = ifi.
ifi = Callocd. sizeof(struct ifi_info)).
*ifipnext = ifi. /* предыдущий указатель указывал на зту структуру */ ifipnext = &ifi->ifi_next. /* указатель на следующую структуру */ ifi->ifi_flags = ifisave->ifi__flags.
ifi->ifi_hlen = ifisave->ifi_hlen,
memcpy(ifi->ifi_name. ifisave->ifi_name, IFI_NAME).
memcpy(ifi->ifi haddr. ifisave->ifi haddr. lFl_HADDR).
}-一 一
ifam = (struct ifa_msghdr *) next.
sa = (struct sockaddr *) (ifam + 1).
get_rtaddrs(ifam->ifam_addrs. sa. rti^info).
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
if ( (sa = rti_info[RTAX_IFA]) != NULL) { ifi->ifi_addr = CallocU. sa->sa_len): memcpy(1fi->ifi addr. sa. sa->sa~len):
}--
if ( (flags & IFF BROADCAST) &&
(sa = rti_mfo[RTAX_BRD]) «= NULL) { ifi->ifi_brdaddr = Callocd, sa->sa_len); memcpydfi->ifi_brdaddr. sa. sa->sa len).
}--
if ( (flags & IFF—POINTOPOINT) &&
(sa = rti_info[RTAX_BRD]) ,= NULL) { ifi->ifi_dstaddr = Callocd. sa->sa_len). memcpy(ifi->ifi dstaddr, sa. sa->sa~len).
}--
43
44
45
46
} else 7f (ifm->ifm_type = RTM_NEWADDR) {
if (ifi->ifi_addr) {/* уже имеется IP-адрес для интерфейса */
if (doaliases == 0)
continue:
47
48
49
50
51
52
53
54
55
56
57
58
59
/* у нас имеется новый IP-адрес для существующего интерфейса */ 1fi save = ifi.
ifi = Callocd. sizeof(struct ifi_info)).
*ifipnext = ifi. /* предыдущий указатель указывал на зту структуру */ ifipnext = &ifi->ifi_next. /* указатель на следующую структуру */ ifi->ifi_flags = ifisave->ifi__flags.
ifi->ifi_hlen = ifisave->ifi_hlen,
memcpy(ifi->ifi_name. ifisave->ifi_name, IFI_NAME).
memcpy(ifi->ifi haddr. ifisave->ifi haddr. lFl_HADDR).
}-一 一
ifam = (struct ifa_msghdr *) next.
sa = (struct sockaddr *) (ifam + 1).
get_rtaddrs(ifam->ifam_addrs. sa. rti^info).
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
if ( (sa = rti_info[RTAX_IFA]) != NULL) { ifi->ifi_addr = CallocU. sa->sa_len): memcpy(1fi->ifi addr. sa. sa->sa~len):
}--
if ( (flags & IFF BROADCAST) &&
(sa = rti_mfo[RTAX_BRD]) «= NULL) { ifi->ifi_brdaddr = Callocd, sa->sa_len); memcpydfi->ifi_brdaddr. sa. sa->sa len).
}--
if ( (flags & IFF—POINTOPOINT) &&
(sa = rti_info[RTAX_BRD]) ,= NULL) { ifi->ifi_dstaddr = Callocd. sa->sa_len). memcpy(ifi->ifi dstaddr, sa. sa->sa~len).
}--
Листинг 17.10 (продолжение)
75err_quit("unexpected message type *d" ifm->ifm_type)
76} 一一
77/* "ifihead' указывает на первую структуру в связном списке */
78 return (ifihead). /* указатель на первую структуру в связном списке */
79 }
Возвращение IP-адресов
3-63 Сообщение RTM NEWADDR возвращается функцией sysctl для каждого адреса, свя* запного с интерфейсом: для первичного адреса и для всех альтернативных имен (псевдонимов). Если мы уже заполнили IP-адрес для этого интерфейса, то мы имеем дело с альтернативным именем. Поэтому если вызывающему процессу нужен адрес псевдонима, мы должны выделить память для другой структуры ifi_info, скопировать заполненные поля и затем заполнить возвращенный адрес.
Возвращение широковещательного адреса и адреса получателя
4-73 Если интерфейс поддерживает широковещательную передачу, возвращается широковещательный адрес, а если интерфейс является интерфейсом типа «точ~ ка-тонка», возвращается адрес получателя.
Функции имени и индекса интерфейса
Документ RFC 2133 [32] определяет четыре функции, обрабатывающие имена и индексы интерфейсов. Эти четыре функции используются при многоадресной передаче по протоколу IPv6, как показано в главе 19 Основной принцип, объявляемый в этом документе, состоит в том, что каждый интерфейс имеет уникальное имя и уникальный положительный индекс (нуль в качестве индекса никогда не используется).
#include <net/if h>
unsigned int if_nametoindex(const char *ifname).
Возвращает положительный индекс интерфейса в случае успешного выполнения. О в случае ошибки
char *i f_indextoname(unsigned mt ifindex char *ifname)
Возвращает указатель на имя интерфейса в случае успешного выполнения NULL в случае ошибки
struct 1f_nameindex *il^nameindex(void)
Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки
void 1f_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), который вызывающий
процесс должен выделить для хранения результата, и этот указатель возвращается в случае успешного выполнения функции i f_i ndextoname.
Функция 1 f_namei ndex возвращает указатель на массив структур i f namei ndex: struct 3f_namemdex { unsigned mt if_index /*12*/
char*if_name /* имя завершаемое нулем '1е0'*/
} 一
Последняя запись в этом массиве содержит структуру с пулевым индексом 1 f_i ndex и с пустнм указателем i f_name. Память для этого массива, а также для имен, на которые указывают элемен гы массива, выделяется динамически и освобождается при вызове функции 1 f_f reenamei ndex
Теперь мы представим реализацию этих четырех функций с использованием маршрутизирующих сокетов.
Функция if_nametoindex
В листинге 17.11 показана функция if_nametoindex.
Листинг 17.11. Возвращение индекса интерфейса по его имени
//1 ibroute/if_nametoindex с1 #i nd ude "unpi fi h"2 #include"unproute h1//1 ibroute/if_nametoindex с1 #i nd ude "unpi fi h"2 #include"unproute h13456789101134567891011unsigned mt
//1 ibroute/if_nametoindex с1 #i nd ude "unpi fi h"
2 #include"unproute h1
//1 ibroute/if_nametoindex с1 #i nd ude "unpi fi h"
2 #include"unproute h1
3
4
5
6
7
8
9
10
11
3
4
5
6
7
8
9
10
11
if_nametoindex(const char *name)
{ ~
unsigned int index
char *buf *next. *lim
size_t len.
struct if_msghdr *ifm
struct sockaddr *sa *rti_info[RTAX_MAX] struct sockaddr_dl *sdl
12 if ( (buf = net_rt」flist(O 0. &len)) == NULL)
13 return (0)--
141516171819202122232425262728293031323334141516171819202122232425262728293031323334lim = buf + len.
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
for (next = buf next < lim next += ifm->ifnMTisg]en) {
ifm = (struct if_msghdr *) next
if (ifm->ifm_type == RTM_IFINFO) {
sa = (struct sockaddr *) (ifm + 1) get_rtaddrs(ifm->ifm_dddrs sa rti_info) if ( (sa = rtijmfo[RTAX」FP]) i= NULL) {
if (sa->sa_family == AF_LINK) { sdl = (struct sockaddr_dl *) sa if (strncmp(&sdl->sdl_data[O] name. sdl->sdl_nlen) == 0) { index = sdl ->sdl_mdex. /* сохраняем перед free О */ free(buf).
return (index)
}
}
}
}
}
free(buf)
return (0)./* нет соответствия имени */
Получение списка интерфейсов
2-13 Наша функция net_rt_i fl i st возвращает список интерфейсов.
Обработка только сообщений RTM_IFINFO
7-30 Мы обрабатываем сообщения в буфере (см. рис. 17.4) в поисках сообщений типа RTM_IFINFO・ Найдя такое сообщение, мы вызываем нашу функцию get^rtaddrs, чтобы установить указатели на структуры адреса сокета, а если присутствует структура имени интерфейса (элемент RTAX_IFP массива rti_inf。),то имя интерфейса сравнивается с аргументом.
Функция ifjndextoname
Следующая функция, if_wdextoname, показана в листинге 17.12.
Листинг 17.12. Возвращение имени интерфейса по его индексу
11broute/if_indextoname с
#include"unpifi h"
#include"unproute h"
char *
1f_indextoname(unsigned int index, char *name)
5
char*buf *next. *lim
size_t len,
structif_msghdr *ifm
structsockaddr *sa *rti_info[RTAX_MAX].
structsockaddr_dl *sdl
if ( (buf = net_rt_iflist(O index &len)) —= NULL)
return (NULL):
lim = buf + len.
for (next = buf. next < lim next += ifm->ifmjnsg】en) {
ifm = (struct if_msghdr *) next
if (ifm->ifm_type == RTM_IFINFO) {
sa = (struct sockaddr *) (ifm + 1)
get_rtaddrs(iТт->1fm_addrs. sa 「ti_】nfo).
if ( (sa = rti_info[RTAX_IFP]) '= NULL) {
if (sa->sa_f加ily == AjLINK) {
sdl = (struct sockaddr_dl *) sa
if (sdl->sdl_index == index) {
strncpy(name. sdl->sdl_data. sdl->sdl_nlen).
name[sdl->sdl_nlen]=0./* завершающий нуль */
free(buf).
return (name).
}
}
}
}
}
free(buf).
return (NULL). /* нет соответствия индексу */
}
Эта функция практически идентична предыдущей, но вместо поиска имени интерфейса мы сравниваем индекс интерфейса с аргументом вызывающего про-
цесса. Кроме того, второй аргумент нашей функции net_rt_i flist — это заданный индекс, поэтому результат должен содержать информацию только для определенного интерфейса. Когда обнаруживается совпадение, возвращается имя интерфейса, к которому добавляется завершающий нуль.
Функция if_nameindex
Следующая функция, if_nameindex, возвращает массив структур if_nameindei^содержащих все имена интерфейсов и индексы. Она показана в листингеДТЛЗ^^
Листинг 17.13. Возвращение всех имен и индексов интерфейсов
//libroute/if nameindex с
1
2
3
4
5
6
7
8
9
10
11
12
#i nd ude - "unpi fi h”
include "unproute h”
struct if_nameindex * if nameindex(void) (_
char *buf. *next. *lim.
s?ze_t len,
struct if_msghdr *ifm.
struct sockaddr *sa. *rti_info[RTAX^MAX); struct sockaddr_dl *sdl,
struct i f^nameiridex *result. *i fptr, char *namptr.
13
14
if ( (buf = net_rt_?fl?st(0. 0. &len)) == NULL) return (NULL)
15
16
17
18
if ( (result n malloc(len)) == NULL)/* завышенная оценка */
return (NULL)
ifptr = result.
namptr = (char *) result + len./* имена начинаются с конца буфера */
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
17гп = buf + len
for (next = buf, next < lim. next += ifm-^fn^msglen) {
】fm = (struct if__msghdr *) next.
if (ifm->ifm_type == RTM_IFINFO) {
sa = (struct sockaddr *) (ifm + 1).
get rtaddrs(ifm->ifm addrs sa rti_info).
if ( (sa = rti^nfo[RTAX_IFP]) «= NULL) {
?f (sa->Sd_fdffll ly — AjLINK) { sdl = (struct sockaddr_dl *) sa namptr -= sdl->sdl_nlen + 1, strперу(namptr. &sdl->sdl_data[0]. sdl->sdl_nlen). namptr[sdl->sdl_nlenj = 0? /* завершающий нуль */ ifptr->if_name = namptr ?fptr->if二index = sdl->sdl_?ndex.
?fptr++.
}
}
}
}
ifptr->if_name = NULL:/* отмечаем конец массива структур */
7fptr->?f二index = 0.
free(buf)"
return (result)./* вызывающий процесс может освободить нанять с помощью
freeO. когда все сделано */
42 }
Получение списка интерфейсов, выделение места для результата
.3-18 Мы вызываем нашу функцию net_rt_i fl i st для возвращения списка интерфейсов. Мы также используем возвращаемый размер в качестве размера буфера, который мы размещаем в памяти для записи массива возвращаемых структур 1 f_namei ndex. Оценка необходимого размера буфера несколько завышена, но это проще, чем проходить список интерфейсов дважды один раз для подсчета числа интерфейсов и общего размера имен, а второй — для записи этой информации. Мы создаем массив i f_namei ndex в начале этого буфера и записываем имена интерфейсов, начиная с конца буфера.
Обработка только сообщений RTMJFINFO
2-36 Мы обрабатываем все сообщения, ища сообщения RTM IFINFO и следующие за ними структуры адреса сокета. Имя и индекс интерфейса записываются в создаваемый нами массив
Завершение массива
8-39 Последняя запись в массиве имеет пустой указатель т f_name и нулевой индекс.
Функция if_freenameindex
Последняя функция, показанная в лис гинге 17.13, освобождает память, которая была выделена для массива структур if_namemdex и хранящихся в нем имен.
Листинг 17.14. Освобождение памяти, выделенной функцией if_namemdex
43 void
44 if freenameindex(struct if nameindex *Dtr)
45 { ~一
46 free(ptr)
47 }
Эта функция тривиальна, поскольку мы хранили и массив структур, и имена в одном и том же буфере. Если бы мы каждый раз вызывали функцию та 11 ос, то для освобождения памяти нам пришлось бы проходить через весь массив, освобождать память, выделенную для каждого имени, а затем удалять сам массив (используя функцию free).
Резюме
Последняя из структур адреса сокета, с которой мы встретились в книге, это sockaddr_dl — структура адреса сокета канального уровня, имеющая переменную длину. Ядра Беркли-реализаций связывают их с интерфейсами, возвращая в одной из этих структур индекс интерфейса, его имя и аппаратный адрес.
В маршрутизирующий сокет процессом могут быть записаны 51 ипов сообщений, и 12 различных сообщений могут быть асинхронно возвращены ядром через маршрутизирующий сокет. Мы привели пример, когда процесс запрашивает у ядра информацию о записи в таблице маршрутизации и ядро отвечает со всеми подробностями. Ответы ядра содержат до восьми структур адреса сокета, поэтому нам приходится анализировать сообщение, чтобы получить все фрагменты информации.
Функция sysctl предоставляет общий способ получения и хранения параметров операционной системы. При выполнении функции sysctl нас интересует получение следующей информации:
список интерфейсов;
< таблица маршрутизации;
кэш ARP.
Изменения API сокетов, требуемые IPv6, включают четыре функции для сопоставления имен интерфейсов и их индексов. Каждому интерфейсу присваивается уникальный положительный индекс. В Беркли-реализациях с каждым интерфейсом уже связан индекс, поэтому нам несложно реализовать эти функции с помощью функции sysctl.
Упражнения
Как вы считаете, что будет хранить поле sdl_len в структуре адреса сокета канального уровня для устройства с именем ethlO, адрес канального уровня которого является 64-разрядным адресом IEEE EUI-64?
В листинге 17.3 отключите параметр сокета SO USELOOPBACK перед вызовом функции write. Что происходит?
ГЛАВА 18
Широковещательная передача
Введение
В этой главе мы расскажем о широковещательной передаче (brodacasting), а в следующей главе — о многоадресной передаче (multicasting). Во всех предыдущих примерах рассматривалась направленная {одноадресная) передача (unicasting, когда процесс общается только с одним определенным процессом. Действительно, TCP работает только с адресами направленной передачи, хотя UDP поддерживает и другие парадигмы передачи. В табл. 18.1 представлено сравнение различных видов адресации.
Таблица 18.1. Различные формы адресации
»esww»wa«fflB)^^
Тип
»»*в»«<вд»иии^^
IPv4?
Ipv6?
TCP?
UDP?
Количество идентифицируемых интерфейсов
Количество интерфейсов, куда доставляется сообщение
Направленная
•
•
•
•
Один
Один
передача
Передача наиболее
•
Пока нет
•
Набор
Один из набора
подходящему узлу
М ногоадресная
Не обяза-
•
•
Набор
Все в наборе
передача
тельно
Широковещатель-
•
•
Все
Все
ная передача
Мы добавили в эту таблицу передачу наиболее подходящему узлу {anycasting), поскольку планируется, что IPv6 в будущем будет поддерживать ее, но на сегодняшний день эта идея еще не реализована. Этот тип передачи описан в RFC 1546 [75].
Вот наиболее важные положения из этой таблицы:
Поддержка многоадресной передачи не обязательна для IPv4, но обязательна для IPv6.
Поддержка широковещательной передачи не обеспечивается в IPv6: любое приложение IPv4, использующее широковещательную передачу, для совместимости с IPv6 должно быть преобразовано так, чтобы использовать вместо широковещательной передачи многоадресную.
Ш ироковещательная и многоадресная передачи требуют наличия протокола UDP и не работают с TCR
Одним из применений широковещательной передачи является поиск сервера в локальной подсети, когда известно, что сервер находится в этой локальной подсети, но его IP-адрес для направленной передачи неизвестен. Иногда эту процедуру называют обнаружением ресурса (resource discovery). Другое применение — минимизация сетевого трафика в локальной сети, когда несколько клиентов взаимодействуют с одним сервером. Можно привести множество примеров ин- тернет-приложений, используюших для этой цели широковещательную передачу.
Протокол разрешения адресов (Address Resolution Protocol, ARP). Хотя это фундаментальная часть IPv4, а не пользовательское приложение, ARP отправляет широковещательный запрос в локальную подсеть, суть которого такова: «Система с IP-адресом a.b.c.d, идентифицируйте себя и сообщите свой аппаратный адрес».
泰 Протокол начальной загрузки (Bootstrap Protocol, ВВОТР), Клиент предполагает, что сервер находится в локальной подсети, и посылает запрос на широковещательный адрес (часто 255.255.255.255, поскольку клиент еще не знает IP-адреса, маски подсети и адреса ограниченной широковещательной передачи в этой подсети),
> Протокол синхронизации времени (Network Time Protocol, NTP). В обычном сценарии клиент NTP конфигурируется с IP-адресом одного или более серверов, которые будут использоваться для определения времени, и опрашивает серверы с определенной частотой (с периодом 64 секунды или больше). Клиент обновляет свои часы, используя сложные алгоритмы, основанные на значении истинного времени (time-of-day), возвращаемом серверами, и величине периода RTT обращения к серверам. Но в широковещательной локальной сети вместо того, чтобы каждый клиент обращался к одному серверу, сервер может отправлять текущее значение времени с помощью широковещательных сообщений каждые 64 секунды для всех клиентов в локальной подсети, оцэаничивая тем самым сетевой трафик.
養: Демоны маршрутизации. Наиболее часто используемый демон маршрутизации routed распространяет по локальной сети широковещательные сообщения, содержащие таблицу маршрутизации. Это позволяет всем другим маршрутизаторам, соединенным с локальной сетью, получать объявления маршрутизации. При этом в конфигурацию каждого маршрутизатора не обязательно должны входить IP-адреса соседних маршрутизаторов. Это свойство также используется (многие могут отметить, что «используется неправильно») узлами локальной сети, прослушивающими объявления о маршрутизации и изменяющими в соответствии с этим свои таблицы маршрутизации.
Следует отметить, что многоадресная передача может заменить оба варианта применения широковещательной передачи (обнаружение ресурса и ограничение сетевого трафика). Проблемы широковещательной передачи мы обсудим ниже в этой главе, а также в следующей главе.
ПРИМЕЧАНИЕ
Широковещательная передача, ограничивающая сетевой трафик в локальной сети, может привести к нежелательному взаимодействию с бездисковыми системами. Будем считать, что сервер NTP рассылает широковещательные сообщения, содержащие текущее значение времени, каждые 64 секунды. Если демон NTP на всех бездисковых клиентах в течение этих 64 секунд не присутствует в основной памяти, то каждые 64 секунды каждый из бездисковых клиентов получает дейтаграмму NTP и немедленно считывает демон NTP в основную память со своего дискового сервера, также через локальную сеть. Каждые 64 секунды в локальной сети возникает резкий скачок активности, поскольку каждый бездисковый клиент считывает демон NTP. Периодичность этих событий можно проследить с помощью широковещательных приложений. К счастью, постоянно снижающаяся цена на дисковые накопители выводит бездиско- выс системы из употребления.
Широковещательные адреса
Если мы обозначим адрес IPv4 в виде {netid. subnetid, hostid}, мы получим четыре типа широковещательных адресов. Поле, целиком состоящее из единичных битов, обозначим -1.
Широковещательный адрес подсети: {neti d, subnetid. -1}. Он предназначается для всех интерфейсов в заданной подсети. Например, если мы используем 8-разрядный идентификатор подсети с адресом класса В 128.7, то 128.7.6.255 будет широковещательным адресом, направленным в подсеть, для всех интерфейсов в подсети 128.7.6.
Обычно маршрутизаторы не передают широковещательные сообщения дальше из подсети [105, с. 226-227]. На рис. 18.1 изображен маршрутизатор, соединенный с двумя подсетями — 128.7.1 и 128.7.6.
Подсеть 128.7.6
128.7.6.44
???
?128.7.6.44
[ Подсеть 128.7.1
IP-адрес получателя=128.7.6.255 J
(Дейтаграмма направленной передач*
Рис. 18.1. Передает ли маршрутизатор дальше широковещательное сообщение, направленное в подсеть?
Маршрутизатор получает дейтаграмму IP направленной передачи в подсети 128.7.1 с адресом получателя 128.7.6.255 (адрес широковещательной передачи для подсети другого интерфейса). Обычно маршрутизатор не передает дейтаграмму дальше в подсеть 128.7.6. У некоторых систем имеется параметр
конфигурации, позволяющий передавать широковещательные сообщения, направленные в подсеть (см. приложение Е [94]).
Широковещательный адрес всех подсетей: {netid, -1, -1}. Он предназначен для всех подсетей в заданной сети. Этот тип адресов используется очень редко, если используется вообще.
3、Широковещательный адрес сети: {neti d, -1}. Этот тип адреса используется в сети, в которой отсутствуют подсети, что на сегодняшний день почти не встречается.
Локальный широковещательный адрес: {-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 для каждого из них, указав в качестве адреса получателя широковещательн ый адрес подсети этого интерфейса.
Сравнение направленной и широковещательной передач
Прежде чем рассматривать широковещательную передачу, необходимо уяснить, что происходит, когда дейтаграмма UDP отправляется на адрес направленной передачи. На рис. 18.2 представлены три узла Ethernet.
IP-адрес получателя=128 7 6 5
Протокоп=и0Р
Рис. 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 со своим собственным адре-
сом 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]. Мы также предполагаем, что отправляющее приложение связано с портом, на который оно отправляет дейта-
IP-адрес получателя=128 7 6 255 RpOTOKor=UDP
Рис. 18.3. Пример широковещательной дейтаграммы UDP
граммы (порт 520), поэтому оно получит копию каждой отправленной им широковещательной дейтаграммы. (Однако в общем случае не требуется, чтобы процесс связывался с портом UDP, на который он отправляет дейтаграммы.)
ПРИМЕЧАНИЕ
В этом примере мы демонстрируем закольцовку, которая осуществляется либо на уровне IP, либо на канальном уровне, создающем копию [105, с 109-110] и отправляющем ее вверх по стеку протоколов. Сеть могла бы использовать физическую закольцовку, но это может вызвать проблемы в случае сбоев сети (например, линия Ethernet без терминатора).
Этот пример отражает фундаментальную проблему, связанную с широковещательной передачей: каждый узел IPv4 в подсети, не участвующий в приложении, должен полностью обрабатывать широковещательную дейтаграмму UDP при ее прохождении вверх по стеку протоколов, включая уровень UDP, прежде чем сможет ее проигнорировать. (Вспомните наше обсуждение следом за листингом 8.11.) Каждый не-1Р-узел в подсети (скажем, узел, на котором работает IPX
Novell) должен также получать целый кадр на канальном уровне, перед тем как он сможет проигнорировать этот кадр (в данном случае мы предполагаем, что узел не поддерживает тип получаемого кадра, для дейтаграммы IPv4 равный 0800). Если приложение генерирует дейтаграммы IP с большой скоростью (например, аудио- или видеоданные), то такая ненужная обработка может серьезно повли* ять на остальные узлы подсети. В следующей главе мы увидим, как эта проблема решается с помощью многоадресной передачи.
ПРИМЕЧАНИЕ
Для рис. 18.3 мы специально выбрали порт UDP 520. Это порт, используемый демоном routed для обмена пакетами по протоколу информации о маршрутизации (Routing Information Protocol, RIP). Все маршрутизаторы в подсети, использующие RIP, будут отправлять широковещательную дейтаграмму UDP каждые 30 секунд Если в подсети имеется 200 узлов, в том числе два маршрутизатора, использующих RIP, то 198 узлов должны будут обрабатывать (и игнорировать) эти широковещательные дейтаграммы каждые 30 секунд, если ни на одном из них не запущен демон routed
Функция dg_cli при использовании широковещательной передачи
Мы еще раз изменим нашу функцию dg_cl i, на этот раз дав ей возможность от* правлять широковещательные сообщения стандартному серверу времени и даты UDP (см. табл. 2.1) и выводить все ответы. Единственное изменение, внесенное нами в функцию mam (см. листинг 8.3), состоит в изменении номера порта получателя на 13:
servaddr sin_port 二 htons(13).
Сначала мы откомпилируем измененную функцию main с прежней функцией dg_cl 1 из листинга 8.4 и запустим ее на узле bsdi:
bsdi % udpcliOl 206.62.226.63 hi
sendto error Permission denied
Аргумент командной строки — это широковещательный адрес подсети для присоединенной сети Ethernet. Мы вводим строку, программа вызывает функцию sendto, и возвращается ошибка EACCESS. Мы получаем ошибку, потому что нам не разрешается посылать дейтаграмму на широковещательный адрес получателя, если мы не указали ядру явно, что будем передавать широковещательное сообщение. Мы выполняем это условие, установив параметр сокета SO_BROADCAST (см. табл. 7.1).
ПРИМЕЧАНИЕ
Беркли-реализации реализуют лу «защиту от дурака» (sanity check). Однако Solaris 2.5 принимает дейтаграмму, предназначенную для широковещательного адреса, даже если мы не задаем параметр сокета SO_BROADCAST. В Posix 1g говорится, что ядро «может» возвратить ошибку.
В 4 2BSD широковещательная передача была привилегированной операцией, н параметра сокета SO BROADCAST не существовало. В 4.3BSD этот параметр был добавлен, и каждому процессу стало разрешено его устанавливать
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 ,,
if_indextoname, функции, 50a 548,
588
исходный код, 510 определение, 508
if_msghdr, структура, 492,507
iLname, поле, 980
if_name, элемент, 509,512
if_nameindex, структура, 980
определение, 509
if_nameindex, функция, 491,509 исходный код, 511
определение, 508 if^nametoindex, функция, 491,508, 548
исходный код, 509
определение, 508
ifa_msghdr, структура, 492 ifam_addrs, элемент, 492,499 ifc_buf, элемент, 474
ifc len, элемент, 100, 475,4 76
ifcreq, элемент, 474
ifconf, структура, 100,471 ifconfig, программа, 55,126,249, 477,485
IFF_B ROADCAST, константа, 485 IFF二 POINTOPOINT,
константа, 485
IFF PROMISC, константа, 761
IFF_UP, константа, 485
IFIALIAS, константа, 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
ifmjype, элемент, 506
IFNAMSIZ, константа, 508 ifr_addr, элемент, 474,485 ifjbroadaddr, элемент, 474,485, 488
ifr_data, элемент, 474
ifr_dstaddr, элемент, 474,485,488 ifr_f]ags, элемент, 474,485
ifr_metric, элемент, 474,485 ifr__name, элемент, 476,485 ifreq, структура, 471,481,483,485,
488,550
IGMP (Internet Group Management Protocol), 65,542, 709,713,943
контрольная сумма, 725
ILP32, модель программирования, 61
imi^_interface, элемент, 544, 550
imr_multiaddr, элемент, 544
in-addr.arpa, домен, 283, 294
in.rdisc, программа, 709
in_addr, структура, 95,220, 287, 293,309,544 ~
определение, 92
in_add jt, тип данных, 94
in_cksum, функция, 725
исходный код, 726
in pcbdetach, функция, 166
m_j)ktinfo, структура, 582, 585,979 определение, 582
in_port_t, тип данных, 94
in6_addr, структура, 96,220, 287, 293
IN6_IS_ADDR_LINKLOCAL, макрос, 310
IN6」S_ADDR一LOOPBACK, макрос, 310
IN6^IS_ADDR_MC_GLOBAL, макрос, 310
IN6JS_ADDR_MC_ LINKLOCAL, макрос, 310
IN6_IS_ADDR_MC_ NODELOCAL? макрос, 310
IN6」S_ADDR_MC_ORGLOCAL, макрос, 310
IN6」S_ADDR_MC_SITELOCAL, макрос, 310
IN6_IS_ADDR_MULTICAST, макрос, 310
IN6_IS_ADDR_SITELOCAL, макрос, 310
IN6_IS_ADDR_UNSPECIFIED, макрос, 310
IN6JS_ADDR_V4COMPAT, макрос, 310
IN6_IS_ADDR_V4MAPPED, макрос, 306, 311, 314,719, 822 определение, 310
in6_pktinfo, структура, 582,612, 706
определение, 612 in6addr_any, константа, 125,955 IN6ADDR_ANY_INIT, константа, 125,320,322, ЗЙ, 418,613,955
in6addr_loopback, константа, 954
IN6ADDR_LOOPBACKJNIT, константа, 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
INETIP, константа, 895 inet_ntoa, функция, 92,105,345, 662
определение, 105 inet_ntop, функция, 92,105,116,】 13 反 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_loose, функция, 117 inet_srcrt add, фуш<ция, 691, 695
inet_srcrt imt, функция, 691,695 inet_srcrt_j)rint, функция, 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_rthdi^_getflags, функция, 706 определение, 705
inet6_rthdr_init, функция, 705 определение, 704
inet6_rthdr lasthop, функция, 705 определение, 704, 705
inet6_rthdi^reverse, функция, 706 inet6_rthdr_segments,
функция, 706
определение, 705
inet6_rthdr_space, функция, 704
определение, 704
metd, программа, 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
IP_DROP_MEMBERSHIP, параметр сокета, 220,543
IP_HDRINCL, параметр сокета, 220, 240, 688, 710,725,727, 759,762,768,775
ip 一 id, элемент, 713,776
ip len, элемент, 711, 713
ip_mreq, структура, 220,543, 550 определение, 544
IP—MULTICAST—IF, параметр сокета, 220,543,1021
IP_MULTICAST_LOOP, параметр сокета, 220,543,546
IP_MULTICAST_TTL, параметр сокета, 220, 543, 546, 943,1022
ip_off, элемент, 711, 713
IP ONESBCAST, параметр сокета, 517,972
IP OPTIONS, параметр сокета, 220,240, 687,697,707, 896,1021
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 ipi ifindex, элемент, 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 IPPROTO_TCP, константа, 244, 319,368,414
IPPROTO_UDP, константа, 319 IPTOS_LOWCOST, константа, 241 一
IPTOS_LOWDELAY, константа, 241 一
IPTOS_RELIABILITY, константа, 241
IPTOS_THROUGHPUT, константа, 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-anpec, преобразованный к виду 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, функция getaddnnfb, 322
клиент и IPv4-сервер, совместимость, 308
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
IPV6_DROP_MEMBERSHIP, параметр сокета, 220, 543
IPV6_DSTOPTS, параметр сокета, 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
IPV6_MULTICAST_HOPS, параметр сокета, 220,543,546, 614,945
IPV6_MULTICAST_IF, параметр сокета, 220,543,613
IPV6_MULTICAST_LOOP, параметр сокета, 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
IPV6_RTHDR_LOOSE, константа, 705
IPV6_RTHDR_STRICT, константа, 705
IPV6_RTHDR_TYPE_0, константа, 705
IPV6_UNICAST_HOPS, параметр сокета, 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
*
Jackson, А., 700,1030
Jacobson, V., 70, 77, 553, 591, 594, 711,758,761,857,896,975,1029
Jamin, S., 30
Johnson, D., 31
Johnson, М.» 31
Johnson, S., 30
Jones, R. A., 30
Josey, A., 59, 1030
Joy,W. N., 128,1030
Kacker, 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_fixedpt, элемент, 560
l_len, элемент, 800
Llinger, элемент, 228, 252,467,835, 896
l_onoff, элемент, 228, 252,467,835, ,96
l_start, элемент, 800
l_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
LinJ.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
LOG_ALERT, константа, 376 LOG_AUTH, константа, 377 LOGAUTHPRIV, константа, 377 LOGCONS, константа, 378 LOGCRIT, константа, 376
LOG_CRON, константа, 377 LOG_DAEMON, константа, 377, 39「
LOG DEBUG, константа, 376 LOG EMERG, константа, 376 LOG—ERR, константа, 376,984 LOG_FTP, константа, 377 LOG」NFO, константа, 376,984 LOG_KERN, константа, 377 LOG_LOCALO, константа, 377 LOG LOCAL1, константа, 377 LOG_LOCAL2, константа, 377 LOG_LOCAL3, константа, 377 LOGLOCAL4, константа, 377 LOGLOCAL5, константа, 377 LOGLOCAL6, константа, 377 LOG_LOCAL7, константа, 377
LOG LPR, константа, 377
LOG_M AIL, константа, 377
LOG NDELAY, константа, 378 LOGNEWS, константа, 377 LOGNOTICE, константа, 376,391 LOGPERROR, константа, 378 LOG—PID, константа, 378
LOG_SYSLOG, константа, 377 LOGUSER, константа, 377,381, 390 ~
LOGUUCP, константа, 377
LOG_W ARNIN G, константа, 376 logger, программа, 378 login-name, поле, 383, 385,438 loom, программа, 32
Lothberg, Р., 952,1032
LP64, модель программиро
вания, 61
1s, программа, 420
Iseek, функция, 185,410,965
Isof, программа, 976
LSRR
гибкая маршрутизация от отправителя с записью, 687
свободная маршрутизация от отправителя с записью, 703
Lucchina, Р., 31
м
M—DATA, константа, 911,923,967
M_PCPROTO, константа, 911,918, 922,967
M—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
MAX LINE, константа, 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
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, Т., 514,1031 meter, функция, 796
Metz, C W., 30,120,1031
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 MODEJCLIENT, константа, 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, 40^ 405,408
MSG_CTRUNC, константа, 402
msg[dontroute,
константа, 224,398,402 MSG_DONTWAIT,
константа, 398,402,408 MSG_EOF, константа, 399,414 MSG_EOR, константа, 399,402, 414Л39,1010
msg flags, элемент, 399,404,582, 58 工 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
MSGOOB, константа, 233,248, 398,402,618,624,626, 629,639, 831,935
MSG_PEEK, константа, 398,402, 408,416,427,972,1009
MSGTRUNC, константа, 402,589 M S G_ WAIT ALL, константа, 113, 398,402,440
msghdr, структура, 100, 399,403, 408,427,434,582,584,589,598 определение, 401
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,
1而3
исходный код, 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
njflag, элемент, 843
njlookups, элемент, 843
nc_netid, элемент, 843
nc_nlookups, элемент, 843 njproto, элемент, 843 ncprotofmly, элемент, 843
nc^semantics, элемент, 843 njunused, элемент, 843 ND_ADDRLIST, константа, 845 nd_addrlist, структура, 844, 851, 881
определение, 844 nd_hostserv, структура, 844. 851,
854'
определение, 844 ND_HOSTSERVLIST, константа, 846
nd_hostservlist, структура, 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
NET二RT[fLAGS, константа, 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
netdir_free, функция, 846,851,860, 881,887”
netdir_getbyaddr
определение, 845
netdir_getbyaddr, функция, 8 io, 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
<netinet /ip_var.h>, заголовочный файл, 693
<netinet/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_pcap, функция, 777
nfds_t, тип данных, 210
NFS, сетевая файловая система, 89, 233,239,254,592,759
N1_DGRAM, константа, 342,372
NI_MAXHOST, константа, 342
NI_MAXSERV, константа, 342
NI_NAMEREQD, константа, 342, 371,373
NI_NOFQDN, константа, 342,371
N1]NUMERICHOST,
константа, 342,371,1007
N1_NUMERICSERV, константа, 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
nip, программа, 567
ntp.h, заголовочный файл, 560,568
ntpdata, структура, 561
NVT (Network Virtual
Terminal), 990
О
O_ASYNC, константа, 248,473, 642, 647
O_NONBLOCK, константа, 248, 473,647,821,926,940
O_RDONLY, константа, 429
ORDWR, константа, 821
OSIGIO, константа, 642
ОЪеП, M., 952,1029
Open Group, 57,820,1031
open, функция, 160,380,422,427, 429,433, 760,803,965
OPEN_MAX, константа, 211
ореп_рсар, функция, 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
OPT_length, элемент, 921
ОРТ_offset, элемент, 921
opt val str, элемент, 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 _PC_SOCK一MAXBUF,
константа, 235
pcap_compile, функция, 759,770 pcap_datalink, функция, 770,776 pcap_lookupdev, функция, 769 pcap_lookupnet, функция, 770 pcap next, функция, 777 pcap_open_live, функция, 769, 777 pcap_pkthdr, структура
определение, 777
pcap setfilter, функция, 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. 1, 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
PosixJg, 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. В., 66,77, 239, 242, 709, 942,943,948,952,956,1029,1032
PPP (Point-to-Point Protocol), 83, 501,776
PJcpu time, функция, 789, 793 prifinfo, программа, 489,504 PRIM_type, элемент, 917, 918,921, 923 i
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 pthreadjoin, функция, 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
PTHREAD_PROCESS_SHARED, константа, 802
pthread_self, функция, 653 pthread setspecific, функция, 666,
669 一一
определение, 669
pthread_l, тип данных, 654
PTR, запись указателя (DNS), 283, 293,334
Pusaten, Т, 534,1032
PUSH, флаг ТСР-заголовка, 831 putc unlocked, функция, 662 putchar_unlocked, функция, 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 г, функция, 662
RARP (Reverse Address Resolution Protocol), 65,757,759
read,функция, 39,41,43,61, 111, 113, 115,116,14t 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
rcad_cred, функция, 436
read fd, функция, 431,434, 750, 80^ исходный код, 432
read_loop, функция, 565, 568,573 readable_conn, функция, 750 readable_hbten, функция, 749
readable_timeo, функция, 395 исходный код, 395
readable_v4, функция, 752 readable v6, функция, 755 readdir, функция, 662
readdirr, функция, 662
readline, функция, 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
readhne 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
recvall, функция, 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
recvfrom_flags, функция, 582,586 recvmsg, функция, 93,100,236,241,
255,267,392,399,407, 415,427, 431,440,545,582,584,587,589, 598,600,612, 620, 701, 703, 706, 893,931,1010,1017
определение, 401
Red Hat Software, 31
Regina, N, 32
Reid, J, 30
Rekhter, Y, 952,1032
release, элемент, 294
rename, функция, 377
Reno, 53,93,98,236,399,401,490, 582.689,711,727
RESINIT, константа, 293
resinit, функция, 290,293,301,356
RES_length, элемент, 921
RES_offset, элемент, 921
RES_O PTIONS переменная окружения, 293
RESOPTIONS, переменная окружения, 290
RES_USE_INET6, константа, 290, 3017308,322,357
revents, элемент, 208,939
rewind, функция, 410
Reynolds, J К, 77,242,709,943, 1032'
RFC (Request for Comments), 60, 66,988,1027
получение, 988
RFC 768, 66,1032
RFC 791, 942,1032
RFC 792, 956,1032
RFC 793, 66, 239
RFC 862, 87
RFC 863, 87
RFC 864, 87
RFC 867, 87
RFC 868, 87
RFC 950, 948,1031
RFC 1071, 725,1028
RFC 1108, 688,1030
RFC 1112, 534,1028
RFC 1122, 75,253,263,517,557, 583,95L 1027
RFC 1185, 77,1030
RFC 1191, 83,1031
RFC 1305, 560,1031
RFC 1323, 70,252,501,59&896;
959, 1028,1030
RFC 1337, 228,1027
RFC 1349, 241,1027
RFC 1379, 412,1027
RFC 1390, 534,1030
RFC 1469, 534,1032
RFC 1519, 948,1028
RFC 1546, 514,1031
RFC 1639, 311,1032
RFC 1644, 412,1028
RFC 1700, 77,242,709,943,1082
RFC 1812, 743,1027
RFC 1826, 698,1027
RFC 1827, 698,1027
RFC 1832, 177,1032
RFC 1883, 243, 700, 703,706,1028
RFC 1884, 952,1029
RFC 1885, 956,1028
RFC 1886, 283,1033
RFC 1897, 953,1029
RFC 1972, 536,1028
RFC 1981, 84,1031
RFC 2019, 536,1028
RFC 2026, 60,1028
RFC 2030, 560,1031
RFC 2073, 952,1029,1032
RFC 2113, 688,1030
RFC 2133, 60,96, 242, 343,508,
545,1029
RFC 2147, 84,1027
RFC, требование к узлам, 1027
RIP (Routing Information
Protocol), 84,88,521
Ritchie, D M , 31,907,984,1030, 1032
rl cnt, элемент, 671
rlkey, функция, 669
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 rml_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, флаг TCP-заголовка, 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
RTABRD, константа, 493
RTADST, константа, 493
RTA GATEWAY, константа, 493 RTAGENMASK, константа, 493 RTAIFA, константа, 493
RTA_IFP, константа, 493
RTA NETMASK, константа, 493
RTAX AUTHOR, константа, 493
RTAX BRD, константа, 493
RTAX DST, константа, 493
RTAX^GATEWAY, константа, 493 RTAX^GENMASK, константа, 493 RTAX IFA, константа, 493
RTAX IFP, константа, 493,510 RTAX—MAX, константа, 493,499 RTAXNETMASK, константа, 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_IF[NFO, константа, 492,502, 506,510,512
RTM LOCK, константа, 492 RTMLOSING, константа, 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
rtt stop, функция, 600, 603 исходный код, 603
rtt_timeout, функция, 600, 604 исходный код, 604
rtt ts, функция, 599, 602, 1017 исходный код, 602
RUSAGE—CHILDREN, константа, 790
RUSAGE_SELF, константа, 790
S
s_addr, элемент, 94
s aliases, элемент, 296
S BANDURG, константа, 933 S_ERROR, константа, 933 s_fixedpt, элемент, 561
S_HANGUP, константа, 933
S HIPRL константа, 933
S_IFSOCK, константа, 115
S—INPUT, константа, 933
S__ISSOCK, константа, 115
S_MSG, константа, 934 s_name, элемент, 296 SOUTPUT, константа, 933 s port, элемент, 296 s_proto, элемент, 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, константа, 212
Schimmel, C., 796,1032
SCM_CREDS, параметр сокета, 405,435
SCMR1GHTS, параметр сокета, 405
SCO, 31
script, программа, 678
sdl alen, элемент, 491, 507,1013 sdl_data, элемент, 491
sdl family, элемент, 491
sdl_index, элемент, 491 sdl_len, элемент, 491, 513 sdl_nlen, элемент, 491, 507,1013 sdl_slen, элемент, 491
sdl_typc, элемент, 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
SEQ_number, элемент, 922 sequence, элемент, 827, 829, 835, 847, 857, 862, 867, 872, 876
Sequent Computer Systems, 856
SERV PORT, константа, 148,150, 214,257,596,606
определение, 980 servent, структура, 296,300
определение, 296 servtype, элемент, 822,824 set addresses, функция, 239 SET TOS, константа, 897 SETBLOCK, константа, 714 SETBLOCKALL, константа, 714 setgid, функция, 385 setnetconfig, функция, 842,859, 1023
setnetpath, функция, 851 определение, 843
SETPASS, константа, 714
SETPASS ALL, константа, 714 setrlnmt, функция, 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_clild, функция, 158,163,279, 789
SIG_DFL, константа, 155,1009 SIGIGN, константа, 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
sigcniptyset, функция, 526
Sigfunc, Tim данных, 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
sm_addr, элемент, 93, 125, 315, 485
sin_family, элемент, 93,270
sin len, элемент, 93
sin_port, элемент, 62, 93, 125
sin zero, элемент, 94
sin6 addr, элемент, 96,125,315 sin6_family, элемент, 96,270 sin6_flowinfo, элемент, 96,945 SIN6_LEN, константа, 93,96 sin6_len, элемент, 96 sin6_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
SIOCG 圧 BRDADDR, константа, 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_us, функция, 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
SOBROADCAST, параметр сокета, 220,223,224,251,517,523, 572,756,896,971,1021
SO_BSDCOMPAT, параметр сокета, 265
SO_DEBUG, параметр сокета, 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
Sd^KEEPALIVE, параметр сокета, 171,177,220,223,225,244, 251,253, 634,898,971
SO_LINGER, параметр сокета, 85, 141,145,166,199,220,223,227,
228,251,467, 835,856,864,873, 896,971
SO_OOBINLINE, параметр сокета, 220,223,233,620,626,628, 639,935
so pgid, элемент, 250
SO_RCVBUF, параметр сокета, 70, 220,223, 233, 251,258, 275,896, 971,1000
SO_RCVLOWAT, параметр сокета, 220,235,896
SORCVTIMEO, параметр сокета, 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
SO_SNDLOWAT, параметр сокета, 191, 220, 235, 896
SO SNDTIMEO, параметр сокета, 220,235,392,397,971 sosocket, функция, 969 so_timeo, структура, 795 SO_TIMESTAMP, параметр сокета, 644
SO一TYPE, параметр сокета, 220, 223,239
SO_USELOOPBACK, параметр сокета, 199, 220,240, 513
sock, программа, 252,281,588,969, 1000
параметры, 971 sock_bind_wild, функция, 110, 744,
75「—
определение, 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
определение, 110
sock_masktop, функция, 497
sockntop, функция, 110,135, 145, 334 342,372,587,848, 1007, 1016
исходный код, 110
определение, 109
sockntophost, функция, 110,497, 523-
определение, 110
sockopts, структура, 221
SOCK PACKET, константа, 64, 120,757,761,780
SOCKRAW, константа, 120,710
SOCK SEQPACKET, константа, 120 —•
sock_set__addr, функция, 110,1006 определение, 110
sock_set_port, функция, 110,733, 1006
определение, 110
sock_set_wild, функция, 110, 562, 56】
определение, 110
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,3式 322,367,482,496,499, 743,823,837,845,880,881,918, 965,989,1003
определение, 92,95
схема, 98
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,43a, 532…
определение, 420
sockfd_to_fainily, функция, 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 sp_family, элемент, 121 sp_protocol, элемент, 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_inodc, элемент, 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 <string.h>f заголовочный
файл, 104
strlen, функция, 990
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
SUNLEN, макрос, 418,979 sun」en, элемент, 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 SYNRCVD, состояние, 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
Vsys/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
TADDR, константа, 847,854,880, 888
T_ALL, константа, 847,854,1023
t alloc, функция, 846, 854, 870, 880, 888,904,928,940,1023
определение, 846
T—BIND, константа, 846 tbind, структура, 827,847,849, 853,860, 875
определение, 827 t_bind, функция, 827, 837,840, 851, 856.860,867,870,875, 880,887,' 919,929,931
определение, 827
T_BIND_ACK, константа, 918,919
T_bind_ack, структура, 919 определение, 918
TBINDREQ, константа, 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
T_conn_con. структура определение, 921
T_CONN_REQ, константа, 966
T_conn_req, структура, 921
определение, 921 t_connect, функция, 457,828,837, 839,852, 856,875,893,926,937, 940,967
определение, 829
T_COTS, константа, 824
T_COTS_ORD・ константа, 822, 近4
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 匚 DEFAULT, константа, 895,899, 902
T_DIS, константа, 846 tdiscon, структура, 827,835,847, 869,933
определение, 835
Т DISCON IND, константа, 922, 923
T_discon_ind, структура определение, 922
T—DISCONNECT, константа, 831, 835, 837, 867, 868,872
t errno, переменная, 825, 827, 830, 831,838,866, 883, 888,926
t_error, функция, 825
определение, 825
T_ERROR_ACK, константа, 918, 919,921
Terrorack, структура определение, 918
TEXDATA, константа, 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_g6dATA, константа, 832
tZgOEXDATA, константа, 832
T_HIREL, константа, 897
T_HITHRPT, константа, 897
T IDLE, константа, 860, 928
匸 IMMEDIATE, константа, 897
T_INCON, константа, 928
T_INET_IP, константа, 892,899
TINETTCP, константа, 892,982 匸 INET二 UDP, константа, 892
T_INETCONTROL, константа, 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
Т IP BROADCAST, константа,' 982
T_IP_BROADCAST, параметр ХТ1?892,896
T__IP_DONTROUTE, параметр
ХТ1?892,896
T_IP_OPTIONS, параметр XTI, 892,~896,902
TIP—REUSEADDR, параметр ХТ1?891,897
T_IP_TOS, параметр XTI, 892,897 匸IP二TTL, параметр XTI, 892,897,
902
t_kpalive, структура, 892
определение, 898
T—LDELAY, константа, 897
tlinger, структура, 892,896
определение, 896
T—LISTEN, константа, 832,867, 872,873,876
tlisten, функция, 832,855,857, 860,867,869,874,876,893,926, 930
определение, 857
T LOCOST, константа, 897 t_look, функция, 832,837,840,867,
872,935,937
определение, 832
TMORE, константа, 830,879,888, 890
T_NECOTIATE, константа, 899, 905
TNETCONTROL, константа, 897
T_NO, константа, 897,898
T NOTOS, константа, 897
TNOTSUPPORT, константа, 902
Т ОК АСК, константа, 966 t_open, функция, 837,840,842,846, 851,856,860,861,866,872,880, 887,926,939
Т_ОРТ, константа, 847,854 匸 ОРТ一DATA, макрос, 895
Т_ОРТ一FIRSTHDR, макрос, 895
Т_ОРТ一NEXTHDR, макрос, 895 jopthdr, структура, 61,894,901, 905,966“
определение, 894
T__OPTMGMT, константа, 846 t_optmgmt, структура, 827,847, 894,899,901'
определение, 899
l_optmgmt, функция, 893,895,905r 929
определение, 899
TORDREL, константа, 831,838, 937
T_ORDREL_IND, констапта, 923, 966,968
T_ordrel_req, структура, 924 определение, 924
T_ORDRELDATA, константа, 824, 933
T OUTCON, константа, 928 T_OUTREL, константа, 928 T OVERRIDEFLASH, константа, 897
Т_Р ARTSUCCESS, константа, 905
T_primitives, структура, 921 匸 PRIORITY, константа, 897
T_PUSH, константа, 831
trcv, функция, 831,838,862,864, 878t 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
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
T_ROUTINE, константа, 897 t_scalar_t, тип данных, 61,822 T SENDZERO, константа, 825 tsnd, функция, 831,839,862,864, 927,932,935,940,972 определение. 830
t_snddis, функция, 824, 834, 856, 896,932
определение, 834 t sndrel, функция, 832, 864, 924, 933
определение, 833 t_sndreldata, функция, 824,932
определение, 933
t sndudata, функция, 832,878,882, 888,890,893,927,932 определение, 878
tsndv, функция, 832,927,932,940 определение, 932
t_sndvudata, функция, 832,893, 927,932,940
определение, 932
t strerror, функция, 825 определение, 825
TSUCCESS, константа, 895,901, 904
tsync, функция, 929 определение, 930
Т ТСР КЕЕРALIVE, параметр XTI, 892,898
T_TCP_MAXSEG, параметр XTI,89Z 898
T TCP NODELAY, параметр ХТ1,892,898
T—UDATA, константа, 847,854
T_UDERR, константа, 832 t_uderr, структура, 827, 847, 884, 894
определение, 884
T_UDERROR, константа, 846
T UDP CHECKSUM, параметр
XTI, 892,898,902,929
t_unbind, функция, 931 определение, 931
T UNBND, константа, 928
T—UNINIT, константа, 928
匸 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, L L., 30
Taylor, R., 30
TBADADDR, константа, 825
TBADF, константа, 825
ТВ ADOPT, константа, 902
TBUFOVFLW, константа, 827
tcflush, функция, 470 tcgetattr, функция, 470
TCP, 68
параллельный сервер один дочерний процесс
каждому клиенту, 787 один программный поток
каждому клиенту, 809 сервер с предварительным порождением потоков, 811
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
tcp_close, функция, 166 tcp__connect, функция, 319,328, 339,372,461, 675,696,744,849
исходный код, 328,850
определение, 328
TCPKEEPALIVE, параметр
сокета, 220, 226, 244
tcplisten, функция, 331,340,372, 389,658,748,789,859, 866, 870, 886,936, 1008
исходный код, 333,858 определение, 331
TCP—MAXRT, параметр сокета, 220, 244
TCP—MAXSEG, параметр сокета, 70,220,245,898,971
TCP NODELAY, параметр сокета, 220/245,253,400,898,971,997
TCP_NOPUSH, параметр сокета, 414
TCP STDURG, параметр сокета, 220,248,618
tepdump, программа, 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
tenncap, файл, 196
Terzis, А., 30
test_udp, функция, 768, 770
TFLOW, константа, 927
TFTP (Trivial File Transfer Protocol), 84,88,269,517,5叫 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 thread make, функция, 812, 815 TIBIND, константа, 965 ticks, константа, 939 ticots, константа, 939
ticotsord, константа, 939 time, программа, 87,452,1008 time, функция, 47,864 time_t, тип данных, 208
TIMEWAIT, состояние, 72,75,88, 154,178, 228,232,252,341, 786, 873,976,989,996
times, функция, 602
tunespec, структура, 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 tinpnam, функция, 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 tpi_connect, функция, 916,919 tpidaytime.h, заголовочный файл, 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
u long, тип данных, 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
UDP (продолжение) определение исходящего интерфейса, 276
отправка, 86
отсутствие управления
потоком, 272
параллельный сервер, 609
порядковый номер, 593
потерянные дейтаграммы, 261
приемный буфер сокета, 275
присоединенный сокет, 267
проверка полученного
ответа, 261
связывание адреса интерфейса, 605
сервер не работает, 264
сокет, 254,581
тайм-аут, 593
увеличение надежности приложения, 593
чтение дейтаграмм по
частям, 888
udp__check, функция, 777, 778
UDP_CHECKSUM, константа, 891
udp_client, функция, 336,372,554, 558,562,567,568,571,616,879, 1010,1017
исходный код, 338,879
определение, 336
udp_connect, функция, 338,372, юГо
исходный код, 339
определение, 338
udp^read, функция, 772, 776, 780
udp_server, функция, 339, 372, 886, 1008
исходный код, 340,886
определение, 339
udp_server_reuseaddr,
функция, 1008
udp_wnte, функция, 774
udpcksuin h, заголовочный
файл, 764
udpInDatagramb, переменная, 275
udplnOverflows, переменная, 275 udpiphdr, структура, 775 ш_1еп, элемент, 775
m_sum, элемент, 775
mntl6_t, тип данных, 94 uint32_t, тип данных, 94,98,822 uint8_t, тип данных, 94
umask, функция, 420f
unaine, функция, 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
UNIXSTR_PATH, конс1анга, 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 h, заголовочный файл, 598, 600
исходный код, 600
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/hb/tcpip so, файл, 842
UTC, универсальное скоординированное время, 47, 87, 556, 564,602,683
_UTS_NAMESIZE, константа, 294
—UTSNODESIZE, константа, 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
WaitJ W,31
wait, функция, 157,177, 387,611, 674,786,793
определение, 160 waitpid, функция, 157,177,387, 430,655,674
определение, 160
wakeup_onc, функция, 796 web h, заголовочный файл, 459 web_child, функция, 663, 791, 794, 81б;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 wntable_timeo, функция, 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
wnte_fd, функция, 433, 744,808 исходный код, 434
wnte get cmd, функция, 462,465, 675" 一 一
wnten,функция, 111, 116, 146,149, 151,168,170,175,195,410,442, 462,635
исходный код, 112 определение, 111
wntev, функция, 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
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_inet h>, заголовочный файл, 821,895
xtiaccept, функция, 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
XTI_LINGER, параметр XTI, 864, 892? 896,898
xti_ntop, функция, 848,853,864
определение, 849
xti_ntop_host, функция, 849,882
XTI^RCVBUF, параметр XTI, 892, 89G
XTI RCVLOWAT, параметр
XTI, 892,896
xti_rdwr, функция, 839,861,872,
935
исходный код, 839
определение, 839
xti_read, функция, 840 xti_serv_dev, переменная, 859, 862,
876 —
xti_serv_dev, устройство, 983
xti_setopt, функция, 903
исходный код, 905
определение, 903
XTI—SNDBUF, параметр XTI, 892 895
XTI_SNDLOWAT, параметр
X 宜,892,896
xti_tlook_str, функция, 937
Y
уасс, программа, 57
Yu,J Y,948,1028
Z
Zhang, L, 77,1030
Ziel, B,31
A
абсолютное врем龜 683
абсолютное имя, DNS, 282
автоматический туннель, 954
административно задаваемая область действия, 537
адрес
бЬопе, тестовый, 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
ввод-вывод (продолжение)
блокируемый, 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
IP_RECVDSTADDR, 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
двухфазный протокол
с подтверждением завершения транзакций, 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,
260,265,277,343,422,610,
740,744,751,989,1015,1022
определение, 78
домен Unix
и функция getaddnnfo и 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
изменение маршрутов, 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
лавинная адресация
сегмента S YN, 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
маршрутизация (продолжение)
счетчик количества
переходов, 485
тип, 703
маршрутизация от отправителя IPv4,689
IPv6,703
масштабирование окна, TCP, 70, 233,896,1030
медленный старт, 414,466,591,1029 методики отладки, 964
минимальная канальная MTU, 83 минимальный размер буфера, 84 минимальный размер буфера
соединения фрагментов, 84 Мичиганский университет, 31 многоадресная передача, 534
IPv4, административно задаваемая область действия, 537
1Ру4-адрес, 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
небуферизованный поток стандартного ввода-вывода, 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
пакет (продолжение)
слишком большой (ICMP), 83, 742,957
пакетный ввод, 195
параллельное программиро
вание, 676
параллельный сервер, 48,138
UDP, 609
и номера порюв, 79
один дочерний процесс каждому клиенту» TCP, 787
один программный поток каждому клиенту, TCP, 809 параметры
TCP, 70
ХТ1,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
переносимость исходного кода, 343 поверхностная копия, 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
получа1ель
IP-адрес, функция recvmsg, 582
IPv6-anpec, 945
недоступен, необходима фрагментация (ICMP), 742, 956
получатель (продолжение) параметры 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,132, 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
привилегированный
пользователь, 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
ipynnoBoii идентификатор, 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
сервер
имен, 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
соединение (продолжение) прерывание, функция 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
трехэтапное рукопожатие, 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
устойчивая неисправность, 1* утечка памяти, 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
и IР-фрагментация, 524
и многоадресная передача, сравнение, 538
и направленная передача, 517
лавина широковещательных
сообщении, 519
лавинная адресация, 543
экспоненциальное смещение, 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
Дробная часть — это 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, возвращаемая функцией gettimeofday, возвращается как два целых числа: число секунд и микросекунд, прошедшее с 1 января 1970 года (UTC). Затем мы вычисляем и выводим разницу между истинным временем узла и истинным временем сервера NTP в микросекундах.
Один из факторов, не учитываемых нашей программой, — это задержка в сети между клиентом и сервером. Но мы считаем, что пакеты NTP обычно приходят как широковещательные или многоадресные пакеты в локальной сети, а в этом случае задержка в сети составит всего несколько миллисекунд.
Если мы запустим эту программу на узле sol ans с сервером NTP на узле bsdi, который с помощью многоадресной передачи отправляет пакеты NTP в сеть Ethernet каждые 64 секунды, то получим следующий результат:
solans # ssntp 224.0.1.1
joined 224 0 1 1 123 on loO
joined 224 011 123 on leO
v3 mode 5v3 mode 5v3. mode 5.v3. mode 5.v3. mode 5.v3. mode 5.strat 3 clock difference = 621 usee
v3 mode 5
v3 mode 5
v3. mode 5.
v3. mode 5.
v3. mode 5.
v3. mode 5.
strat 3. clock difference = 1205 usee
strat 3. clock difference = 1664 usee
v3 mode 5 strat 3. clock difference = 2291 usee
v3 mode 5. strat 3 clock difference = 2942 usee
v3. mode 5.v3. mode 5.strat 3, clock difference = 3558 usee
v3. mode 5.
v3. mode 5.
Перед запуском нашей программы мы завершили на узле работу NTP-сервера, поэтому когда наша программа запускается, время очень близко к времени сервера. Мы видим, что этот узел отстает примерно на 600 микросекунд каждые 64 секунды, то есть примерно на 810 миллисекунд в сутки.
SNTP (продолжение)
Теперь мы расширим наш пример SNTP, дополнив его другими свойствами. При запуске наша программа будет создавать не один сокет, как было показано в разделе 19.10, а три: один сокет для адреса направленной передачи, один — для адреса широковещательной передачи, и еще один — для интерфейса, на котором происходит присоединение к многоадресной передаче. Это позволит определять адрес получателя пришедшего пакета. Далее с помощью широковещател ьной и многоадресной передачи программа будет передавать запрос клиента SNTP со всех присоединенных интерфейсов, чтобы получить начальную оценку разницы во времени.
ВНИМАНИЕ
Этот код не следует воспринимать как рекомендуемый способ создания клиента SNTP Наша цель — рассказать о широковещательной и многоадресной передаче применительно к узлу с несколькими сетевыми интерфейсами или к марпзрутизатору, а также о свойсгве заколы 1овки широковещателыюй и miioi оадреспой передачи. В качестве примера мы взяли клиен! SNTP, поскольку это полезное реально используемое приложение. Мы отправляем запросы клиента SNTP со всех интерфейсов, поддерживающих широковещательную и многоадресную передачу, лишь для того, чтобы продемонстрировать, как некоторые программы осуществляют обнаружение ресурса (resource discovery) при запуске. На самом деле для клиента SNTP это не рекомендуется — более разумно было бы просто прослушивать широковеша1елы1ью или многоадресные сообщения сервера, как показано в разделе 19.10.
На рис. 19.6 представлен обзор функций, составляющих нашу программу. Сначала мы вызываем нашу функцию get_i fi_i nfo (см. раздел 16.6), чтобы получить список интерфейсов. Для каждого интерфейса мы создаем сокет UDP и при помощи функции bind связываем его с адресом направленной передачи, затем создаем другой сокет UDP, который связываем с адресом широковещательной передачи, и, наконец, создаем третий сокет UDP, связываем его с группой NTP (224.0.1.1) и присоединяемся к группе на этом интерфейсе. Наша функция sntp_send отправляет широковещательный запрос на определение текущего времени с каждого сокета, поддерживающего широковещательную передачу, каждому серверу NTP в присоединенной подсети, а также отправляет групповой запрос со всех сокетов, поддерживающих многоадресную передачу. Целью этой первой отправки пакетов является обнаружить все серверы NTP в присоединенных подсетях и получить начальную оценку текущего времени.
и обрабатываети обрабатываетвсе последующие ответывсе последующие ответыРис. 19.6. Обзор функций в нашем клиенте SNTPРис. 19.6. Обзор функций в нашем клиенте SNTP
и обрабатывает
и обрабатывает
все последующие ответы
все последующие ответы
Рис. 19.6. Обзор функций в нашем клиенте SNTP
Рис. 19.6. Обзор функций в нашем клиенте SNTP
Затем программа входит в бесконечный цикл read.loop, считывая все приходящие ответы. Сначала мы предполагаем получить ответы на некоторые дейтаграммы, посланные функцией sntp^send, а затем мы должны просто получать периодические передачи от серверов NTP в присоединенной подсети. Большинство
серверов 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
#include"unpifi h"
#mclude"ntp hM
3
#defme MAXNADDRS 128
/* максимальное количество адресов для функции bindO */
4
5
typedef struct { struct sockaddr *addr_sa.
/* указатель на связанный адрес */
6
socklen_t addr_salen.
/* длина адреса сокета */
7
const char *addr_ifname.
/* имя интерфейса. для многоадресной передачи */
8
int addr_fd.
/* дескриптор сокета */
9
intaddr 二flags.
/* флаги ADDR_xxx (см ниже) */
10
11
JAddrs.
Addrs addrs[MAXNADDRSJ.
/* массив структур */
12
int naddrs,
/* индекс в массиве */
13
14
15
#defwe ADDR一BCAST 1
#define ADDR[MCAST 2
const int on.
/* для setsockopt() */
/* прототипы функций */
voidbind_mcast(const char *. SA *, socklen_t. mt).
void bmd_ubcast(SA *. socklen_t. int. int. int).
void readjoop(void). 一
voidsntp_proc(char *. ssize_t nread. struct timeval *):
void sntp[send(void). 一
Листинг 19.14. Функция main
//sntp/main c
2 const int on = 1.2 const int on = 1.#include "sntp h"
2 const int on = 1.
2 const int on = 1.
/* флаги для setsockoptO */
int
main(int argc. char **argv)
{
mt sockfd. port;
socklen_t salen.
struct ifi_info *i fi.
struct sockaddr *mcastsa, *wild;
if (argc !■ 2)
err_quit(nusage. sntp <IPaddress>p),
sockfd - Udp_client(argv[l]. "ntp". (void **) &mcastsa. &sa1en);
port - sock_get_port(mcastsa. salen).
Closetsockfd),-
/* получение списка интерфейсов и работа с ними */
for (ifi = Get_ifi_info(mcastsa~>sa_family. 1). ifi i= NULL.
ifi = ifi->ifi_next) {
bind_ubcast(ifi->ifi_addr. salen, port.
ifi->ifijnyfTags & IFI_ALIAS. 0)./* направленная передача */
\一 一
if (if-i->ifijlags & IFF^BROADCAST)
Ыnd_ubcast(ifi->ifi_brdaddr salen. port.
if-i->ifi_myfTags & IFI_ALIAS. 1)./* широковещательная передача */
#ifdef MCAST
if (ifi->ifi_flags & IFF_MULTICAST)
bindjncastTifi->ifi_name, mcastsa. salen.
一 ifi->ifi_myfldgs & IFI_ALIAS). /* многоадресная передача */
#endif--
}
wild = Malloc(salen). /* структура адреса сокета для универсального адреса */ ' 30 memcpy(wild. mcastsa. salen), 31 sock_set_wild(wild. salen),
bmd_ubcast(wild, salen. port. 0. 0).
sntp_send()./* посылаем первый за叩ос */
read_loop()./* никогда не завершается */
}—
Задание структуры Addrs
3-12 Мы задаем структуру типа Addrs, содержащую необходимую нам информацию о каждом адресе, возвращаемом для данного интерфейса. Поскольку структура iri_】nfo, возвращаемая нашей функцией get_ifi_mfo, может иметь два адреса (например, адрес направленной передачи и широковещательный адрес), нам нужно поддерживать две структуры Addrs, по одной для каждого адреса. Элемент addr_sa указывает на структуру адреса сокета, возвращаемую функцией get_i fi_i nfo, a addr_salen — это ее длина. Мы сохраняем указатель на имя интерфейса в функции addr_i fname. Этот указатель будет использоваться для многоадресной передачи. Для каждого адреса мы создадим по сокету, при этом дескриптор будет сохранен в элементе add「_fd. Нам также нужно знать, был ли этот сокет связан с широковещательным или групповым адресом. Эта информация сохраняется в элементе addr^flags.
Получение заранее известного адреса многоадресной передачи и заранее известного порта
10-14 Обычно аргументом командной строки является имя ntp.mcast.net, сопоставляемое с адресом многоадресной передачи 224.0,1.1. Имя службы — ntp. Наша функция udp_cl tent выделяет в памяти пространство для структуры адреса сокета нужного типа (IPv4 или IPv6) и записывает в эту структуру адрес многоадресной передачи и порт. Если эта программа выполняется на узле, не поддерживающем многоадресную передачу, может быть задан любой IP-адрес, поскольку в этой структуре будет задействован только порт. Затем мы закрываем сокет, поскольку Целью вызова функции udp_cl i ent является только заполнение структуры адреса
сокета.
Получение списка интерфейсов
L5-17 Наша функция get_i f i」nfo возвращает информацию обо всех интерфейсах и адресах. Запрашиваемое нами семейство адреса берется из структуры адреса сокета, заполненной функцией udp_client на основе аргумента командной строки. Второй аргумент функции get_ifi_info, имеющий значение 1, указывает, что требуется возвратить информацию о дополнительных (альтернативных) адресах.
Обработка каждого адреса направленной и многоадресной передачи
18-22 Мы вызываем нашу функцию bind_ubcdst один или два раза для каждого адре» са. При первом вызове последний аргумент равен 0 — это означает, что первый аргумент указывает на адрес направленной передачи, а при втором вызове этот аргумент равен 1, следовательно, первый аргумент указывает на широковещательный адрес. Затем мы передаем флаг IFI_ALIAS нашим функциям bmd_XXX, предоставляя каждой функции возможность решить, как обрабатывать альтернативные адреса, что мы рассмотрим далее.
Присоединение к группе
?3-27 Если интерфейс имеет возможность передавать многоадресные сообщения, мы вызываем нашу функцию bind_mcast также для каждого адреса направленной передачи, поскольку мы хотим присоединиться к группе на каждом интерфейсе.
Связывание с универсальным адресом
29-32 После обработки всей информации об интерфейсах мы размещаем в памяти еще одну структуру адреса сокета и копируем в нее содержимое из структуры, заполненной нашей функцией udp client. Затем мы вызываем нашу функцию sock_set_wi 1 d, чтобы сохранить в этой структуре соответствующий универсальный адрес. Функция bind_ubcdst создает сокет и связывает его с универсальным адресом. Он обрабатывает дейтаграммы, предназначенные для адресов, с которыми мы не можем связаться, таких как 255.255.255.255.
Отправка запросов и чтение последующих ответов
13 34 Функция sntp_send посылает широковещательный запрос SNTP со всех интерфейсов, поддерживающих широковещательную передачу, и групповой запрос SNTP со всех интерфейсов, поддерживающих многоадресную передачу. Функция read_l оор затем будет считывать все ответы на эти запросы, а также все пакеты NTP широковещательной или многоадресной передачи, которые будут приняты впоследствии.
На рис. 19.7 показаны сокеты, которые будут созданы для нашего узла bsdi. Вспомните, что на рис. 1.7 этот узел действительно является маршрутизатором с двумя интерфейсами Ethernet. Мы показали интерфейсы и их IP-адреса направленной и широковещательной передачи в примерах, следующих за листингом 16.3, но теперь мы считаем, что дополнительные (альтернативные) адреса отсутствуют. Мы создадим девять сокетов и при помощи функции bind свяжем их с одним и тем же портом девять раз.
Чтобы различать связывание сокета с адресом направленной, широковещательной и многоадресной передачи, мы пометили восемь верхних сокетов симво-
匸|/0•寸сена匚①HrahxLrou°① dqE,d_匸|/0•寸сена匚①HrahxLrou°① dqE,d_Клиент SNTPу 夕 /р U 7P У 0 7PКлиент SNTPу 夕 /р U 7P У 0 7PПодсеть 206.62 226 64/27Подсеть 206.62 226 64/27Подсеть 206 62 226.32/27Подсеть 206 62 226.32/27
匸|/0•寸сена匚①HrahxLrou°① dqE,d_
匸|/0•寸сена匚①HrahxLrou°① dqE,d_
Клиент SNTP
у 夕 /р U 7P У 0 7P
Клиент SNTP
у 夕 /р U 7P У 0 7P
Подсеть 206.62 226 64/27
Подсеть 206.62 226 64/27
Подсеть 206 62 226.32/27
Подсеть 206 62 226.32/27
Рис. 19.7. Девять сокетов, созданных нашим клиентом SNTP на узле bsdi
лами 弘 й или т соответственно. Мы показываем IP-адреса получателей пакетов, которые будут получены па каждом сокете, и отмечаем, что сокет, связанный с адресом 0.0.0.0, изображенный справа, может получать пакеты, предназначенные для любого другого IP-адреса (часто 255.255.255.255), приходящие на любой интерфейс.
Наша функция Ы nd_ubcdst показана в листинге 19.15. Эта функция вызывается для всех адресов направленной передачи, всех широковещательных и всех универсальных адресов. Последний аргумент в трех вызовах листинга 19.14 будет равен 1 только для широковещательного адреса.
Листинг 19.15. Функция bind_ubcast: создание сокета для адреса направленной или широковещательной передачи
//sntp/bind_ubcast с
include "sntp h"
void
bind_ubcast(struct sockaddr *sabind socklen_t sdlen, int port.
mt alias, mt beast)
{
int i, fd
Листинг 19.15 (продолжение)
Setsockopt(fd. SOL_SOCKET SO_REUSEADDR. &on. sizeof(on)).
printfC'bindmg Zs\en". Sock_ntop(sabind. salen)).
if (b】nd(fd. sabind. salen) < 0) {
if (errno == EADORINUSE) {
pnntfC (addressalready in use)\en").
close(fd).
return.
} else
err_sys("bind error").
}—
addrs[naddrs] address - sabind; /* сохраняем указатель на структуру sockaddr{}
/~
addrs[naddrs] addr_sa1en 二 salen,
addrs[naddrs] addr[fd 』fd.
if (beast) ~
addrs[naddrs] addr_flags 二 ADDR_BCAST:
naddrs++.-
}
Определяем, связан ли адрес
7-11 Сначала мы проверяем, связан ли уже этот адрес. Для адреса направленной передачи этого не может произойти, но в том случае, когда несколько альтернативных адресов имеют общий широковещательный адрес, мы хотим связываться с широковещательным адресом только один раз.
Создание сокета и связывание с адресом
12-23 Мы создаем сокет UDP, задаем порт и устанавливаем параметр сокета S0_ REUSEADDR (поскольку мы связываемся с одним и тем же портом для каждого адреса). При помощи функции bind мы связываем адрес с сокетом. Структура адреса сокета, в которую мы записываем порт для функции bi nd, — это структура, заполненная и возвращенная функцией get_ifi_info. Мы допускаем неудачное выполнение функции bi nd, что возможно, только если демон NTP сам запущен на этом узле.
Сохранение информации об этом сокете
24-29 Информация хранится в нашей структуре Arrds, включая флаг, указывающий, является ли этот адрес широковещательным.
Для интерфейсов, поддерживающих многоадресную передачу, нам нужно создать сокет и связать его с известным портом, азатем присоединиться к группе на интерфейсе. Это делается с помощью нашей функции bi nd_mcast, показанной в листинге 19.16.
Листинг 19.16. Функция bind_mcast: присоединение к группе на интерфейсе //sntp/bind_mcast с
#inc】ud6”sntp h"
void
bmd_mcast(const char *ifname. SA *mcastsa. socklen_t salen. mt alias)
{__
#ifdef MCAST
mt fd.
struct sockaddr *msa.
if (alias)
return /* для каждого интерфейса допускается только одно присоединение к группе */
printf(*'joining 打 on ^s\en", Sock_ntop_host(mcastsa. salen). 】fname).
fd = Socket(rncastsa->sa_famly. SOCK_DGRAM. 0).
Setsockopt(fd. SOL_SOCKET. SO_REUSEADDR. &on. s】zeof(on)),
Bind(fd, mcastsa. salen).
Mcast_jo?n(fd. mcastsa. salen, тfname 0).
15161718192021221516171819202122addrs[naddrs] addrs[naddrs] addrstnaddrs] addrs[naddrs] addrsinaddrs] naddrs++.#endif}addrs[naddrs] addrs[naddrs] addrstnaddrs] addrs[naddrs] addrsinaddrs] naddrs++.#endif}addr_sa = mcastsa. addjsalen 二 salen.addr_ifname = ifname: addr_fd = fd.add匸flags - ADDR_MCAST,addr_sa = mcastsa. addjsalen 二 salen.addr_ifname = ifname: addr_fd = fd.add匸flags - ADDR_MCAST,/* сохраняем указатель, а не копию строки *//* сохраняем указатель, а не копию строки */
15
16
17
18
19
20
21
22
15
16
17
18
19
20
21
22
addrs[naddrs] addrs[naddrs] addrstnaddrs] addrs[naddrs] addrsinaddrs] naddrs++.
#endif
}
addrs[naddrs] addrs[naddrs] addrstnaddrs] addrs[naddrs] addrsinaddrs] naddrs++.
#endif
}
addr_sa = mcastsa. addjsalen 二 salen.
addr_ifname = ifname: addr_fd = fd.
add匸flags - ADDR_MCAST,
addr_sa = mcastsa. addjsalen 二 salen.
addr_ifname = ifname: addr_fd = fd.
add匸flags - ADDR_MCAST,
/* сохраняем указатель, а не копию строки */
/* сохраняем указатель, а не копию строки */
Создание сокета и связывание
8-13 Если этот адрес является альтернативным (дополнительным), мы немедленно возвращаемся, поскольку нам нужно присоединиться к группе только один раз для каждого интерфейса независимо от того, сколько у интерфейса имеется дополнительных адресов направленной передачи. Создается сокет, и группа и заранее известный порт связываются с этим сокетом.
Присоединение к группе и сохранение информации
14-20 Присоединение к группе происходит на интерфейсе. Мы сохраняем информацию в нашей структуре Addrs. Указатель mcastsa, сохраненный в элементе addr_sa, указывает на структуру адреса сокета, размещенную в памяти в результате вызова нашей функции udp_client в функции main. Элемент addr_sa для каждого интерфейса, поддерживающего многоадресную передачу, указывает на ту же структуру, но это нормально, поскольку структура никогда не изменяется. Указатель dddr_i fname указывает на строку имени интерфейса в структуре ifi_info, что также нормально, поскольку наша функция free_i f i_i nfo никогда не вызывается для освобождения этой области памяти.
Следующая функция, sntp_send, представленная в листинге 19.17, вызывается функцией main после того, как вся информация об интерфейсах обработана и все сокеты созданы.
Листинг 19.17. Функция sntp_send: широковещательные и многоадресные запросы SNTP
//sntp/sntp_send с
include "sntp h"
void
sntp_send(void),
{-
mt fd.продолжение 夕
Листинг 19_17 (продолжение)
Addrs *aptr
struct ntpdata msg
/* используем для отправки сокет связанный с адресом ООО 0/123 */
fd = addrs[naddrs - 1] addr_fd
Setsockopt(fd SOLJOCKET SO_BROADCAST &on sizeof(on))
bzero(&msg sizeof(msg))
msg status = (0 « 6) | (3 << 3) | MODE_CLIENT /* cm RFC 2030 */
for (aptr = &addrs[0] aptr < &addrs[naddrs] aptr++) {
if (aptr->addr_flags & ADDR BCAST) {
printfC sending broadcast to ^s\en
Sock_ntop(aptr >addr__sa 叩tr >addr_salen))
Sendto(fd &msg sizeof(msg) 0
aptr >addr_sa aptr->addr_salen)
}—
#1fdef MCAST
if (aptr >addr_flags & ADDR_MCAST) {
/* сначала нужно соответствующим образом установить интерфейс для исходящих
пакетов */
Mcast_set_if(fd aptr >addr_ifname 0)
Mcdst_set_loop(fd 0)/* отключаем закольцовку */
prmtf( sending multicast to交on ^s\en
Sock_ntop(aptr >addr_sa aptr >addr_salen)
aptr >addr_ifname)
Sendto(fd &msg sizeof(msg)0
叩tr >addr_sa aptr >addr_sa]en)
}—
#endif
}
}
Установка параметра сокета SO_BROADCAST и формирование запроса
8 12 Вмест о создания специального сокета для отправки мы используем для вызовов функции sendto сокет, связанный с универсальным адресом. Поскольку IP- адрес задан через символы подстановки, ядро использует в качестве IP-адреса отправителя дейтаграммы UDP первичный адрес направленной передачи исхо- дящего интерфейса Сначала мы устанавливаем параметр сокета SO_BROADCAST Затем мы формируем запрос SNTP обнуляем LI (leap indicator — индикатор прм- ращения), задаем версию 3 и режим MO0E_CLIENT В запросе клиента нужно устанавливать только эти поля
Отправка широковещательного запроса
14 19 Если адрес является широковещательным адресом, запрос отправляется соответствующим образом
Отправка группового запроса
21 30 Если адрес является адресом многоадресной передачи, мы сначала задаем интерфейс для исходящих групповых сообщений на сокете (наша функция mcast_
set_i f), а затем отключаем на этом сокете закольцовку многоадресной передачи (наша функция mcast_set_1 оор). Если мы не отключим закольцовку, мы можем получить множество копий пакета на принимающих сокетах (см. упражнение 19 И) Дейтаграмма отправляется на адрес многоадресной передачи
ПРИМЕЧАНИЕ
Этот код иллюстрирует типичную парадигму направленной передачи, что мы можем обобщить так
for (каждый интерфейс) {
mcast_set_】f( )
sendto(-)
)
Дейтаграмма отправляется с каждого интерфейса, таким образом, исходящий интерфейс должен быть установлен с использованием параметра сокета перед каждым вызовом функции send to Это включает дополнительный системный вызов для каждой исходящей дейтаграммы Альтернативой может быть создание одного отправляюще! о сокета для каждого интерфейса и однократная установка исходящего интерфейса для каждого сокета, выполняемая после создания сокета В разделе 20 8 мы увидим, что IPv6 допускает использование вспомогательн ых данных при вызове функции send msg для задания исходящего интерфейса, что сокращает число системных вызовов в этом сценарии
Существует также класс приложений многоадресной передачи, которые не заботятся об исходящем интерфейсе, отправляя за один раз только одну дейтаграмму и по ^воляя ядру выбирагь исходящий интерфейс
Для ситуации, изображенной на рис 19 7, функцией sntp_send будут отправлены пять дейтаграмм, по одной для каждого широковещательного и многоадресного сокета Через сокеты направленной передачи ничего не отправляется
Следующей будет расмотрена функция「ead_l оор Ее первая половина находится в листинге 19 18 Эта функция вызывается в конце функции main и только считывает из созданных сокетов любые широковещательные, групповые или направленные пакеты NTP, появляющиеся на любом из интерфейсов узла Мы предполагаем, что большинство получаемых пакетов NTP будут широковещательными или групповыми, но ответы на запросы, отправляемые функцией sntp_send, будут направленными
Листинг 19.18. Функция read_loop первая часть
2
3
static int static mt
check_loop(struct sockaddr * check_dup(socklen_t)
4
static char
bufl[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 mt -
currb 二 0 lastb - 1
9
void
10
read loop(void)
И
{ -
12
int nsel
maxfd
//sntp/readjоор с
1 #】nclude sntp h
socklen_jt)f
Листинг 19.18 (продолжение)
Add” *aptr,
fd_set rset. allrset:
socklen_t len:
struct timeval now:
/* размещаем в памяти две структуры адреса сокета */
from[0] = Malloc(addrs[0].addr^salen);
from[l] = Malloc(addrs[0].addr_salen):
maxfd 二-1.
for (aptr = &addrs[0]. aptr < &addrs[naddrs]: aptr++) {
FO_SET(apt「->addr_fd. &a11rset):
if (aptr->addr_fd > maxfd)
maxfd ■ aptr->addr fd;
}-
Выделение множества буферов и структур адресов сокетов
4-19 Мы выделяем два буфера, в которые получаем дейтаграммы, две структуры адресов сокетов, в которых хранятся соответствующие адреса отправителей, и две переменные, хранящие размеры дейтаграмм. Индекс, соответствующий текущей дейтаграмме, — currb, а индекс последней дейтаграммы — 1 astb. Один индекс будет иметь значение 0, другой — значение 1. Следует предусмотреть место для двух дейтаграмм, поскольку мы будем получать множество копий дейтаграмм многоадресной передачи и нам необходима возможность обнаруживать эти копии и игнорировать их. Даже на узле, имеющем только один интерфейс, пакет многоадресной передачи NTP может быть получен на сокет, связанный с этим интерфейсом, 〃 на сокет, связанный с универсальным адресом (см. упражнение 19.10).
Множественные копии появляются в данном примере многоадресной передачи, потому что мы связались с одним и тем же портом несколько раз: по одному разу для интерфейсов и еще раз — для универсального адреса.
Вспомните из нашего обсуждения параметра сокета SO_REUSEADDR в главе 7, что одна копия дейтаграммы многоадресной или широковещательной передачи доставляется на каждый подходящий сокет, в то время как дейтаграмма направленной передачи доставляется только на один сокет. Наш сокет, связанный с универсальным адресом, является «подходящим» для каждого полученного группового пакета.
Если нам не обязательно знать, для какого адреса был предназначен данный пакет многоадресной передачи, мы можем просто создать один сокет и связать его с адресом многоадресной передачи (224.0.1.1) и известным портом (123), в результате чего мы всегда будем получать только одну копию. Так мы поступили в нашем примере в листинге 19.4. Этот пример SNTP несколько сложнее за счет того, что мы хотим знать адрес получателя каждой дейтаграммы.
Подготовка набора дескрипторов для функции select
20-25 Мы подготавливаем набор дескрипторов для функции select и вычисляем максимальный из дескрипторов, из которого можем читать.
0123456789 012343333333333 444440123456789 012343333333333 444445 6 7 8 9 0 1234567894 4 4 4 4 5 5558555555 6 7 8 9 0 1234567894 4 4 4 4 5 5558555550 12 3 4 56 6 6 6 6 60 12 3 4 56 6 6 6 6 6
0123456789 01234
3333333333 44444
0123456789 01234
3333333333 44444
5 6 7 8 9 0 123456789
4 4 4 4 4 5 555855555
5 6 7 8 9 0 123456789
4 4 4 4 4 5 555855555
0 12 3 4 5
6 6 6 6 6 6
0 12 3 4 5
6 6 6 6 6 6
Вторая часть нашей функции read_loop показана в листинге 19.19. Она вызывает функцию select, которая ждет, когда один или более дескрипторов будут готовы для чтения, затем считывает дейтаграмму и обрабатывает пакет NTP.
Листинг 19.19. Функция readjoop: вторая часть
//sntp/read_loop.c
for (.;) {
rset = allrset:
nsel - Select(maxfd + 1. &rset. NULL. NULL. NULL).
Gettimeofday(&now. NULL). /* узнаем время возвращения функиии select */
for (aptr - &addrs[0]. aptr < &addrs[naddrs], aptr++) { if (FD_ISSET(aptr->addr_fd. &rset)) {
len • aptr->addr_salen.
nread[currb] * recvfrom(aptr->addr_fd.
buf[currb]. MAXLINE. 0.-
from[currb]. &len):
if (aptr->addr_flags & ADDR_MCAST) {
printf("^d bytes from Xs"7 nread[currb], Sock__r)top(from[currb]. len)):
printf("^multicast to 纪二 aptr->addr_1 fname):
} else if (aptr->addr_flags & ADDR_BCAST) { printf("Xd bytes from Is". nread[currb].
Sock_ntop(from[currb]. len)):
p门ntf("-broadcast to 况",
Sock_ntop(aptr->addr_sa. len)):
} else {
printf(”d bytes from Xs'*. nread[currb], Sock_ntop(from[currb]. len)):
printf("-to *s".
Sock_ntop(aptr->addr_sa, len));
】f (check_loop(from[currb]. len)) {
printf(" (ignored)\en"):
continue./* это наш пакет, который вернулся назад через закольцовку */
}
If (check_dup(len)) {
pnntfF (dup)\enM);
continue; /* это дубликат */
)
sntp_proc(buf[lastb]. nread[lastb], &now):
】f (--nse】<-0)
break:/* работа с функцией select закончена */
)
Функция select и получение текущего времени и даты
27-29 Как только функция select возвращает управление, мы вызываем функцию gettweofday, чтобы получить текущее время, которое будет использовано для вычисления разности со временем пакета NTP.
Определение, какой из дескрипторов готов для чтения
Мы проверяем каждый дескриптор, чтобы определить, какой из ннх готов для чтения, вызываем функцию 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 sock1er 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, Функция check_dup: возвращает 1, если дейтаграмма является дублированной^
//sntp/read_loop с
76 mt
77 check_dup(sock1en_t salen)
78 {
79 mt temp,
80 if (nread[currb] == nread[lastb] &&
81 memcmpCfromEcurrb]. from[lastb] sdlen) — 0 &&
82 memcmp(buf[currbj. buf[lastb] nread[currb]) -- 0) {
83 return (1)./* это дубликат */
84}
85 temp = currb/* меняем currb и lastb */
86currb = lastb.
87lastb = temp,
88 return (0).
89 }
Проверка длины, адреса отправителя и содержимого дейтаграммы
80-88 Мы считаем одну дейтаграмму копией другой, если у этих дейтаграмм совпадают длина, адреса протокола отправителя п действительное содержимое. Если дейтаграмма не дублирована, индексы curbb и 1 astb меняют свои значения. Обратите внимание, что вызов функции sntp_proc в конце листинга 19.19 передает дейтаграмму с индексом lastb, поскольку при вызове функции check_dup индексы меняются для следующего вызова функции reevfrom.
Вспомните девять сокетов, изображенных на рис. 19.7. В листинге 19.22 показан результат запуска программы на нашем узле bsdi с сервером NTP, запущенным на узле solans. Первые девять строк показывают созданные сокеты, IP-адреса, связанные с сокетами, и интерфейсы, на которых происходит присоединение к группе.
Следующие пять строк показывают паке гы, отправленные функцией sntp_send: по одному пакету на каждый широковещательный адрес и по одному — на каждый адрес многоадресной передачи.
В следующих шести строках показаны первые пять дейтаграмм, полученных на различных сокетах. Эта дейтаграммы приходят в ответ на запросы, отправленные функцией sntp_send. Первая дейтаграмма игнорируется, поскольку она является прошедшей через закольцовку копией одного из широковещательных запросов, которьп! мы отправили (посмотрите на адрес отправителя). Вторая дейтаграмма пришла с сервера NTP (режим 4 — это MODE_SERVER в листинге 19.10) на узле solans (206.62.26.33), который является версией 3 NTP-сервера, работающего на слое 3. Разница во времени между двумя узлами составляет около 116 миллисекунд. Следующие гри дейтаграммы игнорируются: первая является копией (пришедшей через закольцовку) широковещательной дейтаграммы, отправленной в другую есть Ethernet, а две других — это копни наших широковещательных дейтаграмм, полученных на сокет, связанный с универсальным адресом, которые также вернулись благодаря наличию закольцовки. Вот что произошло с двумя отправленными широковещательными дейтаграммами: по одной копии каждой из них оказалось в закольцовке, а затем одна копия каждого пакета, пришедшего через закольцовку, была доставлена на сокет, связанный с соответствующим широковещательным адресом, а другая —- на сокет, связанный с универсальным адресом. Двумя широковещательными пакетами были сгенерированы четыре копии, пришедшие через закольцовку. В случае многоадресной передачи мы можем отключить закольцовку, но для широковещательной передачи это невозможно.
В последних пяти строках представлена информация о четырех полученных дейтаграммах, соответствующих одному полученному от узла solans пакету NTP. Первая дейтаграмма получается как групповая и обрабатывается для вычисления разницы во времени — около 117 миллисекунд. Следующие три дейтаграммы являются копиями этого пакета многоадресной передачи, полученными на двух других сокетах, связанных с одним и тем же IP-адресом и портом (224.0.1.1 123), и на сокете, связанном с тем же портом (0.0.0.0.123).
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
binding 0 0 0 0 123
loO
bsdi # sntp 224 0.1.1bsdi # sntp 224 0.1.1Листинг 19.22. Вывод нашей программы sntp
bsdi # sntp 224 0.1.1
bsdi # sntp 224 0.1.1
направленная дейтаграннд 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 48 bytes v3 mode 48 bytes 48 bytes48 bytes48 bytes v3 mode 48 bytes 48 bytes 48 bytes48 bytes 48 bytes v3 mode 48 bytes 48 bytes48 bytes48 bytes v3 mode 48 bytes 48 bytes 48 bytesfrom 206 62 226 66 123 broadcast to 206 62 226.95.123 (Ignored) from 206 62 226 33 123 to 206 62 226 35 123*
48 bytes 48 bytes v3 mode 48 bytes 48 bytes
48 bytes
48 bytes v3 mode 48 bytes 48 bytes 48 bytes
48 bytes 48 bytes v3 mode 48 bytes 48 bytes
48 bytes
48 bytes v3 mode 48 bytes 48 bytes 48 bytes
4 strat 3 clock difference = -116013 usee
from 206 62 226 35 123 broadcast to 206 62 226 63.123 (ignored) from 206 62 226 66 123 to 0 0 0 0 123 (ignored)
from 206 62 226 35 123 to 0 0 0 0 123 (ignored)
from 206 62 226 33 123 multicast to weO
5 strat 3 clock difference = -117043 usee from 206 62 226 33 123 multicast to efO (dup) from 206 62 226 33 123 multicast to 1。0 (dup) from 206 62 226 33 123 to 0 0 0 0 123 (dup)
Если мы продолжим выполнение программы в этой же среде, мы увидим, что каждые 64 секунды сервер NTP на узле solans передает с помощью многоадресной передачи пакет NTP, и наша программа получает четыре копии. Первая из четырех копий обрабатывается, а остальные три — это дубликаты, которые игно
рируются.
Резюме
Для запуска приложения многоадресной передачи в первую очередь требуется присоединиться к группе, заданной для этого приложения. Тем самым уровень IP получает указание присоединиться к группе, что, в свою очередь, указывает канальному уровню на необходимость получать кадры многоадресной передачи, отправляемые на соответствующий адрес многоадресной передачи аппаратного уровня. Многоадресная передача использует преимущество аппаратной фильтрации, имеющееся у большинства интерфейсных карт, и чем качественнее филь-
трация, тем меньше число нежелательных получаемых пакетов. Использование аппаратной фильтрации сокращает нагрузку на все узлы, не задействованные в приложении.
Многоадресная передача в глобальной сети требует наличия маршрутизаторов, поддерживающих многоадресную передачу, и протокола маршрутизации многоадресной передачи. Поскольку не все маршрутизаторы в Интернете имеют возможность многоадресной передачи, для этой цели используется виртуальная сеть МВопе (см. раздел Б.2).
API для многоадресной передачи обеспечивают пять параметров сокетов: присоединение к группе на интерфейсе;
& выход из группы;
>< установка интерфейса по умолчанию для исходящих пакетов многоадресной передачи;
會 установка значения TTL или предельного количества транзитных узлов для исходящих пакетов многоадресной передачи;
к включение или отключение закольцовки для пакетов многоадресной передачи.
Первые два параметра предназначены для получения пакетов многоадресной передачи, последние три — для отправки. Существует достаточно большая разница между указанными параметрами сокетов IPv4 и IPv6. Вследствие этого код многоадресной передачи, зависящий от протокола, очень быстро становится «замусорен» директивами #1 fdef. Мы разработали восемь наших собственных функций с именами, начинающимися с mcast_, для упрощения написания приложений многоадресной передачи, работающих как с IPv4, так и с IPv6.
Упражнения
Скомпилируйте программу, показанную в листинге 18.5, и запустите ее, задав в командной строке IP-адрес 224.0.0.1. Что произойдет?
Измените программу из предыдущего примера, чтобы связать IP-адрес 224.0.0.1 и порт 0 с сокетом. Запустите ее. Разрешаеася ли вам связывать адрес многоадресной передачи с сокетом при помощи функции bi nd? Если у вас есть такая программа, как tcpdump, понаблюдайте за пактами в сети. Каков IP-адрес отправителя посылаемой вами дейтаграммы?
Один из способов определить, какие узлы в вашей подсети имеют возможность многоадресной передачи, заключается в запуске утилиты ping для группы всех узлов, то есть для адреса 224.0.0.1. Попробуйте это сделать.
Если мы введем команду pi ng 224 0 0 1 на нашем узле um xware, который имеет возможность многоадресной передачи, мы получим следующий вывод: umxware % ping 224.0.0.1
PING 224 0 0 1 56 databytes
64 bytes from gw konalacom (20662 226 62)icmp_seq-0time-0ms
64 bytes from gw kohalacom (20662 226 62)icmp[seq=ltime=0ms
64 bytes from gw kohalacom (20662 226 62)icmp 二seq=2time=0ms
Что происходит?
Одним из способов обнаружения маршрутизаторов многоадресной передачи в вашей подсети является запуск утилиты ртпд для группы всех маршрутизаторов —224.0.0.2. Попробуйте это сделать.
Один иэ способов узнать, соединен ли ваш узел с виртуальной сетью МВопе,— запустить нашу программу из раздела 19.8, подождать несколько минут и посмотреть, появляются ли анонсы сеанса. Попробуйте сделать это и посмотрите, получите ли вы какие-нибудь анонсы.
Идентификатор сеанса и версия в строке о= в листинге 19 6 часто являются отметками времени NTP. Имеют ли показанные значения какой-либо смысл?
Выполните вычисления в листинге 19.12, когда дробная част ь о гметки времени NTP равна 1 073 741 824 (одна четвертая от 232).
Выполните еще раз эти же вычисления для максимально возможной дробной части (2 " - 1).
В листингах 19.15и 19.16 мы установили параметр сокета SOREUSEADDR, чтобы позволить связывать один и тот же порт несколько раз. Но в листинге 19.16 мы выполняем полностью дублированное связывание для каждого адреса многоадресной передачи (224.0.1.1) и порта 123, и три из них мы видим в листинге 19.22. Как мы можем выполнить это для Беркли-ядра, не устанавливая параметр сокета SO^REUSEPORT вместо SO__REUSEADDR?
Последняя строка в листинге 19.22 показывает дейтаграмму многоадресной передачи, получаемую сокетом, связанным с универсальным адресом. Но если мы запустим наш клиент SNTP в Solaris 2.5, этот сокет не будет получать дейтаграмм многоадресной передачи. Почему?
Сколько дополнительных копий мы получили бы в примере, приведенном в листинге 19.22, если бы в листинге 19.17 мы не отключили закольцовку для многоадресной передачи?
Измените реализацию функции mcast_set_if для IPv4 так, чтобы запоминать имя каждого интерфейса, для которого она получает IP-адрес. Это позволит избежать нового вызова функции ioctl для данного интерфейса.
ГЛАВА 20
Дополнительные сведения о сокетах UDP
Введение
Эта глава объединяет различные темы, которые затрагиваются приложениями, использующими сокеты UDP. Для начала нас интересует, как определяется адрес получателя дейтаграммы UDP и интерфейс, на котором дейтаграмма была получена, поскольку сокет, связанный с портом UDP и универсальным адресом, может получать дейтаграммы направленной, широковещательной и многоадресной передачи на любом интерфейсе.
TCP — это потоковый протокол, использующий окно переменной величины {sliding window), поэтому в TCP отсутствуют такие понятия, как граница записи или переполнение буфера получателя отправителем в результате передачи слишком большого количества данных. Однако в случае UDP каждой операции ввода соответствует дейтаграмма UDP (запись), поэтому возникает вопрос: что произойдет, когда полученная дейтаграмма окажется больше приемного буфера приложения?
UDP — это ненадежный протокол, однако существуют приложения, в которых UDP целесообразно использовать вместо TCP. Мы рассмотрим факторы, под влиянием которых UDP оказывается предпочтительнее TCP. В UDP-приложения необходимо включать ряд свойств, в некоторой степени компенсирующих ненадежность UDP: тайм-аут и повторную передачу, обработку потерянных дейтаграмм и порядковые номера для сопоставления ответов запросам. Мы разработаем набор функций, которые сможем вызывать из наших приложений UDP для реализации перечисленных свойств.
Когда реализация не поддерживает параметр сокета IP_RECMDSTADDR, один из способов определить IP-адрес получателя UDP-дейтаграммы заключается в связывании всех интерфейсных адресов и использовании функции select. Подобный пример мы показали в разделе 19.11, и в этой главе мы еще немного поработаем с этой технологией.
Большинство серверов UDP являются последовательными, но существуют приложения, обменивающиеся множеством дейтаграмм UDP между клиентом н сервером, что требует определенного согласования. Примером может служить TFTP (Trivial File Transfer Protocol — простейший протокол передачи файлов). Мы рассмотрим два варианта подобного согласования — с использованием суперсервера inetd и без него.
В завершение этой главы мы рассмотрим информацию о пакете, которая может быть передана во вспомогательных данных для дейтаграммы IPv6: IP-адрес отправителя, отправляющий интерфейс, предельное количество транзитных узлов исходящих дейтаграмм и адрес следующего транзитного узла. Аналогичная информация — IP-адрес получателя, принимающий интерфейс и предельное количество транзитных узлов входящих дейтаграмм — может быть получена вместе с дейтаграммой IPv6.
Получение флагов, IP-адреса получателя и индекса интерфейса
Исторически функции sendmsg и recvmsg использовались только для передачи дескрипторов через доменные сокеты Unix (см. раздел 14.7), но даже это происходило сравнительно редко. Однако в настоящее время популярность этих двух функций растет по двум причинам:
Элемент msg_f 1 ags, добавленный в структуру msghdr в реализации 4.3BSD Reno, возвращает приложению флаги. Эти флаги мы перечислили в табл. 13.2.
Вспомогательные данные используются для передачи все большего количества информации между приложением и ядром. В главе 24 мы увидим, что в IPv6 эта тенденция продолжается.
В качестве примера функции recvmsg мы напишем функцию recvfrom_flags, аналогичную функции recvfrom, но дополнительно позволяющую получить:
возвращаемое значение msg_f〕ags;
^ адрес получателя полученной дейтаграммы (из параметра сокета IP_RECVDST - ADDR);”-
幾 индекс интерфейса, на котором была получена дейтаграмма (параметр сокета IP_RECVIF)・
Чтобы можно было получить два последних элемента, мы определяем в нашем заголовке unp. h следующую структуру:
struct in_pktinfo {
struct 】n_addr ipi_addr:/* IPv4-адрес получателя */
mtipi_ifindex. /* индекс интерфейса. на котором была получена дейтаграмма */
}. 一
Мы выбрали имена структуры и ее элементов так, чтобы сохранить сходство со структурой IPv61 n6_pkt 1 nfo, возвращающей те же два элемента для сокета IPv6 (см. раздел 20.8). Наша функция recvfrom_flags будет получать в качестве аргумента указатель на структуру in_pktinf。, и если этот указатель ие нулевой, возвращать структуру через указатель.
Проблема формирования этой структуры состоит в том, что неясно, что возвращать, если недоступна информация, которая должна быть получена из параметра сокета IP^RECVDSTADDR (то есть реализация не поддерживает данный параметр сокета). Обработать индекс интерфейса легко, поскольку нулевое значение соответствует неизвестному индексу. Но для IP-адреса все 32-разрядные значения являются действительными. Мы выбрали такое решение: адрес получателя 0.0 0 0 возвращается в том случае, когда действительное значение недоступно.
Хотя это реальный IP-адрес, использовать его в качестве IP-адреса получателя не разрешается (RFC 1122 [9]). Он будет действителен только в качестве IP-адреса отправителя во время начальной загрузки узла, когда узел еще не знает своего IP-адреса.
ПРИМЕЧАНИЕ
К сожалению, Беркли-ядра iфинимают дейтаграммы, iфедназпачеиныс для адреса 0.0.0.0 [105, с. 218-219]. Это устаревшие адреса широковещательной передачи, генерируемые ядрами 4.2BSD.
Первая часть нашей функции recvfrom_fl ags представлена в листинге 20.11. Эта функция предназначена для использования с сокетом UDP.
Листинг 20.1. Функция recvfromjlags: вызов функции reevmsg
//advio/reevfromf1ags c
include "unp h"
^include <sys/param h> /* макрос ALIGN для макроса CMSG_NXTHDR() */
#ifdef HAVE_SOCKADDR_DL_STRUCT"
#ind ude <net/if_dT h>
#endif
ssize_t
recvfrom_flags(int fd. void *ptr. size_t nbytes. int *flagsp,
~SA *sa, socklen_t *salenptr. struct in_pktinfb *pktp)
{__
struct msghdr msg,
struct lovec iov[l],
ssize_t n.
#ifdef -HAVE_MSGHDR_MSG_CONTROL
struct emsghdr *cmptr.
union {
struct emsghdr cm.
char control[CMSG_SPACE(sizeof(struct in_addr))
CMSG_SPACE(sizeof(struct in_pktinfo))]:
} control_un:
msg.msg_control - control_un control;
msg.msg二controller! - sizeof(control_un.control);
msg msg^f1ags - 0:
#else ~
24 bzero(&msg. sizeof(msg)); /* убеждаемся, что m3g_accrightslen - 0 */
#end\f
msg.msg_name ■ sa.
msg msg_namelen - *salenptr,
iOv[0].Tov_base = ptr;
iov[0].iovjen = nbytes:
msg msg_iov = iov.
msg msg_】ovlen = 1:
if ( (n = 「ecvmsg(fd. &msg. *flagsp)) < 0)
return (n).
*salenptr = msg msg_namelen. /* передаем обратно результаты */ продолжение 按
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http'// www piter.com/ download.{Р
Листинг 20.1 (продолжение)
if (pktp)
bzero(pktp sizeof(struct in_pktmfo)), /* 0 0 0 0 индекс интерфейса - 0 */
Подключаемые файлы
2-5 Использоваиие макроопределения CMSG_NXTHDR требует подключения заголовоч- него файла <sys/param h>. Нам также нужно подключить заголовочный файл <net/ 1 f_dl h>, определяющий структуру sockaddr_dl, в которой возвращается полученный индекс интерфейса.
Аргументы функции
6-8 Аргументы функции аналогичны аргументам функции rev from за исключением того, что четвертый аргуменг является указателем на целочисленный флаг (так что мы можем возвратить флаги, возвращаемые функцией reevmsg), а седьмой аргумент новый: это указатель на структуру in_pktinfo, содержащую IPv4-a〃pec получателя пришедшей дейтаграммы и индекс интерфейса, на котором дейта・ грамма была получена.
Различия реализаций
13-25 При работе со структурой msghdr и различными константами MSGjtxr мы встречаемся со множеством различий в реализациях. Одним из вариантов обработки таких различий может быть использование имеющейся в языке С возможности условного подключения (директива #i fdef). Если реализация поддерживает элемент msg_control, то выделяется пространство для храпения значений, возвращаемых параметрами сокета IP_RECVDSTADDR и IP_RECVIF, н соотвегствующие элементы инициализируются.
Заполнение структуры msghdr и вызов функции reevmsg
26-36 Заполняется структура msghdr, и вызывается функция reevmsg. Значения элементов msg_namelen и msg_flags должны быть переданы обратно вызывающему процессу. Они являются аргументами типа «значение-результат». Мы также инициализируем структуру вызывающего процесса in_pktinfo, устанавливая IP-адрес 0.0.0.0 и нулевой индекс интерфейса.
В листинге 20.2 показана вторая часть нашей функции.
Листинг 20.2. Функция recvtromjlags: возвращаемые флаги и адрес получателя
#ifndef HAVE_MSGHDR_MSG_CONTROL
*flagsp - 0,/* передаем результаты обратно */
return (п).
#else
*flagsp - msg msg_flags. /* передаем результаты обратно */
if (msg msg_controllen < sizeofCstruct emsghdr) ||
(msg msg_f 1sgs & MSG^CTRUNC) || pktp — NULL)
return (n).—
for (emptr - CMSG_FIRSTHDR(&msg). emptr J NULL.
emptr - CMSG_NXTHDR(&msg. emptr)) {
#ifdef IP—RECVDSTADDR
if (cmptr->cmsg_level — IPPROTO_IP &&
cmptг->cmsg_tyре == IP_RECVDSTADDR) {
50memcpy(&pktp->ipi_addr CMSG_DATA(cmptr)
51sizeof(struct 】n_addr))
52continue
53}
54 #endif
55 #ifdef IP_RECVIF
56 if (cmptr->cmsg_level == IPPROTO_IP &&
57cmptr->cmsg_type == IP_RECVIF) {
58struct sockaddr_dl *sdT
59 sdl = (struct sockaddr_dl *) CMSG_DATA(cmptr):
60pktp->ipi_ifindex = sdT->sdl_mdex. '
61continue
62}
63 #endif
64err_quit('unknown ancillary data len =第d level = Zd type ■ ,d”.
65"cmptr->cmsgJen cmptr >cmsg_level cmptr->cmsg一type)
66}一一
67 return (n)
68 #endif /* HAVE_MSGHDR_MSG_CDNTROL */
69 }一—
Если реализация не поддерживав г элемента msg_control, мы просто обнуляем возвращаемые флаги и завершаем функцию. Оставшаяся часть функции обрабатывает информацию, содержащуюся в структуре msg_control.
Возвращение при отсутствии управляющей информации
41-44 Мы возвращаем значение msg_flags п возвращаемся в вызывающую функцию в том случае, если или пет никакой управляющей информации, или управляющая информация была обрезана, или вызывающий процесс не требует возвращения структуры m^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. Возвращаемая нами информация — это индекс интерфейса.
cmsg_len
cmsg_level
cmsgtype
len
fam
index
len
fam
alen|slen
sockaddr_dl{}sockaddr_dl{}cmsghdr{}
sockaddr_dl{}
sockaddr_dl{}
20
IPPROTO_IP IP—RECU工F 8,AF_L工NK 工FT—NONE,0,0,0
Рис. 20.1. Объект вспомогательных данных, возвращаемый для параметра IP_RECVIF
Пример: вывод IP-адреса получателя и уведомления о том, что дейтаграмма обрезана
Для проверки нашей функции мы изменим функцию dg_echo (см. листинг 8.2) так, чтобы она вызывала функцию recvfrom_flags вместо функции recvfrom. Новая версия функции dg_echo показана в листинге 20.3.
Листинг 20.3. Функция dg echo, вызывающая нашу функцию recvfrom_flags
//advio/dgechoaddr с
include*unpifi h"
#undef MAXLINE
#define MAXLINE 20 /* устанавливаем новое значение чтобы пронаблюдать за эффектом */
void
dg_echo(int sockfd SA *pcliaddr. socklen_t clilen)
{一一
wt flags,
const int on = 1
socklen_t len.
ssize_t n
char - mesg[MAXLINE]. str[INET6_ADDRSTRLEN]. ifname[IFNAMSIZ]:
struct 】n_addr w_zero.
struct in_pktinfo pktinfo.
#ifdef IP_RECVDSTADDR
if (setsockopt(sockfd IPPROTOJP. IP^RECVDSTADDR. &on sizeof(on)) < 0)
err_ret("setsockopt of IP_rEcvdsTA5dr”).
#endif
#ifdef IP_RECVIF
if (setsockoot(sockfd IPPROTO_IP. IP_RECVIF. &on sizeof(on)) < 0)
err_ret("setsockopt of IP_RECVIFM)
#endif
bzero(&in_zero sizeof(struct in_addr)). /* 1Ру4-адрес состоящий из одних нулей */
for (.,) {
len = clilen
flags = 0
n = Recvfrom_f1ags(sockfd mesg MAXLINE &flags.
pcliaddr &len &pktinfo)
printf(^d-byte datagram from %s'. n. Sock_ntop(pchaddr, len));
if (memcmp(&pktinfo ipi_addr. &in_zero. sizeof(in_zero)) 〔= 0)
prwtfC to 犯"Inet_ntop(AF_INET, &pktmfo*i pi_addr.
str sizeof(str)))
if (pktinfo ipi_ifindex > 0)
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
pnntf(". reev i/f = Zs,.
If_indextoname(pktinfo ipi_i fmdex, ifname)),
#ifdef MSG_TRUNC-
if (flags & MSG_TRUNC)
prmtfC (datagram truncated)")
#endif
#ifdef MSG_CTRUNC
if (flags & MSG_CTRUNC)
printfC* (control info truncated)'1):
#endif
#ifdef MSG—BCAST
if (flags & MSG—BCAST)
printfC* (broadcast)")
#endif
#ifdef MSG_MCAST
if (flags & MSG—MCAST)
pnntfC' (multicast)").
#endif
p门ntf("\en”)
52
53
54
Sendto(sockfd. mesg, n 0 pcliaddr. len): }
}
Изменение MAXLINE
2-3 Мы удаляем существующее определение MAXLINE, имеющееся в пашем заголовочном файле unp h, и задаем новое значение — 20. Это позволит нам увидеть, что произойдет» когда мы получим дейта! рамму (JDP, превосходящую размер буфера, заданный для функции ввода (в данном случае функции recvmsg).
Установка параметров сокета IP_RECVDSTADDR и IP_RECVIF
14-21 Если определен параметр сокета IP_RECVDSTADDR, он включается. Аналогично включается параметр сокета IP_RECVIF.
ПРИМЕЧАНИЕ
К сожалению, проверок подобного рода недостаточно, потому что некоторые системы (например, Solans 2 5) определяют параметр сокета IP RECVDSTADDR, даже если он не иоддерживае*!ся. Следовательно, мы допускаем возможность неудачного вызова функции setsockopt, и если зто происходит, то просто выводим некоторое сообщение и продолжаем работу Мы даже не можем точно определить, что произошла именно эта ошибка, поскольку различные реализации возвращают для неизвестного парамегра сокета различные ошибки. Например, в 4 4BSD функция getsockopt возвращает ошибку ENOPROTOPORT, но функция setsockopt возвращает EINVAL, а для неизвестного параметра сокета многоадресной передачи возвращается ошибка EOPNOTSUPP
Чтение дейтаграммы, вывод IP-адреса отправителя и порта
24-28 Дейтаграмма читается с помощью вызова функции recvfrom_fl ags. IP-адрес отправителя и порт ответа сервера преобразуются в формат представления функцией sock_ntop.,
Вывод IP-адреса получателя
29-31 Если возвращаемый IP-адрес ненулевой, он преобразуется в формат представления функцией inet_ntop и выводится.
Вывод имени интерфейса, на котором была получена дейтаграмма
32-34 Если индекс интерфейса ненулевой, его имя будет получено при помощи вызова функции 1 f_i ndextoname и выведено.
Проверка различных флагов
35-51 Мы проверяем четыре дополнительных флага и выводим сообщение, если какие-либо из них установлены.
Если мы запустим наш сервер под BSD/OS 3.0 на узле bsdT (который имеет несколько сетевых интерфейсов), то увидим различные 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 41178 to 206 62 226 66 recv i/f = efO
Для удобства чтения мы поместили строки со значениями флагов в скобки. Чтобы сгенерировать эти пять строк вывода, мы запустили нашу программу sock (см. раздел В.З) на различных узлах.
Первая строка выводится от узла solans и предназначена для адреса 206.62.226.35 — одного из адресов направленной передачи узла сервера.
Вторая строка соответствует узлу laptop и предназначена для адреса 206.62.226.95 — это широковещательный адрес Ethernet, используемый совместно клиентом и сервером (см. рис. 1.7). Интерфейс отличается от указанного в первой строке, как мы и предполагали. Наш сервер получает широковещательное сообщение, поэтому установлен флаг MSG_BCAST, указывающий на то, что дейтаграмма была получена как широковещательное сообщение на канальном уровне.
Третья строка выводится от узла solans и предназначена для адреса 224.0.0.1 — адреса многоадресной передачи всехузлов (all-hostsgroup). В эту группу должны входить все узлы подсети, имеющие возможность многоадресной передачи. Наш сервер получает многоадресные сообщения, поскольку операционная система BSD/OS поддерживает многоадресную передачу, и порт получателя совпадает с портом нашего сервера. Установленный флаг MSG_MCAST указывает, что дейтаграмма была получена как многоадресное сообщение на канальном уровне.
Четвертая строка выводится от самого узла и предназначена для адреса 127.0.0.1, адреса закольцовки. Указывается интерфейс 1о0, как мы н предполагали. На этот раз мы ввели 41-байтовую строку на стороне клиента (здесь
мы это не показываем). Сервер получает только первые 20 байт дейтаграммы, и при этом установлен флаг MSG_TRUNC, указывающий, что дейтаграмма была обрезана.
Последняя строка выводится от узла 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 преобразует его в широковещательный адрес полученного интерфейса.
Обрезанные дейтаграммы
Пример из предыдущего раздела показывает, что когда в BSD/OS приходит дейтаграмма UDP, размер которой больше буфера приложения, функция recvmsg устанавливает флаг MSG—TRUNC в элементе msg_f 1 ags структуры msghdr (см. табл. 13.2). Все Беркли-реализации, поддерживающие структуру msghdr с элементом msg_fl ags, предоставляют это уведомление.
ПРИМЕЧАНИЕ
Это пример флага, который должен быть возвратен процессу ядром. В разделе 13.3 мы упомянули о проблеме разработки функций reev и reevfrom: их аргумент flags является целым числом, что позволяет передавать флаги от процесса к ядру, но не наоборот.
К сожалению, не все реализации подобным образом обрабатывают ситуацию, когда размер дейтаграммы UDP оказывается больше, чем предполагалось. Возможны три сценария:
Лишние байты игнорируются, и приложение получает флаг MSG_TRUNC, что требует вызова функции recvmsg.
Игнорирование лишних байтов без уведомления приложения.
Сохранение лишних байтов и возвращение их в последующих операциях чтения на сокете.
Мы уже видели развитие первого сценария в BSD/OS. Второй тип сценария можно увидеть в Solaris 2.5: лишние байты игнорируются, но поскольку структура msghdr не поддерживает элемента msg_fl ags, нет возможности возвратить ошибку приложению.
ПРИМЕЧАНИЕ
Posix.lg задает первый тип поведения: игнорирование лишних байтов и установку флага MSG TRUNC. Болес ранние реализации SVR4 действовали в подобных ситуациях по третьему сценарию.
Поскольку способ обработки дейтаграмм, превышающих размер приемного буфера приложения, зависит от реализации, одним из решений, позволяющих обнаружить ошибку, будет всегда использовать буфер приложения на 1 байт больше самой большой дейтаграммы, которую приложение предположительно может получить. Если все же будет получена дейтаграмма, длина которой равна размеру буфера, это послужит сигналом возможной ошибки.
Когда UDP оказывается предпочтительнее TCP
В разделах 2.3 и 2.4 мы описали основные различия между UDP и TCP. Поскольку мы знаем, что TCP надежен, a UDP — нет, возникает вопрос: когда следует использовать UDP вместо TCP и почему? Сначала перечислим преимущества UDP:
* Как мы показали в табл. 18.1, UDP поддерживает широковещательную и направленную передачу. Действительно, UDP должен быть использован, если приложение использует широковещательную или многоадресную передачу. Эти два режима адресации мы рассматривали в главах 18 и 19.
加 В UDP нет установления соединения или его разрыва. В соответствии с рис. 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), а затем могут не обращаться друг к другу в течение часов или дней.~
Теперь мы перечислим свойства TCP, отсутствующие в UDP. Это означает, что приложение должно само предоставлять эти свойства, если они ему необходимы. Мы говорим «необходимы», потому что не все свойства требуются всем приложениям. Например, может не возникнуть необходимости повторно передавать потерянные сегменты для аудиоприложений реального времени, если приемник способен интерполировать недостающие данные. Также для простых транзакций «запрос-ответ» может не потребоваться оконное управление потоком, если два конца соединения заранее договорятся о размерах наибольшего запроса логвега.
Положительные подтверждения, повторная передача потерянных пакетов, обнаружение дубликатов и упорядочивание пакетов, порядок следования которых был изменен сетью. TCP подтверждает получение всех данных, позволяя обнаруживать потерянные пакеты. Реализация этих двух свойств требует, чтобы каждый сегмент дадных TCP содержал порядковый номер, по которому можно впоследствии проверить получение данного сегмента. Требуется также, чтобы TCP прогнозировал значение тайм-аута повторной передачи для соединения и чтобы это значение последовательно обновлялось по мере изменения сетевого трафика на обоих концах сети.
Оконное управление потоком. Принимающий TCP сообщает отправляющему, какое буферное пространство он выделил для приема данных, и отправляющий не может превышать этого значения, то есть количество неподтвержденных данных отправителя никогда не может стать больше объявленного размера окна принимающего.
Медленный старт и предотвращение перегрузки. Это форма управления потоком, осуществляемого отправителем, служащая для определения текущей пропускной способности сети и позволяющая контролировать ситуацию во время переполнения сети. Все современные TCP-приложения должны поддерживать эти два свойства, и опыт (накопленный еще до того, как эти алгоритмы были реализованы в конце 80-х) показывает, что протоколы, не отступающие в случае переполнения, лишь усугубляют его (см., например, [43]).
Суммируя вышесказанное, мы можем сформулировать следующие рекомендации:
UDP должен использоваться для приложений широковещательной и многоадресной передачи. Если требуется какая-либо разновидность защиты от ошибок, то соответствующая функциональность должна быть добавлена клиентам и серверам. Но приложения часто используют широковещательную или многоадресную передачу, когда некоторое (предположительно небольшое) количество ошибок (таких, как потеря аудио- нтш видеопакетов) допустимо. Имеются приложения многоадресной передачи, требующие надежной доставки (например, пересылка файлов при помощи многоадресной передачи), но в каждом конкретном случае мы должны решить, компенсируется ли выигрышем в производительности, получаемым за счет использования многоадресной передачи (отправка одного пакета N получателям вместо отправки N копий пакета через N соединений TCP), дополнительное усложнение приложения для обеспечения надежности соединений.
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 Environment —среда распределенных вычислений), применявшейся до появления RPC (Remote Procedure Call — удаленный вызов процедур), сначала ис~ пользовали UDP, а не TCP, хотя современные реализации поддерживают и UDP, и TCP.
У нас мог бы возникнуть соблазн сказать, что применение UDP сокращается, поскольку сегодня хорошие реализации TCP не уступают в скорости сетям и все меньше разработчиков готовы встраивать в приложения UDP функциональность, свойственную TCP. Но увеличение количества мультимедиа-приложений за последнее десятилетие приводит к повышению популярности UDP, поскольку их работа обычно подразумевает использование многоадресной передачи, требующей наличия UDP.
Добавление надежности приложению UDP
Если мы хотим использовать UDP для приложения типа «запрос-ответ», как было отмечено в предыдущем разделе, мы должны добавить нашему клиенту две функциональных возможности:
тайм-аут и повторную передачу, которые позволяют решать проблемы, возникающие в случае потери дейтаграмм;
порядковые номера, позволяющие клиенту проверить, что ответ приходит на определенный запрос.
Эти два свойства предусмотрены в большинстве существующих приложений UDP, использующих простую модель «запрос-ответ»: например, распознаватели DNS, агенты SNMP, TFTP и RPC. Мы не пытаемся использовать UDP для передачи большого количества данных: наша цель — приложение, посылающее запрос и ожидающее ответа на этот запрос.
ПРИМЕЧАНИЕ
Использование дейтаграмм по определению не может быть надежным, следовательно, мы специально не называем данный сервис «надежным сервисом дейтаграмм». Действительно, термин «падежная дейтаграмма» — это оксюморон. Речь идет лишь о том, что приложение до некоторой степени обеспечивает надежность, добавляя соответствующие функциональные возможности «поверх» ненадежного сервиса дейтаграмм (UDP)
Добавление порядковых номеров осуществляется легко. Клиент подготавливает порядковый номер для каждого запроса, а сервер должен отразить этот номер обратно в своем ответе клиенту. Это позволяет клиенту проверить, что данный ответ пришел на соответствующий запрос.
Более старый метод реализации тайм-аутов и повторной передачи заключался в отправке запроса и ожидании в течение N секунд. Если ответ не приходил, осуществлялась повторная передача и снова на ожидание ответа отводилось N секунд. Если это повторялось несколько раз, отправка запроса прекращалась. Это так называемый линейный таймер повторной передачи (на рис. 6.8 [94] показан пример клиента TFTP, использующего эту технологию. Многие клиенты TFTP до сих пор пользуются этим методом).
Проблема при использовании этой технологии состоит в том, что количество времени, в течение которого дейтаграмма совершает цикл в объединенной сети, может варьироваться от долей секунд в локальной сети до нескольких секунд в глобальной. Факторами, влияющими на время обращения (RTT), являются рас-
стояние, скорость сети и переполнение. Кроме того, RTT между клиентом и сервером может значительно меняться со временем при изменении условий в сети. Нам придется использовать тайм-ауты и алгоритм повторной передачи, который учитывает действительное (измеряемое) значение периода RTT и изменения RTT с течением времени. В этой области ведется большая работа, в основном направленная на TCP, но некоторые идеи применимы к любым сетевым приложениям.
Мы хотим вычислить тайм-аут повторной передачи (RTO), чтобы использовать его при отправке каждого пакета. Для того чтобы выполнить это вычисление, мы измеряем RTT — действительное время обращения для пакета. Каждый раз, измеряя RTT, мы обновляем два статистических показателя: srtt — сглаженную оценку RTT, и rttvar — сглаженную оценку среднего отклонения. Последняя является лишь приближенной оценкой стандартного отклонения, но ее легче вычислять, поскольку для этого не требуется извлечения квадратного корня. Имея эти два показателя, мы вычисляем RTO как сумму srtt и rttvar, умноженного на четыре. В [43] даются все необходимые подробности этих вычислений, которые мы можем свести к четырем следующим уравнениям:
delta »measuredRTT - srtt srtt— srtt + gx delta 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 и т. д.
Алгоритмы Джекобсона Qacobson) реализуют вычисление RTO при измерении RTT и увеличение RTO при повторной передаче. Однако, когда нам необходимо ретранслировать пакет и затем получить ответ, возникает проблема. Это называется проблемой неопределенности повторной передачи (retransmission ambiguity problem). На рис. 20.2 показаны три возможных сценария, при которых истекает время ожидания повторной передачи:
發 запрос потерян;
藻 ответ потерян;
目 значение RTO слишком мало.
Когда клиент получает ответ на запрос, отправленный повторно, он не может сказать, какому из запросов соответствует ответ. На рисунке, изображенном справа, ответ соответствует начальному запросу, в то время как на двух других рисунках ответ соответствуют второму запросу.
Рис. 20.2. Три сценария, возможных при истечении времени таймера повторной передачиРис. 20.2. Три сценария, возможных при истечении времени таймера повторной передачи
Рис. 20.2. Три сценария, возможных при истечении времени таймера повторной передачи
Рис. 20.2. Три сценария, возможных при истечении времени таймера повторной передачи
Алгоритм Карна (Karn) [49] обрабатывает этот сценарий в соответствии со следующими правилами, применяемыми в любом случае, когда ответ получен на запрос, отправленный более одного раза.
Если RTT измерялось, не используйте его для обновления оценочных значений, так как мы не знаем, какому запросу соответствует ответ.
Поскольку ответ пришел до того как истекло время нашего таймера повторной передачи, используйте для следующего пакета текущее значение RTO. Только когда мы получим ответ на запрос, который не был передан повторно, мы изменяем значение RTT и снова вычисляем RTO.
При написании наших функций RTT применить алгоритм Карна несложно, однако существует и более изящное решение. Оно связано с дополнительными возможностями TCP для сетей высокой вместимости (long fat pipe), то есть сетей, обладающих либо широкой полосой пропускания, либо большим значением RTT, либо обоими этими свойствами (RFC 1323 [45]). Кроме добавления порядкового номера к началу каждого запроса, который сервер должен отразить, мы добавляем отметку времени, которую сервер также должен отразить. Каждый раз, отправляя запрос, мы сохраняем в этой отметке значение текущего времени. Когда приходит ответ, мы вычисляем величину RTT для этого пакета как текущее время минус значение отметки времени, отраженной сервером в своем ответе, Поскольку каждый запрос несет отметку времени, отражаемую сервером, мы можем вычислить RTT для каждого ответа, который мы получаем. Теперь нет никакой неопределенности. Более того, поскольку сервер только отражает отметку времени клиента, клиент может использовать для отметок времени любые удобные единицы, и при этом не требуется, чтобы клиент и сервер синхронизировали часы.
Пример
Свяжем теперь всю эту информацию воедино в примере. Мы запускаем фуи циюmain нашего клиента UDP, представленного в листинге 8.3, и изменяем то」 ко номер порта с SERV_PORT на 7 (стандартный эхо-сервер, см. табл. 2.1).
В листинге 20.4 показана функция dg_cli. Единственное изменение по срг нению с листингом 8 4 состоит в замене вызовов функций sendtu и recvfrom вне вом нашей новой функции dg_send一recv
Перед тем как представить функцию dg_send_recv и наши функции RTT, кот рые она вызывает, мы показываем в листинге 20 5 нашу схему реализации фун циональных свойств, повышающих надежность клиента UDP Все функции, име которых начинаются с rtt_, описаны далее
Листинг 20.4. Функция dg~ch, вызывающая нашу функцию dg_send_recv //rtt/dg_cli с
include unp h'
ssizjt Dg_send_recv('int const void * sizjt void * sizjt
const SA 去 socklen』)
void
dg_cli(FILE *fp int sockfd const SA *pservaddr socklen.t servlen)
{-.
ssize_t n
char - send】】ne[MAXLINE] recvline[MAXLINE + 1]
while (Fgets(sendline MAXLINE fp) {= NULL) {
n = Dg_send_recv(sockfd send") ine strlen(sendl we)
&recvline MAXLINE pservaddr servlen)
recvline[n] = 0/* завершающий нуль */
Fputs(recvline stdout)
}
}
Листинг 20・5・ Схема функций RTT и последовательность их вызоеа
static sigjmp_buf jmpbuf
запрос
signal (SIGALW,slg^ata): /★ ^тонанлмваам обработчик сигнала */
rtt_newpack();父 инициализируем значение счетчика rexmt,нулем */
sendagaw”'
sendto()
alarm(rtt_start()). /* задаем аргумент функции alarm равным значению RTD#/ if (sigsetjmp(jmpbuf 1) ,= 0) {
if (rtt_timeuut()) /* удваиваем RT。 обновляем оценочные значения */
откдзывдеися от дальнейших попыток
got。 sendagain /* поеторная передача */
aldrm(O) rtt_stop()aldrm(O) rtt_stop()} while (неправильный порядковый номер)
aldrm(O) rtt_stop()
aldrm(O) rtt_stop()
/* отключаем функцию alarm */
/* вычисляем RTT и обновляем оценочные значения */
обрабатываем ответ
void
s'ig_alrm(int signo)
{ 一
siglongjmpCjmpbuf 1)
}
Если приходит ответ, но его порядковый номер отличается от предполагаемого, мы снова вызываем функцию recvfrom, но не отправляем снова тот же запрос и не перезапускаем работающий таймер повторной передачи Обратите внимание, что в крайнем правом случае на рис. 20 2 последний ответ, полученный на отправленный повторно запрос, будет находиться в приемном буфере сокета дс тех пор, пока клиент не решит отправить следующий запрос (и получить на неге ответ) Это нормально, поскольку клиент прочитает этот ответ, отметит, что порядковый номер отличается от предполагаемого, проигнорирует ответ и снова вызовет функцию reevfrom
Мы вызываем функции sigsetjmp и siglongjmp, чтобы предотвратить возникновение ситуации гонок с сигналом SIGALRM, который мы описали в разделе 18 5
В листинге 20 6 показана первая часть нашей функции dg_send_recv
Листинг 20.6. Функция dg_send一reev: первая часть
//гtt/dg_s end^rec v с
#】nc7ude - 'unprtt h'
include <setjmp h>
#defwe RTTJEBUG
stsfic struct rtt—info rttinfo
static mt rttimt = 0
static struct msghdr msgsend msgrecv
/* предполагается что обе структуры инициализированы нумем **/
static struct hdr {、
snt32_t seq/* порядковый номер */
u】nt32]t ts/* отметка времени при отправке */
} sendhdr recvhdr'
ii static void sig_a1rm(】nt sign。)
static sigjmp一buf jmpbuf
ssize_t
dg_send_recv(int fd const void *outbuff size_t outfc^es,
void 去頂bu幵 size」mbytes
const SA *destaddr socklen t destlen)
U一
ssize_t n
rtt_imt(&rttinfo)rtt_imt(&rttinfo)rttinit = 1rttinit = 1/* первый вызов *//* первый вызов */rtt_d_flag = 1 }
rtt_imt(&rttinfo)
rtt_imt(&rttinfo)
rttinit = 1
rttinit = 1
/* первый вызов */
/* первый вызов */
sendhdr seq++
msgsend msg_name = destaddr msgsend msg_namelen - destlen msgsend msg_iov = lovsend msgsend msg_iovlen = 2 iovsend[0] iov_base - &sendhd「 iovsend[0] ?ov_len e sizeof(struct hdr) iovsend[l] iov_base - outbuff iovsend[l] iov_len = outbytes
msgrecv msg_name = NULL msgrecv msg_namelen = 0 msgrecv msg_iov = novrecv msgrecv msg_iovlen e 2 iovrecv[0] iov_base 上 &recvhdr iovrecv[0] iov_len = sizeof(struct hdr) iovrecv[l] iov_base - inbuff iovrecv[l] iciv_len = inbytes
1 5 Мы включаем новый заголовочный файл unprtt h, показанный в листинге 20 8, который определяет структуру rtt_i nfo, содержащую информацию RTT для клиента Мы определяем одну из этих структур и ряд других переменных
Определение структур msghdr и структуры hdr
6 10 Мы хотим скрыть от вызывающего процесса, что добавляем порядковый номер и отметку времени в начало каждого пакета Проще всего использовать для этого функцию wri tev, записав свой заголовок (структура hdr), за которым следуют данные вызывающего процесса, в виде одной дейтаграммы UDP Вспомните, что результатом выполнения функции wn tev на дейтаграммном сокете является отправка одной дейтаграммы Это проще, чем заставлять вызывающий процесс выделять для нас место в начале буфера, а также быстрее, чем копировать наш заголовок и данные вызывающего процесса в один буфер (под который мы должны выделить память) для каждой функции sendto Но поскольку мы работаем с UDP н нам необходимо задать адрес получателя, следует использовать возможности предоставляемые структурой iovec функций sendmsg и recvmsg и отсутствующие в функциях sendto и recvfrom Вспомните из раздела 13 5, что в некоторых системах доступна более новая структура msghdr, включающая вспомогательные данные (msg_control), тогда как в более старых системах вместо них применяются элементы msg_accright (так называемые права доступа — access rights), расположенные в конце структуры Чтобы избежать усложнения кода директивами #i fdef для обработки этих различий, мы объявляем две структуры msghdr как static При этом они инициализируются только нулевыми битами, а затем неиспользованные элементы в конце структур просто игнорируются
Инициализация при первом вызове
20-24 При первом вызове нашей функции мы вызываем функцию rtt_imt
Заполнение структур msghdr
25~41 Мы заполняем две структуры msghdr, используемые для ввода и вывода Для данного пакета мы увеличиваем на единицу порядковый номер отправки, но не устанавливаем отметку времени отправки, пока пакет не будет отправлен (поскольку он может отправляться повторно, а для каждой повторной передачи требуется текущая отметка времени)
Вторая часть функции вместе с обработчиком сигнала signal rm показана в листинге 20 7
Листинг 20.7. Функция dg_send_recv вторая половина
//rtt/dg_send_recv с
42 Signal(SIGALRM sig_alrm)
43rtt_newpack(&rttinfo)/* инициализируем для этого пакета */
44 sendagain
45 sendhdr ts = rtt_ts(&rttinf。)
46Sendmsg(fd &msgsend 0)
47 alarm(rtt_start(&rtt?nfo)) /* вычисляем величину тайм аута и запускаем таймер */
48if (sigsetjmp(jnipbuf 1)『0) {
49 if (гtt_t1meout(&rttтnfо) < 0) {
50 err_msg( dg_send_recv no response from server giving up )
51 rttinit = 0/* повторная инициализация для следующего й^зоба */«
52errno e ETIMEDOUT
53return ( 1)
54}
55goto sendagain
56}
57 do {
58 n = Recvmsg(fd &msg「ecv 0)
59} while (n < sizeof(struct hdr) || recvhdr seq i= sendhdr seq).
60 з1агт(0) /* останавливаем таймер SIGALRM */
61/* вычисляем и записываем новое значение оценки RTT */
62 rtt_stop(&rttmfo rtt_ts(&「ttinf。) recvhdr ts)
63 return (nsizeof(struct hdr)) /* возвращаем размер полученной дейтагранны
64 }
65 static void
66 sig_alrm(int sign。)
67 { ~
68 siglongjmpCjmpbuf 1)
69 }
Установка обработчика сигналов
42 43 Для сигнала SIGALRM устанавливается обработчик сигналов, а функция rtt_newpack сбрасывает счетчик повторных передач в нуль
Отправка дейтаграммы
45 47 Функция rtt ts получает текущую отметку времени Отметка времени хранится в структуре hdr, которая добавляется к данным пользователя Одиночная лей-
таграмма UDP отправляется функцией sendmsg. Функция rtt_start возвращает количество секунд для этого тайм-аута, а сигнал SIGALRM контролируете夕 функцией alarm.
Установка буфера перехода
48 Мы устанавливаем буфер перехода для нашего обработчика сигналов с помощью функции sigsetjmp. Мы ждем прихода следующей дейтаграммы, вызывая функцию reevmsg. (Совместное использование функций sigsetjmp и siglongjmp вместе с сигналом SIGALRM мы обсуждали применительно к листингу 18.5.) Если время таймера истекает, функция sigsetjmp возвращает 1.
Обработка тайм-аута
19-55 Когда возникает тайм-аут, функция rtt_timeout вычисляет следующее значение RTO (используя экспоненциальное смещение) и возвращает ~1, если нужно прекратить попытки передачи дейтаграммы, или 0, если нужно выполнить очередную повторную передачу. Когда мы прекращаем попытки, мы присваиваем переменной errno значение ETIMEDOUT и возвращаемся в вызывающую функцию.
Вызов функции reevmsg, сравнение порядковых номеров
)7-59 Мы ждем прихода дейтаграммы, вызывая функцию reevmsg. Длина полученной дейтаграммы не должна быть меньше размера структуры hdr, а ее порядковый номер должен совпадать с порядковым номером запроса, ответом па который предположительно является эта дейтаграмма. Если прп сравнении хотя бы одно из этих условий не выполняется, функция reevmsg вызывается снова.
Выключение таймера и обновление показателей RTT
)0~62 Когда приходит ожидаемый ответ, функция alarm отключается, а функция rtt_stop обновляет оценочное значение RTT. Функция rtt ts возвращает текущую отметку времени, и отметка времени из полученной дейтаграммы вычитается из текущей отметки, что дает в результате RTT.
Обработчик сигнала SIGALRM
>5-69 Вызывается функция sig longjmp, результатом выполнения которой является то, что функция sigsetjmp в dg_send_recv возвращает 1.
Теперь мы рассмотрим различные функции RTT, которые вызывались нашей функцией dg_send_recv. В листинге 20.8 показан заголовочный файл unprtt. h.
Листинг 20.8. Заголовочный файл unprtt.h
//lib/unprtt h
#ifndef —unp_rtt_h
#define —unp_rtt_h
4 5 6 7 8 94 5 6 7 8 9struct rtt_info { float rtt_rtt. float rttsrtt. float rtt_rttvar, float rtt_rto int rtt nrexmt.struct rtt_info { float rtt_rtt. float rttsrtt. float rtt_rttvar, float rtt_rto int rtt nrexmt.#i nd ude"unp h"
4 5 6 7 8 9
4 5 6 7 8 9
struct rtt_info { float rtt_rtt. float rttsrtt. float rtt_rttvar, float rtt_rto int rtt nrexmt.
struct rtt_info { float rtt_rtt. float rttsrtt. float rtt_rttvar, float rtt_rto int rtt nrexmt.
/* самое последнее измеренное значение RTT. в секундах */ /* сглаженная оценка RTT. в секундах */■
/* сглаженные средние значения отклонений в секундах */ /* текущее используемое значение RTO в секундах */ /* количество повторных передач 0 1.2*/
10 uint32_t rtt_base.11 }•10 uint32_t rtt_base.11 }•12 #define RTT_RXTMIN 212 #define RTT_RXTMIN 213 #define RTT_RXTMAX 6013 #define RTT_RXTMAX 6014 #define RTT.MAXNREXMT 314 #define RTT.MAXNREXMT 3/* минимальное значение тайм-аута для повторной передачи, в секундах *//* максимальное значение тайм-аута для повторной передачи, в секундах *//* максимально допустимое количество повторных передач одной дейтаграммы *//* минимальное значение тайм-аута для повторной передачи, в секундах *//* максимальное значение тайм-аута для повторной передачи, в секундах *//* максимально допустимое количество повторных передач одной дейтаграммы */
10 uint32_t rtt_base.
11 }•
10 uint32_t rtt_base.
11 }•
12 #define RTT_RXTMIN 2
12 #define RTT_RXTMIN 2
13 #define RTT_RXTMAX 60
13 #define RTT_RXTMAX 60
14 #define RTT.MAXNREXMT 3
14 #define RTT.MAXNREXMT 3
/* минимальное значение тайм-аута для повторной передачи, в секундах */
/* максимальное значение тайм-аута для повторной передачи, в секундах */
/* максимально допустимое количество повторных передач одной дейтаграммы */
/* минимальное значение тайм-аута для повторной передачи, в секундах */
/* максимальное значение тайм-аута для повторной передачи, в секундах */
/* максимально допустимое количество повторных передач одной дейтаграммы */
/* количество секунд. прошедших после 1 1 1970 в начале */
/* прототипы функций */
voidrtt_debug(struct rtt_info *).
voidrtt_i mt (struct rtt_info *).
voidrtt[newpdck(struct Ftt_inf。*).
19intrtt[s tart (struct rtt_info *)
20voidrtCstop(struct rtt_吊nfo *. uint32_t)i
21intrtt_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, функции rtt^minmax и rttjnit
//lib/rtt.c
1 #i nclude “unprtt.h"
2 int2 intrtt_d_flag = 0:rtt_d_flag = 0:/* отладочный флаг, может быть установлен в ненмлааое значение вызывающим процессом */
2 int
2 int
rtt_d_flag = 0:
rtt_d_flag = 0:
/* Вычисление значения RT0 нй основе текущих значений:
* сглаженное оценочное значение RTT + 4 х сглаженная
* величина отклонения.
*/
#define RTT_RTOCALC(ptr) ((ptr)->rtt_srtt +(40* (ptr)->rtt_rttvar))
static float
rtt imninaxCfl oat rto)
io {-
if (rto < RTT—RXTMIN)
rto - RTT RXTMIN:
else If (rto > RTT RXTMAX)
rto * rtt_rxtmax7
return (rtoT:
}
17 void
18 rtt 1 nit(struct rtt_info *ptr)
19 {--
20 struct timeval tv.
21 Gettimeofday(&tv. NULL)
22 ptr->rtt_base = tv tv_sec. /* количество секунд прошедших с 1 1 1970 в начале */
23 ptr->rtt_rtt = О
24ptr->rtt_srtt = О
25ptr->rtCrttvar = 0 75.
26 ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr))
27/* первое RTO (srtt + (4 * rttvan)) = 3 секунды */
28 }
3-7 Макрос вычисляет RTO как сумму оценочной величины RTT и оценочной величины среднего отклонения, умноженной на четыре.
-16 Функция rttjm птах проверяет, что RTO находится между верхним и нижним пределами, заданными в заголовочном файле unprtt h.
7-28 Функция rttjimt вызывается функцией dg_send_recv при первой отправке пакета. Функция gettimeofday возвращает текущее время и дату в той же структуре timeval, которую мы видели в функции se】ect (см. раздел 6.3), Мы сохраняем только текущее количество секунд с момента начала эпохи Unix, то есть с 00:00.00 1 января 1970 года (UTC). Измеряемое значение RTT обнуляется, а сглаженная оценка RTT и среднее отклонение принимают соответственно значение 0 и 0,75, в результате чего начальное RTO равно 3 секундам (4x0,75).
ПРИМЕЧАНИЕ
Функция gcttimeoMay еще не является частью Posix.lg и может не поддерживаться некоторыми реализациями. Она требуется в 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 rtt_info *ptr)
36 {--
37 uint32_t ts.
38 strucCtimeval tv.
39 Gettweofday(&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
rtt_s tart (struct rtt_mfo *ptr)
{--
return ((int) (ptr->rtt_rto + 0 5))/* округляем float до int */
/* возвращенное значение может быть использовано как аргумент
а1 a rm(rtt_sta rt(&fоо)) */
}—
4-42 Функция rtt_ts возвращает текущую отметку времени для вызывающего процесса, которая должна содержаться в отправляемой дейтаграмме в виде 32-раз- рядного целого числа без знака. Мы получаем текущее время и дату из функции gettimeofday и затем вычитаем число секунд в момент вызова функции rtt_init (значение, хранящееся в элементе rtt_base структуры rtt_mfo). Мы преобразуем это значение в миллисекунды, а также преобразуем в миллисекунды значение, возвращаемое функцией gettimeofday в микросекундах. Тогда отметка времени является суммой этих двух значений в миллисекундах.
Разница между двумя вызовами функции rtt_ts заключается в числе миллисекунд между этими двумя вызовами. Но мы храним отметки времени в 32-раз- рядном целом числе без знака, а не в структуре timeval
”47 функция rtt_newpack просто обнуляет счетчик повторных передач. Эта функция должна вызываться всегда, когда новый пакет отправляется в первый раз.
"53 Функция rtt_start возвращает текущее значение RTO в миллисекундах. Возвращаемое значение затем может использоваться в качестве аргумента функции alarm.
Функция rtt_stop, показанная в листинге 20.11, вызывается после получения ответа для обновления оценочного значения RTT и вычисления нового значения RTO.
Листинг 20.11. Функция rtt_stop: обновление показателей RTT и вычисление нового
〃lib/rtt с
void
rtt_stop(struct rttjnfo *ptr, innt32_t ms)
{ "一一
double delta,
ptr->rtt_rtt - ms / 1000 0; /* измеренное значение RTT в секундах */
/*
* Обновляем оценочные значения RTT среднего отклонения RTT
69* см статью Джекобсона (Jacobson). SIGCOMM'88, приложение А
* Здесь мы для 叩остотм используем числа с плавающей точкой
*/
delta = ptr->rtt_rtt - ptr->rtt_srtt
ptr->rtt_srtt += delta / 8. /* g - 1/8 */
if (delta < 0 0)
delta = -delta /* ]delta] */
ptr->rtt_rttvar += (delta - ptr->rtt_rttvar) / 4;/* h * 1/4 */
ptr->rtt_rto = rtt_nn nmax(RTT_RTOCALC(pt r)).
}■-_
-78 Вторым аргументом является измеренное RTT, Полученное вызывающим процессом при вычитании полученной в ответе отметки времени из текущей (функ-
ция rtt_ts). Затем применяются уравнения, приведенные в начале этого раздела, и записываются новые значения переменных rtt_s「tt, rtt_rttvar и rtt_rto.
Последняя функция, rtt_ti meout, показана в листинге 20.12. Эта функция вызывается, когда истекает время таймера повторных передач.
Листинг 20.12. Функция rtt_timeout: применение экспоненциального смещения //lib/rtt с 83 int
84 rtt timeout(struct rtt inf。 *ptr)
85 { _-
86 ptr->rtt_rto *= 2./* следующее значение RTO */
87 if (+-*-ptr->rtt_nrexmt > RTT^MAXNREXMT)
88 return (-1)/* закончилось время, отпущенное на попытки отправить этот пакет */
89 return (0).
90 }
86 Текущее значение RTO удваивается — в этом и заключается экспоненциаль' ное смещение.
J7-89 Если мы достигли максимально возможного количества повторных передач, возвращается значение -1, указывающее вызывающему процессу, что дальнейшие попытки передачи должны прекратиться. В противном случае возвращается 0.
В нашем примере клиент соединялся дважды с двумя различными эхо-серве- рами в Интернете утром рабочего дня. Каждому серверу было отправлено по 500 строк. По пути к первому серверу было потеряно 8 пакетов, по пути ко второму —16. Один из потерянных шестнадцати пакетов, предназначенных второму серверу, был потерян дважды, то есть пакет пришлось дважды передавать повторно, прежде чем был получен ответ. Все остальные потерянные пакеты пришлось передать повторно только один раз. Мы могли убедиться, что эти пакеты были действительно потеряны, посмотрев на выведенные порядковые номера каждого из полученных пакетов. Если пакет лишь опоздал, но не был потерян, после повторной передачи клиент получает два ответа: соответствующий запоздавшему первому пакету и повторно переданному. Обратите внимание, что у нас нет возможности определить, что именно было потеряно (и привело к необходимости повторной передачи клиентского запроса) — сам клиентский запрос или же ответ сервера, высланный после получения такого запроса.
ПРИМЕЧАНИЕ
Для первого издания этой книги автор написал для проверки этого клиента сервер UDP, который случайным образом игнорировал пакеты. Теперь он нс используется. Все, что нам нужно сделать — соединить клиепт с сервером через Интернет, и тогда нам почти гарантирована потеря некоторых пакетов!
20.6. Связывание с адресами интерфейсов
Одно из типичных применений функции get_1f1_info связано с приложениями UDP, которым нужно выполнять мониторинг всех интерфейсов на узле, чтобы знать, когда и на какой интерфейс приходит дейтаграмма. Это позволяет полу-
чающей программе узнавать адрес получателя дейтаграммы UDP, так как именно по этому адресу определяется сокет, на который доставляется дейтаграмма, даже если узел не поддерживает параметр сокета PI_RECVDSTADDR.
ПРИМЕЧАНИЕ
Вспомните наше обсуждение в конце раздела 20.2. Если узел использует более распространенную модель системы с гибкой привязкой (см. раздел 8.8), IP-адрес получателя может отличаться от IP-адреса принимающего интерфейса. Все, что мы в этом случае можем определить, — это адрес получат еля дейт аграммы, который не обязательно должен быть адресом, присвоенным принимающему интерфейсу. Чтобы определить принимающий интерфейс, требуется параме гр сокета IP_RECV1F или IPV6 PKTINFO.
Ранее в нашем примере SNTP (см. раздел 19.11) мы продсмонстировали связывание всех адресов интерфейса.
В листинге 20.13 показана первая часть примера применения этой техноло。 гии к эхо-серверу UDP, который связывается со всеми адресами направленной передачи, широковещательной передачи и, наконец, с универсальными адресами.
Листинг 20.13. Первая часть сервера UDR который с помощью функции bind связывается со всеми адресами
//advw/udpserv03 с
include "unpifi h"
void mydg_echo(int. SA * socklen_t. SA *).
int
main(mt argc. char **argv)
{
mtsockfd
constmt on =1,
pid_tpid.
struct ifi_wfo *ifi. *ifihead.
structsockaddr_in *sa. cliaddr, wildaddr.
for (ifihead = ifi = Get_ifi_info(AF_INET. 1);
ifi i= NULL ifi = ifi->ifi_next) j
/* связываем направленный адрес */
sockfd = Socket(AF_INET SOCKJGRAM 0)
Setsockopt(sockfd. SOL_SOCKET. SO_REUSEADDR. &on. Sizeof(on));
sa = (structsockaddr_in *) ifi->ifi_addr
sa->sin_family = AF」fJET.
18sa->sm_port= htons(SERV_PORT).
19Bind(sockfd,(SA *) sa. sizeof(*sa)).
20 pnntf("bound Zs\en” Sock_ntop((SA *) sa. sizeof(*sa))):
21 if ( (pid = ForkO) == 0) { /* дочерний процесс */
22mydg_echo(sockfd (SA *) &cliaddr sizeof(cliaddr). (SA *) sa):
exitTo)./* не выполняется */
}
Вызов функции getjfijnfo для получения информации об интерфейсе
1-12 Функция get 1 f i _i nfo получает все адреса IPv4, включая дополнительные (псевдонимы), для всех интерфейсов. Затем в программе выполняется цикл по всем возвращенным структурам ifi_mfo.
Создание сокета UDP и связывание адреса направленной передачи
3-20 Создается сокет UDP, и с ним связывается адрес направленной передачи. Мы также устанавливаем параметр сокета SO_REUSEADDR, поскольку мы связываем один и тот же порт (параметр SERV_PORT) для всех IP-адресов.
ПРИМЕЧАНИЕ
Не все реализации требуют, чтобы был установлен этот параметр сокета. Например, Беркли-реализации не требуют этого параметра и позволяют с помощью функции bind связать уже связанный порт, если новый связываемый IP-адрес не является универсальным адресом и отличается от всех IP-адресов, уже связанных с портом. Однако Solaris 2.5 для успешного связывания с одним и тем же портом второго адреса направленной передачи требует установки этого парамспра.
Вызов функции fork и порождение дочернего процесса для данного адреса
〃“24 Вызывается функция fork, порождающая дочерний процесс. В этом дочернем процессе вызывается функция mydg_echo, которая ждет прибытия любой дейтаграммы на сокет и отсылает ее обратно отправителю.
В листинге 20.14 показана следующая часть функции mai п, которая обрабатывает широковещательные адреса.
Листинг 20.14. Вторая часть сервера UD巳 который с помощью функции bind связывается со всеми адресами
//advio/udpserv03 с
if (ifi~>ifi_fldgs & IFF—BROADCAST) {
/* пытаемся связать широковещательный адрес */
sockfd = Socket(AF_INET. SOCK_DGRAM. 0).
Setsockopt(sockfd,"SOL_SOCKET, SO_REUSEADDR &on. sizeof(on)):
sa = (struct sockaddrjn *) ifi->ifi_brdaddr.
sa->sin_femily = AF_INET.-
sa->sin二 port = htons(SERV_PORT).
if (bind(sockfd (SA *) sa. sizeof(*sa)) < 0) {
if (errno == EADDRINUSE) {
printfC'EADDRINUSE 犯\en”.
35Sock_ntop((SA *) sa. sizeof(*sa))),
36Close(sockfd).
37continue.
38} else
39err_sys(“bind errorfor *s”
40Sock_ntop((SA *) sa. sizeof(*sa))).
41} 一
42 printfc"bound *s\en". Sock_ntop((SA *) sa. sizeof(*sa)));
43 if ( (pid = ForkO) == 0) { /* дочерний процесс */
44mydg_echo(sockfd. (SA *) &cliaddr. sizeof(cliaddr),
45(SA *) sa).
46exit(O):/* не выполняется */
47}
48}
49}
Связывание с широковещательными адресами
5-42 Если интерфейс поддерживает широковещательную передачу, создается сокет UDP и с ним связывается широковещательный адрес. На этот раз мы позволим функции bi nd выполниться неудачно с ошибкой EADDRINUSE, поскольку если у интерфейса имеется несколько дополнительных адресов (псевдонимов) в одной подсети, то каждый из различных адресов направленной передачи будет иметь один и тот же широковещательный адрес. Подобный пример приведен после листинга 16.3. В этом сценарии мы предполагаем, что успешно выполнится только первая функция bi nd.
Порождение дочернего процесса
3-47 Порождается дочерний процесс, и он вызывает функцию mydg echo.
Заключительная часть функции main показана в листинге 20.15. В этом коде при помощи функции bi nd происходит связывание с универсальным адресом для обработки любого адреса получателя, отличного от адресов направленной и широковещательной передачи, которые уже связаны. На этот сокет будут приходить только дейтаграммы, предназначенные для ограниченного широковещательного адреса (255.255.255.255).
Листинг 20.15. Заключительная часть сервера UDR связывающегося со всеми адресами
//advio/udpserv03 с
50 /* связываем универсальный адрес */
51 sockfd = Socket(AF_INET SOCK_DGRAM 0)
52Setsockopt(sockfd. SOL_SOCKET SO_REUSEADDR. &on. sizeof(on)):
53 bzero(&wildaddr. sizeof(wildaddr)).
54 wildaddr sin_famly = AF_INET.
55wildaddr sin_addr s_addr = hton1(INA00R_ANY).
56wildaddr sin_port = htons(SERV_PORT)
57Bind(sockfd. (SA *) &wi1daddr. sizeof(wildaddr)).
58printfC11bound %s\en" Sock_ntop((SA *) &wildaddr. sizeof(wildaddr))).
59 if ( (pid = ForkO) == 0) { /* дочерний процесс */
60 mydg_echo(sockfd (SA *) &cliaddr. sizeof(clladdr). (SA *) sa),
61 exit?0)./* не выполняется*/
62}
63exit(0),
64 }
Создание сокета и связывание
с универсальным адресом
0-62 Создается сокет UDP, устанавливается параметр сокета SO_REUSEADDR и происходит связывание с универсальным IP-адресом. Порождается дочерний процесс, вызывающий функцию mydg_echo.
Завершение работы функции main
63 Функция main завершается, и сервер продолжает выполнять работу, как и все порожденные дочерние процессы.
Функция rnydg_echo, которая выполняется всеми дочерними процессами, показана в листинге 20.16.
Листинг 20.16. Функция mydg_echo
//advio/udpserv03 с
65 void
66 mydg_echo(int sockfd SA *pcliaddr socklen t clilen, SA *nyadd「)
67 {-’-
68mtn
69charmesg[MAXLINE].
70 socklen_t len
71 for ( ) {
72len = clilen.
73 n = Recvfrom(sockfd mesg MAXLINE 0 pcliaddr &len):
74pnntf("child %d datagram from Ms' getpidO
75 Sock_ntop(pcliaddr. len))
76 printf?". to Zs\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 Z udpse「v03bound 206 62 226 66 9877 bound 206 62 226 95 9877 bound 206 62 226 35 9877 bound 206 62 226 63 9877 bound 206 62 226 50 9877 EADDRINUSE 206 62 226 63 bound 206 62 226 51 9877EADDRINUSE 206 62 226 63 bound 206 62 226 52 9877 EADDRINUSE 206 62 226 63bsdi Z udpse「v03bound 206 62 226 66 9877 bound 206 62 226 95 9877 bound 206 62 226 35 9877 bound 206 62 226 63 9877 bound 206 62 226 50 9877 EADDRINUSE 206 62 226 63 bound 206 62 226 51 9877EADDRINUSE 206 62 226 63 bound 206 62 226 52 9877 EADDRINUSE 206 62 226 63Запустим эту программу на нашем узле bsdi после установки трех псевдонимов (дополнительных адресов) для интерфейса efO Ethernet. Идентификаторы узлов для этих дополнительных адресов — 50, 51 и 52, но все они имеют один и тот же широковещательный адрес 206.62.226.63.
bsdi Z udpse「v03
bound 206 62 226 66 9877 bound 206 62 226 95 9877 bound 206 62 226 35 9877 bound 206 62 226 63 9877 bound 206 62 226 50 9877 EADDRINUSE 206 62 226 63 bound 206 62 226 51 9877
EADDRINUSE 206 62 226 63 bound 206 62 226 52 9877 EADDRINUSE 206 62 226 63
bsdi Z udpse「v03
bound 206 62 226 66 9877 bound 206 62 226 95 9877 bound 206 62 226 35 9877 bound 206 62 226 63 9877 bound 206 62 226 50 9877 EADDRINUSE 206 62 226 63 bound 206 62 226 51 9877
EADDRINUSE 206 62 226 63 bound 206 62 226 52 9877 EADDRINUSE 206 62 226 63
направленный адрес интерфейса weO
широковещательный адрес интерфейса weO первичный направленный адрес интерфейса weO широковещательный адрес интерфейса efO первый псевдоним адреса ндправленной передзчи 9877
второй псевдоним адреса направленной передачи 9877
третий псевдоним адресе направленной передачи 9877
bound 127 О О 1 9877интерфейс заКольцовни
bound 0 0 0 0 9877универсальный адрес
Обратите внимание» что три попытки связаться при помощи функции bind с широковещательными адресами оказываются неудачными, как мы и предполагали. Теперь при помощи утилиты netstat мы можем проверить, что все три сокета связаны с указанным IP-адресом и портом:
bsdi % netstat -па I
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.) Часто бывает проще использовать отдельный процесс или поток для каждой операции или дескриптора вместо мультиплексирования множества различных операций или дескрипторов одним процессом.
Параллельные серверы UDP
Большинство серверов UDP являются последовательными (iterative): сервер ждет запрос клиента, считывает запрос, обрабатывает его, отправляет обратно ответ и затем ждет следующий клиентский запрос. Но когда обработка запроса клиента занимает длительное время, желательно так или иначе совместить во времени обработку различных запросов.
Определение «длительное время» означает, что другой клиент вынужден ждать в течение некоторого заметного для него промежутка времени, пока обслуживается текущий клиент. Например, если два клиентских запроса приходят в течение 10 миллисекунд и предоставление сервиса каждому клиенту занимает в среднем 5 секунд, то второй клиент будет вынужден ждать ответа около 10 секунд вместо 5 секунд, если запрос был принят в обработку сразу же по прибытии.
В случае TCP проблема решается просто 一 требуется лишь породить дочерний процесс с помощью функции fork (или создать новый поток, что мы увидим в главе 23) и дать возможность дочернему процессу выполнять обработку нового клиента. При использовании TCP ситуация существенно упрощается за счет того, что каждое клиентское соединение уникально: пара сокетов TCP уникальна для каждого соединения. Но в случае с UDP мы вынуждены рассматривать два различных типа серверов.
Первый тип — простой сервер UDP, который читает клиентский запрос, посылает ответ и затем завершает работу с клиентом. В этом сценарии сервер, читающий запрос клиента, может с помощью функции fork породить дочерний процесс и дать ему возможность обработать запрос. «Запрос», то есть содержимое дейтаграммы и структура адреса сокета, содержащая адрес прото* кола клиента, передаются дочернему процессу в виде копии содержимого области памяти из функции fork. Затем дочерний процесс посылает свой ответ непосредственно клиенту.
Второй тип — сервер UDP, обме![ивающийся множеством дейтаграмм с клиентом. Проблема здесь в том, что единственный номер порта сервера, известный клиенту, — это номер заранее известного порта. Клиент посылает первую дейтаграмму своего запроса на этот порт, но как сервер сможет отличить последующие дейтаграммы этого клиента от запросов новых клиентов? Типичным решением этой проблемы для сервера будет создание лового сокета для каждого клиента, связывание при помощи функции bind динамически назначаемого порта с этим сокетом и использование「того сокета для всех своих ответов. При этом требуется, чтобы клиент запомнил номер порта, с которого был отправлен первый ответ сервера, и отправлял последующие дейтаграммы на этот порт.
Примером второго типа сервера UDP является сервер TFTP (Trivial File Transfer Protocol — простейший протокол передачи файлов). Передача файла с помощью TFTP обычно требует большого числа дейтаграмм (сотен или тысяч, в зависимости от размера файла), поскольку этот протокол отправляет в одной дейтаграмме только 512 байт. Клиент отправляет дейтаграмму на известный порт сервера (69), указывая, какой файл нужно отправить или получить. Сервер читает запрос, но отправляет ответ с другого сокета, который он создает и связывает с динамически назначаемым портом. Все последующие дейтаграммы между клиентом и сервером используют для передачи этого файла новый сокет. Это позво-
Сервер создает сокет, связывает его с заранее известным портом (69) при помощи функции bind, вызывает функцию recvfrom, блокируется до прибытия ответа от клиента, при помощи функции fork порождает дочерний процесс, затем вновь вызывает функцию recvfrom...
Создает новый сокет; связываетего с динамически назначаемымпортом (2134) при помощифункции bind, обрабатыааетклиентский запрос, обмениваетсяс клиентом дополнительнымидейтаграммами через новый сокет
Рис. 20.3. Процессы, происходящие на автономном параллельном UDP-сервере
ляет главному серверу TFTP продолжать обработку других клиентских запросов, приходящих на порт 69, в то время как происходит передача файла (возможно, в течение нескольких секунд или даже минут).
Если мы рассмотрим автономный сервер TFTP (то есть случай, когда не используется демон 1 netd), мы получим сценарий, показанный на рис. 20.3. Мы считаем, что динамически назначаемый порт, связанный дочерним процессом с его новым сокетом, — это порт 2134.
Если используется демон inetd, сценарий включает еще один шаг. Вспомните из табл. 12.4, что большинство серверов UDP задают аргумент wait-fl ад как wait. В описании, которое следовало за рис. 12.4, мы сказали, что при указанном значении этого флага демон inetd приостанавливает выполнение функции select на сокете до завершения дочернего процесса, давая возможность этому дочернему процессу считать дейтаграмму, доставленную на сокет. На рис. 20.4 показаны все шаги.
inetdinetdСервер создает сокет и связывает егос заранее известным портом (69)при помощи функции bind.
inetd
inetd
' Когда прибывает запрос клиента TFTP, сервер при помощи функции fork порождает дочерний процесс, а затем отключает возможность вызова функции select на сокете, связанном
с портом UDP 69с портом UDP 69ехесехесВызыаает функцию reevfrom,при помощи функции forkпорождает дочерний процесс,затем вызывает функцию exit
с портом UDP 69
с портом UDP 69
ехес
ехес
Создает новый сокет; связываетего с динамически назначаемымпортом (2134) при помощифункции bind, обрабатываетклиентский запрос, обмениваетсяс клиентом дополнительнымидейтаграммами через новый сокет
Рис. 20.4. Параллельный сервер UDR запущенный демоном inetd
Сервер TFTP, являясь дочерним процессом функции inetd, вызывает функцию reevfrom и считывает клиентский запрос. Затем он с помощью функции fork порождает собственный дочерний процесс, и этот дочерний процесс будет обра-
батывать клиентский запрос. Затем сервер TFTP вызывает функцию exi t, отправляя демону 1 netd сигнал SIGCHLD, который, как мы сказали, указывает демону i netd снова вызвать функцию select на сокете, связанном с портом UDP 69.
Информация о пакете IPv6
IPv6 позволяет приложению определять до четырех характеристик исходящей дейтаграммы:
I Руб-адрес отправителя.
Индекс интерфейса для исходящих дейтаграмм.
Предельное количество транзитных узлов для исходящих дейтаграмм.
Адрес следующего транзитного узла.
Эта информация отправляется в виде вспомогательных данных с функцией sendmsg. Для полученного пакета могут быть возвращены три аналогичных характеристики. Они возвращаются в виде вспомогательных данных с функцией recvmsg: 1. IPv6-адрес получателя.
Индекс интерфейса для входящих дейтаграмм.
Предельное количество транзитных узлов для входящих дейтагарамм.
На рис. 20.5 показано содержимое вспомогательных данных, о которых рассказывается далее.
cmsghdr{}cmsg__len32cmsg_levelIPPROTO IPV6cmsg_typeIPV6 RKTINFO1Ру6-адрес〉in€j>ktinfo{}Индекс интерфейсаcmsghdr{}cmsg__len32cmsg_levelIPPROTO IPV6cmsg_typeIPV6 RKTINFO1Ру6-адрес〉in€j>ktinfo{}Индекс интерфейсаcmsghdr{}cmsg_len cmsg_level cmsg_type Предельное количество транзитных узловcmsghdr{}cmsg_len cmsg_level cmsg_type Предельное количество транзитных узлов16IPPROTO_IPV6IPV6_HOPLIMIT16IPPROTO_IPV6IPV6_HOPLIMIT
cmsghdr{}
cmsg__len
32
cmsg_level
IPPROTO IPV6
cmsg_type
IPV6 RKTINFO
1Ру6-адрес
〉in€j>ktinfo{}
Индекс интерфейса
cmsghdr{}
cmsg__len
32
cmsg_level
IPPROTO IPV6
cmsg_type
IPV6 RKTINFO
1Ру6-адрес
〉in€j>ktinfo{}
Индекс интерфейса
cmsghdr{}
cmsg_len cmsg_level cmsg_type Предельное количество транзитных узлов
cmsghdr{}
cmsg_len cmsg_level cmsg_type Предельное количество транзитных узлов
16
IPPROTO_IPV6
IPV6_HOPLIMIT
16
IPPROTO_IPV6
IPV6_HOPLIMIT
cmsghdr{}
cmsg_len36
cmsg_level IPPROTO_IPV6
cmsg typeIPV6_NEXTHOP
Структура
адреса сокета
Рис. 20.5. Вспомогательные данные для информации о пакете IPv6
Структура in6_pktinfo содержит либо 1Р〃6-адрес отправителя и индекс интерфейса для исходящей дейтаграммы, либо 1Р\,6-адрес получателя и индекс интерфейса для получаемой дейтаграммы:
struct in6_pktinfo {
struct 】n6_8ddr ip】6_dddr. /* IPv6-адрес отправителя/получателя*/
wt ipi6_ifindex /* индекс интерфейса для исходящей/получаемой дейтаграммы*/
}.
Эта структура определяется в заголовочном файле <netwet/m h>, подключение которого позволяет ее использовать. В структуре cmsghdr, содержащей вспомогательные данные, элемент cmsgjevel будет иметь значение IPPR0T0_IPV6, и первый байт данных будет первым байтом структуры i n6_pkt т nfо. В примере, приведенном на рис. 20.5, мы считаем, что между структурой cmsghdr и данными нет заполнения и целое число занимает 4 байта.
Чтобы отправить эту информацию, никаких специальных действий не требуется —нужно только задать управляющую информацию во вспомогательных данных функции sendmsg. Но возвращать эту информацию функция recvmsg будет только если приложение включит параметр сокета IPV6_PKTINF0.
Исходящий и входящий интерфейс
Интерфейсы на узле IPv6 идентифицируются небольшими целыми положительными числами, как мы сказали в разделе 17.6. Вспомните, что ни одному интерфейсу не может быть присвоен нулевой индекс. При задании исходящего интерфейса ядро само выберет исходящий интерфейс, если значение ipi6_ifindex нулевое. Если приложение задает исходящий интерфейс для пакета многоадресной передачи, то любой интерфейс, заданный параметром сокета IPV6_MULTICAST_IF, заменяется на интерфейс, заданный вспомогательными данными (но только для данной дейтаграммы).
Адрес отправителя и адрес получателя IPv6
IPv6-anpec отправителя обычно определяется при помощи функции bind. Но если адрес отправителя поставляется вместе с данными, это может снизить непроизводительные затраты. Э1 от параметр также позволяет серверу гарантировать, что адрес отправителя ответа совпадает с адресом получателя клиентского запроса — некоторым клиентам требуется такое условие, которое сложно выполнить в случае IPv4 (см. упражнение 20.4).
Когда 1Ру6-адрес отправителя задан в качестве вспомогательных данных и элемент ipi6_add「структуры in6_pktinfo имеет значение IN6ADDR_ANY_INIT, возможны следующие сценарии. Если адрес в настоящий момент связан с сокетом, он используется в качестве адреса отправителя. Если в настоящий момент никакой адрес не связан с сокетом, ядро выбирает адрес отправителя. Если же элемент ip】6_addr не является неопределенным адресом, но сокет уже связался с адресом отправителя, то значением элемента ipi6_addr перекрывается уже связанный адрес, но только для данной операции вывода. Затем ядро проверяет, что запрашиваемый адрес отправителя действительно является адресом направленной передачи, присвоенным узлу.
Когда структура i n6_pkti nfo возвращается в качестве вспомогательных данных функцией recvmsg, элемент ipi6_add「содержит IPv6-a〃pec получателя из полученного пакета. По сути, это аналог параметра сокета IP RECVDSTADDR для IPv4. —
Задание и получение предельного количества транзитных узлов
Предельное количество транзитных узлов обычно задается параметром сокета IPV6_UNICAST_H0PS для дейтаграмм направленной передачи (см. раздел 7.8) или параметром сокета IPV6_MULTICAST_H0PS для дейтаграмм многоадресной передачи (см. раздел 19.5). Задавая предельное количество транзитных узлов в составе вспомогательных данных, мы можем заменить как значение этого предела, задаваемое ядром по умолчанию, так и ранее заданное значение — и для направленной, и для многоадресной передачи, но только для одной операции вывода. Предел количества транзитных узлов полученного пакета используется в таких программах, как traceroute, и в некоторых приложениях IPv6, которым нужно проверять, что полученное значение равно 255 (то есть пакет не был переслан с других адресов).
Полученное предельное количество транзитных узлов возвращается в виде вспомогательных данных функцией recvmsg, только если приложение включает параметр сокета IPV6_H0PLIMIT. В структуре cmsghdr, содержащей эти вспомогательные данные, элемент cmsg_1eve1 будет иметь значение IPPR0T0_IPV6, элемент cmsg_type — значение IPV6_H0PLIMIT, а первый байт данных будет первым байтом целочисленного предела повторных передач. Мы показали это на рис. 20.5. Нужно понимать, что значение, возвращаемое в качестве вспомогательных данных,— это действительное значение из полученной дейтаграммы, в то время как значение, возвращаемое функцией getsockopt с параметром IPV6_UNICAST_H0PS, является значением по умолчанию, которое ядро будет использовать для исходящих дейтаграмм на сокете.
Чтобы задать предельное количество транзитных узлов для исходящих пакетов, никаких специальных действий не требуется — нам нужно только указать управляющую информацию в виде вспомогательных данных для функции sendmsg. Обычные значения для предельного количества транзитных узлов лежат в диапазоне от 0 до 255 включительно, но если целочисленное значение равно -1, это указывает ядру, что следует использовать значение по умолчанию.
ПРИМЕЧАНИЕ
Предельное количество транзитных узлов “е содержится в структуре in6_pktinfb по следующей причине. Некоторые серверы UDP хотят отвечать па запросы клиентов, посылая ответы на том же интерфейсе, на котором был получен запрос, с совпадением IРуб-адреса отправителя ответа и IPvG-адрсса получателя запроса. Для этого приложение может включить параметр сокета IPV6_PKTINFO, азатем использовать полученную управляющую информацию из функции reevmsg в качестве управляющей информации для функции sendmsg при отправке ответа. Приложению вообще никак не нужно проверять »ли изменять структуру in6_pktinfo. Но если в этой структуре содержался бы предел количества транзитных узлов, приложение должно было бы проана- 刀изировать полученную управляющую информацию и изменить значение этого предела, поскольку полученный предел не является желательным значением для исходящего пакета.
Задание адреса следующего транзитного узла
Объект вспомогательных данных IPV6_NEXTH0P задает адрес следующего транзитного узла дейтаграммы в виде структуры адреса сокета. В структуре emsghdr, со-
держащей эти вспомогательные данные, элемент cmsg_l evel будет иметь значение IPPROTO」PV6, элемент cmsg_type — значение I PV6_NEXTH0P, а первый байт данных будет первым байтом структуры адреса сокета.
На рис. 20.5 мы показали пример такого объекта вспомогательных данных, считая, что структура адреса сокета — это 24-байтовая структура sockaddr_in6. В этом случае узел, идентифицируемый данным адресом, должен быть соседним для отправляющего узла. Если этот адрес совпадает с адресом получателя IРуб- дейтаграммы, мы получаем эквивалент параметра сокета SO_DONTROUTE. Установка этого параметра требует прав привилегированного пользователя.
Резюме
Существуют приложения, которым требуется знать IP-адрес получателя дейтаграммы UDP и интерфейс, на котором была получена эта дейтаграмма. Чтобы получать эту информацию в виде вспомогательных даннмх для каждой дейтаграммы, можно установить параметры сокета IP^RECVDSTADDR и IP_RECVIF. Аналогичная информация вместе с предельным значением количества транзитных узлов полученной дейтаграммы для сокетов IPv6 становится доступна при включении параметра сокета IPV6_PKTINF0.
Несмотря на множество полезных свойств, предоставляемых протоколом TCP и отсутствующих в UDP, существуют ситуации, когда следует отдать предпочтение UDP. UDP должен использоваться для широковещательной или многоадресной передачи. UDP может использоваться в простых сценариях «запрос-ответ», но тогда приложение должно само обеспечить некоторую функциональность, повышающую надежность протокола UDP. UDP ие следует использовать для передачи большого количества данных.
В разделе 20.5 мы добавили нашему клиенту UDP определенные функциональные возможности, повышающие его надежность за счет обнаружения факта потери пакетов, для чего используются тайм-аут п повторная передача. Мы изменяли тайм-аут повторной передачи динамически, снабжая каждый пакет отметкой времени и отслеживая два параметра: период обращения RTT и его среднее отклонение. Мы также добавили порядковые номера, чтобы проверять, что данный ответ — это ожидаемый нами ответ на определенный запрос. Наш клиент продолжал использовать простой протокол остановки и ожидания (stop-and-wait), а приложения такого типа допускают применение UDP.
Упражнения
Почему в листинге 20.16 функция pri ntf вызывается дважды?
Может ли когда-нибудь функция dg_send一recv (см. листинги 20.6 и 20.7) возвратить нуль?
Перепишите функцию dg_send_「ecv с использованием функции sei ect и ее таймера вместо alarm, SIGALRM, sigsetjmp и siglongjmp.
Как может сервер IPv4 гарантировать, что адрес отправителя в его ответе совпадает с адресом получателя клиентского запроса? (Аналогичную функциональность предоставляет параметр сокета IPV6_PKTINF0.)
Функция main в разделе 20.6 является зависящей от протокола (IPv4). Перепишите ее, чтобы она стала не зависящей от протокола. Потребуйте, чтобы пользователь задал один или два аргумента командной строки, первый из которых —необязательный IP-адрес (например, 0.0.0.0 или 0::0\ а второй — обязательный номер порта. Затем вызовите функцию udp cl lent, чтобы получить семейство адресов, номер порта и длину структуры адреса сокета.
Что произойдет, если вы вызовете функцию udp_c1ient, как было предложено, не задавая аргумент hostname, поскольку функция udp_client не задает значение AI_PASSI VE функции getaddE nfo?
Соедините клиент, показанный в листинге 20.4, с эхо-сервером через Интернет, изменив функции rtt_ так, чтобы выводилось каждое значение RTT. Также измените функцию dg_send_「ecv, чтобы она выводила каждый полученный порядковый номер. Изобразите на графике полученные в результате значения RTT вместе с оценочными значениями RTT и среднего отклонения.
ГЛАВА 21
Внеполосные данные
Введение
Ко многим транспортным уровням применима концепция внеполосных данных (out-of-band data), которые иногда называются срочными данными (expediteddata). Суть этой концепции заключается в том, что если на одном конце соединения происходит какое-либо важное событие, то требуется быстро сообщить об этом собеседнику. В данном случае «быстро» означает, что сообщение должно быть послано прежде, чем будут посланы какие-либо обычные данные (называемые иногда данными из полосы пропускания), которые уже помещены в очередь для отправки, то есть внеполосные данные имеют более высокий приоритет, чем обычные данные. Но вместо того, чтобы создавать новое соединение между сервером и клиентом, для передачи внеполосных данных используется уже существующее соединение.
К сожалению, когда мы переходим от общих концепций к реальной ситуации, почти в каждом транспортном уровне имеется своя реализация внеполосных данных. В этой главе мы уделим основное внимание модели внеполосных данных TCP. Мы приведем различные примеры обработки внеполосных данных в API сокетов, а затем используем их для написания нескольких простых клиент-серверных функций, способных определить, когда процесс-собеседник становится недоступным или дает сбой, — так называемых функций проверки пульса (heartbeat functions').
Внеполосные данные протокола TCP
В протоколе TCP нет настоящих внеполосных данных. Вместо этого в TCP предусмотрен так называемый срочный режим1 (urgent mode), к рассмотрению которого мы сейчас и приступим. Предположим, процесс записал N байт данных в сокет протокола TCP, и эти данные образуют очередь в буфере отправки сокета н ожидают отправки собеседнику. Эту ситуацию иллюстрирует рис. 21.1. Байты данных пронумерованы от 1 до N.
Теперь процесс записывает один байт внеполосных данных, содержащий символ ASCII а, используя функцию send и флаг MSG OOB:
send(fd “Г, 1. MSG—OOB).
Иногда переводился как «зкстренпыи режим» или «режим срочности». — Примеч персе.
1N1NБуфер отправки сокетаБуфер отправки сокетаПервый отправляемый байтПервый отправляемый байтПоследний отправляемый байтПоследний отправляемый байт
1
N
1
N
Буфер отправки сокета
Буфер отправки сокета
Первый отправляемый байт
Первый отправляемый байт
Последний отправляемый байт
Последний отправляемый байт
Рис. 21-1. Буфер отправки сокета, содержащий данные для отправки
TCP помещает данные в следующую свободную позицию буфера отправки сокета и устанавливает указатель на срочные данные (или просто срочный указатель —uigentpomte?) для этого соединения на первую свободную позицию Этот буфер показан па рис 21 2, а байт, содержащий внеполосные данные, помечен буквами 00В
NN4Пераый отправляемый байт4Пераый отправляемый байтСПОСПОБуфер отправки сокета
N
N
4
Пераый отправляемый байт
4
Пераый отправляемый байт
СП
О
СП
О
Последний Срочныйотправляемый указатель TCPбайт
Рис. 21.2, Буфер отправки сокета, в который добавлен один байт внеполосных данных
ПРИМЕЧАНИЕ
Срочный у казатель TCP указывает на бан г данных, который следует за посчсднпм бай том внеполосных данных (то сеть данных снабженных флагом MSG一ООВ) В киите [941 на с 292-296 говорится чюэто исторически сложившаяся особенность, которая теперь эмулпр\стся во всех реализациях Если посылающий и принимающий прото колы TCP оцинаково интерпрет ируют срочный указатель TCP беспокоиться не о чем В разделе 7 9 мы упомянули новый параметр сокета TCP STDURG, позволяющий нзменягь ишерпретацпю срочною указателя, то есть определя гь па какой байт он указывае г на последним байт срочных данных или ид байт, с^юдующпи за ним Необходимое!и в установке этою параметра возникать не должно
Если сосюяние буфера таково, как показано на рис 21 2, то в заголовке TCP следующего отправленного сегмента будет установлен флаг URG, а поле смещения срочных данных (или просто поле срочного смещения1) будет указывать на байт, следующий за байтом с внеполосными данными Но згот сегмент может содержать бант, помеченный как 00В, а может ине содержа гь ci о Будет ли послан этот байт, зависи! от количества предшесгвующих ему байтов в буфере отправки сокета, от размера cei мента, который TCP пересылает собеседнику, йог текущего размера окна, объявленпо] о собеседником
Также (ошибочно) используется крмш【<указатель срочности» — Примеч перво
ПРИМЕЧАНИЕ
Выше мы использовали 1срмины «срочный уклзлель» (uigcnt pointti) и срочное смещение (urgent offset) На уровне TCP ли термины имею г рл шчныс значения Величина, представленная 16 бигами в заголовке ТС1\ называется срочным смещением и должна бьп ь прибавлена к по 1Ю нослсдовач ельпо! о номера в заюловкс TCP для получения 32 разрядного послсдовагельно! о номера последив! о байта срочных пашнях (то есть срочного указателя) TCP использует срочное смещение, юпько сс ш в 3al。刀овке установлен другой бш, называемый флагом URG Программисту можно не забоппъ- ся об этом различии и работать только со срочным указателем TCP
Важная характеристика срочного режима TCP заключается в следующем заголовок TCP указывает на то, что отправитель вошел в срочнып режим (то есть фпаг URG установлен вместе со срочным смещением), но фактически посы пать байт данных, на который указывает срочнып указатель, не требуется Действительно, если поток данных ТСР остановлен функциями управления потокочг (когда буфер приема сокета получателя заполнен и TCP получателя объявил нулевое окно для отправляющего TCP), то срочное уведомление отправляется без каких-либо данных [105, с 1016-1017], как показано в листингах 21 8 и 21 9 Это одна из причин, по которой в приложениях используется срочный режим TCP (то есть внеполосные данные) срочное уведомление всегда отсылается собеседнику, даже если поток данных остановлен функциями управления потоком TCP
Что произойдет, еош мы отправим несколько байтов внеполосных данных, как в следующем примере send(fd abc 3 MSG_OOB)
В этом примере срочный указатель TCP указывает иа баш, следующий за последним байтом, и, таким образом, последний Gain (с) считается баш ом внеполосных данных
Посмотрим теперь, как выглядит процесс отправки внеполосных данных с точки зрения принимающей стороны
Koi да TCP получает сегмент, в котором установлен флаг URG срочный указатель проверяется для выяснения того, указывает ли он на новые внеполосные данные Иначе говоря, проверяется, впервые ли этот конкретный бант передается в срочном режиме TCP Депо в том, что часто отправляющий TCP посылает несколько cei меитов (обычно в течение короткого промежутка времени), содержащих флаг URG, в ко горых срочный указатель указывает на один и тот же байт данных Только первый из этих сегментов фактически уведомляет принимающий процесс о прибытии новых внеполосных данных
Принимающий процесс извещается о гом, что прибыли новые внепопосные данные Сначала владельцу сокета посылается сигнал SIGURG При этом предполагается, что для установления владельца сокета была вызвана функция fent! или wctl (см табл 7 5) и что для данного сигнала процессом был установлен обработчик сигнача Затем, если процесс блокирован в вызове функции select, которая ждет возникновения исключительной ситуации для дескриптора сокета, происходит возврат из этой функции
Когда байт данных, на который указывает срочный указа 1 ель, фактически прибывает на принимающим TCP, этот баи г может быть помещен отдельно или оставлен вместе с другими данными По умолчанию параметр сокета
SO_OOBINLINE не установлен, поэтому внеполосный байт не размещается в приемном буфере сокета. Вместо этого содержащиеся в нем данные помещаются в отдельный внеполосный буфер размером в один байт, предназначенный специально для этого соединения [105, с. 986-988]. Для процесса единственным способом прочесть данные из этого специального однобайтового буфера яв- 刀яется вызов функции reev, reevfrom или reevmsg с заданием 力刀ага MSG_OOB. Однако еелн процесс устанавливает параметр сокета SLOOBINLINE, то байт данных, на который указывает срочный указатель TCP, остается в обычном буфере приема сокета. В этом случае процесс не может задать 4)刀ar MSG_OOB для считывания данных, содержащихся во внеполосном байте. Процесс сможет распознать этот байт, только коща дойдет до него и проверит отметку внеполосных данных {out-of-band mark) для данного соединения, как показано в разделе 21.3.
Возможны некоторые ошибки:
Если процесс запрашивает внеполосные данные (то есть устанавливает 巾刀аг MSG_OOB), но собеседник таких данных не послал, возвращается EINVAL.
Если процесс был уведомлен о том, что собеседник послал содержащий вне- по刀оенне данные байт (например, с помощью функции select нлн сигнала SIGURG), и пытается считать эти данные, когда указанный байт еще не прнбььл, возвращается ошибка EWOULDBLOCK. В такой ситуации все, что может сделать процесс, — это считать данные из приемного буфера сокета (возможно, сбрасывая данные, еслиогсутствует свободное место для их хранения), чтобы освободить место в буфере для приема байта внено刀осшях данннх, носи刀аеммх собеседником.
Еслн процесс пытается считать одни и те же внеполосные данные несколько раз, возвращается ошибка EINVAL
Если процесс установи刀 параметр сокета SO_OOBINLINE, а затем пытается считать внепо刀оенме данные, задавая флаг MSG_OOB, возвращается EINVAL.
Простой пример использования сигнала SIGURG
Теперь мы рассмотрим тривиальный пример отправки и по刀уиенн只 внеполосных данных. В листинге 21.11 показана программа отправки этих данных.
Листинг 21.1. Простая программа отправки внеполосных данных //oob/tcpsendOl с
2 3 4 5 6 72 3 4 5 6 7fmclude "unp h" mt
2 3 4 5 6 7
2 3 4 5 6 7
mamCint argc. char **argv) { mt sockfd.
if (argc i= 3)
err_quit("usage tcpsendOl <host> <port#>"), sockfd = Tcp_connect(argv[1], argv[2]).
Write(sockfd7 ”123”. 3).
pnntf( "wrote 3 bytes of normal data\en")
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http*// www piter com/download.
sleep(l).
SendCsockfd. "4". 1. MSG_OOB)
prmtf( "wrote 1 byte of 00B dataXen"), 14 sleep(l).
15 WnteCsockfd. "56\ 2).
16 pn ntf( "wrote 2 bytes of normal data\en"), 17 sleep(l).
18 SendCsockfd. и7\ 1. MSG_OOB).
19p门ntf("wrote 1 byte of 00B data'en”).
20 sleep(l).
21 WnteCsockfd, "89". 2):
22 prmtf( "wrote 2 bytes of normal data'en").
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 00B data wrote 2 bytes of normal data wrote 1 byte of 00B data wrote 2 bytes of normal data
В 刀нстннге 21.2 показана принимаюшая программа.
Листинг 21.2. Простая программа для получения внеполосных данных
//oob/tcprecvOl с
frinclude "unp h"
int1istenfd. connfd:
voidsig_urg(int).
mt
main(int argc, char **argv)
{
mt n.
char buff[100].
if (argc == 2)
listenfd = Tcp_liSten(NULL. argv[l]. NULL).
else if (argc ==一3)
listenfd = Tcp_listen(argv[l] argv[2]. NULL).
else一
err_quit("usage tcprecvOl [ <host> ] <port#>").
connfd = Accept(listenfd NULL. NULL).
Листинг 21.2 (продолжение)
Signal(SIGURG, sig_urg).
FcntKconnfd. F—SETOWN. getpidO):
18 for (..) {
19 if ( (n = Read(connfd. buff, sizeof(buff) - D) — 0) {
20 prmtfCreceived EOFXen"):
21exit(O)-
22}
23 buff[n] = 0:/* завершающий нуль */
24printfC'read/d bytes: ^s\en". n. buff):
25}
26 }
27 void
28 sig urg(mt signo)
29 { “
30mtn;
31charbuff[100]:
32 pri ntfCSIGURG recei ved\en"):
33n = Recv(connfd, buff, sizeof(buff) - 1, MSG_OOB).
34buff[n] =0:/* завершающий нуль */ 一
35 printfC'read Md 00B byte %s\en", n, buff):
36 }
Установка обработчика сигнала и владельца сокета
16-17 Устанавливается обработчик сигнала для SIGURG н функция fcntl задает владельца сокета для данного соединения.
ПРИМЕЧАНИЕ
Обратите внимание, что мы не задаем обработчик сигнала, пока нс завергиастся функция accept. Существует небольшая вероятность того, что внеполосные данные могут прибыть после того, как TCP завершит трехэтап ное рукопожатие, но до завершения функции accept. Внеполосные данные мы в этом случае потеряем. Допустим, что мы установили обработчик сигнала перед вызовом функции accept, а также задали владельца п рослу ши васмого сокета (которьн! затем стал бы владельцем присоединенного сокета). Тогда, если внеполосные данные прибудут до завершения функции accept, наш обработчик сигналов еще не получит значения для дескриптора connfd. Если данный сценарий важен для приложения, следует иiппдиализировать connfcl, «вручную» присвоив этому дескриптору значение -1, добавить в обработчик проверку равенства connfd == -1 и при исти1пюсг1и этого условия просто установить флаг, который будет проверяться в главном цикле после вызова accept. За счет этого главный цикл сможет узнать о поступлении внетюлосных ;iaiiиых и считать их.
18-25 Процесс считывает данные из сокета и выводит каждую строку, которая возвращается функцией read. После того как отправитель разрывает соединение, то же самое делает и получатель.
Обработчик сигнала SIGURG
27-36 Наш обработчик сигнала вызывает функцию prmtf, считывает внеполосные данные, устанавливая 巾刀ar MSG_OOB, а затем выводит полученные данные. Обратите внимание, что при вызове функции reev мн запрашиваем до 100 байт, но,
как мы вскоре увидим, всегда возвращается только один байт внепо刀осних данных.
ПРИМЕЧАНИЕ
Как сказано ранее, вызов ненадежной функции printf из обработчика сигнала не рекомендуется. Мы делаем это просто для того, чтобы увидеть, что произойдет с нашей программой.
Ниже приведен результат, который получается, когда мы запускаем эту программу, а затем — программу для отправки внеполосных данных, приведенную в листинге 21.1.
bsdi % tcprecvOl 9999
read 3 bytes: 123
SIGURG received
read 1 00B byte: 4
read 2 bytes. 56
SIGURG received
read 1 00B byte: 7
read 2 bytes• 89
received EOF
Результаты оказались такими, как мы и ожидали. Каждый раз, когда отправитель посылает внеполосные данные, для получателя генерируется сигнал SIGURG, после чего получатель считывает один байт, содержащий внеполосные данные.
Простой пример использования функции select
Теперь мы переделаем код нашего получателя внеполосных данных и вместо сигнала SIGURG будем испо刀ьзовать функцию seiect. В листинге 21.3 показана принимающая программа.
Листинг 21.3. Принимающая программа, в которой (ошибочно) используется функция select для уведомления о получении внеполосных данных
//oob/tcprecv02 с 1 #1 nd ude"unp h"
mt
main(int агде. char **argv)
{
mtlistenfd. connfd.n:
charbuff[100],
fd_setrset. xset.
8
if (argc == 2)
9
listenfd = TcpJistenCNULL, a「gv[l]. NULL):
10
else if (argc == 3)
11
listenfd = Tcp_listen(argv[l] argv[2] NULL).
12
el se
13
err_quit("usage tcprecv02 [ <host> ] <port#>"):
14
connfd = Accept(l】stenfd. NULL. NULL).
15
FD_ZERO(&rset).
16
FD[ZERO(&xset).
17
for (..) {
18
FD SET(connfd. &rset).
FD_SETCconnfd &xset),
Select(connfd + 1 &rset. NULL &xset NULL)
if (FD_ISSET(connfd. &xset)) {
n = Recv(connfd. buff sizeof(buff) - 1. MSG_OOB);
buff[n] = 0./* завершающий нуль */
prmtf( 'read %d COB byte ^s\en" n. buff).
}
if (FDJSSEKconnfd, &rset)) {
if ( (n = Read(connfd. buff, sizeof(buff) - 1)) *= 0) {
prmtfCreceived E0F\en”).
exit(0)
}
buff[n] = 0/* завершающий нуль */
prmtf("read%d bytes %s\en". n buff).
}
}
}
15-20 Процесс вызывает функцию sei ect, которая ждет либо обычные данные (набор дескрипторов для чтения, rset), либо внеполосные (набор дескрипторов для обработки исключений, xset). В обоих случаях полученные данные выводятся.
Если мы запустим эту программу, а затем — программу для отправки, которая приведена в 刀нстннге 21.1, то столкнемся со следующей ошибкой: bsdi % tcprecv02 8888 read 3 bytes 123 read 1 00B byte 4 reev error Invalid argument
Проблема заключается в том, что функция sei ect будет сообщать об исключи- те刀ьной ситуации, пока процесс не считает данные, находящиеся за отметкой внеполосных данных (то есть после них [105, с. 530-531]). Мы не можем считывать внеполосные данные больше одного раза, так как после первого же их считывания ядро очищает буфер, содержащий один байт внепо刀оеннх данных. Когда мы вызываем функцию reev, устанавливая флаг MSG_OOB во второй раз, BSD/ OS возвращает ошибку EINVAL, в то время как Solans 2.5 возвращает ошибку EAGAIN. (Согласно Posix.lg в данном случае должна возвращаться ошибка EINVAL.)
Чтобы решить эту проб刀ему, нужно вызывать функцию select д刀月 проверки на наличие нсклюинте刀ьной ситуации только после того, как будут приняты все обычные данные. В листинге 21.4 показана модифицированная версия принимающей программы из листинга 21.3. В этой версии описанный сценарий обрабатывается корректно.
Листинг 21.4. Модификация программы, приведенной в листинге 21.3. Функция select применяется для проверки исключительной ситуации корректным образом
//oob/tcprecv03 с
#include 'unp h"
2 3 4 5с2 3 4 5сmt
2 3 4 5с
2 3 4 5с
main(int агде. char **argv)
{
int11stenfd connfd. n. justreadoob 二 0
ck r s Kt e-F-Fri ЛЛТ
fd set rset. xset
if (argc == 2)
listenfd = Tcp_listen(NULL, argv[l] NULL).
else if (argc == 3)
listenfd = Tcp_listen(argv[l] argv[2]. NULL)
else-
err_quit( 'usage tcprecv03 [ <host> ] <port#>").
connfd = AcceptClistenfd NULL NULL)
FD_ZERO(&rset).
FDJEROC&xset),
for (..) {
FD_SET(connfd. &rset),
if (justreadoob == 0)
FD_SET(connfd. &xset),
Select(connfd + 1 &rset. NULL &xset NULL).
if (FDJSSEKconnfd. &xset)) {
n = Recv(connfd. buffsizeof(buff)-1MSG_OOB):
buff[n] = 0./* завершающий нуль */
prmtfC'read %d 00B byte^s\en".n. buff),
justreadoob = 1
27FD_CLR(connfd &xset).
28} 一
29if (FD_ISSET(connfd &rset)) {
30 if ( (n = Read(connfd. buff, sizeof(buff) - 1)) »= 0) {
31pnntf("received EOFXen")
32exit(0)
33}
buff[n] = 0./* завершающий нуль */
printf("read %d bytes ^s\en'. n. buff),
justreadoob =0.
}
}
}
Мы объявляем новую переменную с именем justreadoob, которая указывает, какие данные мы считываем — внеполосные или обычные. Этот флаг определяет, нужно ли вызывать функцию sei ect для проверки на на刀нине исключительной ситуации.
26-27 Когда мы устанавливаем флаг justreadoob, мы также должны выключить бит соответствующего дескриптора в наборе для проверки исключительных ситуаций.
Теперь программа работает так, как мы ожидали.
1.3. Функция sockatmark
С приемом внеполосных данных всегда связана так называемая отметка внеполосных данных {out-of-band mark). Это позиция в потоке обычных данных на стороне отправителя, соответствующая тому моменту, когда посььлающнЛ процесс отправляет байт, содержащий внеполосные данные. Считывая данные из сокета, принимающий процесс путем вызова функции sockatmark определяет, находится ли он в ламньтй мсмрнт чтпй стмрткр
#mclude <sys/socket h>
mt sockatmarkCmt sockfd)
/* Возвращает 1. если находится на отметке внеполосных данных 0 если не на отметке, -1 в случае ошибки */
ПРИМЕЧАНИЕ
Эта функция появилась в Posix.lg Разработчики стандарта Роых стремятся заменить отдельными функциями все вызовы ioctl с различными параметрами
В листинге 21.5 показана реализация этой функции с помощью поддерживаемого в большинстве систем параметра SIOCATMARK функции ioctl.
Листинг 21.5. Функция sockatmark реализована с использованием функции ioctl //lib/sockatmark с
#inc1ude "unp h"
mt
sockatmark(mt fd)
{
mt flag
if (ioctl(fd SIOCATMARK &flag) < 0)
return (-1).
return (flag 片 0 ? 1 0).
}
Отметка внеполосных данных применима независимо от того, как принимающий процесс получает внеполосные данные: вместе с обычными данными (параметр сокета SO_OOBINLINE) или отдельно (флаг MSG_OOB). Отметка внеполосных данных часто используется для того, чтобы принимающий процесс мог интерпретировать получаемые данные специальным образом до тех пор, пока он не дойдет до этой отметки.
Пример
Ниже мы приводим простой пример, иллюстрирующий следующие две особенности отметки внеполосных данных:
Отметка внеполосных данных всегда указывает на один байт дальше конечного байта обычных данных. Это означает, что когда внеполосные данные по- 刀унентя вместе с обычными, функция sockatmark возвращает 1, если следующий считываемый байт бььл послан с флагом MSG_OOB. Если параметр SO_OOBINLINE не включен (состояние по умолчанию), то функция sockatmark возвращает 1, когда следующий байт данных является первым байтом, посланным следом за внеполосными данными.
Операция считывания всегда останавливается на отметке внеполосных данных [105, с. 519-520]. Это означает, что если в приемном буфере сокета 100 байт, но только 5 из них расположены перед отметкой внеполосных данных, то когда процесс выполнит функцию read, запрашивая 100 байт, возвратятся только 5 байт, расположенные до этой отметки. Эта вынужденная остановка на отметке позволяет процессу вызвать функцию sockatmark, которая определит, мяуллитгя ли vk2R2tptta АугЪрпя МЯ СТМРТкР НМРПО ппгных данных.
В листинге 21.6 показана наша программа отправки. Она посылает 3 байта обычных данных, 1 байт внеполосных данных, а затем еще один байт обычных данных. Паузы между этими операциями отсутствуют.
В листинге 21.7 показана принимающая программа. В ней не используется ни функция select, ни сигнал SIGURG. Вместо этого в ней вызывается функция sock- atmark, чтобы определить, когда встречается байт внеполосных данных.
Листинг 21.6. Программа отправки
//oob/tcpsen04 с
I include "unp h'
mt
maindnt argc char **argv)
{
int sockfd.
if (argc J 3)
err_quit("usage tcpsend04 <host> <port#>M):
sockfd н Tcp__connect(argv[1] argv[2])
WriteCsockfd ”123' 3).
p门ntf("w「ote 3 bytes of normal data\en")
Send(sockfd ”4' 1 MSG_OOB)
prmtf( ''wrote 1 byte of 00B data\en").
WriteCsockfd "5” 1).
printf("wrote 1 byte of normal data\en").
exit(0)
}
Листинг 21.7. Принимающая программа, в которой вызывается функция sockatmark
"oob/tcp「ecv04 с
#include 'unp h"
mt
main(int argc char **argv)
{
int1istenfd connfd n. on - 1:
charbuff[100]
sp 0 5v
if (argc == 2)
Ъstenfd = Tcp_liSten(NULL argv[l] NULL).
else if (argc = 3)
listenfd = Tcp_hsten(argv[l] argv[2]. NULL)
else~
err_quit(* usage tcprecv04 [ ^host> ] <port#>'*)4
sp 0 5v 一
Setsockopt(1istenfd SOL_SOCKET. SO_OOBINLINE. &on slzeof(on)):
sp 0 5v-“
connfd = AcceptClistenfd NULL NULL)
sleep(5)
sp 0 5v
for ()(
Листинг 21.7 {продолжение)
if (Sockatmark(connfd))
18 prir)tf("at 00B mark'en")
sp 0 5V
19 if ( (n = Read(connfd buff sizeof(buff) - 1)) «« 0) {
20printfCreceived E0F\en").
21exit(0).
22}
buff[n] = 0/* завершающий нуль */
printfCVead td bytes ls\en". n. buff).
}
}
Включение параметра сокета 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), так как мы дали ядру указание поместить внепо- 刀оенме данные вместе с обычными.
Пример
Теперь мы покажем другой столь же простой пример, иллюстрирующий две до- полннте刀ьные особенности внено刀осных данных, о которых мы уже упоминали ранее.
TCP посььлает уведомление об отправке внеполосных данных (их срочный указатель), даже еслн поток данных остановлен функциями управления потоком.
Принимающий процесс может но刀уинть уведом刀енне о том, что отправитель отослал внепо刀осные данные (с помощью сигнала SIGURG нлн функции select) до того, как эти данные фактически прибудут. Еслн после получения этого уведомление процесс вызывает функцию recv, задавая флаг MSG OOB, а внепо- 刀осные данные еще не прнбьглн, то будет возвращена ошибка EWOULDBLOCK.
В листинге 21.8 приведена программа отправки.
Листинг 21.8. Программа отправки //oob/tcpsend05 с
#1nc1ude'unp h"
ir)t
maindnt argc. char **argv)
{
mtsockfd size
6charbuff[16384]
7 if (argc 』3)
8 err_quit( 'usage tcpsend04 <host> vport#>“)
9sockfd = Tcp_connect(argv[l] argv[2])
10size = 32768
11 SetsockoptCsockfd SOL_SOCKET SO_SNDBUF &size sizeof(size)).
12 Wnte(sockfd. buff. 16384),
13printf("wrote 16384 bytes of normal data\en").
14 sleep(5)
15 SencKsockfd. "a". 1 MSG/OB)
16 printfCwrote 1 byte of 00B data\en")
17 Write(sockfd buff 1024),
18 prmtf( wrote 1024 bytes of normal data\en"):
19 exit(0).
20 }
9-19 Этот процесс устанавливает размер буфера отправки сокета равным 32 768 байтам, записывает 16 384 байта обычных данных, а затем на 5 секунд переходит в спящее состояние. Чуть ниже мы увидим, что приемник устанавливает размер приемного буфера сокета равным 4096 байтам, поэтому данные, отправленное отсылающим TCP, с гарантией заполнят приемный буфер сокета получателя. Затем отправитель поем刀ает один байт внеполосных данных, за которым следуют 1024 байта обычных данных, и. наконец, закрывает соединение.
В 刀нстннге 21.9 представлена принимающая программа.
Листинг 21.9. Принимающая программа //oob/tcprecv05 с
1 #шс1 ude иипр h”
2 int2 int11stenfd. connfd11stenfd. connfdГ?Г>лЛл Л "VCPUHP т^Г?Г>лЛл Л "VCPUHP т^
2 int
2 int
11stenfd. connfd
11stenfd. connfd
Г?Г>лЛл Л "VCPUHP т^
Г?Г>лЛл Л "VCPUHP т^
Листинг 21.9 (продолжение)
void sig_urg(int):
mt
mann(int argc. char **argv)
{
mt size,
if (argc == 2)
listenfd = Tcp_listen(NULL. argv[l]. NULL).
else if (argc == 3)
П listenfd = Tcp_listen(argv[l], argv[2], NULL).
else
err_quit("usage tcprecv05 [ <host> ] <port#>"),
size = 4096,
Setsock opt(11stenfd, SDL_SOCKET. SO_RCVBUF, Ssize, sizeof(size)):
connfd = Accepts 1stenfd, NULL, NULL),
Signal(SIGURG, sigjirg),
Fcntl (connfd, F—SETOWN. getpidO),
for (;;)
pause().
}
void
sig_urg(int signo)
24「
intn.
charbuff[2048]:
printfC'SIGURG received\en").
n = Recv(connfd buff, sizeof(buff) - L MSG_OOB):
buff[n] = 0./* завершающий пустой байт */
pnntfC'read 2d COB byte\en". n).
}
4-20 Принимающий процесс устанавливает размер приемного буфера сокета приемника равным 4096 байт. Этот размер наследуется присоединенным сокетом после установления соединения. Затем процесс вызывает функцию accept, задает обработчик для сигнала SIGURG и задает владельца сокета. В главном цикле вызывается функция pause.
2-31 Обработчик сигнала вызывает функцию recv для считывания внеполосных данных.
Если мы запускаем сначала принимающую программу, а затем программу отправки, то получаем следующий результат выполнения программы отправки: solans % tcpsend05 bsdi 5555 wrote 16384 bytes of normal data wrote 1 byte of 00B data wrote 1024 bytes of normal data
Как и ожидалось, все данные помещаются в буфер отправки сокета отправителя, и программа завершается. Ниже приведен результат работы принимающей программы:
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-1017J, можно отмстить две близких черты. Во-первых, даже если приемный буфер сокета заполнен, ядро всегда принимает от процесса внеполосные данные для отправки собеседнику. Во-вторых, когда отправитель посылает байт с внеполосными данными, немедленно посылается сегмент TCP, содержащий срочное уведомление. Все обычные проверки вывода TCP (алгоритм Нагла, предо! вращение синдрома «глупого окна») при этом блокируются.
Пример
Напшм очередным примером мы иллюстрируем тот факт, что для данного соединения TCP существует всего одна отметка внеполосных данных, и если новые внеполосные данные прибудут прежде, чем принимающий процесс начнет считывать пришедшие ранее внеполосные данные, то предыдущая отметка будет утеряна.
В листинге 21.10 показана посылающая программа, аналогичная программе, приведенной в листинге 21.6. Отличие заключается в том, что сейчас мы добавили еще одну функцию send для отправки внеполосных данных и еще одну функцию write для записи обычных данных.
Листинг 21.10. Отправка двух байтов внеполосных данных друг за другом //oob/tcpsend06 с
#include "unp h"
int
main(int argc char **argv)
{
mt sockfd.
if (argc i= 3)
err_quit("usage tcpsendO4 <host> <port#>”).
8 sockfd = Tcp_connect(argv[l]. argv[2]);
9 Wnte(sockfd. "123", 3):
10 printf("wrote 3 bytes of normal data'en"):
11 Send(sockfd, "Л". 1, MSG 00B).i ,“垃
-продолжение 夕
Листинг 21.10 (продолжение)
12printf("wrote 1 byte of 00B data\enH)
13 Write(sockfd, "5", 1)
14printf("wrote 1 byte of normal data\en");
15Send(sockfd ”6". 1. MSG_OOB)
16pnntf("wrote 1 byte of OOB data\en");
17 Write(sockfd, "7", 1),
18printf("wrote 1 byte of normal ddta'en"):
19 exit(O), 20 }
В данном случае отправка данных происходит без пауз, что позволяет быстро переслать данные принимающему TCP.
Принимающая программа идентична программе, приведенной в листинге 21.7, где вызывается функция sleep, которая после установления соединения переводит получателя в спящее состояние на 5 секунд, чтобы позволить данным прибыть на принимающий TCP. Ниже приводится результат выполнения этой программы: bsdi % tcprecv06 5555 read 5 bytes 12345 at 00B mark read 2 bytes 67 received EOF
Прибытие второго байта внеполосных данных (6) изменяет отметку, которая ассоциировалась с первым прибывшим байтом внеполосных данных (4). Как мы сказали, для конкретного соединения TCP допускается только одна отметка внеполосных данных.
Резюме по теме
внеполосных данных TCP
Все приведенные до сих пор примеры, иллюстрирующие использование внеполосных данных, были весьма тривиальны. К сожалению, когда мы начинаем учитывать возможные проблемы, связанные с согласованием во времени при пересылке внеполосных данных, ситуация заметно усложняется. В первую очередь, нужно осознать, что концепция внеполосных данных подразумевает передачу получателю трех различных фрагментов информации:
Сам факт того, что отправитель вошел в срочный режим. Принимающий процесс получает уведомление об этом либо с помощью сигнала SIGURG, либо с помощью функции sei ect. Это уведомление передается сразу же после того, как отправитель посылает байт внеполосных данных, поскольку, как показано в листинге 21.9, TCP посылает уведомление, даже если поток каких-либо данных от сервера к клиенту остановлен функциями управления потоком. В результате получения такого уведомления получатель может входить в определенный специальный режим обработки последующих данных.
Позиция байта, содержащего внеполосные данные, то есть расположение этого байта по отношению к остальным данным, посланным отправителем, иначе говоря, отметка внеполосных данных.
Фактическое значение внеполосного байта. Поскольку TCP является потоковым протоколом, который не интерпретирует данные, посланные приложением, это может быть любое 8-разрядное значение.
В срочном режиме TCP мы можем рассматривать флаг URG как уведомление, а срочный указатель как внеполосную отметку.
Проблемы, связанные с концепцией внеполосных данных, сформулированы в следующих пунктах:
Для каждого соединения имеется только один срочный указатель.
Для каждого соединения допускается только одна отметтса внеполосных данных.
Для каждого соединения имеется только один одиобайтовып буфер, предназначенный для внеполосных данных (это имеет значение, только если внеполосные данные не считываются вместе с обычными данными).
В листинге 21.10 показано, что вновь прибывшая отметка внеполосных данных отменяет все предыдущие отметки, до которых принимающий процесс еще не дошел. Если внеполосные данные считываются вместе с обычными данными, то в случае прибытия новых внеполосных данных предыдущие не теряются, но теряются их отметки.
Широкое применение внеполосных данных связано с протоколом Rlogin, когда клиент прерывает программу, выполняемую на стороне сервера [94, с. 393394]. Сервер должен сообщить ктшенту, что нужно сбросить все данные, принятые от сервера, буферизованные и предназначенные для вывода на терминал. Сервер посылает клиенту специальный байт внеполосных данных, указывая тем самым, что необходимо сбросить все полученные данные. Когда клиент получает сигнал SIGURG, он просто считывает данные из сокета, пока не встречает отметку внеполосных данных, после чего он сбрасывает все данные вплоть до этой отметки. (На с. 398-401 [94] показан пример подобного использования внеполосных данных вместе с выводом программы tcpdump.) Если в этом сценарии сервер посылает несколько внеполосных байтов, следующих с небольшими промежутками друг за другом, то такая последовательность не оказывает влияния на клиент, поскольку клиент просто сбрасывает все данные, расположенные до последней отметки внеполосных данных.
В итоге можно сказать, что польза применения внеполосных данных зависит от того, для каких целей они служат в приложении. Если их назначение в том, чтобы сообщить собеседнику о необходимости сбросить все обычные данные, расположенные до отметки, то утрата промежуточных внеполосных данных и их отметок не повлечет никаких последствий. Но если потеря внеполосных данных недопустима, то эти данные следует получать вместе с обычными данными. Более того, байты, посланные как внеполосные данные, требуется каким-то образом отличать от обычных данных, так как промежуточные отметки могут быть перезаписаны при получении новых внеполосных данных. Telnet, например, посылает свои собственные команды в потоке обычных данных между клиентом
и сервером, но ставит перед этими командами байт, содержащий 255 (поэтому для отправки этого значения требуется послать последовательно два байта, содержащих 255). Эти байты позволяют отличить команды сервера от обычных пользовательских данных, но при этом для обнаружения команд сервера требуется, чтобы клиент и сервер обрабатывали каждый байт данных.
Клиент-серверные функции проверки пульса
Теперь мы разработаем несколько простых функций проверки пульса для наших эхо-сервера и эхо-клиента. Эти функции способны быстро выявить сбой как на самом узле, так и в канале связи с ним.
Перед тем как рассматривать эти функции, мы хотели бы привести несколько советов по их применению. Некоторые программисты пытают ся использовать для обеспечения этой функциональности параметр 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, а при получении внеполосного байта обнуляют ее. Если этот
Раз в секундуРаз в секундуОдно соединение TCPОдно соединение TCPРис. 21.3. Использование внеполосных данных для определения «жизнеспособности» клиента и сервераРис. 21.3. Использование внеполосных данных для определения «жизнеспособности» клиента и сервера
Раз в секунду
Раз в секунду
Одно соединение TCP
Одно соединение TCP
Рис. 21.3. Использование внеполосных данных для определения «жизнеспособности» клиента и сервера
Рис. 21.3. Использование внеполосных данных для определения «жизнеспособности» клиента и сервера
счетчик доходит до 5 (это значит, что в течение 5 секунд от собеседника не был получен внеполосный байт), считается, что произошел сбой. И клиент, н сервер для получения уведомления о прибытии внеполосного байта используют сигнал SIGURG. На указанном рисунке видно, что обычные данные, эхо-данные (отраженные данные) и внеполосные данные передаются по одному и тому же соединению TCP.
Функция mam клиента — это та же функция main, которая была представлена в листинге 5.3. Наша функция str_cl 1 (которую мы не приводим здесь) лишь немногим отличается от функции, показанной в листинге 6.2.
Мы вызываем нашу функцию hea rtbeat_cl i перед входом в цикл for для установления параметров на стороне клиента:
heartbeat_cli(sockfd, 1, 5)
Второй аргумент функции — это период (в секундах) отправки внеполосного байта, а третий — максимально допустимое время (количество секунд), по прошествии которого соединение будет закрыто.
Если функция select возвращает ошибку EINTR, мы продолжаем (continue) выполнять цикл и снова вызываем функцию sei ect. Обратите внимание, что, согласно рис. 21.3, клиент теперь перехватывает два сигнала — SIGURG и SIGALRM, так что мы должны быть готовы к обработке прерванного системного вызова.
Вместо того чтобы вызывать функцию fputs для помещения отраженной строки в стандартный поток вывода, мы вызываем функцию writein. Мы делаем так, поскольку, как только что было сказано, при перехвате двух сигналов могут быть прерваны медленные системные вызовы, а некоторые версии стандартной библиотеки ввода-вывода не способны корректно обрабатывать прерванные системные вызовы [58].
В листинге 21.11 показаны три функции, обеспечивающие функциональность проверки пульса (сервера клиентом).
Листинг 21.11- Проверка пульса сервера клиентом
//oob/heartbeatcli с
#】nclude *'unp h"
static mt servfd,
static mt nsec/* количество секунд между сигналами*/
static 】nt maxnprobes, /* максимально допустимое число сигналов до закрытия соединения */
static int nprobes,/* число сигналов отправленных с момента последнего ответа
сервера*/
static void sig_urg(int), signalrm(int).
void
hedrtbeat_cli(int servfd_arg.】nt nsec_arg. int maxnprobes_arg)
{—一一"
servfd к servfd_arg. /* задание значений глобальных переменных для обработчиков сигналов */
if ( (nsec н nsec_arg) < 1)
nsec = 1
if ( (maxnprobes = maxnprobes^arg) < nsec)
maxnprobes =» nsec,
nprobes * 0,
Signal(SIGURG. sig_urg),
Fcntl(servfd. F_SET0WN. getpidO);
Signal(SIGALRM. signal rm):
alarm(nsec).
}
static void
sig_urg(int signo)
{ ~
intn.
charc.
if ( (n = recv(servfd. &c, 1. MSG 00B)) < 0) {
if (errno J EWOULDBLOCK) -
err_sys("recv error"),
}―
nprobes = 0 /* обнуление счетчика */
return. /* может прервать выполнение клиентского кода */
}
static void
signalrm(int sign。)
{ ~
if (++nprobes > maxnprobes) {
fpnntf(stderr, "server is unreachable\enM):
exit(O)
}
Send(servfd "Г 1. MSG_OOB),
alarm(nsec)
return, /* может прервать выполнение клиентского кода */
}
Глобальные переменные
2-5 Первые три переменные — это копии аргументов функции heartbeat一cli: дескриптор сокета (он необходим обработчикам сигналов для отправки и получе-
ния внеполосных данных), период сигналов SIGALRM и максимально допустимое количество таких сигналов (если на вес эти сигналы не поступило ответа от сервера, то клиент решает, что сервер или соединение неисправны). Переменная nprobes представляет собой счетчик сигналов SIGALRM, отправленных с момента последнего ответа сервера.
Функция heartbeat_cli
7-20 Функция heartbeat_cl 1 проверяет правильность аргументов и сохраняет их. Для сигналов SIGALRM и SIGURG устанавливаются обработчики сигналов, а функция fcntl делает процесс владельцем сокета. Функция alarm задает время отправки первого сигнала SIGALRM.
Обработчик сигнала SIGURG
21-32 Этот сигнал генерируется, когда прибывает внеполосное уведомление. Мы пытаемся считать внеполосный байт, но даже если он не прибыл (ошибка EWOULDBLOCK), это игнорируется. Обратите внимание, что мы не получаем внеполосный байт вместе с обычными данными, так как это нарушило бы считывание клиентом обычных данных.
Поскольку сервер функционирует нормально, переменная nprobes обнуляется.
Обработчик сигнала SIGALRM
33-43 Этот сигнал генерируется регулярно. При этом увеличивается счетчик nprobes, и если он достигает величины maxprobes, мы делаем заключение, что узел сервера либо недоступен, либо неисправен. В данном примере мы заканчиваем на этом клиентский процесс, хотя возможны и другие варианты: сигнал может быть послан в основной цикл пли функция heartbeat_cl i может иметь другой аргумент — функцию, вызываемую в том случае, если сервер не отвечает в течение заданного времени.
Байт, содержащий символ "1" (фактическое содержимое этого байта не имеет никакого специального значения), посылается в качестве внеполосных данных, а функция alarm управляет временем отправки очередного сигнала SIGALRM.
Функция main сервера идентична той, которая приведена в листинге 5.9. Единственное ее отличие от функции str_echo из листинга 5.2 заключается в дополнительной строке heartbeat_serv(sockfd 1 5)
перед началом цикла for. Таким образом происходи г ртнициализация функции проверки пульса клиента сервером.
В листинге 2L12 показаны функции проверки пульса клиента сервером.
Листинг 21.12. Функции проверки пульса клиента //oob/heartbeatserv c
#mclude 'unp h"
static int servfd.
static mt nsec /* количество секунд между сигналами */
static mt maxnalarms: /* максимально допустимое число сигналов до закрытия соединения */
static mt nprobes,/* число сигналов отправленных с момента последнего ответа
клиента */продолжение 少
Листинг 21.12 (продолжение)
static void sig_urg(int) sig_alrm(mt),
void
heartbeat_serv(wt servfd_arg. int nsec_arg. mt maxnalarms^arg)
---
servfd = servfd_drg /* задание значений глобальных переменных для обработчиков ~ сигналов */
И if ( (nsec н nsejarg) < 1)
nsec = L 一
if ( (maxnalarms = maxnalarms_arg) < nsec)
maxnalarms = nsec,
Signal(SIGURG, sig_urg).
FcntKservfd. F^SETOWN, getpidO):
Signal(SIGALRM. signal rm),
alarm(nsec).
}
static void
sig urg(mt signo)
{ “
intn.
charc.
25 if ( (n » recv(servfd. &c, 1, MSG 00B)) < 0) {
if (errno != EWOULDBLOCK) - '
err sysC'recv error").
}“
Send(servfd. &c, 1, MSG_OOB). /* отражение внеполосного байта */
nprobes e 0,/* установка в 0 счетчика */
return./* может 叩ервать выполнение кода сервера */
}
33 static void
34 sig_alrm(mt signo)
35 {-
36 if (++nprobes > maxnalarms) {
37printf(f,no probes from chent\en"),
38exit(0).
39}
40 alarm(nsec),
41 return/* может прервать выполнение кода сервера */
42 }
Функция heartbeat_serv
7-19 Объявления переменных и функция heartbeat_serv практически идентиянН соответствующим переменным и функции на стороне клиента.
Обработчик сигнала SIGURG
20-32 Когда внеполосное уведомление принимается сервером, он пытается считать полученный байт. Как и на стороне клиента, даже если внеполосный байт не прибыл, ошибка игнорируется. Этот внеполосный байт отражается и посылается назад клиенту также в виде внеполосных данных. Обратите внимание, что если
функция recv возвращает ошибку EWOULDBLOCK, то что бы ни содержалось в динамической локальной переменной (automatic variable) с, оно отражается и отсылается назад клиенту. Мы не используем значение (содержимое) байта внеполосных данных, поэтому здесь не возникает никаких проблем. Имеет значение лишь сам факт отправки одного байта внеполосных данных, каково бы ни было его содержимое. Переменная nprobes обнуляется, поскольку мы только что получили уведомление о том, что с клиентом все в порядке.
Обработчик сигнала SIGALRM
33-42 Переменная nprobes увеличивается на 1, и если ее значение достигает заданной величины maxprobes, процесс на стороне сервера завершается. В противном случае посылается очередной сигнал SIGALRM.
Резюме
В TCP не существует настоящих внеполосных данных. Вместо этого при переходе отправителя в срочный режим собеседнику отсылается в TCP-заголовке срочный указатель. Получение этого указателя на другом конце соединения служит уведомлением для процесса о том, что отправитель вошел в срочный режим, а указатель указывает на последний байт внеполосных (срочных) данных. Но эти данные отсылаются через то же соединение и подчиняются обычным функциям управления потоком данных TCP.
В API сокетов срочный режим TCP сопоставляется внеполосным данным. Отправитель входит в срочный режим, задавая флаг MSG_OOB при вызове функции send. Последний байт данных, переданных с помощью этой функции, считается внеполосным байтом. Приемник получает уведомление о том, что его TCP получил новый срочный указатель. Это происходит либо с помощью сигнала SIGURG, либо с помощью функции select, которая указывает, что на сокете возникла исключительная ситуация. По умолчанию TCP извлекает байт с внеполосными данными и помешает его в специальный однобайтовый буфер для внеполосных данных, откуда принимающий процесс считывает его с помощью вызова функции recv с флагом MSG_OOB. Имеется другой вариант — получатель может включить параметр сокета SO_OOBINLINE, и тогда внеполосный байт остается в потоке обычных данных. Независимо от того, какой метод используется принимающей стороной, уровень сокета поддерживает отметку внеполосных данных в потоке данных, и операция считывания остановится, когда дойдет до этой отметки. Чтобы определить, достигнута ли эта отметка, принимающий процесс использует функцию sockatmark.
Внеполосные данные применяются не очень широко. Они используются в протоколах Telnet и Rlogin, а также FTP. В FTP они находят применение лишь потому, что ранние реализации этого протокола не обеспечивали мультиплексирование ввода-вывода. Внеполосные данные были предложены тогда, когда ресурсов (памяти и времени центрального процессора) не хватало. Сейчас при создании новых приложений, для которых требуется второй канал связи между собеседниками, обладающий высоким приоритетом и не подверженный контролю со стороны функций управления потоком, предпочтительнее создать новое соединение, чем использовать внеполосные данные.
Упражнения
Есть ли разница между одним вызовом функции
send(fd. "ab". 2. MSG_OOB)
и двумя последовательными вызовами
send(fd 飞二 1 MSG_OOB).
send(fd. -b". 1. MSG[OOB)
Переделайте программу, приведенную в листинге 21.4, так, чтобы использовать функцию poll вместо функции select.
Переделайте функцию s i g_a 1 rm из листингов 21.11 и21.12 так, чтобы в случае положительного значения счетчика nprobes (то есть если предыдущий запрос остался без ответа) с помощью функции write выводилось бы сообщение об этом. Запустите клиентскую и серверную программы на двух узлах в одной локальной сети и посмотрите, как часто будет выводиться это сообщение. Подключите стандартный поток ввода клиента к большому текстовому файлу, а стандартный поток вывода направьте во временный файл. Запустите снова клиентскую программу н сравните входной файл с выходным, чтобы проверить, все ли данные были доставлены. Стало ли сообщение выводиться чаще при передаче большого количества данных? Запустите клиентскую и серверную программы на двух узлах в глобальной сети и сравните результаты с передачей по локальной сети.
Перепишите клиент-серверные функции проверки пульса для случая, когда используется второе соединение TCP. а не срочные данные. Выполните задания из предыдущего упражнения и сравните результаты.
ГЛАВА 22
Управляемый сигналом ввод-вывод
Введение
Ввод-вывод, управляемый сигналом, подразумевает, что мы указываем ядру проинформировать нас сигналом, если что-либо произойдет с дескриптором. Исторически такой ввод-вывод назвали асинхронным вводом-выводом, но в действительности описанный ниже управляемый сигналом ввод-вывод асинхронным не является. Последний обычно определяется как операция ввода-вывода с немедленным возвратом управления процессу после инициирования операции в ядре. Процесс продолжает выполняться во время того, как производится ввод-вывод. Когда операция ввода-вывода завершается или обнаруживается некоторая ошибка, процесс некоторым образом оповещается. В разделе 6.2 проводилось сравнение всех возможных типов ввода-вывода и было показано различие между вводом-выводом, управляемым сигналом, и асинхронным вводом-выводом.
Следует отметигь, что неблокируемый ввод-вывод, описанный в главе 15, также нс является асинхронным. При неблокпруемом вводе-выводе ядро не возвращает управление после инициирования операции ввода-вывода. Управление возвращается немедленно, только если операция не может быть выполнена без блокирования процесса.
ПРИМЕЧАНИЕ
Стандарт Posix 1 обеспечивает истинный асинхронный в вод-вывод с помощью функций аю_ХХХ Эти функции позволяю г процессу решить, гепсри])овать ли при завершении ввода-вывода сигнал и какой именно
Беркли-реализации поддерживают ввод-вывод, управляемый сигналом, для сокетов и устройств вывода с помощью сигнала SIGIO SVR4 поддерживает ввод- вывод, управляемый сигналом, для потоковых устройств с помощью сигнала SIGPOLL, который в данном случае приравнивается к SIGIO.
Управляемый сигналом ввод-вы вод для сокетов
Для использования ввода-вывода, управляемого сигналом, с сокетом (SIGIO) не, обходимо, чтобы процесс выполнил три следующих этапа:
Установка обработчика сигнала SIGI0.
Определение владельца сокета. Обычно это выполняется с помощью команды F_SETOWN функции fcntl (см. табл. 7.5).
Для сокета должен быть разрешен управляемый сигналом ввод-вывод, что обычно выполняется с помощью команды F_SETFL функции fcntl или путем включения флага О_ASYNC (см. табл. 7.5).
ПРИМЕЧАНИЕ
Флаг O ASYNC появился в стандарте Posix.lg. Ни в одной из систем, приведенных на рис. 1.7, этот флаг не поддерживается. Для разрешения управляемого сигналом ввода- вывода вместо этого флага мы используем в листинге 22.2 функцию ioctl с флагом FIOASYNC. Следует отметить, что разрабогчики Posix. 1g выбрали не самое удачное имя для нового флага: ему больше подходит имя O SIGIO.
Обработчик сигнала должен быть установлен до того, как будет задан владелец сокета. В Беркли -реализациях порядок вызова этих функций не имеет значения, поскольку но умолчанию сигнал SIGIO игнорируется. Поэтому если изменить порядок вызова функций на противоположный, появится небольшая вероятность того, что сигнал будет сгенерирован после вызова функции fcntl, но перед вызовом функции signal. Однако если это произойдет, то сигнал просто нс будет учитываться. В SVR4 SIGIO определяется в заголовочном файле <sys/signal.h> как SIGPOLL, а действием по умолчанию для SIGPOLL является прерывание процесса. Таким образом, в SVR4 желательно быть уверенным в том, что обработчик сигнала установлен до задания владельца сокета.
Перевести сокет в режим ввода-вывода, управляемого сигналом, несложно. Сложнее оказывается определение условий, которые должны приводить к генерации сигнала SIGIO для владельца сокета. Это зависит от лежащего в основе протокола.
Сигнал SIGIO и сокеты UDP
Использовать ввод-вывод, управляемый сигналом, с сокетами UDP довольно легко. Сигнал генерируется в следующих случаях:
на сокет прибывает дейтаграмма;
на сокете возникает асинхронная ошибка.
Таким образом, когда мы перехватываем сигнал SIGIO для сокета UDP, вызывается функция reevfrom как для чтения дейтаграммы, так и для получения асинхронной ошибки. Асинхронные ошибки, касающиеся UDP-сокетов, обсуждались в разделе 8.9. Напомним, что эти сигналы генерируются, только если сокет UDP является присоединенным (создан с помощью вызова функции connect).
ПРИМЕЧАНИЕ
Сигнал SIGIO генерируется для этих двух условий путем вызова макроса sorwakeup, описываемого в книге [105, с. 775, с. 779 с. 784].
Сигнал SIGIO и сокеты TCP
К сожалению, использовать управляемый сигналом ввод-вывод для сокетов TCP почти бесполезно. Проблема состоит в том, что сигнал генерируется слишком
часто, а само по себе возникновение сигнала нс позволяет выяснить, что произошло. Как отмечается на с. 439 книги [105], генерацию сигнала SIGIO для ТСР-соке- та вызывают все нижеперечисленные ситуации (притом, что ввод-вывод, управляемый сигналом, разрешен):
на прослушиваемом сокете выполнен запрос на соединение;
инициирован запрос на отключение;
запрос на отключение выполнен;
половина соединения закрыта;
данные доставлены на сокет;
данные отправлены с сокета (то есть в1 буфере отправки имеется свободное место);
произошла асинхронная ошибка.
Например, если одновременно осуществляется и чтение, и запись в ТСР-со- кет, то сигнал SIGIO генерируется, и когда поступают новые данные, и когда подтверждается прием ранее записанных данных, а обработчик сигнала не имеет возможности их различить. Если используется сигнал SIGIO, то для предотвращения блокирования при выполнении функции read или write TCP-сокет должен находиться в режиме неблокируемого ввода-вывода. Следует использовать сигнал SIGIO лишь с прослушиваемым сокетом TCP, поскольку для прослушиваемого сокета эют сигнал генерируется только при завершении установления нового соединения.
Единственное реальное применение управляемого сигналом ввода-вы вода с сокетами, которое удалось обнаружить автору, — это сервер NTP (Network Time Protocol — сетевой протокол синхронизации времени), использующий протокол UDP. Основной цикл этого сервера получает дейтаграмму от клиента н посылает ответ. Но обработка клиентского запроса на этом сервере требует некоторого ненулевого количества времени (больше, чем для нашего тривиального эхо-сервера).
Процесс-сервер
Рис. 22.1. Два варианта построения UDP-сервера
Серверу важно записать точные отметки времени для каждой принимаемой дейтаграммы, поскольку это значение возвращается клиенту и используется им для вычисления времени обращения к серверу (RTT). На рис. 22.1 показаны два варианта построения такого UDP-сервера.
Большинство UDP-серверов (включая эхо-сервер, описанный в главе 8) построены так, как показано на рисунке слева. Однако NTP-сервер использует способ, показанный справа: когда прибывает новая дейтаграмма, она читается обработчиком сигнала SIGIO, который также записывает время прибытия дейтаграммы. Далее дейтаграмма помещается в другую очередь внутри процесса, из которой она будет удалена, а затем обработана основным циклом сервера. Это усложняет код сервера, но зато обеспечивает точные отметки времени прибытия дейтаграмм.
ПРИМЕЧАНИЕ
Напомиим, что согласно листингу 20.3, процесс может установить параметр сокета IPRECVDSTADDR, чтобы получить адрес получателя пришедшей U D Р-дейтаграм - мы. Можно возразить, что вместе с полученной дейтаграммой UDP должны быть воз- враьцены два дополнительных фрагмента информации — интерфейс, на котором была получена дейтаграмма (этот интерфейс может отличаться от адреса получателя, если узел использует более типичную модель системы с гибкой привязкой), и время прибытия дей таграммы.
Для IPv6 шпсрфсйс, па котором была получена дей га грамма, можно получить, если включен параметр сокета IРV6 PKTINFО (см. раздел 20.8). Апало! ичнып параметр сокета IP—RECVIF для IPv4 описывался в разделе 20 2.
В FreeBSD также предусмогрсн параметр сокета SO_TIMESTAMP, возвращающий время получения дей гахраммы как bciюмогателыibie данные в структ урс timeval. В Linux существует флаг SIOCGSTAMP для функции ioctl, которая возвращает структуру timcval, содержащую время прибытия дейтаграммы.
Эхо-сервер UDP с использованием сигнала SIGIO
В этом разделе мы приведем пример, аналогичный правой части рис. 22.1: UDP- сервер, использующий сигнал SIGIO для получения приходящих дейтаграмм. Этот пример также иллюстрирует использование надежных сигналов стандарта Posix.
В данном случае клиент совсем не изменен по сравнению с листингами 8.3 и 8.4, а функция сервера mai п не изменилась по сравнению с листингом 8.1. Единственные внесенные изменения касаются функции dg_echo, которая будет приведена в следующих четырех листингах. В листинге 22.11 представлены глобальные объявления.
Листинг 22.1. Глобальные объявления
//sigio/dgechoOl с
#1 nd ude "unp hM
static int sockfd
Вес исходные коды iipoipaMM, опубликованные в этой-книге, вы можете найти по адресу Mtp:// www.picer.com/download
3 #define QSIZE 84 #define MAXDG40963 #define QSIZE 84 #define MAXDG40965678910111213141516171856789101112131415161718typedef struct { void *dg_data. size_t dg_len.struct sockaddr *dg_sa: socklen t dg salen} DG. 一 一static DG dg[QSIZE]static long cntread[QSIZE + 1].static intigetstatic intiputstatic intnqueue..static socklen_t clilen.static void sig_io(int).static void s、g_hup(int):typedef struct { void *dg_data. size_t dg_len.struct sockaddr *dg_sa: socklen t dg salen} DG. 一 一static DG dg[QSIZE]static long cntread[QSIZE + 1].static intigetstatic intiputstatic intnqueue..static socklen_t clilen.static void sig_io(int).static void s、g_hup(int):/* размер входной очереди */
3 #define QSIZE 8
4 #define MAXDG4096
3 #define QSIZE 8
4 #define MAXDG4096
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct { void *dg_data. size_t dg_len.
struct sockaddr *dg_sa: socklen t dg salen
} DG. 一 一
static DG dg[QSIZE]
static long cntread[QSIZE + 1].
static intiget
static intiput
static intnqueue..
static socklen_t clilen.
static void sig_io(int).
static void s、g_hup(int):
typedef struct { void *dg_data. size_t dg_len.
struct sockaddr *dg_sa: socklen t dg salen
} DG. 一 一
static DG dg[QSIZE]
static long cntread[QSIZE + 1].
static intiget
static intiput
static intnqueue..
static socklen_t clilen.
static void sig_io(int).
static void s、g_hup(int):
/* максимальный размер дейтаграммы*/
/* указатель на текущую дейтаграмму */
/* длина дейтаграммы */
/* указатель на sockaddr{} с адресом клиента */
/* длина sockaddr{} */'
/* очередь дейтаграмм для обработки */
/* диагностический счетчик*/
/* следующий элемент для обработки в основном цикле */
/* следующий элемент для считывания обработчиком сигналов */
/* количество дейтаграмм в очереди на обработку
в основном цикле */
/* максимальная длина sockaddr{} */
Очередь принимаемых дейтаграмм
150150dg[i]dg[i]dg[QSIZE-l]dg[QSIZE-l]—ы Структура адреса сокета16—ы Структура адреса сокета16ДейтаграммаДейтаграмма3-12 Обработчик сигнала SIGI0 помещает приходящие дейтаграммы в очередь. Эта очередь является массивом структур DG, который интерпретируется как кольцевой буфер. Каждая структура содержит указатель на принятую дейтаграмму, ее длину и указатель на структуру адреса сокета, содержащую адрес протокола клиента и размер адреса протокола. В памяти размещается столько этих структур, сколько указано в QSIZE (в данном случае 8), и в листинге 22.2 будет видно, что функция dg_echo для размещения в памяти всех структур дейтаграмм и адресов сокетов вызывает функцию mall ос. Также происходит выделение памяти под диагностический счетчик cntread, который будет рассмотрен чуть ниже. На рис. 22.2
150
150
dg[i]
dg[i]
dg[QSIZE-l]
dg[QSIZE-l]
—ы Структура адреса сокета
16
—ы Структура адреса сокета
16
Дейтаграмма
Дейтаграмма
Рис. 22.2- Структуры данных, используемые для хранения прибывающих дейтаграмм и структур адресов их сокетов
приведен массив структур, при этом предполагается, что первый элемент указывает па 150-байтовую дейтаграмму, а длина связанного с ней адреса сокета равна 16
Индексы массивов
13 15 Переменная iget является индексом следующего элемента массива для обработки в основном цикле, а переменная т put — это индекс следующего элемента массива, в котором сохраняется результат действия обработчика сигнала Переменная nqueue обозначает полное количество дейтаграмм, предназначенных для обработки в основном цикле
В листинге 22 2 показан основной цикл сервера — функция dg_echo
Листинг 22.2. Функция dg_echo основной обрабатывающий цикл сервера
//sigio/dgechoOl с
19 void
20 dg_echo(int sockfd_arg SA *pcliaddr socklen_t clilen_arg)
21 {----
22int i
23const int on = 1
24sigset_tzeromask newmaskoldmask
25sockfd =sockfd_arg
26clilen =clilen_arg
27
for (1 !
=0 i < QSIZE i++) { /* иниииализация очереди */
28
dg[i]
dg_data = Malloc(MAXDG)
29
dg[i]
dg_sa = Malloc(clilen)
30
dg[i]
dg_salen = cli1en
31
}
32
iget =
iput = nqueue = 0
33
Signal(SIGHUP sig_hup)
34
Signal(SIGIO sig_io)
35
Fcntl (sockfd F_SETOWN getpidO)
36
loctl(sockfd FIOASYNC &on)
37
Iocti (sockfd FIONBIO &on)
38Sigemptyset(&zeromask)/* инициализация трех наборов сигналов */
39Sigemptyset(&oldmask)
40Sigemptyset(&newmask)
41Sigaddset(&newmask SIGIO) /* сигнал который хотим блокировать*/
4243444542434445Sigprocmask(SIG_BLOCK &newmask &oldmdsk)
42434445
42434445
for ( ) {
while (nqueue == 0)
sigsuspend(&zeromask)/* ждем дейтаграмму для обработки */
46/* разблокирование SIGIO */
47S1др госта sk(SIG_S ETMAS К &oldmask NULL)
48 Sendto(sockfd dgtiget] dg_ddta dgEiget] dg_len 0.
49 dgtiget] dg_sa dgtiget] dg_sdlen) ~
50 if (++iget >= QSIZE)50 if (++iget >= QSIZE)5151iget = 0iget = 0
50 if (++iget >= QSIZE)
50 if (++iget >= QSIZE)
51
51
iget = 0
iget = 0
52/* блокировка SIGIO */
53Siдргостаsk(SIG_BLOCK &newmask &oldmask)
54 nqueue
55}
56 }
Инициализация очереди принятых дейтаграмм
27 32 Дескриптор сокета сохраняется в глобальной переменной, поскольку он необходим обработчику сигналов Происходит инициализация очереди принятых дей- 1аграмм
Установка обработчиков сигналов и флагов сокетов
33 37 Для сигналов SIGHUP (он используется для диа! ностических целей) и SIGIO устанавливаются обработчики С помощью функции fcntl задается владелец сокета, а с помощью функции 】octi устанавливаются флаги ввода-вывода, управляемого сигналом, и неблокируемого ввода-вывода
ПРИМЕЧАНИЕ
Ранее отмечалось, что для разрешения ввода-вывода, управляемо г о сигналом, в Posix 1g применяется флаг O ASYNC функции fcntl. но поскольку большинство систем пока его не иоддерживают, мы нспоиьмуем функцию ioctl Поско аьку большинство систем нс поддерживают флаг ONONBLOCK для включения пеблокируемо! о ввода-вы вода, здесь гакже рассмотрен вариант использования функции ioctl
Инициализация наборов сигналов
38 41 Инициализируется три набора сигналов zeromask (никогда не изменяется), oldmask (хранит старую маску сигнала, когда SIGIO блокируется) и newmask Функция sigaddset включает в набор newmask бит, соответствующий SIGIO
Блокирование SIGIO и ожидание дальнейших действий
42 45 Функция s 1 gprocmask сохраняет текущую маску сигналов процесса в ol dmask, а затем выполняет логическое сложение, сравнивая newmask с текущей маской сигналов Такие действия блокируют сшнал SIGIO и возвращают текущую маску сигналов Далее мы заходим в цикл for и проверяем счетчик nqueue Пока этот счетчик равен нулю, ничего делать не нужно, и мы вызываем функцию sigsuspend Эга функция Posix, сохранив в одной из локальных переменных текущую маску сигналов, н присваивает текущей маске значение аргумента zeromask Так как zeromask является пустым набором сигналов, то разрешаются любые сигналы Как только перехватывается сигнал и завершается обработчик, функция sigsuspend также завершается (Это необычная функция, поскольку опа всегда возвращает ошибку EINTR ) Прежде чем завершиться, функция sigsuspend всегда устанавливает такое значение маски сигналов, которое предшествовало ее вызову (в данном случае newmask) Таким образом i арантпруется, что koi да функция sigsuspend возвращает значение, сигнал SIGIO блокирован Именно поэтому можно проверять счешик nqueue, поскольку известно, что пока он проверяется, сигнал SIGIO не может быть доставлен
ПРИМЕЧАНИЕ
А что произойдет, если сигнал SIGIO нс будет блокирован во время проверки переменной nqueue, используемой совмеспюосновным циклом и обработчиком сигналов*? Может случиться так, что проверка nqueuc покажет пулевое значение, а сразу после проверки возникнет сигнал и nqueuc станет равна 1. Далее мы вызовем функцию sigsuspend и перейдем в режим ожидания, в результате чего пропустим сигнал. После вызова функции sigsuspend мы не выйдем из режима ожидания, пока нс нос гунит дру- юй сит нал Эго похоже на ситуацию гонок (race condition), описанную в разделе 8 5.
Разблокирование SIGIO и отправка ответа
46 51 Разблокируем сигнал SIGIO с помощью вызова sigprocmask, чтобы вернуть маске сигналов процесса значение, сохраненное ранее (oldmask). В этом случае ответ посылается с помощью функции sendto. Индекс iget увеличился на 1, и если его значение совпадает с количеством элементов массива, он снова обнуляется Массив трактуется как кольцевой буфер. Обратите внимание, чго нет необходимости блокировать сигнал SIGIO во время изменения переменной 】get, поскольку этот индекс используется только в основном цикле и никогда не изменяется обработчиком сигнала.
Блокирование SIGIO
52-54 Сигнал SIGIO блокируется, а значение переменной nqueue уменьшается на 1. Во время изменения данной переменной необходимо заблокировать сигнал, поскольку она используется совместно основным циклом и обработчиком сигнала. Также необходимо, чтобы сигнал SIGIO был заблокирован, когда в начале цикла происходит проверка переменной nqueue.
Альтернативным способом является удаление обоих вызовов функции s】g- procmask, находящихся внутри цикла for, что предотвращает разблокирование сигнала н его последующее блокирование. Однако проблема состоит в следующем. в такой ситуации весь цикл выполняется при блокированном сигнале, что уменьшает быстроту реагирования обработчика сигнала. При этом дейтаграммы не будут теряться (если, конечно, буфер приема сокета достаточно велик), но выдача сигнала процессу будет задерживаться на то время, в течение которого сигнал находится в состоянии блокировки. Одной из задач при создании приложений, производящих обработку сигнала, должна быть минимизация времени блокирования сигнала.
Листинг 22.3. Обработчик сигнала SIGIO //sigio/dgechoOl с 57 static void 58 sig_w(int sign。) 59 {- 60ssize_t len
61mtnread
62 DG *ptr 63 for (nread = 0. ) { 64 if (nqueue >= QSIZE) 65err_quit(*receive overflow'1 ):
6768697071727374757667686970717273747576ptr->dg_salen = clilen
67
68
69
70
71
72
73
74
75
76
67
68
69
70
71
72
73
74
75
76
len = recvfrom(sockfd ptr-ndg_data MAXDG 0, ptr->dg_sa &ptr->dg_salen)
if (len < 0) f
if (errno == EWOULDBLOCK)
break/* все сделано очередь на чтение отсутствует */
else
err_sys('recvfrom error')
}_
ptr->dg_len = len
77nread++
78nqueue++
79if (++iput >= QSIZE)
80iput = 0
81}
82 cntread[nread]++,/* гистограмма количества дейтаграмм считанных для
каждого сигнала */
83 }
Во время создания этих обработчиков сигналов была обнаружена следующая проблема, в стандарте Posix сигналы обычно не помещаются в очередь Эю означает, что если во время пребывания внутри обработчика сигналов (при этом гарантируется, что сигнал заблокирован) возникает еще два сигнала, то сигнал выдается еще один раз.
ПРИМЕЧАНИЕ
В ciaiwapie Posix 1 предусмотрено несколько сигналов реального времени, для которых обоепечивается буферизация, однако ряд других сш палов, в том числе и SIGIO, обычно не буферизуются, то ссгь не помещаются в очередь па доставку
Рассмотрим следующий сценарий. Прибывает дейтаграмма и выдается сигнал Обработчик сигнала считывает дей гаграмму и помещает ее в очередь к основному циклу. Но во время работы обработчика сигнала приходят еще две дейтаграммы, вызывая генерацию сигнала еще дважды Поскольку сигнал блокирован, то когда обработчик сигналов возвращает управление после обработки первого сигнала, он запустится снова всего лишь один раз. После второго запуска обработчик считывает вторую дейтаграмму, а третья будет оставлена в очереди приходящих дейтаграмм сокета. Эта третья дейтаграмма будет прочитана, только если (и только когда) придет четвертая Когда придет четвертая дейтаграмма, считана и поставлена в очередь на обработку основным циклом будет именно третья, а не четвертая дейтаграмма.
Поскольку сигналы ие помещаю гея в очередь, дескриптор, установленный для управляемого сигналом ввода-вывода, обычно переводится в неблокируемый режим Обработчик сигнала SIGIO мы кодируем таким образом, чтобы он считывал дейтаграммы в цикле, который прерывается, только когда при считывании возвращается ошибка EWOULDBLOCK.
Проверка переполнения очереди
64-65 Если очередь переполняется, происходит завершение работы. Для обработки такой ситуации существуют и другие способы (например, можно размещать в па-
мяти дополнительные буферы), но для данного примера достаточно простого завершения.
Чтение дейтаграммы
5-76 На неблокируемом сокете вызывается функция reevfrom. Элемент массива, обозначенный индексом 1 put, — это то место, куда записывается дейтаграмма. Если нет дейтаграмм, которые нужно считывать, мы выходим из цикла for с помощью оператора break.
Увеличение счетчиков и индекса на единицу
г-80 Переменная nread является диагностическим счетчиком количества дейтаграмм, читаемых за 1 сигнал. Переменная nqueue — это количество дейтаграмм для обработки основным циклом.
82 Прежде чем обработчик сигналов возвращает управление, он увеличивает счетчик на единицу в соответствии с количеством дейтаграмм, прочитанных за 1 сигнал. Этот массив приведен в листинге 22.4 и представляет собой диагностическую информацию для обработки сигнала SIGHUP.
Последняя функция (листинг 22.4) представляет собой обработчик сигнала SIGHUP, который выводит массив entread. Он считает количество дейтаграмм, прочитанных за 1 сигнал.
Листинг 22.4. Обработчик сигнала SIGHUP
//sigio/dgechoOl с
static void
sig_hup(int signo)
{-
mt
for (i = 0: i <= QSIZE r++)
89 printf("cntread[^d] = ^ld\en", i, cntread[i]),
90 }
Чтобы проиллюстрировать, что сигналы не буферизуются и что в дополнение к установке флага, указывающего на управляемый сигналом ввод-вывод, необходимо перевести сокет в неблокируемый режим, запустим этот сервер с шестью клиентами одновременно. Каждый клиент посылает серверу 3645 строк (для отражения). При этом каждый клиент запускается из сценария интерпретатора (shell script) в фоновом режиме, так что все клиенты стартуют примерно одновременно. Когда все клиенты завершены, серверу посылается сигнал SIGHUP, в результате чего сервер выводит получившийся массив entread:
bsdi % udpservOl
cntread[0] = 2
cntread[l] = 21838
entread[2] = 12
cntread[3] = 1 entread[4] = 0 cntread[5] = 1 entread[6] = 0 entread[7] = 0 cntread[8] = 0
Большую часть времени обработчик сигналов читает только одну дейтаграмму, но бывает, что готово больше одной дейтаграммы. Поскольку мы считываем
дейтаграммы в цикле обработчика сигнала, дейтаграмма, прибывшая во время считывания других дейтаграмм, будет считана вместе с этими дейтаграммами (в том же вызове обработчика), а сигнал об ее прибытии будет отложен и доставлен процессу после завершения обработчика. Это приведет к повторному вызову обработчика, но считывать ему будет нечего (отсюда cntread[0]>0). Наконец, можно проверить, что взвешенная сумма элементов массива (21 838 х 1+12 х 2+1 х 3+ +1x5 = 21 870) равна 6 х 3645 (количество клиентов х количество строк клиента).
.4. Резюме
При управляемом сигналом вводе-выводе ядро уведомляет процесс сигналом SIGIO, если «что-нибудь» происходит на сокете.
Для присоединенного TCP-сокета существует множество ситуаций, которые вызывают такое уведомление, что делает эту возможность практически бесполезной.
Для прослушиваемого TCP-сокета уведомление приходит процессу в случае готовности принятия нового соединения.
Для UDP такое уведомление означает, что либо пришла дейтаграмма, либо произошла асинхронная ошибка: в обоих случаях вызывается reevfrom.
С помощью метода, аналогичного применяемому для сервера NTP, был изменен зхо-сервер UDP для работы с вводом-выводом, управляемым сигналом: мы стремимся выполнить чтение дейтаграммы как можно быстрее после ее прибытия, чтобы получить точную отметку времени прибытия и постановить дейтаграмму в очередь для дальнейшей обработки.
Упражнение
Ниже приведен альтернативный вариант цикла; рассмотренного в -дастин- 乏 22.2:
for (. ) {
Si gprocmask (SIG_BLOCK &newmask &ol drnask)
while (nqueue == 0)
sigsuspend(&zerornask). /* ожидание дейтаграммы для обработки*/ nqueue--.
/* разблокирование SIGIO */
Sigprocmask(SIG_SETMASK, &oldrnask. NULL),
Sendto(sockfd. dg[iget] dg_data. dg[iget].dg_len. 0. dg[iget] dg_sa. dgfiget] dg_salen), 一
if (++iget >= QSIZE) iget = 0.
}
Верна ли такая модификация?
ГЛАВА 23
Программные потоки
Введение
Согласно традиционной модели U nix, когда процессу требуется, чтобы некое действие было выполнено каким-либо другим объектом, он порождает дочерний процесс, используя функцию fork, и этим порожденным процессом выполняется необходимое действие. Большинство сетевых серверов под Unix устроены именно таким образом, как мы видели при рассмотрении примера параллельного (concurrent) сервера: родительский процесс осуществляет соединение с помощью функции accept и порождает дочерний процесс, используя функцию fork, а затем дочерний процесс занимается обработкой клиентского запроса.
Хотя эта концепция с успехом использовалась на протяжении многих лет, с функцией fork связаны определенные неудобства.
Стоимость функции fork довольно высока, так как при се использовании требуется скопировать все содержимое памяти из родительского процесса в дочерний, продублировать все дескрипторы и т. д. Текущие реализации используют технологию, называемую копированием при записи {copy-on-write)у при которой копирование пространства данных из родительского процесса в дочерний происходит лишь тогда, когда дочернему процессу требуется своя собственная копия. Но несмотря на эту оптимизацию, стоимость функции fork остается высокой.
Для передачи данных между родительским и дочерним процессами после вызова функции fork требуется использовать средства взаимодействия процессов (IPC). Передача информации перед вызовом fork не вызывает затруднений, так как при запуске дочерний процесс получает от родительского копию пространства данных и копии всех родительских дескрипторов. Но возвращение информации из дочернего процесса в родительский требует большей работы.
Обе проблемы могут быть разрешены путем использования программных потоков (threads). Программные потоки иногда называются облегченными процесс сами (lightweight processes), так как поток проще, чем процесс. Это означает, что создание потока требует в 10-100 раз меньше времени, чем создание процесса.
Все потоки одного процесса совместно используют его глобальные переменные, поэтому им легко обмениваться информацией, но это приводит к необходимости синхронизации. Однако общими становятся не только глобальные переменные. Все потоки одного процесса разделяют:
,инструкции процесса;
большую часть данных;
открытые файлы (например, дескрипторы);
『 обработчики сигналов и вообще настройки для работы с сигналами (действие сигнала);
текущий рабочий каталог;
идентификаторы пользователя и группы пользователей.
У каждого потока имеются собственные:
'идентификатор потока;
набор регистров, включая счетчик команд и указатель стека;
стек (для локальных переменных и адресов возврата);
переменная errno;
о маска сигналов;
приоритет.
ПРИМЕЧАНИЕ
Как сказано в разделе 11.14, можно рассматривать обработчик сигнала как некую разновидность потока. В традиционной модели Unix у нас имеется основной поток выполнения и обработчик сигнала (другой поток). Если в основном потоке в момент возникновения сш нала происходит корректировка связного списка и обработчик сигнала также пытается изменить связный список, обычно начинается путаница. Основной поток и обработчик сигнала совместно используют одни и те же глобальные переменные, по у каждого из них имеется свой собственный сгск.
В этой книге мы рассматриваем потоки Posix, которые также называются Pthreads (Posix threads). Они были стандартизованы в 1995 году как часть Posix.lc и будут поддерживаться большинством версий Unix. Мы увидим, что все названия функций Pthread начинаются с символов pthread_. Эта глава является введением в концепцию потоков, необходимым для того, чтобы в дальнейшем мы могли использовать потоки в наших сетевых приложениях. Более подробную информацию бы можете найти в [16].
Основные функции для работы с потоками: создание и завершение потоков
В этом разделе мы рассматриваем пять основных функций для работы с потоками, а в следующих двух разделах мы используем эги функции для модификации клиента и сервера TCP таким образом, чтобы в них вместо функции fork использовались программные потоки.
Функция pthread_create
Когда программа запускается с помощью функции ехес, создается один поток, называемый начальным (initial) или главным (main). Дополнительные потоки создаются функцией pthead_create.
#iпс?ude vpthread h>
int pthread_create(pthread_t *tid const pthread_attr_t *attr. void *(*ftjnc)(void *) void *”g)
Возврашает 0 в случае успешного выполнения положительное значение Еххх в случае ошибки
Каждый поток процесса обладает собственным идентификатором потока (thread /О), относящимся к типу данных pthread_t (как правило, это целое число без знака, unsigned mt). При успешном создании нового потока его идентификатор возвращается через указатель tid.
У каждого потока имеется несколько атрибутов-, его приорите г, исходный размер стека, указание па то, должен ли этот поток являться демоном или нет, и т. д. При создании потока мы можем задать эти атрибуия, инициализируя переменную pthread_attr_t, что позволяет заменить значение, заданное по умолчанию. Обычно мы используем значение по умолчанию, в этом случае мы задаем аргумент attr равным пустому указателю.
Наконец, при создании потока мы должны указать, какую функцию будет выполнять этот поток. Выполнение потока начинается с вызова заданной функции, а завершается либо явно (вызовом pthread_exit), либо неявно (когда вызванная функция возвращает управление). Адрес функции задается архументом func, и она вызывается с единственным аргументом-указателем arg. Если этой функции необходимо передать несколько аргументов, следует поместить их в некоторую структуру и передать адрес этой структуры как единственный аргумент функции.
Обратите внимание иа объявления func и arg. Функции передается один аргумент —указатель на неопределенный тип (generic pointer type), vol d *. Это позволяет нам передавать потоку с помощью единственного указателя все, что требуется, и точно так же поток возвращает любые данные, используя этот указатель.
Возвращаемое значение функций Pthread — это обычно 0 в случае успешного выполнения или ненулевая величина в случае ошибки. Но в отличие ог функций сокетов и большинства системных вызовов, для которых в случае ошибки возвращается -1 и переменной errno присваивается некоторое положительное значение (код ошибки), функции Pthread возвращают сам код ошибки. Например, если функция pthread_create не может создать новый когок, так как мы превысили допустимый системный предел количества потоков, функция возвратит значение EAGAIN. Функции Pthread не присваивают переменной errno никаких значений. Соглашение о том, что 0 является индикатором успешного выполнения, а ненулевое значение — индикатором ошибки, не приводит к противоречию, так как все значения Еххх, определенные в заголовочном файле <sys errno h>, являю гея положительными. Ни одному из имен ошибок Еххх ие сопоставлено нулевое значение.
Функция pthreadjoin
Мы можем приостановить выполнение текущего потока и ждать завершения выполнения какого-либо другого потока, используя функцию phtreadjoi п. Сравнивая потоки и процессы Unix, можно сказать, что функция pthread_create аналогична функции fork, а функция pthreadjoin — функции waitpid.
#include <pthread h>
int pthread_join(pthread_t tid void **status)
Возврашает 0 в случае успешного выполнения положительное значение Еххх в случае ошибки
Следует указать идентификатор tid того потока, завершения которого мы ждем.
К сожалению, нет способа указать, что мы ждем завершения любого потока данного процесса (тогда как при работе с процессами мы могли с помощью функции waitpi d ждать завершения любого процесса, задав аргумент идентификатора процесса, равный -1). Мы вернемся к этой проблеме при обсуждении листинга 23.11.
Если указатель status непустой, то значение, возвращаемое потоком (указатель на некоторый объект), хранится в ячейке памяти, на которую указывает status.
Функция pthread_self
Каждый поток снабжен идентификатором, уникальным в пределах данного процесса. Идентификатор потока возвращается функцией pthread_create и, как мы видели, используется функцией pthreadjoi п. Поток может узнать свой собственный идентификатор с помощью вызова pthread_self.
#include <pthread h>
pthredd_t pthread_se1f(void).
Возвращает идентификатор вызывающего потока
Сравнивая потоки и процессы Unix, можно отметить, что функция pthread sel f аналогична функции getpid.
Функция pthread_detach
Поток может быть либо присоединяемым (joinable), каким он является по умолчанию, либо отсоединенным (detached). Когда присоединяемый поток завершает свое выполнение, его статус завершения и идентификатор сохраняются, пока другой поток данного процесса не вызовет функцию pthreadjoin. В свою очередь, отсоединенный поток напоминает процесс-демон: когда он завершается, все занимаемые им ресурсы освобождаются и мы не можем отслеживать его завершение. Если один поток должен знать, когда завершится выполнение другого потока, нам следует оставить последний присоединяемым.
Функция 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^ex?t(void ^status)
Ничего не возвращает вызвавшему потоку
Если поток не является отсоединеныым, идентификатор потока и статус завершения сохраняются до того момента, пока какой-либо другой поток данного процесса не вызовет функцию pthreadjoi п.
Указатель status нс должен указывать на объект локальный по отношению к вызывающему потоку так как этот объект будет уничтожен при завершении потока
Существуют и другие способы завершения потока
Функция, которая была вызвана потоком (третий аргумент функции pthread_ create), может возвратить управление в вызывающий процесс Поскольку со гласно своему объявлению эта функция возвращает указатель void возвращаемое ею значение играет роль статуса завершения (exit status) даннох о потока
Если функция main данного процесса возвращает управление или любой по ток вызывает функцию exit, процесс завершается, в том числе завершается выполнение всех потоков
Использование потоков
в функции str cli
В качестве первого примера использования потоков мы перепишем нашу функ цию 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 cli, использующая потоки //threads/strclithread с
1 #】nclude unpthread h
void *copyto(void *)
static wt sockfd /* глобальная переменная к которой имеют доступ оба потока */
Все исходные коды программ опубликованные в этой книге вы можете найти по адресу http 〃 wwu piter com/download
4567890145678901static FILE *fp
45678901
45678901
void
str_cli(FILE *fp_arg 、nt sockfd_arg)
charrecvline[MAXLINE]
pthread_t tid
sockfd - sockfd/rg /* копирование 叩「ументо© во внешние Иеремённые */ fp = fp arg
Pth readme reate(&tid NULL copyto NULL)
while (Readline(sockfd recvline MAXLINE) > 0)
Fputs(recvl me stdout)
}
void *
copyto(void *arg)
{
charsendline[MAXLINE]
while (Fgets(sendhne MAXLINE fp)』NULL)
Wnten(sockfd sendline strlen(sendline))
Shutdown(sockfd SHUT_WR) /* признак конца файла в стандартном потоке вво/и
отправка сегмента FIN */
return (NULL)
/* завершение потока происходит когда в стандартном потоке ввода встречается
признак конца файла */
}
Заголовочный файл unpthread-h
Мы впервые встречаемся с заголовочным файлом unpthread h Он включает наш обычный заголовочный файл Posix 1 unp h, затем — заголовочный файл Pthread <pthread h>, и далее определяет прототипы для наших потоковых функций-оберток Pthread_XXX (см раздел 1 4), название каждой из которых начинается с Pthread_
Сохранение аргументов во внешних переменных
10 11 Для потока, который мы собираемся создать, требуются значения двух аргументов функции str_cl 1 fp — стандартный указатель структуры FILE для входного файла и sockfd — сокет TCP связанный с сервером Для простоты мы храним эти два значения во внешних переменных Альтернативой является запись этих двух значений в структуру, указатель на которую затем передается в качестве аргумента создаваемому потоку
Создание нового потока
Создается поток и значение нового идентификатора ротока сохраняется в tid Функция, выполняемая новым потоком, — это copyto. Никакие аргументы потоку не передаются
Главный цикл потока: копирование из сокета в стандартный поток вывода
14 В основном цикле вызываются функции read] i ne и fputs, которые осуществляют копирование из сокета в стандартный поток вывода
Завершение
Когда функция str_cl i возвращает управление, функция mam завершается при помощи вызова функции exit (см. раздел 5.4). При этом завершаются все потоки данного процесса. В обычном сценарии другой поток уже должен завершиться в результате считывания признака конца файла из стандартного потока ввода. Но в случае, когда сервер преждевременно завершил свою работу (см. раздел 5.12), при вызове функции exit завершается также и другой поток, чего мы и добиваемся.
Поток copyto
6-25 Этот поток просто осуществляет копирование из стандартного потока ввода в сокет. Когда он считывает признак конца файла из стандартного потока ввода, на сокете вызывается функция shutdown и отсылается сегмент FIN, после чего поток возвращает управление. При выполнении оператора return (то есть когда функция, запустившая поток, возвращает управление) поток также завершается.
В конце раздела 15.2 мы привели результаты измерений времени выполнения для пяти различных способов реализации функции str_c1i. Мы отметили, что версия, с применением потоков выполняется всего 8,5 секунды — немногим быстрее, чем версия, использующая функцию fork (как мы и ожидали), но медленнее, чем версия с неблокируемым вводом-выводом. Тем ие менее, сравнивая устройство версии с неблокируемым вводом-выводом (см. раздел 15.2) и версии с использованием потоков, мы заметили, что первая гораздо сложнее. Поэтому мы рекомендуем использовать именно версию с потоками, а не с неблокируемым вводом-выводом.
Использование потоков в эхо-сервере TCP
Теперь мы перепишем эхо-сервер TCP, приведенный в листинге 5.1, используя для каждого клиента по одному потоку вместо одного процесса. Кроме того, с помощью нашей функции tcp_l i sten мы сделаем эту версию не зависящей от протокола. В листинге 23.2 показан код сервера.
Листинг 23.2. Эхо-сервер TCR использующий потоки //threads/tcpservOl с
#】nclude"unptbread h”
static void *doit(void *),/* каждый поток выполняет эту функцию */
3 4 5 6 7 8 93 4 5 6 7 8 9int
3 4 5 6 7 8 9
3 4 5 6 7 8 9
main(int argc. char **argv) {
int listenfd. connfd. pthread t tid, socklen_t addrlen len. struct sockaddr *cliaddr.
if (argc == 2)
1istenfd = Tcp_listen(NULL. argv[l] &addrlen),
else if (argc ==一3)
listenfd = Tcp listen(argv[l], argv[2], &addrlen).
else
15 err_quit("usage tcpservOl [ <host> ] <ser^ice or port〉”),
cliaddr = Malloc(addrlen).
for ( J {
len = addrlen.
connfd = Acceptdistenfd cliaddr. &len)
20Pthread create(&tid. NULL. &doit. (void *) conAfd):
21}
}
static void *
doit(void *arg)
{
Pth read_detach(pth read_sel f()).
str_echo((int) arg)./* та же функция. что и раньше */
Close((int) arg)./* мы закончили с присоединенным сокетом */
return (NULL).
}
Создание потока
17-21 Когда функция accept возвращает управление, мы вызываем функцию pthread_ create вместо функции fork. Мы передаем функции doi t единственный аргумент — дескриптор присоединенного сокета connfd.
ПРИМЕЧАНИЕ
Мы преобразуем целочисленный дескриптор соке га к указателю на неопределенный тип (void). В ANSI С не гарантируется, чт о такое преобразование будет выполнено корректно, 一 мы можем быть уверены лишь в том, что оно сработает в гсх системах, в которых размер целого чпота не превышает размера указателя. К счастью, большинство реализаций Unix обладают этим свойством (см. табл. 1 5) Чуть ниже мы поговорим об этом подробнее.
Функция потока
23-30 doit — это функция, выполняемая потоком. Поток отделяет себя с помощью функции pthread_detach, так как нет причины, ио которой для главного потока имело бы смысл ждать завершения каждого созданного им потока. Функция str_echo не изменилась п осталась такой же, как в листинге 5.2. Когда эта функция завершается, следует вызвать функцию cl ose для того, чтобы закрыть присоединенный сокет, поскольку этот поток использует все дескрипторы совместно с главным потоком. При использовании функции fork дочерний процесс не должен специально закрывать присоединенный сокет, так как при завершении дочернего процесса все открытые дескрипторы закрываются (см. упражнение 23.5).
Обратите также внимание на то, что главный поток не закрывает присоединенный сокет, что всегда происходило, когда параллельный сервер вызывал функцию fork. Это объясняется тем, что все потоки внутри процесса совместно используют все дескрипторы, поэтому если главному потоку потребуется вызвать функцию close, эго приведет к закрытию соединения. Создание нового потока не влияет на счетчики ссылок для открытых дескрипторов, в отличие от того, что происходит при вызове функции fork.
В этой программе имеется одна неявная ошибка о которой рассказывается в разделе 23 5 Можете ли вы ее обнаружить? (См упражнение 23 5 )
Передача аргументов новым потокам
Мы уже упомянули, что в листише 23 2 мы преобразуем целочисленную переменную connfd к указателю на неопределенный тип (void), но этот способ не работает в некоюрых системах Для корректнои обработки данной ситуации требуются дополнительные усилия
В первую очередь, заметим, что мы не можем просто передать адрес connfd нового потока, то есть следующий код не будет работать
int
main(int агде char **argv)
{
mt listenfd connfd
for () {
len = addrlen
connfd = Accept(listenfd cliaddr &]en)
Pthreadmereate(&tid NULL &doit tannfd)
static void * doit(void *arg) {
1 nt connfd
connfd = *((int *) arg)
Pthread_detach(pthread_self())
str_echo(connfd)/* та же функция что и прежде */
Сlose(connfd)/* мм закончили с присоединенным сокетом */
return(NULL)
}
С точки зрения ANSI С здесь все в порядке мы гарантированно можем преобразовать целочисленный указатель к типу vol d * и затем обратно преобразовагь получившийся указатель на неопределенный тип к целочисленному указателю Проблема заключается в другом — на что именно он будет указывать*?
В главном потоке имеется одна целочисленная переменная connfd, и при каждом вызове функции accept значение этой переменной изменяется на новое (в соответствии с новым присоединенным сокетом) Может сложиться следующая ситуация
Функция accept возвращает управление, записывается новое значение переменной connfd (допустим, новый дескриптор равен 5) и в главном потоке вызывается функция pthread_create Указатель на connfd (а не фактическое его значение^ является последним аргументом функции pthread_create
Создается новый поток, и начинает выполняться функция doi t
Готово другое соединение, и главный поток снова начинает выполняться (прежде, чем начнется выполнение вновь созданного потока) Завершается функция accept, записывается новое значение переменной connfd (например,
значение нового дескриптора равно 6) и главный поток вновь вызывает функцию pthread_create
Хотя созданы два новых потока, оба они будут работать с одним и тем же последним значением переменной connfd, которое, согласно нашему предположению, равно 6 Проблема заключается в том, что несколько потоков получают доступ к совместно используемой переменной (целочисленному значению, хранящемуся в connfd) при отсутствии синхронизации В листинге 23 2 мы решаем эту проблему, передавая значение переменной connfd функции pthread_create, вместо того чтобы передавать указатель на это значение Этот метод работает благодаря тому способу, которым целочисленные значения в С передаются вызываемой функции (копия значения помещается в стек вызванной функции)
В листинге 23 3 показано более удачное решение описанной проблемы
Листинг 23.3. Зхо-сервер TCR использующий потоки с более переносимой передачей аргументов
//threads/tcpserv02 с
include unpthread h
2345678923456789/* каждый поток выполняет эту функцию *//* каждый поток выполняет эту функцию */static void *doit(void *)
23456789
23456789
/* каждый поток выполняет эту функцию */
/* каждый поток выполняет эту функцию */
】nt
main(int argc char **argv)
{
mt listenfd *iptr
thread_t tid
socklen t addrlen len struct sockaddr *cliaddr
if (argc == 2)
listenfd = Tcp_listen(NULL argv[l] &addrlen)
else if (argc == 3)
listenfd = Tcp_listen(argv[l] argv[2] Saddrlen)
else
err_quit( usage tcpservOl [ <host> ] 〈service or port》").
16 cliaddr = Malloc(addrlen)
for ( ) {
len = addrlen
iptr = Malloc(sizeof(int))
*iptr = Acceptdistenfd cliaddr&len).
Pthread_create(&tid NULL&doitiptr)
}“
}
static void *
doit(void *arg)
{
int connfd
connfd - *((int *) arg)
free(arg)
Pthread_detach(pthread_self())
str_echo(connfd)/* та же функция что и раньше */
Close(connfd)/* мы закончили с прнсоепмненнмм сокетом */
продо жжение 挨
Листинг 23.3 (продолжение)
return (NULL).
34 }
.7-22 Каждый раз перед вызовом функции accept мы вызываем функцию mal 】ос и выделяем в памяти пространство для целочисленной переменной (дескриптора присоединенного сокета). Таким образом каждый поток получает свою собственную копию этого дескриптора.
?8-29 Поток получает значение дескриптора присоединенного сокета, а затем освобождает занимаемую им память с помощью функции free.
Исторически функции mall ос и free не допускали повторного вхождения. Это означает, что при вызове той или иной функции из обработчика сигнала в то время, когда главный поток выполняет одну из них, возникает большая путаница, так как эти функции оперируют статическими структурами данных. Как же мы можем вызывать эти две функции в листинге 23.3? Дело в том, что в Posix.l требуется, чтобы эти две функции, так же как и многие другие, были безопасными в многопоточной среде (thread-safe). Обычно это достигается с помощью некоторой разновидности синхронизации, осуществляемой внутри библиотечных функций и являющейся для нас прозрачной (то есть незаметной).
Функции, безопасные в многопоточной среде
Стандарт Posix.l требует, чтобы все определенные в нем функции, а также функции, определенные в стандарте ANSI С, были безопасными в многопоточной среде. Исключения из этого правила приведены в табл. 23.1.
К сожалению, в Posix.l ничего не сказано о безопасности в многопоточной среде по отношению к функциям сетевого API. Последние пять строк в этой таблице появились благодаря Unix 98. В разделе 11.14 мы говорили о том, что функции gethostbyname и gethostbyaddr не допускают повторного вхождения. Как уже отмечалось, некоторые производители определяют версии этих функций, обладающие свойством безопасности в многопоточной среде (их названия заканчиваются на _「),но поскольку они не стандартизованы, лучше от них отказаться. Все функции getXXX, не допускающие повторного вхождения, были приведены в табл. 9.2.
Таблица 23.1. Функции, безопасные в многопоточной среде
Могут не быть безопасными в многопоточной среде
Должны быть безопасными в многопоточной среде
mtttmMK^/VKrtvi»^w««mKtM^WM(^Mi9»Ki7K-MK<<f>»Ktmmw
Комментарии
asctime
asctime_r
Безопасна в многоно-
ctime
ctermid
ctime_r
точно» среде только в случае непустого аргумента
getc_unlocked getcharunlocked getgnd
getgnd」
getgrnam
getgrnam_r
getlogin
getlogm_r
getpwnam
getpwnam」
Могут не быть безопасными Должны быть безопасными Комментарии в многопоточной среде в многопоточной среде
getpwuid gm time localtime
gctpumd_r gintimcr local tun e_r
put с unlocked putchar_unlocked
rand readdir strtock
rand_r
readdir r
strtock_r
tmpnamБезопасна в мпоюпо-
точпои〔реке только в случае непустою
аргумента
tiyname gethoslXXX getnetXXX getprotoXXX getservXXX inet_ntoa
ttyname_r
Приведенная таблица позволяет заключить, что общим способом сделать функцию допускающей повторное вхождение является определение новой функции с названием, оканчивающимся на _г. Обе функции будут безопасными в многопоточной среде, только если вызывающий процесс выделяет в памяти место для результата и передает соответствующий указатель как аргумент функции.
23.5. Собственные данные потоков
При написании программного кода к главе 27 автор совершил обычную программистскую ошибку, характерную для ситуации, когда приложение, не использующее потоки, преобразуется к виду, в котором потоки используются. Эга ошибка была обнаружена только при запуске сервера, представленного в листинге 27.20, ио сама ошибка содержится не в этом листиш е, а в функции readl i ne, которая вызывается для обслуживания клиентских запросов. Та же самая функция вызывается в листинге 23.2.
Как и в случае многих других программных ошибок, связанных с потоками, эта ошибка была недетерминированной. Фактически обе программы в листингах 23.2 и 27.20 работали, но ошибка была обнаружена при выполнении тестов синхронизации для версии с потоками в листинге 27.22. Эта версия работала, когда клиент и сервер были на одном узле, но когда клиент и сервер находились на разных узлах, в различных точках происходили сбои и наша функция web child (листинг 27.5) выдавала ошибку, сообщая, что клиент запросил 0 байт. После несколькмх часов безрезультатной отладки выяснилось, что в процессе увеличения быстродействия функции readline (см. листинг 3.10) были добавлены статические переменные (см. листинг 3.11). Это увеличение быстродействия и стало причиной сбоев, когда функция вызывалась из различных потоков в пределах
одного процесса.
Эта проблема часто возникает в ситуациях, когда существующие функции преобразуются к виду, необходимому для работы с потоками. Существует несколько способов решения этой проблемы,
Использование собственных данных потоков (thread-specific data). Это нетривиальная задача, и функция при этом преобразуется к такому виду, что может использоваться только в системах, поддерживающих потоки. Преимущество этого подхода заключается в том, что не меняется вызывающая последовательность, и все изменения связаны с библиотечной функцией, а не с приложениями, которые вызывают эту функцию. Позже в этом разделе мы покажем безопасную в многопоточной среде версию функции read] i ne, созданную с при- мененим собственных данных потоков.
Изменение вызывающей последовательности таким образом, чтобы вызывающий процесс упаковывал все аргументы в некую структуру, а также записывал в нее статические переменные из листинга 3.11. Это также было сделано, и в листинге 23.4 показана новая структура и новые прототипы функций.
mt
rl_cnt.
/*
char
*rl_bufptr.
/*
char
} Rime,
rl_buf[MAXLINE].
typedef struct {mt read_fd./*char*read_pt「:/*size_tread_maxlen. /*~ 一 /*typedef struct {mt read_fd./*char*read_pt「:/*size_tread_maxlen. /*~ 一 /*Листинг 23.4. Структура данных и прототип функции для версии функции readline, допускающей повторное вхождение
typedef struct {
mt read_fd./*
char*read_pt「:/*
size_tread_maxlen. /*
~ 一 /*
typedef struct {
mt read_fd./*
char*read_pt「:/*
size_tread_maxlen. /*
~ 一 /*
дескриптор, указывающий, откуда считываются данные */
буфер. куда передаются данные */
максимальное количество байтов, которое может быть считано */
следующие три элемента для внутреннего использования функцией */ инициализируется нулем */
инициализируется значением rl_buf */
void readline_rimt(int void * size_t. Rime *), ssize_t readlme_r(Rline *).
ssize_t Readline^rCRlme *).
Эти новые функции могут использоваться как в системах с поддержкой потоков, так и в тех, где потоки не поддерживаются, но все приложения, вызывающие функцию read! те, должны быть изменены.
Отказ от увеличения быстродействия, достигнутого в листинге 3.11, и возвращение к более старой версии, представленной в листинге 3.10.
Использование собственных данных потоков — это распространенный способ сделать существующую функцию безопасной в многопоточной среде. Прежде чем описывать функции Pthread, работающие с такими данными, мы опишем саму концепцию и возможный способ реализации, так как эти функции кажутся более сложными, чем являются на самом деле.
ПРИМЕЧАНИЕ
Частично осложнения возникают ио той причине, что во всех книгах, где идет речь о потоках, описание собственных данных потоков дается по образцу стандарга Pthrcad. Пары ключ-значение и ключи рассматриваются в них как непрозрачные объекты. Мы описываем собственные данные потоков в терминах индексов и указателей, так как обычно в реализациях в качестве ключей используются небольшие положительные целые числа (индексы), а значение, ассоциированное с ключом, — это просто указатель на область памяти, выделяемую с помощью функции malloc.
В каждой системе поддерживается ограниченное количество объектов собственных данных потоков. В Posix.l требуется, чтобы этот предел не превышал 128 (на каждый процесс), и в следующем примере мы используем именно это значение. Система (вероятно, библиотека потоков) поддерживает один массив структур (которые мы называем структурами Key) для каждого процесса, как показано иа рис. 23.2.
Кеу[0]Кеу[1]Key[127]Кеу[0]Кеу[1]Key[127]6ла「
Кеу[0]
Кеу[1]
Key[127]
Кеу[0]
Кеу[1]
Key[127]
Указатель-деструктор
5а「
Указатель-деструктор
5а「
Указател ь-деструктор
Рис. 23.2. Возможная реализация собственных данных потока
Флаг в структуре Key указывает» используется ли в настоящий момент данный элемент массива. Все флаги инициализируются как указывающие на то, что элемент не используется. Когда поток вызывает функцию pthread_key_create для создания нового элемента собственных данных потока, система отыскивает в массиве структур Key первую структуру, не используемую в настоящий момент. Индекс этой структуры, который может иметь значение от 0 до 127, называется ключом и возвращается вызывающему потоку как результат выполнения функции. О втором элементе структуры Key, так называемом указателе -деструкторе, мы поговорим чуть позже.
В дополнение к массиву структур Key, общему для всего процесса, система хранит набор сведений о каждом потоке процесса в структуре Pthread. Частью этой структуры является массив указателей, состоящий из 128 элементов, который мы называем ркеу. Это показано на рис. 23.3.
pkey[Ql pkeyfl]pkey[Ql pkeyfl]pkey[127]pkey[127]Элементы собственных данных потокаЭлементы собственных данных потокаРис. 23.3. Информация, хранящаяся в системе для каждого потокаРис. 23.3. Информация, хранящаяся в системе для каждого потока
pkey[Ql pkeyfl]
pkey[Ql pkeyfl]
pkey[127]
pkey[127]
Элементы собственных данных потока
Элементы собственных данных потока
Рис. 23.3. Информация, хранящаяся в системе для каждого потока
Рис. 23.3. Информация, хранящаяся в системе для каждого потока
Все элементы массива ркеу инициализируются пустыми указателями. Эта 128 указателей являются «значениями», ассоциированными с каждым из 128 «ключей» процесса.
Когда мы с помощью функции pthread_key_create создаем ключ, система сообщает нам фактическое значение ключа (индекс). Затем каждый поток может записать значение (указа! ель), связаииое с этим ключом, и как правило, каждый поток получает этот указатель в виде возвращаемого значения функции та 11 ос. Частично путаница с собственными данными потока обусловлена тем, что указатель в паре ключ-значение играет роль значения, по сами собственные данные потока — это то, на что указывает данный указатель.
Теперь мы перейдем к примеру применения собственных данных потока, предполагая, что наша функция readlme использует их для хранения информации о состоянии каждого потока при последовательных обращениях к ней. Вскоре мы покажем код, выполняющий эту задачу, в котором функция readlme модифицирована так, чтобы реализовать представленную ниже последовательность тагов. 1. Запускается процесс, и создается несколько потоков.
Один из потоков вызовет функцию readl i ne первой, а та, в свою очередь, вызовет функцию pht read_key_c reate. Система О1ыщет первую неиспользуемую структуру Key (см. рис. 23.2) и возвратит вызывающему процессу ее индекс. В данном примере мы предполагаем, что индекс равен 1.
Мы будем использовать функцию pthread_once, чтобы гарантировать, что функция pthread_key_create вызывается только первым потоком, вызвавшим функцию 「eadline.
Функция read】1 ne вызывает функцию pthread_getspeci fi с для получения значения ркеу[1] («указатель» на рис. 23.3 для ключа, имеющего значение 1) для данного потока, и эта функция возвращает пустой указатель. Затем функция readl те вызывает функцию mall ос для выделения памяти, которая необходима для хранения информации о каждом потоке при поеледовательных вызовах функции readl i ne. Функция readl i ne инициализирует эти области памяти по мере надобности и вызывает функцию pthread_setspeci fi с, чтобы установить
Другая информация о потокеNULLУказательУказательNULLУказательДругая информация о потокеNULLУказательУказательNULLУказательПоток ОПоток Оркеу[0]ркеу[1]ркеу[127]ркеу[0]ркеу[1]ркеу[127]Элементы собственных данных потокаЭлементы собственных данных потока
Другая информация о потоке
NULL
Указатель
Указатель
NULL
Указатель
Другая информация о потоке
NULL
Указатель
Указатель
NULL
Указатель
Поток О
Поток О
ркеу[0]
ркеу[1]
ркеу[127]
ркеу[0]
ркеу[1]
ркеу[127]
Элементы собственных данных потока
Элементы собственных данных потока
Системные
структуры данных
Память, выделенная потоком
Фактическиеданные
Рис. 23.4. Соответствие между областью памяти, выделенной функцией malloc и указателем собственных данных потока
указатель собственных данных потока (ркеу[Ц), соответствующий данному ключу, на только что выделенную область памяти. Мы показываем этот процесс на рис. 23.4, предполагая, что вызывающий поток — это поток с номером О в данном процессе.
На этом рисунке следует обратить внимание на то, что структура Pt bread поддерживается системой (вероятно, библиотекой потоков), но фактически собственные данные потока, которые мы размещаем в памяти с помощью функции mall ос, поддерживаются нашей функцией (в данном случае readli ne). Все, что делает функция pthread_setspecific, — это установка указателя для данного ключа в структуре Pthread на выделенную область памяти. Аналогично, действие функции pth read_getspeci f i с сводится к возвращению этого указателя.
Другой поток, например поток с номером 〃, вызывает функцию readline, возможно, в тот момент, когда поток с номером 0 все еще находится в стадии выполнения функции read! те.
Функция read! те вызывает функцию pthread_once, чтобы инициализировать ключ этого элемента собственных данных, но так как эта функция уже была однажды вызвана, то больше она не вызывается.
Функция read] i ne вызывает функцию pthreadjetspeci fi с для получения значения указателя ркеу[1] для данного потока, в результате чего возвращается пустой указатель. Затем поток вызывает функцию mall ос и функцию pthread_ setspecific, как и в случае с потоком номер 0, инициализируя элемент собственных данных потока, соответствующий этому ключу (1). Этот процесс иллюстрирует рис. 23.5.
Поток номер п продолжает выполнять функцию read! i ne, используя и модифицируя свои собственные данные потока.
Один вопрос, который мы пока не рассмотрели, заключается в следующем: что происходит, когда поток завершает свое выполнение? Если поток вызвал
Поток 0Поток л
Рис. 23.5. Структуры данных после того, как поток п инициализировал свои собственные данные
функцию 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 *onc叩tr void (*7n?t)(void))
mt pth rea d_key_cr.ea t e (pth rea d_key_t ^keyptr void (^des true tor) (vo i d *va7t/e))
Обе функции возвращают 0 в случае успешного выполнения положительное значение Еххх в случае ошибки
Функция pthread_once обычно вызывается при вызове функции, манипулирующей с собственными данными потока, ио pthread__once использует значение переменной, на которую указывает onceptr, чтобы гарантировать, что функция imt вызывается для каждого процесса только один раз.
Функция pthread_key_create должна вызываться только один раз для данного ключа в пределах одного процесса. Значение ключа возвращается с помощью указателя keypt г, а функция-деструктор (если аргумент является непустым указателем) будет вызываться каждым потоком по завершении его выполнения, если этот поток записывал какое-либо значение, соответствующее этому ключу.
Обычно эти две функции используются следующим образом (если игнорировать возвращение ошибок):
pthread_k ey_t rl_key
pthread_once_t rl_once = PTHREAD_ONCE_ INIT
void
readl me_dest rue tor (void *ptr)
{ 一
free(ptr)
void
readline_once(void)
{ 一
pthread_key_create(&rl_key, readline_destructor).
}'~~
ssize_t
readl1 ne()
pthread_once(&r1_once readlwe once)
if ( (ptг = pthread_getspecific(rl_key)) == NULL) { ptг = Malloc(),
pthread_setspecific(rl_key ptr)
/* инициализация о&ласти памяти на которую указывает ptr */
}
/* используются значения на которые указывает ptr */ }
Каждый раз, когда вызывается функция read! те, она вызывает функцию pthread_once. Эта функция использует значение, на которое указывает ее аргумент-указатель onceptr (содержащийся打 в переменной rl_once), чтобы удостовериться, что функция imt вызывается только один раз. Функция инициализации readl 1 пе_опсе создает ключ для собственных данных потока, который хранится в rl_key и который функция readl те затем использует в вызовах функций pthread_getspecif 1 с и pthread_setspecific
Функции pthread getspeci f i с н pthread_setspeci f i с используются для того, чтобы получать и задавать значение, ассоциированное с данным ключом. Это значение представляет собой тот указа1ель, который показан на рис 23.3. На что указывает этот указатель — зависит от приложения, но обычно он указывает на динамически выделяемый участок пам£1 и.
include <pthread h>
void *pthread_getspecific(pthread_key_t key)
Возвращает указатель на собственные данные потока (возможно пустой указатель)
mt pthread_setspecific(pthread_key_t key const void ★value)
Возвращает 0 в случае успешного выполнения положительное значение Еххх в случае ошибки
Обратите внимание на ю, что аргументом функции pthread_key_create является указатель па ключ (поскольку э га функция хранит значение, присвоенное ключу), в ю время как аргументами функций get н set являются сами ключи (которые, скорее всего, представляют собой небольшие целые числа, как говорилось выше).
Пример: функция readline, использующая собственные данные потока
В этом разделе мы приводим полный пример использования собственных данных потока, преобразуя оптимизированную версию функции readl i ne из листинга 3.11 к виду, безопасному в многопоточной среде, не изменяя последовательность вызовов.
В листинге 23 5 показана первая часть функции: переменные pthread_key_t и pthread_once_t, функции readl i ne_destructor и readl me_once и наша структура Rime, которая содержит всю информацию, нужную нам для каждого потока
Листинг 23・5・ Первая часть функции readline, безопасной в многопоточной среде //threads/readlme с
#】nclude 'unpthread h'
static pthread key t rl_key
static pthread_once_t rT_once = PTHREAD ONCE INIT.
- 一 一一 -продолжение 夕
Листинг 23.5 (продолжение)
static void
readline_destructor(void *ptr)
{"
free(ptr)
}
static void
readline once(void)
{-
Pthread_key_create(&rl_key readline_destructor)
3}—-”一
typedef struct {
int rl_cnt/* инициализируется нулем */
char *rl_bufptr /* инициализируется значением rl buf */
charr匸 buf[MAXLINE]
} Rime
Деструктор
4 8 Наша функция-деструктор просто освобождает всю пшять, которая была выделена для данно! о потока
«Одноразовая» функция
9 13 Мы увидим что наша «одноразовая» (то есть вызываемая только один раз) функция вызывается однократно из функции pthredd once и просто создает ключ, который затем используется в функции readl i ne
Структура Rline
14 18 Наша структура R】i ne содержит три переменные которые, будучи обьявленными как статические (static) переменные в листинге 311, привели к возникновению описанных выше проблем Такая структура динамически выделяется в памяти для каждого потока, а по завершении выполнения этого потока она освобождается функцией-деструктором
В листин! е23 6 показана сама функция readl i ne а также функция my_redd, которую она вызывает Этот листинг является модификацией листинга 311
Листинг 23.6. Вторая часть функции readline, безопасной в многопоточной среде //threads/readlтпе с
19 static ssizjt
20 my_read(Rline *tsd int fd char *ptr)
21 {~
22 if (tsd >rl_cnt <= 0) {
23again 一
24 if ( (tsd >rl_cnt = read(fd tsd >「l_buf MAXLINE)) < 0) {
25 if (errno == EINTR)
26goto again
27return ( 1)
28} else if (tsd >rl_cnt == 0)
29 return (0)
tsd >rl_bufptr = tsd >rl_buf
}——
tsd >rl_cnt
*ptr = *tsd >rl_bufptr++
34
35
return (1) }
36
37
38
39
40
41
ssizej
readlTne( mt fd void *vptr size t maxlen)
{一
intn rc
charc *ptr
Rime *tsd
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Ptliread_once(&rl_once read】 ine^once)
if ( (tsd = pthread_getspecificlrl_key)) == NULL) {
tsd = Callocd sizeof(Rline))/* инициализируется ну饱м */
Pthread setspecific(「l key tsd)
} 一一
ptr = vptr
for (n = 1 n < mdxlen 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 данные не были считаны */
el se
break/* EOF было считано некоторое количество данных */
} else
return ( 1)/* ошибка errno устанавливается функцией read() */
}
61
62
63
*ptr = 0 return (n)
Функция my_read
19 35 Первым аргументом функции является теперь указатель на структуру Rime, которая была размещена в памяти для данного потока (и содержит собственные данные этого потока)
Размещение собственных данных потока в памяти
42 Сначала мы вызываем функцию pthread_once, так чтобы первый поток, вызывающий функцию readl те в этом процессе, вызвал бы функцию readlme once для создания ключа собственных данных потока
Получение указателя на собственные данные потока
43 46 Функция pthread_getspeci f 1 с возвращает указатель на структуру R】i ne для данного потока Но если это первый вызов функции readline данным потоком, то возвращаемым значением будет пустой указатель В таком случае мы выделяем в памяти место для структуры Rli ne, а элемент r】_cnt этой структуры инициализируется нулем с помощью функции call ос Затем мы записываем этот указатель для данного потока, вызывая функцию pthread_setspeci fi с Когда этот поток вызовет функцию read] i ne в следующий раз, функция pthread_getspeci f i с возвратит этот указатель, который был только что записан
3.6. Web-клиент и одновременное соединение (продолжение)
Вернемся к нашему примеру с web-клиентом из раздела 15 5 и перепишем его с использованием потоков вместо неблокируемой функции connect Мы можем ос1авить сокеты в их заданном по умолчанию виде — блокируемыми, и создать один поток на каждое соединение Каждый поток может блокироваться в вызове функции connect, так как ядро будет просто выполнять какой-либо другой поток, готовый к работе
В листинге 23 7 показана первая часть нашей программы, глобальные переменные и начало функции main
Листинг 23.7. Глобальные переменные и начало функции main
//threads/webOl с
2
#include unpthread h
#include <thread h>
/* потоки Solans */
3
4
#define MAXFILES
#define SERV
20
80
/* ноиер порта ипи имя службы */
5
6
7
8
9
10
11
12
struct file { char★匸 name
char *f_host intf:fd
mtf_f 1 ags
pthread_t~f_tid
} file[MAXFILES]
#define F—CONNECTING
1
/* имя файла */
/* имя узла или IP адрес */
/* дескриптор */
/* F_xxx ниже */
/* идентификатор потока */
/* функция connect() в 叩оиессе выполнения */
13
#define F~READING
2
/* функция connect() завершена выполняется считывание */
14
#define F]DONE
4
/* все сделано */
15
#define GET—CMD
GET Xs
НТТР/1 0\ег\еп\ег\еп
int nconn nfiles nlefttoconn nlefttoread
void*do_get_read(void *)
void home_page(const char * const char *)
voidwrite_get_cmd(struct file *)
int
mainUnt argc char **argv)
{'
int in maxnconn
pthredd_t tid
struct file *fptr
if (argc < 5)
err_quit( usage web <#conns> <IPaddr> 3omepage> fil風 ")
maxnconn = atoi(argv[l])
nfiles = nnn(argc4MAXFILES)
for (i=0 i < nfiles i++) {
file[i] f_name = argv[i + 4]
file[i] fjiost = argv[2]
fileEi] f_flags = О
}
printfC nfiles = ^d\en nfiles)
home_page(argv[2] argv[3])
nlefttoread = nlefttoconn = nfiles
nconn = 0
Глобальные переменные
16 Мы включаем заголовочный файл <thread h> вдобавок к обычному <pthread h>, так как нам требуется использовать потоки Solans в дополнение к потокам Pthread, как мы вскоре покажем
Мы добавили к структуре file один элемент — идентификатор потока f_tid Остальная часть этого кода аналогична коду в листинге 15 9 В зто負 версии нам не нужно использовать функцию select, а следовательно, не нужны наборы дескрипторов и переменная maxfd
36 Функция home_page не изменилась относительно листинга 15 10
В листинге 23 8 показан основной рабочий цикл потока main
Листинг 23.8. Основной рабочий цикл потока mam
//threads/webOl с
39 while (nlefttoread > 0) {
40 while (nconn < maxnconn && nlefttoconn > 0) {
41/* находим файл для считывания */
42 for (1 = 0 1 < nfiles i++)
if (file口] f_flags == 0)
break
if (i == nfiles)
err_quit( nlefttoconn = %d but nothing found nlefttoconn)
file[i] f_flags = F_CONNECTING
Pthreadmereate(&tid NULL &do_get_read &file[i])
file[i]-f_tid= tid --
nconn++
51nlefttoconn
52}
53' if ( (n = thr_join(0 &tid (void **) &fpt「)) •= 0)
54 errno = n err~sys( th「_join error )
55nconn
56nlefttoread
printfC threadid %d for %s done\en tid fptr >f_name)
}—
59 exit(0)
60 }
По возможности создаем другой поток
)52 Если имеется возможность создать другой поток (nconn меньше, чем maxconn), мы так и делаем Функция, которую выполняет каждый новый поток, — это do_get_read, а ее аргументом является указатель на структуру fi 1 е
Ждем, когда завершится выполнение какого-либо потока 53-54 Мы вызываем функцию потоков thr join Solaris с нулевым первым аргументом, чтобы дождаться завершения выполнения какого-либо из наших потоков. К сожалению, в Pthread не предусмотрен способ, с помощью которого мы могли бы ждать завершения выполнения любого потока, и функция pthreadjoi п требует, чтобы мы точно указали, завершения какого потока мы ждем. В разделе 23.9 мы увидим, что решение этой проблемы в случае применения технологии Pthread оказывается сложнее и требует использования условной переменной для сообщения главному потоку о завершении выполнения дополнительного потока.
ПРИМЕЧАНИЕ
Показанное здесь решение, в котором используется функция потоков thr_join Solans, не является, вообще говоря, совместимым со всеми системами. Тем не менее мы приводим здесь эту версию web-клиента, использующую потоки, чтобы не осложнять обсуждение рассмотрением условных переменных и взаимных исключений (mutex). К счастью, в Solaris допустимо смешивать потоки Pthread и потоки Solaris.
Когда автор этой книги выступил в Usenet с замечанием о том, что функция pthread_join не способна обеспечить ожидание завершения любого потока, некоторые программисты, работавшие над стандартом Pthread, утверджали, что такое конструктивное решение оправданно, так как никакая функция, в том числе и pthread_joint не может быть использована во всех ситуациях. Было также высказано утверждение, что в модели процессов существует взаимоотношение типа родительский процесс—дочерний процесс, поэтому имело смысл обеспечить возможность ожидания родительским процессом завершения выполнения дочернего процесса (с помощью функций wait или waitpid). Но в случае, когда речь идет о потоках, нет иерархических отношений типа «родитель- потомок», поэтому ожидание завершения любого потока не имеет смысла. Поток, статус которого возвращается функцией, ожидающей завершения любого потока, — это не обязательно поток, созданный вызвавшим указанную функцию потоком. Также было сказано, что если кому-то действительно нужно дождаться завершения выполнения любого потока, это можно сделать (нетривиальным способом), используя условные переменные, как вскоре будет показано. Несмотря на эту аргументацию, автор книги не считает устройство функции pthread join обоснованным.
В листинге 23.9 показана функция do get redd, которая выполняется каждым потоком. Эта функция устанавливает соединение TCP, посылает серверу команду HTTP GET и считывает ответ сервера.
Листинг 23.9. Функция do_geVead
//threads/webOl с
void *
do__get_read (void *vptr)
{ " ~
intfd, n.
charline [MAXLINE],
struct file *fptr.
fptr = (struct file *) vptr.
fd = Tcp_connect (fptr->f_host. SERV).
fptr->f_fd = fd. -
pnntf ?"do_get_read for %s. fd %d. thread %d\n".
fptr->f_name~ fd. fptr->f_tid).
write_get_cmd (fptr).
/*Чтение ответа сервера*/
for ( . . ) {
if ( ( n = Read (fd. line. MAXLINE)) == 0)
break. /*сервер закрывает соединение*/
pnntf ("read 紀 bytes from %s\n" n, fptr->f_name),
}—
printf ("end-of-file on Xs\n". fptr->f_name).
Close (fd).
fptr->f_fl ags = F_DONE./*сбрасываем F READING*/
return (fptr)./*завершение потока*/
}
Создание сокета TCP, установление соединения
68-71 Создается сокет TCP, и с помощью функции tcp_connect устанавливается соединение. В данном случае используется обычный блокируемый сокет, поэтому поток будет блокирован при вызове функции connect, пока не будет установлено соединение.
Запись ответа сервером
72 Функция write_get_cmd формирует команду HTTP GET и отсылает ее серверу. Мы не показываем эту функцию заново, так как единственным отличием от листинга 15.12 является то, что в версии, использующей потоки, не вызывается макрос FD_SET и не используется maxfd.
Чтение ответа сервера
73-82 Затем считывается ответ сервера. Когда соединение закрывается сервером, устанавливается флаг F_DONE и функция возвращает управление, завершая выполнение потока.
Мы также не показываем функцию home_page, так как она полностью повторяет версию, приведенную в листинге 15.10.
Мы вернемся к этому примеру, заменив функцию Solaris thr_join на более переносимую функцию семейства Pthread, но сначала нам необходимо обсудить взаимные исключения и условные переменные.
Взаимные исключения
Обратите внимание на то, что в листинге 23.8 при завершении выполнения очередного потока в главном цикле уменьшаются на единицу и псопп, и nl efttoread. Мы могли бы поместить оба эти оператора уменьшения в одну функцию do_get_ read, что позволило бы каждому потоку уменьшать эти счетчики непосредственно перед тем, как выполнение потока завершается. Но это привело бы к возникновению трудноуловимой серьезной ошибки параллельного программирования.
Проблема, возникающая при помещении определенного кода в функцию, которая выполняется каждым потоком, заключается в том, что обе эти переменные являются глобальными, а не собственными переменными потока. Если один поток в данный момент уменьшает значение переменной и это действие приостанавливается, чтобы выполнился другой поток, который также станет уменьшать
на единицу эту переменную, может произойти ошибка. Предположим, например, что компилятор С осуществляет уменьшение переменной на единицу в три этапа: загружает информацию из памяти в регистр, уменьшает значение регистра, а затем сохраняет значение регистра в памяти. Рассмотрим возможный сценарий. 1. Выполняется поток А, который загружает в регистр значение переменной псопп (равное 3).
Система переключается с выполнения потока А на выполнение потока В. Регистры потока А сохранены, регистры потока В восстановлены.
Поток В выполняет три этапа, составляющие оператор инкремента в языке С (псопп--), сохраняя новое значение переменной псопп, равное 2.
Впоследствии в некоторый момент времени система переключается на выполнение потока А. Восстанавливаются регистры потока А, и он продолжает выполняться с того места, на котором остановился, а именно начиная со второго этапа из трех, составляющих оператор декремента. Значение регистра уменьшается с 3 до 2, и значение 2 записывается в переменную псопп.
Окончательный результат таков: значение псопп равно 2, в то время как оно должно быть равным 1. Это ошибка
Подобные ошибки параллельного программирования трудно обнаружить по многим причинам. Во-первых, они возникают нечасто Тем не менее это ошибки, которые вызывают сбои в программах Во-вторых, ошибки такого типа возникают не систематически, так как зависят от недетерминированного совпадения нескольких событий. Наконец, в некоторых системах аппаратные команды могут быть атомарными. Это значит, что имеется аппаратная команда уменьшения значения целого числа на единицу (вместо трехступенчатой последовательности, которую мы предположили выше), а аппаратная команда не может быть прервана до окончания своего выполнения Но это не гарантировано для всех систем, так что код может работать в одной системе и не работать в другой.
Программирование с использованием потоков является параллельным (parallel), или одновременным (concurrent), программированием, так как несколько потоков могут выполняться параллельно (одновременно), получая доступ к одним и тем же переменным. Хотя ошибочный сценарий, рассмотренный нами выше, предполагает систему с одним центральным процессором, вероятность ошибки также присутствует, если потоки А и В выполняются в одно и то же время на разных процессорах в многопроцессорной системе В обьганом программировании под Unix мы не сталкиваемся с подобными ошибками, так как при использовании функции fork родительский и дочерний процессы не используют совместно ничего, кроме дескрипторов. Тем не менее мы столкнемся с ошибками этого типа при обсуждении совместного использовании памяти несколькими процессами.
Эту проблему можно с легкостью продемонстрировать на примере потоков. В листинге 23.11 показана программа, которая создает два потока, после чего каждый поток инкрементирует некоторую глобальную переменную 5000 раз.
Мы повысили вероятность ошибки за счет того, что потребовали от программы получить текущее значение переменной counter, вывести это значение и записать его. Если мы запустим эту программу, то получим результат, представленный в листинге 23.10.
Листинг 23.10. Результат выполнения программы, приведенной в листинге 23.11
1
2
3
4
продолжение выполнения потока номер 4
4 4 5 5 54 4 5 5 5517
4 4 5 5 5
4 4 5 5 5
518
518 теперь выполняется поток номер 5
519
520
продолжение выполнения потока номер 5
926
927
4 519 теперь выполняется поток номер 4 записывая неверные значения
4 520
Листинг 23.11. Два потока, которые неверно увеличивают значение глобальной переменной
//threads/exampl eO 1 с
#inc】ude "unpthread h'
#defme NLOOP 5000
int counter/* потоки должны увеличивать значение ^той переменной */
void *doit(void *).
int
main(int argc char **argv)
{
pthread_t tidA tidB
Pthread_create(&tidA NULL.&doitNULL)
Pthread_create(&tidB NULL&doitNULL).
/* ожидание завершения обоих потоков */
Pthread一join(tidA NULL)
Pthread_join(tidB. NULL).
exit(0)
}
void *
doit(void *vptr)
{
int i val,
/* Каждый поток получает выводит и увеличивает на
* единицу переменную counter NLOOP раз Значение
* переменной должно увеличиваться монотонно
*/
for (т=0 1 < NLOOP i++) {
val = counter
p门 ntf(噴d %d\en' pthread_sel f() va】+ 1)
counter = val + 1
}продолжение 身
Листинг 23.11 (продолжение)
return (NULL).
}
Обратите внимание на то, что в первый раз ошибка происходит при переключении системы с выполнения потока номер 4 на выполнение потока номер 5: каждый поток в итоге записывает значение 518. Это происходит множество раз на протяжении 10 000 строк вывода.
Недетерминированная природа ошибок такого типа также будет очевидна, если мы запустим программу несколько раз: каждый раз результат выполнения программы будет отличаться от предыдущего. Также, если мы переадресуем вывод результатов в файл на диске, эта ошибка иногда не будет возникать, так как программа станет работать быстрее, что приведет к уменьшению вероятности переключения системы между потоками. Наибольшее количество ошибок возникнет в случае, если программа будет работать интерактивно, записывая результат на медленный терминал, но при этом также сохраняя результат в файл при помощи программы Unix script (которая описана в главе 19 книги [93]).
Только что описанная проблема, возникающая, когда несколько потоков изменяют значение одной переменной, является самой простой из проблем параллельного программирования. Для решения этой проблемы используются так называемые взаимные исключения (mutex — mutual exclusion), с помощью которых контролируется доступ к переменной. В терминах Pthread взаимное исключение —это переменная типа pthread_mutex_t» которая может быть заблокирована и разблокирована с помощью следующих двух функций:
#include <pthread h>
1nt pthread_mutex_lock(pthread_mutex_t *mptr).
int pthread_mutex_unlock(pthread_mutex_t ★叩tr)
D6e функции возвращаю! О в случае успешного выполнения, положительное значение Еххх в случае ошибки
Если некоторый поток попытается блокировать взаимное исключение, которое уже блокировано каким-либо другим потоком (то есть принадлежит ему в данный момент времени), этот поток окажется заблокированным до освобождения взаимного исключения.
Если переменная-исключение размещена в памяти статически, следует инициализировать ее константой PTHREAD_MUTEX_INITIALIZER. В разделе 27.8 мы увидим, что если мы размещаем исключение в совместно используемой (разделяемой) памяти, мы должны инициализировать его во время выполнения программы путем вызова функции pthread_mutex_i m t.
ПРИМЕЧАНИЕ
Некоторые системы (например, Solaris) определяют константу PTHREAD_MUTEX_ INITIALIZER как 0. Если данная инициализация будет опущена, это ни на что не повлияет, так как статически размещаемые переменные все равно автоматически инициализируются нулем. Но для других систем такой гарантии дать нельзя — например, в Digital Unix константа инициализации ненулевая.
В листинге 23Д2 приведена исправленная версия листинга 23.11, в которой используется одно взаимное исключение для блокирования счетчика при работе с двумя потоками.
Листинг 23.12. Исправленная версия листинга 23.11, использующая взаимное исключение для защиты совместно используемой переменной
//threads/ехатр]еО1 с
1 #inc】ude "unpthread h"
2 #define NLOOP 5000
wt counter. /* переменная, значение которой увеличивают оба потока */
void *doit(void *).
int
mam(int argc. char **argv)
{
pthread_t tidA tidB.
Pthread_create(&tidA, NULL &doit. NULL).
Pthreadmereate(&tidB, NULL &dcnt NULL).
/* ждем завершениявыполнения обоих потоков */
Pthreadjoin(tidA.NULL).
Pthread_join(tidB,NULL).
exit(0).
}
void *
doit(void *vptr)
{
mt i. val
/* Каждый поток получает, выводит и увеличивает на
* единицу переменную counter NLOOP раз Значение
* переменной должно увеличиваться монотонно
*/
24 for (1=0 1 < NLOOP. i++) {
25val = counter.
26 printfC^d %d\en". pthread self(). val + 1).
27counter = val + 1.
28}
29 return (NULL).
30 }
Мы объявляем взаимное исключение с именем counter_mutex. Это исключение должно быть заблокировано потоком на то время, когда он манипулирует переменной counter. Когда мы запускали эту программу, результат всегда был правильным: значение переменной увеличивалось монотонно, а ее окончательное значение всегда оказывалось равным 10 000.
Насколько серьезной является дополнительная нагрузка, связанная с использованием взаимных исключений? Мы изменили программы, приведенные в листингах 23.11 и 23.12, заменив значение NLOOP на 50 000 (вместо исходного значения 5000), и засекли время, направив вывод на устройство/dev/nulL Время работы центрального процессора в случае корректной версии, использующей взаимное исключение, увеличилось относительно времени работы некорректной версии без
взаимного исключения на 10%. Это означает, что использование взаимного исключения не связано со значительными издержками.
Условные переменные
Взаимное исключение позволяет предотвратить одновременный доступ к совместно используемой (разделяемой) переменной, но для того, чтобы перевести поток в состояние ожидания (спящее состояние) до момента выполнения некоторого условия, необходим другой механизм. Продемонстрируем сказанное на следующем примере. Вернемся к нашему web-клиенту из раздела 23.6 и заменим функцию Solaris thr_join на pthreadjom. Но мы не можем вызвать функцию Pthread до тех пор, пока не будем знать, что выполнение потока завершилось. Сначала мы объявляем глобальную переменную, которая служит счетчиком количества завершившихся потоков, и организуем управление доступом к ней с помощью взаимного исключения.
intndone /* количество потоков завершивших выполнение */
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER.
Затем мы требуем, чтобы каждый поток по завершении своего выполнения увеличивал этот счетчик на единицу, используя соответствующее взаимное исключение.
void *
do_get_read(void *vptr)
Pthread_mutex_lock(&ndone_mutex)
ndone++
Pth rea d_mutex_un1oc k(&ndone_mutex).
return(fptr),/* завершение выполнения потока */
}
Но каким при этом получается основной цикл? Взаимное исключение должно быть постоянно блокировано основным циклом, который проверяет; как^е потоки завершили свое выполнение.
while (nlefttoread > 0) {
while (nconn < maxnconn && nlefttoconn > 0) {
/* находим файл для чтения */
}
:/* Проверяем. не завершен ли поток */
/* Pthread_mutex_lock(&ndone_mutex),
if (ndone > ОУ { 一
for (i = 0. i < nfiles i++) {
if (file[i] Jflags & F_DDNE) {
Pthread_join(fi1e[i] f_tid (void **) &fptr)
/* обновляем file[i] для завершенного потока */
}
}
}
Pthread_mutex_unlоск(&ndone_mutex).
} 一 - 一
Это означает, что главный поток никогда не переходит в спящее состояние, а просто входит в цикл, проверяя каждый раз значение переменной ndone. Этот процесс называется опросом (polling) и рассматривается как пустая трата времени центрального процессора.
Нам нужен метод, с помощью которого главный цикл мог бы входить в состояние ожидания, пока один из потоков не оповестит его о том, что какая-либо задача выполнена. Эта возможность обеспечивается использованием условной переменной (conditional variable) вместе с взаимным исключением. Взаимное исключение используется для реализации блокирования, а условная переменная обеспечивает сигнальный механизм.
В терминах Pthread условная переменная — это переменная типа pthread_cond_t. Эти переменные используются в следующих двух функциях:
#1 nclude <pthread h>
mt pthread_condjwait(pthread_cond_t *cptr pthread_mutex_t *mptr)
mt pthread_cond_signal(pthread_cond_t *cptr)
Обе функции возвращают 0 в случае успешного выполнения положительное значение Еххх в случае ошибки
Слово s 1 gna 1 в названии второй функции не имеет отношения к сигналам Unix SIGxxx.
Проще всего объяснить действие этих функций на примере. Вернемся к нашему примеру web-клиента. Счетчик ndone теперь ассоциируется н с условной переменной, и с взаимным исключением: intndone.
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER:
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER.
Поток оповещает главный цикл о своем завершении, увеличивая знание счетчика, пока взаимное исключение принадлежит данному потоку (блокировано им), и используя условную переменную для сигнализации.
Pthread_mutex_lock(&ndone_mutex)
ndone++
Pthread_cond_signa1(&ndone_cond).
Pth readjnutex_unlock(&ndone_mutex)
Затем основной цикл блокируется в вызове функции p"thredd_con廿ai.t» 9%祈 дая оповещения о завершении выполнения потока:
while (nlefttoread > 0) {
while (nconn < maxnconn && nlefttoconn > 0) {
/* находим файл для чтения */
}
/* Ждем завершения выполнения какого-либо потока */
Pthread_mutex_lock(&ndone_mutex).
while (ndone == 0)
-Pthread_cond_wait(&ndone_cond &ndone_mutex)
if (file[i] f_flags & F_DONE) {
Pthread_join?f11 e[i ] f_tid (void **) &fptr)
/* обновляем file[i] для завершенного потока */
}
}
Pthread_mutex_unlock(&ndone_mutex)
Обратите внимание на то, что переменная ndone по-прежнему проверяется, только если потоку принадлежит взаимное исключение. Тогда, если не требуется выполнять какое-либо действие, вызывается функция pthread_cond_wa 11. Таким образом, вызывающий поток переходит в состояние ожидания, 〃 разблокируется взаимное исключение, которое принадлежало этому потоку. Кроме того, когда управление возвращается потоку функцией pthread_cond_wai t (после того, как поступил сигнал от какого-либо другого потока), он снова блокирует взаимное исключение.
Почему взаимное исключение всегда связано с условной переменной? «Условие» обычно представляет собой значение некоторой переменной, используемой совместно несколькими потоками Взаимное исключение требуется для того, чтобы различные потоки могли задавать н проверять значение условной переменной. Например, если в примере кода, приведенном выше, отсутствовало бы взаимное исключение, то проверка в главном цикле выглядела бы следующим образом:
/* Ждем завершения выполнения одного или нескольких потоков */
while (ndone == 0)
Pthread__cond_wa11(&ndone_cond &ndone_mutex)
Но при этом существует вероятность, что последний поток увеличивает значение переменной ndone после проверки ndone == 0, но перед вызовом функции pthread_cond_wai t. Если это происходит, то последний «сигнал» теряется, н основной цикл оказывается заблокированным навсегда, так как он будет ждать события, которое никогда не произойдет
По этой же причине при вызове функции pthread_cond_wa11 поток должен блокировать соответствующее взаимное исключение, после чего эта функция разблокирует взаимное исключение н помещает вызывающий поток в состояние ожидания, выполняя эти действия как одну атомарную операцию Если бы эта функция не разблокировала взаимное исключение и не заблокировала его снова после своего завершения, то выполнять эти операции пришлось бы потоку, как показано в следующем фрагменте кода:
/* Ждем завершения выполнения одного или нескольких потоков */
Pth readout ех_1 ock( &ndone_mutex)
while (ndone == 0) {
Pthread_mutex_un1ock(&ndone_mutex)
Pthread_cond_wait(&ndone_cond &ndone_mutex)
Pthread_mutex_lock(&ndone_mutex)—
}- 一 一
Существует вероятность того, что по завершении выполнения поток увеличит на единицу значение переменной ndone н это произойдет между вызовом функций pthread_niutex_unl оск и pthread_cond_wai t.
Обычно функция pthredd_cond_signdl выводит из состояния ожидания один поток, на который указывает условная переменная Существуют ситуации, когда некоторый поток знает, что из состояния ожидания должны быть выведены несколько потоков. В таком случае используется функция pthread_cond__broadcast, выводящая из состояния ожидания все потоки, которые блокированы условной переменной.
#include <pthread h>
1 nt pthread_cond_broadcast(pthread_cond_t *cptr)
mt pthread_cond_t1medwait(pthread_cond_t *cptr pthread_mutex_t *mptr const struct timespec ^dbstime)
Обе функции возвращают 0 в случае успешного выполнения положительное значение Еххх в случае ошибки
Функция pth read_cond_t i medwa 11 позволяет потоку задать предельное время блокирования. Аргумент abstime представляет собой структуру timespec (определенную в разделе 6 9 при рассмотрении функции pselect), которая задает системное время для момента, когда функция должна возвратить управление, даже если к этому моменту условная переменная не подала сигнал Если возникает такая ситуация, возвращается ошибка ETIME.
В данном случае значение времени является абсолютным значением времени, в отличие от относительного значения разницы во времени (time delta) между некоторыми событиями. Иными словами, abstime — это системное время, то есть количество секунд и наносекунд, прошедших с 1 января 1970 года (UTC) до того момента, когда эта функция должна вернуть управление Здесь имеется различие как с функцией pselect, так н с функцией select, задающими количество секунд (и наносекунд в случае pselect) до некоторого момента в будушем, когда функция должна вернуть управление. Обычно для этого вызывается функция gettimeofday, которая выдает текущее время (в виде структуры timeval), а затем оно копируется в структуру timespec и к нему добавляется требуемое значение: struct timeval tv struct timespec ts
if (gettimeofday(&tv NULL) < 0) err_sys('gettimeofday error")
ts tv_sec = tv tv_sec + 5./* 5 секунд в будущее */
ts tv二nsec = tv tv二usee * 1000 /* микросекунды переводим e наносекунды */
pthread_cond_timedwait(&ts)
Преимущество использования абсолютного времени (в противоположность относительному) заключается в том, что функция может завершиться раньше (возможно, из-за перехваченного сигнала). Тогда функцию можно вызвать снова, не меняя содержимое структуры timespec. Недостаток этого способа заключается в необходимости вызывать дополнительно функцию gettimeofday перед тем, как в первый раз вызывать функцию pth read__cond_t i medwa 11.
ПРИМЕЧАНИЕ
В Posix 1 определена новая функция clock^gettime, возвращающая текущее время в виде структуры timespec
Web-клиент и одновременный доступ
Изменим код нашего web-клиента из раздела 23.6: уберем вызов функции Solans th「_joi п н заменим его вызовом функции pthreadjoin. Как сказано в разделе 23.6, теперь нам нужно точно указать, завершения какого потока мы ждем. Для этого мы используем условную переменную, как показано в разделе 23.8.
Единственным изменением в отношении глобальных переменных (см. листинг 23.7) является добавление нового флага и условной переменной:
#define F_JOINED 8/* количество потоков */
mtndone. /* количество завершившихся потоков */
pthread__mutex_t ndone_rnutex = PTHREAD_MUTEX_INITIALIZER
pthread_cond_t ndonjcond = PTHREAD_COND_INITIAL IZER
Единственным изменением функции do_get_「ead (см. листинг 23.9) будет увеличение на единицу значения переменной ndone н оповещение главного цикла о завышении выполнения потока:
printf("end-of-file on ^s\en" fptr->f_name).
Close(fd)
Pthread_mutex_lock(&ndone_mutex)
fptr->f_flags = F_DONE. /* сбрасывает флаг F_READING */
ndone++.
Pthread_cond_signal(&ndone_cond)
4Pthread_mutex_unlock(&ndone_mutex).
return(fptr)./* завершение выполнения потока */
}
Большинство изменений касается главного цикла, представленного в листинге 23.8. Новая версия показана в листинге 23.13.
Листинг 23.13. Основной рабочий цикл функции main
//threads/web03 с
43 while (nlefttoread > 0) {
while (nconn < maxnconn && nlefttoconn > 0) {
/* находим файл для считывания */
46 for (1 = 0. 1 < nfiles.卄+)
if (file[i] f—flags == 0)
break
if (1 == nfiles)
err_quit("nlefttoconn = Zd but nothing found" nlefttoconn);
file[i] f.flags = RECONNECTING
Pthread_create(&tid~ NULL. &do_get_read&file[i]).
' file[i] f_tid = tid.~
ПСОПП++.
nlefttoconn--,
}
/* Ждем завершениявыполненияодного из потоков */
Pth read_mutex_l ock(&ndonejnutex)
while (ndone == 0)
Pthread_cond_wait(&ndone_cond&ndone_mutex).
626364656667686970717273747576 }626364656667686970717273747576 }if (file口] f_flags & F_00NE) {
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 }
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 }
Pthread_join(fi1e[i]吊 _t?d. (void **) &fptr).
if (&fiTe[i] i= fptr)—
err_quit('file[i] l= fptr')
fptrbf一flags = F—JOINED /* clears F_DONE */
ndone--
nconn--
nlefttoread--.
pnntf(* thread %d for %s done\en' fptr->f_tid, fptr->f_name);
}一 "
Pthread_mutex_unlock(&ndone_mutex).
exit(O)
По возможности создаем новый поток
44-56 Эта часть кода не изменилась.
Ждем завершения выполнения потока
57-60 Мы ждем завершения выполнения потоков, отслеживая, когда значение ndone станет равно нулю. Как сказано в разделе 23.8, эта проверка должна быть проведена перед тем, как взаимное исключение будет блокировано, а переход потока в состояние ожидания осуществляется функцией pthread_cond_wa 11.
Обработка завершенного потока
51-73 Когда выполнение потока завершилось, мы перебираем все структуры f11 е, отыскивая соответствующий поток, вызываем pthreadjoin, а затем устанавливаем новый флаг F JOINED.
В табл. 15.1 показано, сколько времени требует выполнение этой версии web- клиента, а также версии, использующей неблокируемую функцию connect.
Резюме
Создание нового потока обычно требует меньше времени, чем порождение нового процесса с помощью функции fork. Одно это уже является большим преимуществом использования потоков на активно работающих сетевых серверах. Многопоточное программирование, однако, представляет собой отдельную технологию, требующую большей аккуратности при использовании.
Все потоки одного процесса совместно используют глобальные переменные н дескрипторы, тем самым эта информация становится доступной всем потокам процесса. Но совместное использование информации вносит проблемы, связанные с синхронизацией доступа к разделяемым переменным, н поэтому нам следует использовать примитивы синхронизации технологии Pthread — взаимные исключения н условные переменные. Синхронизация доступа к совместно используемым данным — необходимое условие почти для любого приложения, работающего с потоками.
При разработке функций, которые могут быть вызваны таким приложением, нужно учитывать требование безопасности в многопоточной среде. Это требова-
ние выполнимо при использовании собственных данных потоков (thread-specific data), пример которых мы показали при рассмотрении функции readline в этой главе.
К модели потоков мы вернемся в главе 27, где сервер при запуске создает пул (накопитель — pool) потоков. Для обслуживания очередного клиентского запроса используется очередной свободный поток.
Упражнения
Сравните использование дескриптора в случае, когда в коде сервера применяется функция fork, н в случае, когда используются потоки. Предполагается, что одновременно обслуживается 100 клиентов.
Что произойдет в листинге 23.2, если поток при завершении функции str_echo не вызовет функцию close для закрытия сокета?
В листингах 5.4 и 6.2 мы выводили сообщение Server terminated prematurely (Сервер завершил работу преждевременно) в случаях, когда мы ждали от сервера прибытия отраженной строки, а вместо этого получали признак конца файла (см. раздел 5.12). Модифицируйте листинг 23.1 таким образом, чтобы в соответствующих случаях также выдавалось аналогичное сообщение.
Модифицируйте листинги 23.5 н 23.6 таким образом, чтобы программы можно было компилировать в системах, не поддерживающих потоки.
Чтобы увидеть ошибку в функции readline, приведенной в листинге 23.2, запустите эту программу на стороне сервера. Затем измените эхо-клиент TCP из листинга 6.2, корректно работающий в пакетном режиме. Возьмите какой- либо большой текстовый файл в своей системе и трижды запустите клиент в пакетном режиме, чтобы он считывал текст из этого файла и записывал результат во временный файл. Если есть возможность, запустите клиенты на другом узле (не на том, на котором запущен сервер). Если все три клиента выполнят работу правильно (часто они зависают), посмотрите на файлы с результатом и сравните их с исходным файлом.
Теперь создайте версию сервера, используя корректную версию функции readline из раздела 23.5. Повторите тест, используя три эхо-клиента. Теперь все три клиента должны работать исправно. Также поместите функцию pn ntf в функции readli ne dest「ucto「, readl i ne_once и в вызов функции таИосв readl i ne. Это даст вам возможность увидеть, что ключ создается только один раз, но для каждого потока выделяется область памяти и вызывается функция-деструктор.
ГЛАВА 24
Параметры IP
Введение
В IPv4 допускается, чтобы после фиксированного 20-байтового заголовка шло до 40 байт, отведенных под различные параметры. Хотя определено 10 параметров, чаще всего используется параметр маршрута от отправителя (source route option). Доступ к этим параметрам осуществляется через параметр сокета IP_OPTIONS, что мы покажем на примере использования маршрутизации от отправителя (source routing).
В IPv6 допускается наличие расширяющих заголовков (extension headers) между фиксированным 40-байтовым заголовком IPv6 н заголовком транспортного уровня (например, ICMPv6, TCP или UDP). В настоящее время определены 6 различных расширяющих заголовков. В отличие от подхода, использованного в IPv4, доступ к расширяющим заголовкам IPv6 осуществляется через функциональный интерфейс, что не требует от пользователя понимания фактических деталей того, как именно эти заголовки расположены в пакете IPv6.
Параметры IPv4
На рис. АЛ мы показываем параметры, расположенные после 20-байтового заголовка IPv4. Как отмечено при рассмотрении этого рисунка, 4-разрядное поле длины ограничивает общий размер заголовка IPv4 до 15 32-разрядных слов (что составляет 60 байт), так что на параметры IPv4 отведено 40 байт. Для IPv4 определено 10 различных параметров.
NOP (no-operation — нет действий). Этот однобайтовый параметр используется для выравнивания очередного параметра по 4-байтовой границе.
EOL (end-of-list — конец списка параметров). Этот однобайтовый параметр обозначает конец списка параметров. Он необходим только в том случае, если конец списка параметров не совпал с окончанием заголовка. Поскольку суммарный размер параметров IP должен быть кратным 4 байтам, после последнего параметра добавляются байты EOL.
LSRR (Loose Source and Record Route — гибкая1 маршрутизация от отправителя с записью) (см. раздел 8.5 [94]). Пример использования этого параметра мы вскоре продемонстрируем.
Иногда название этого параметра переводится как «потеря отправителя и запись маршрута», но это, вероятно, следствие путаницы: loose — свободный, несвязанный; loss — потеря. — Примеч. перев
SSRR (Strict Source and Record Route — жесткая маршрутизация от отправителя с записью) (см. раздел 8.5 [94]). Пример использования этого параметра мы также вскоре продемонстрируем.
Отметка времени (timestamp) (см. раздел 7.4 [94]).
Запись маршрута (record route) (см. раздел 7.3 [94]).
Основной параметр обеспечения безопасности (basic security).
Расширенный параметр обеспечения безопасности (extended security).
Идентификатор потока (устаревший параметр) (stream identifier).
Извещение маршрутизатора (router alert). Этот новый параметр описан в RFC 2113 [52]. Он включается б дейтаграмму IP, для того чтобы все пересылающие эту дейтаграмму маршрутизаторы обрабатывали ее содержимое.
В главе 9 книги [105] приводится более подробное рассмотрение первых шести параметров, а в указанных выше разделах [94] имеются примеры их использования. В RFC 1108 [53] приведено более подробное рассмотрение двух параметров, связанных с безопасностью (параметры 7 и8в расположенном выше списке), которые не слишком широко используются.
Функции getsockopt и setsockopt (для которых аргумент 1 evel равен IPPROTO_I Р, а аргумент optname — IP_OPTIOHS) предназначены соответственно для получения и установки параметров IP. Четвертый аргумент функций getsockopt н setsockopt — это указатель на буфер (размер которого не превосходит 44 байт), а пятый аргумент —это размер буфера. Причина, по которой размер буфера может на 4 байта превосходить максимальный суммарный размер параметров, заключается в способе обработки параметров маршрута от отправителя, как мы вскоре увидим. Все остальные параметры помещаются в буфер именно в том виде, в котором они потом упаковываются в заголовок дейтаграммы.
Когда параметры IP задаются с использованием функции setsockopt, указанные параметры включаются во все дейтаграммы, отсылаемые с данного сокета. Этот принцип работает для сокетов TCP, UDP н для символьных сокетов. Для отмены этого параметра следует вызвать функцию setsockopt и задать либо пустой указатель в качестве четвертого аргумента, либо нулевое значение в качестве пятого аргумента (длина),
ПРИМЕЧАНИЕ
Установка параметров IP для символьного сокета IP не работает во всех реализациях, если уже установлен параметр IP HDRINCL (который мы обсудим в последующих главах). Многие Беркли-реализации не отсылают параметры, установленные с помощью IP OPTIONS, если включен параметр IP_HDRINCL, так как приложение может устанавливать свои собственные параметры в формируемом им заголовке IP [105, с. 10561057] В других системах (например, в FreeBSD) приложение может задавать свои параметры IP, либо используя параметр сокета IP_OPTIONS, либо установив параметр IP—HDRINCL и включив требуемые параметры в создаваемый им заголовок IP, но одновременное применение обоих этих способов не допускается.
При вызове функции getsockopt для получения параметров IP присоединенного сокета TCP, созданного функцией accept, возвращается лишь обращенный параметр маршрута от отправителя, полученный вместе с клиентским сегментом SYN на прослушиваемом сокете [105, с. 931]. TCP автоматически обращает мар-
трут от отправителя, поскольку маршрут, указанный клиентом, — это маршрут от клиента к серверу, а сервер должен использовать для отсылаемых им дейтаграмм обратный маршрут. Если вместе с сегментом SYN не был получен маршрут от отправителя, то значение пятого аргумента (этот аргумент типа «значение-результат», как было указано выше, задает длину буфера) при завершении функции getsockopt будет равно нулю. Для прочих сокетов TCP, всех сокетов UDP и всех символьных сокетов IP при вызове функции getsockopt вы просто получите копию тех параметров IP, которые были установлены для этих сокетов с помощью функции setsockopt. Заметим, что для символьных сокетов IP полученный заголовок IP, включая все параметры IP, всегда возвращается всеми входными функциями, поэтому полученные параметры IP всегда доступны.
ПРИМЕЧАНИЕ
В Беркли-ядрах полученный маршрут от отправителя, так же как и другие параметры IP, никогда не возвращается для сокетов UDP Показанный на с. 775 [105] код, предназначенный для получения параметров IP, существовал со времен 4 3BSD Reno, но так как он не работал, его всегда приходилось превращать в комментарий Таким образом, для сокетов UDP невозможно использовать обращенный маршрут от отправителя полученной дейтаграммы для того, чтобы отослать ответ.
Многие Беркли-ядра дают сбой (то есть система прекращает работу) при попытке вызвать функцию getsockopt или setsockopt на символьном сокете IP Но для создания символьного сокета требуются права привилегированного пользователя, а тот, кто обладает такими правами, имеет возможность нанести системе гораздо более серьезный вред
Параметры маршрута от отправителя 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
Routes — SSRR в случае жесткой маршрутизации), так как при проходе дейтаграммы через каждый из перечисленных в списке узлов происходит замена указанного адреса на адрес интерфейса для исходящих дейтаграмм Это позволяет получателю дейтаграммы обратить полученный список, превратив его в маршрут, по которому будет послан ответ отправителю Примеры этих двух маршрутов от отправителя вместе с соответствующим выводом программы tcpdump, приведены в разделе 8 5 книги [94].
Маршрут от отправителя мы определяем как массив адресов IPv4, которому предшествуют три однобайтовых поля, как показано на рис 24 1 Это формат буфера, который передается функции setsockopt
4 байта ►
NOP
code
len
ptr
IP-адрес 1
IP-адрес 2
IP-адрес 3
IP-адрес 9
IP-адрес получателя
11114 байта 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 Задаваемое нами значение поля 1еп — это размер параметра в байтах, включая 3-байтовый заголовок и дополнительный адрес получателя, приведенный в конце списка Для маршрута, состоящего из одного IP-адреса, это значение будет равно 11, для двух адресов —15, и так далее вплоть до максимального значения 43 Параметр NOP не является частью обсуждаемого параметра, и его длина не включается в значение поля 1 еп, но она входит в размер буфера, который мы сообщаем функции setsockopt Когда первый адрес в списке удаляется из параметра маршрута от отправителя и добавляется в поле адреса получателя в заголовок IP, значение поля 1 еп уменьшается на 4 (см рис 9 32 и 9 33 [105]) Поле pt г — это указатель, или сдвиг, задающий положение следующего IP-адреса из списка, который должен быть обработан Мы инициализируем это поле значением 4, что соответствует первому адресу IP Значение этого поля увеличивается на 4 каждый раз, когда дейтаграмма обрабатывается одним из перечисленных в маршруте узлов
Теперь мы переходим к определению трех функций, с помощью которых мы инициализируем, создаем и обрабатываем параметр маршрута от отправителя Наши функции предназначены для работы только с этим параметром Хотя в принципе возможно объединить параметр маршрута от отправителя с другими пара-
метрами IP (такими, как параметр временной отметки), но все эти параметры, за исключением параметров маршрутизации, используются редко В листинге 24 1 приведена функция i net_srcrt_i nit, а также некоторые статические переменные, используемые при составлении параметра
Листинг 24.1. Функция met_srcrt_init инициализация перед записью маршрута от отправителя-
//1popts/s ou rce route с
#include unp h
includevnetinet/in_systm h>
include<netir)et/ip h>
4
static u char *optr
/* указатель на формируемые параметры */
5
static u_char *lenptr
/* указатель на поле длины в гмраметре SRR */
6
static int ocnt
/* счетчик количества адресов */
7
u_char *
8
inet_srent init(void)
10
optr 二 Mallос(44)
/* NOP code len ptг ло 10 адресов */
11
bzero(optr 44)
/* гарантия EOL в конце */
12
ocnt = 0
13
return (optr)
/* указатель для функции setsockoptО */
14
}
Инициализация
10 13 Мы выделяем в памяти буфер, максимальный размер которого — 44 байта, и обнуляем его содержимое Значение параметра EOL равно нулю, так что тем самым параметр инициализируется как содержащий EOL байт Указатель на параметр возвращается вызывающему процессу, а затем передается как четвертый аргумент функции setsockopt
Следующая функция, i net_srcrt_add, добавляет один 1Ру4-адрес к создаваемому маршруту от отправителя
Листинг 24.2. Функция inet_srcrt_add добавление одного IPv4-адреса к маршруту от отправителя
//1popts/sourceroute с
mt
1net_srcrt_add(char *hostpt「mt type)
{一~
mtlen
struct addrinfo *ai
struct sockaddr_in *sm
if (ocnt > 9)
e「r_quit( too many source routes with 鸵".hostptr).
if (ocnt == 0) {
*optr++ = IPoBt_NOP/*NOP для выравнивания */
*optr++ = type 5 IPOPT—SSRR IPOPT_LSRR
lenptr = optr++/*поле длины заполним позже */
*optr++ = 4/*сдвиг первого адреса */
продолжение 少
илил8 9 0 122 2 3 3 38 9 0 122 2 3 3 33 4 5 6 7 83 3 3 3 3 33 4 5 6 7 83 3 3 3 3 3:тинг 24.2 (Продолжение)
ил
ил
8 9 0 12
2 2 3 3 3
8 9 0 12
2 2 3 3 3
3 4 5 6 7 8
3 3 3 3 3 3
3 4 5 6 7 8
3 3 3 3 3 3
}
ai = Host_serv(hostptr.""・ AF_INET 0).
sin = (struct sockaddr_in *) aT->ai_addr.
memcpy(opt「 &sin->sin_addr sizeof,st「uct in_addr)): freeaddrmfo(ai)-
optr += sizeof( struct in addr).
ocnt++.
len = 3 + (ocnt * sizeof(struct in_addr)),
*lenptr = len.
return (len + 1),/* размер для функции setsockoptO */
}
Аргументы
L6 Первый аргумент указывает либо на имя узла, либо на адрес IP в точечно-десятичной записи, второй аргумент в случае гибкой маршрутизации равен нулю, а в случае жесткой маршрутизации имеет ненулевое значению. Мы увидим, что от типа первого адреса, добавленного к маршруту, зависит, гибким или жестким является маршрут.
Проверка переполнения и последующая инициализация
Мы проверяем, не слишком ли много задано адресов, а затем инициализируем статические переменные, если это первый адрес. Как уже было сказано, мы всегда помещаем параметр NOP перед параметром маршрута от отправителя. Мы сохраняем указатель на поле len н будем записывать в него соответствующее значение при добавлении к списку очередного адреса.
Получение двоичного IP-адреса и запись маршрута
Наша функция host_serv обрабатывает либо имя узла, либо адрес IP в точечнодесятичной записи, и полученный двоичный адрес мы заносим в список. Мы обновляем значение поля len и возвращаем суммарный размер буфера (включая NOP), который вызывающий процесс должен передать функции setsockopt.
Когда полученный маршрут от отправителя возвращается приложению функцией getsockopt, формат этого параметра отличается от того, что было показано на рис. 24.1. Формат полученного параметра маршрута от отправителя показан на рис. 24.2.
|< 4 байта AI
IP адрес #1
NOP
code
len
Ptr
IP-адрес 2
IP-адрес 3
IP-адрес 4
IP-адрес получателя
4 байта 11114 байта 4 байта4 байта
Рис. 24.2. Формат параметра маршрута от отправителя, возвращаемого функцией getsockopt
В первую очередь, мы можем отметить, что порядок следования адресов изменен ядром на противоположный относительно полученного маршрута от отправителя. Имеется в виду следующее: если в полученном маршруте содержались
адреса А, В, С и D в указанном порядке, то под противоположным порядком подразумевается следующий: D, С, В, А. Первые 4 байта содержат первый IP-адрес из списка, затем следует однобайтовый параметр NOP (для выравнивания), затем —3-байтовый заголовок параметра маршрута от отправителя, и далее остальные IP-адреса. После 3-байтового заголовка может следовать до 9 IP-адресов, и максимальное значение поля 1 еп в возвращенном заголовке равно 39. Поскольку параметр NOP всегда присутствует, длина буфера, возвращаемая функцией getsockopt, всегда будет равна значению, кратному 4 байтам.
ПРИМЕЧАНИЕ
Формат, приведенный на рис. 24.2, определен в заголовочном файле <netinet/ip_var.h> в виде следующей структуры:
#defme MAX」POPTLEN 40
struct ipoption {
struct in_add「 ipopt_dst. /* адрес первого получателя */
charipopt_list[MAX_IPOPTLEN],/* соответствующие параметры */
}•■ 一
В листинге 24.3 мы анализируем эти данные, не используя указанную структуру.
Возвращаемый формат отличается от того, который был передан функции setsockopt. Если нам было бы нужно преобразовать формат, показанный на рис. 24.2, к формату, показанному на рис. 24.1, нам следовало бы поменять местами первые и вторые 4 байта и изменить значение поля 1 еп, добавив к имеющемуся значению 4. К счастью, нам не нужно этого делать, так как Беркли-реализации автоматически используют обращенный маршрут от получателя для сокета TCP. Иными словами, данные, возвращаемые функцией getsockopt (представленные на рис. 24.2), носят чисто информативный характер. Нам не нужно вызывать функцию setsockopt, чтобы указать ядру на необходимость использования данного маршрута для дейтаграмм IP, отсылаемых по соединению TCP, — ядро сделает это автоматически. Подобный пример с нашим сервером TCP мы вскоре увидим.
Следующей из рассматриваемых нами функций, связанных с параметром маршрутизации, полученный маршрут от отправителя передается в формате, показанном на рис. 242 Затем она выводит соответствующую информацию. Эту функцию 1 net_srtcrt_pri nt мы показываем в листинге 24.3.
Листинг 24.3. Функция inet_srtcrt_print: вывод полученного маршрута от отправителя ~
//1popts/sourceroutе с
39 void
40 inet_srcrt_print(ujchar *ptr, int len)
{ 一 一 一
u_char c.
charstr[INET_ADDRSTRLEN].
struct in_addr hopl.
memcpy(&hopl. ptr, sizeof(struct in_addr)):
ptr += sizeof(struct in_add「).一
while ( (c = *ptr++) == IPOPT NOP) ./* пропускаем pee параметры NOP */
продолжение 少продолжение 少48 if (c — IPUPT—LSRR)
продолжение 少
продолжение 少
илил
ил
ил
90123456789012345455555555556666669012345678901234545555555555666666pnntf("received LSRR ").
90123456789012345
45555555555666666
90123456789012345
45555555555666666
else if (c == IPOPT_SSRR)
pnntfCreceived SSRR ”).
else {
printfC'received option type Xd\en", c), return.
}
prmtfC^s ". Inet_ntop(AF_INET, &hopl. str. sizeof(str))).
len = *ptr++ - sizeof(struct in_addr) /* убираем IP-адрес получателя */
ptr++./* пропускаем указатель */
while (len > 0) {
prmtfC^s ", Inet_ntop(AF_lNET. ptr. str. sizeof(str)))
ptr += sizeof(struct in_addr), len -= sizeof(struct in_add「).
}一
printf("\en"),
}
Сохраняем первый адрес IP, пропускаем все параметры NOP
Первый адрес IP в буфере сохраняется, а все следующие за ним параметры NOP мы пропускаем.
Проверяем параметр маршрута от отправителя
Мы выводим информацию о маршрутизации (гибкая или жесткая) и проверяем значение поля code, содержащегося в 3-байтовом заголовке, получаем значение поля len и пропускаем указатель ptr. Затем мы выводим все IP-адреса, следующие за 3-байтовым заголовком, кроме IP-адреса получателя.
Пример
Теперь мы модифицируем наш эхо-сервер TCP таким образом, чтобы выводить полученный маршрут от отправителя, а эхо-клиент TCP — так, чтобы маршрут от отправителя можно было задавать. В листинге 24.4 показан код для эхо-клиента TCP.
Листинг 24.4. Эхо-клиент TCR задающий маршрут от отправителя
//ipopts/tcpcliDl с
#1 nd ude"unp h"
int
main(int argc. char **argv)
{
int c. sockfd, len = 0,
u_cha「 *ptr.
struct addrinfo *ai.
if (argc < 2)
err_quit("usage tcpcliOl [ -[gG] <hostname> ... ] 〈hostname〉”);
ptr = inet_srcrt_imt().
opterr = 0. /* чтобы функция getopt() не записывала сообщения об ошибках в поток stderr */
while ( (с = getopt(argc. argv. "g G '*)) '= -1) {
switch (с) {
case 'д'/* гибкая маршрутизация от отправителя */
len = inet_srcrt_add(optarg.0).
break.
case 'G'/* жесткая маршрутизация от отправителя */
len = inet_srcrt_add(optarg.1).
19break.-
20 case '?'
21 err_quit("unrecognized option.紀".c),
22)—
23 j
24 if (optind i= argc - 1)
25 err_quit("missing <hostname>**).
26 ai = Host_se「v(a「gv[optind] SERV_PORT_STR. AF_INET 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). /* адрес получателя в кон* 汽/
30Setsock叩t(sockfd.【PPROTO—IP. IP_OPTIONS ptr len)
31free(ptr).
32}
33Connect(sockfd di->a】_addr. ai->ai_addrlen).
34str_cli(stdin, sockfd)./* обработка в цикле */
35 exit(0).
36 }
Обработка аргументов командной строки
8-23 Мы вызываем нашу функцию т net_srcrt_i 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, по не все реализации поддерживают это соглашение.
Обработка адреса получателя и создание сокета
Последний аргумент командной строки — это имя узла или адрес сервера в точечно-десятичной записи, который обрабатывается нашей функцией host_serv. Мы не можем вызвать функцию tcp_connect, так как мы должны задать маршрут от отправителя между вызовом функций socket и connect. Последняя инициирует трехэтапное рукопожатие, а нам нужно, чтобы сегмент SYN отправителя и все последующие пакеты проходили по одному и тому же маршруту.
Если маршрут от отправителя задан, следует добавить IP-адрес сервера в конец списка адресов (см рис. 24.1) Функция setsockopt устанавливает маршрут от отправителя для данного сокета. Затем мы вызываем функцию connect, а потом — нашу функцию str_cl 1 (см. листинг 5.4).
Наш TCP-сервер имеет много общего с кодом, показанным в листинге 5 9, и содержит следующие изменения Во-первых, мы выделяем место для параметров: int len u_char *opts
opts = Malloc(44)
Во-вторых, мы получаем параметры IP после вызова функции accept, но перед вызовом функции fork* len = 44
Getsockopt(connfd IPPROTOJP IP_UPTIONS opts &len).
if (len > 0) {一 ~
pmntf(*'received IP options len = Zd\en' len), inet_srcrt_print(opts len)
}- 一
Если сегмент SYN, полученный от клиента, не содержит никаких параметров IP, переменная len по завершении функции getsockopt будет иметь нулевое значение (эта переменная относится к типу «значение-результат»). Как упоминалось выше, нам не нужно предпринимать какие-либо шаги для того, чтобы на стороне сервера использовался бы обращенный маршрут от отправителя, это делается автоматически без нашего участия [105, с. 931]. Вызывая функцию getsockopt, мы просто получаем копию обращенного маршрута от отправителя Если мы не хотим, чтобы TCP использовал этот маршрут, то после завершения функции accept следует вызвать функцию setsockopt и задать нулевую длину (последний аргумент), тем самым удалив все используемые в текущий момент параметры IP Но маршрут от отправителя, тем не менее, уже был использован в процессе трехэтапного рукопожатия при пересылке второго сегмента, а если мы уберем параметры маршрутизации, IP составит н будет использовать для пересылки последующих пакетов какой-либо другой маршрут.
Теперь мы покажем пример клиент-серверного взаимодействия при заданном маршруте от отправителя. Мы запускаем наш клиент на узле solan s следующим образом:
solans 先 tcpcliOl -g gw -g sunos5 bsdi
Тем самым дейтаграммы IP отсылаются с узла solans на маршрутизатор gw (см. рис. 1 7), далее на узел sunos5, а затем на узел bsdi, где запущен наш сервер. Две промежуточные системы, gw и sunos5, должны переправить дейтаграммы по маршруту от отправителя, чтобы этот пример работал.
Когда соединение установлено, на стороне сервера выдается следующий результат:
bsdi % tcpservOl
received IP options len = 16
received LSRR 206 62 226 36 206 62 226 62 206 62 226 33
Первый выведенный IP-адрес — это первый узел обратного маршрута (sunos5, как показано на рис. 24 2), а следующие два адреса идут в том порядке, который используется сервером для отправки дейтаграмм обратно клиенту. Если мы понаблюдаем за процессом взаимодействия клиента и сервера с помощью программы tcpdunip, мы увидим, как используется параметр маршрутизации для каждой дейтаграммы в обоих направлениях.
ПРИМЕЧАНИЕ
К сожалению, действие параметра сокета IP OPTIONS никогда не было документировано, поэтому в различных системах вы можете увидеть различные вариации, не имеющие отношения к исходному коду Беркли Например, в системе Solans 2 5 первый адрес, возвращаемый функцией getsockopt (см рис 24 2), — это не первый адрес в обращенном маршруте, а адрес собеседника Тем не менее обратный маршрут, используемый TCP, будет корректен Кроме того, в Solans 2 5 всем параметрам маршрутизации предшествует четыре параметра NOP, что ограничивает параметр маршрутизации восемью IP-адресами, а не девятью, которые реально могли бы поместиться
Уничтожение маршрута, полученного от отправителя
К сожалению, использование параметра маршрутизации образует брещь^ ср<:те- ме обеспечения безопасности. Начиная с выпуска Net/1 (1989), серверы riogind н rshd использовали код, аналогичный следующему: u_char buf[44] char lbuf[BUFSIZ] mt optsize optsize = s】zeof(buf).
if (getsockoptCO IPPROTOJP I PORTIONS buf &optsize) = 0 && optsize t= 0) { /* форматируем гшрамвтрм как шестнадцатеричные числа для записи в lbuf[] */ syslog(LOG_NOTICE
'Connection received using IP options (ignored) 2s Ibuf)
setsockopt(0 ipproto IP_OPTIONS NULL 0)
}一
Если устанавливается соединение с какими-либо параметрами IP (значение переменной optsize, возвращенное функцией getsockopt, не равно нулю), то с помощью функции sysl од делается запись соответствующего сообщения и вызывается функция setsockopt для очистки всех параметров Таким образом предотвращается отправка последующих сегментов TCP для данного соединения по обращенному маршруту от отправителя. Сейчас уже известно, что этой технологии недостаточно, так как к моменту установления соединения трехэтапное рукопожатие TCP будет уже завершено и второй сегмент (сегмент SYN-ACK на рис. 2 4) будет уже отправлен по обращенному маршруту от отправителя к клиенту. Даже если этот сегмент не успеет дойти до клиента, то во всяком случае он дойдет до неко-
торого промежуточного узла, входящего в маршрут от отправителя, где, возможно, затаился хакер. Так как предполагаемый хакер видел порядковые номера TCP в обоих направлениях, даже если никаких других пакетов по маршруту от отправителя послано не будет, хакер по-прежнему может отправлять клиенту сообщения с правильным порядковым номером.
Единственным решением этой возможной проблемы является запрет на прием любых соединений TCP, приходящих по обращенному маршруту от отправителя, когда вы используете IP-адрес от отправителя для какой-либо формы подтверждения (как, например, в случае с rlogin или rshd). Вместо вызова функции setsockopt во фрагменте кода, приведенном выше, закройте только что принятое соединение и завершите только что порожденный процесс сервера. Второй сегмент трехэтапного рукопожатия отправится, но соединение не останется откры- тьш и не будет использоваться далее.
Заголовки расширения IPv6
Мы не показываем никаких параметров в заголовке IPv6 на рис. А.2 (кот(^)ый всегда имеет длину 40 байт), но следом за этим заголовком могут идти заголовки расширения} {extension headers}.
Параметры для транзитных узлов2 (hop-by~hop options) должны следовать непосредственно за 40-байтовым заголовком IPv6. В настоящее время не определены какие-либо параметры для транзитных узлов, которые могли бы использоваться в приложении.
Параметры получателя (destination options). В настоящее время не определены какие-либо параметры получателя, которые могли бы использоваться в приложении.
Заголовок маршрутизации. Этот параметр маршрутизации от отправителя аналогичен по своей сути тем, которые мы рассматривали в случае IPv4.
Заголовок фрагментации. Этот заголовок автоматически генерируется узлом при фрагментации дейтаграммы IPv6, а затем обрабатывается получателем при сборке дейтаграммы из фрагментов.
Заголовок аутентификации (АН — authentication header). Использование этого заголовка документировано в RFC 1826 [3] и [54].
Заголовок шифрования3 (ESH — encapsulating security payload header). Использование этого заголовка документировано в RFC 1827 [4] н в [55].
Мы уже говорили о том, что заголовок фрагментации целиком обрабатывается ядром, а предложения по обработке заголовков АН и ESP изложены в [64]. Остаются еще три параметра, которые мы обсудим в следующем разделе.
Используются также термины «расширенные заголовки» и «дополнительные заголовки» — Примеч перев
Встречается также термин «заголовок переходов» — Примеч перев.
Используются также термины «заголовок защиты» и «дополнительный заголовок аутентификации». Он применятся для обеспечения конфиденциальности при передаче зашифрованных данных.— Примеч перев
Параметры транзитных узлов и параметры получателя IPv6
Параметры для транзитных узлов и параметры получателя IPv6 имеют одинаковый формат, показанный на рис. 24.3.8-разрядное поле следующий заголовок (next header) идентифицирует следующий заголовок, который следует заданным заголовком. 8-разрядное поле длина заголовка расширения {header extension length) содержит длину заголовка расширения в условных единицах (1 у. е. = 8 байт), но не учитывает первые 8 байт заголовка. Например, если заголовок занимает всего 8 байт, то значение поля длины будет равно нулю. Если заголовок занимает 16 байт, то соответственно значение этого поля будет равно 1, и т. д. Оба заголовка заполняются таким образом, чтобы длина каждого была кратна 8 байтам. Это достигается либо с помощью параметра padl, либо с помощью параметра padN, которые мы вскоре рассмотрим.
О7 815 1623 2431
Следующий заголовок
Длина заголовка расширения
Параметры транзитных узлов или параметры получателя
Рис. 24.3. формат параметра для транзитных узлов и параметра получателя
Заголовок параметра транзитных узлов и заголовок параметра получателя могут содержать произвольное количество отдельных параметров, как показано на рис. 24.4.
Тип
Длина
Значение параметра
11
Рис. 24.4. Формат отдельных параметров, входящих в загрловок параметра транзитных узлов и заголовок параметра получателя
Этот формат иногда называется TLV, так как для каждого отдельного параметра указывается его тип, длина и значение (type, length, value). 8-разрядное поле типа (type) указывает тип параметра. В дополнение к этому 2 старших разряда указывают, что именно узел IPv6 будет делать с этим параметром в том случае, если он не сможет в нем разобраться:
鈴 00 — пропустить параметр и продолжить обработку заголовка.
燃 01 ~ игнорировать пакет.
綫 10 — игнорировать пакет и отослать отправителю сообщение об ошибке ICMP Parameter problem (Проблема с параметром) типа 2 (табл. А.4), независимо от того, является ли адрес получателя пакета групповым адресом.
綫 И — игнорировать пакет и отослать отправителю сообщение об ошибке ICMP Parameter problem (Проблема с параметром) типа 2 (табл. А.4), но только в том случае, если адрес получателя пакета не является адресом многоадресной передачи.
Следующий разряд указывает, могут ли меняться данные, входящие в этот параметр.
落 0 — данные параметра не могут быть изменены.
綫 1 — данные параметра могут быть изменены.
Оставшиеся пять младших разрядов задают сам параметр.
8'разрядное поле длины задает длину данных этих параметров в байтах. Длина поля типа и длина самого поля длины не входят в это значение.
Два параметра заполнения (pad options) определены в RFC 1883 [25] и могут быть использованы как в заголовке параметров для транзитных узлов, так и в заголовке параметров получателя. Один из параметров транзитных узлов — параметр размера увеличенного поля данных (jumbo payload length option) — также определен в RFC 1883. Ядро генерирует этот параметр по мере необходимости и обрабатывает при получении. Новый параметр увеличенного объема данных для IPv6, аналогичный параметру извещения маршрутизатора (router alert), предложен в [52]. Эти параметры изображены на рис. 24.5.
Padi: 0
1
Длина
Нулевые байты
Количество байтов, заданное в поле длины
194
4
Размер увеличенного поля данных
Размер увеличенного поля данных
4 байта
Тип
2
Значение
Извещение маршрутизатора
2 байта
Рис. 24.5. Параметры IPv6 для транзитных узлов
Параметр padl — это единственный параметр, для которого не указывается длина и значение. Его назначение — вставка одного пустого байта для заполнения. Параметр padN используется, когда требуется вставить 2 или более байтов заполнения. Для 2 байт заполнения длина параметра будет иметь нулевое значение, а сам параметр будет состоять из поля типа и поля длины. В случае 3 байт заполнения длина будет равна 1, а следом за полем длины будет стоять один нулевой байт. Параметр размера увеличенного поля данных допускает увеличение поля размера дейтаграмм до 32 бит и используется, когда 16-разрядное поле размера, показанное на рис. А.2, оказывается недостаточно большим.
Мы показываем эти параметры схематически, потому что для всех параметров получателя и транзитных узлов действует так называемое условие выравнивания (alignment requirement), записываемое как ли + ^. Это означает, что сдвиг данного параметра относительно начала заголовка равен числу, п раз кратному х байтам, к которому добавлено , байт (то есть величина сдвига в байтах равна т + у). Например, условие выравнивания для параметра размера увеличенного поля данных записывается как 4п + 2. Это означает, что 4-байтовое значение параметра (длина размера увеличенного поля данных) будет выровнено по 4-байтовой границе. Причина, по которой значение у для этого параметра равно 2, заключается в том, что параметры транзитных узлов и получателя начинаются именно с двух байтов — один байт используется для указания типа, другой — для указания длины (см. рис. 24.4).
Параметры транзитных узлов и параметры получателя обычно задаются как вспомогательные данные в функции sendrnsg и возвращаются функцией recvmsg также в виде вспомогательных данных. От приложения не требуется никаких специальных действий для отправки этих параметров — нужно только задать их при вызове функции sendrnsg. Но для получения этих параметров должен быть включен соответствующий параметр сокета: IPV6_H0P0PTS для параметра транзитных узлов и IPV6 DST0PTS для параметров получателя. Например, чтобы можно было получить оба параметра, нужен следующий код: const mt on = 1.
set$ockopt($ockfd, IPPR0T0_IPV6, IPV6 H0P0PTS. &on. sizeof(on)). set5ockopt(sockfd. IPPR0T0_IPV6, IPV6 二 DSTOPTS. &on, sizeof(on)).
На рис. 24.6 показан формат объектов вспомогательных данных, используемый для отправки и получения параметров транзитных узлов и параметров получателя.
cmsghdr{}cmsghdr{}
cmsg_len
IPPROTO_IPV6
IPV6_HOPOFTS
cmsg_len
IPPROTO_IPV6
IPV6_DSTOPTS
cmsg_level
cmsg_level
cmsg_type
cmsg_type
Параметры транзитных узлов
Параметры получателя
Рис. 24.6. Объекты вспомогательных данных, используемые для параметров транзитных узлов и параметров получателя
В отличие от других объектов вспомогательных данных IPv6 (см. рис. 20.5), в данном случае каждая реализация определяет, что передается между пользователем и ядром в той части этих объектов, которая обозначена как cmsg_data. Вместо того чтобы определять содержимое этой части, определены шесть функций, которые создают и обрабатывают эти вспомогательные объекты данных. Следующие четыре функции формируют отправляемый параметр.
#include <netinet/in h>
int 1net6_option^space(int nbytes).
Возвращает количество байтов. занимаемых данным параметром
wt 1 net6__option_imt(void *buf. struct cmsghdr ★★cmsgp int type):
Возвращает 0 в случае успешного выполнения. -1 в случае ошибки
int 1net6_option_append(struct cmsghdr *cmsg. const uint8_t Чурер. int multx. int plusy):
Возвращает 0 в случае успешного выполнения, -1 в случае ошибки
uint8_t *inet6_option_al1ос(struct cmsghdr *cmsg int data Zen. mt multx, int plusy);
Возвращает указатель на поле типа параметра в случае успешного выполнения. NULL в случае ошибки
Функция 1 net6_opti on_space возвращает количество байтов, необходимое для данного параметра, включая структуру cmsghdr в начале и произвольное количество байтов заполнения в конце. Аргумент nbytes — это размер структуры, определяющей параметр, который должен включать произвольное количество байтов заполнения в начале (значение у из суммы 如 + 夕),тип параметра, длину и данные.
Функция 1 net6_opti on_i гл t вызывается один раз для каждого объекта вспомогательных данных, который будет содержать либо параметр транзитных узлов, либо параметр получателя. Аргумент buf указывает на буфер, который будет содержать объект вспомогательных данных. Аргумент cmsgp — это адрес указателя на структуру cmsghdr. Эта функция инициализирует данную структуру в буфере, на который указывает аргумент buf, и возвращает указатель на эту структуру в аргументе *cmsgp. Аргумент type может принимать значение IPV6_H0P0PTS либо IPV6_DST0PTS и хранится в поле cmsg_type формируемой структуры cmsghdr.
Функция 1 net6_opti on_append добавляет либо параметр транзитных узлов, либо параметр получателя к вспомогательному объекту данных, инициализированному функцией 1 net6__option_i m t. Аргумент cmsg — это указатель на структуру cmsghdr, которая была инициализирована функцией inet6_option_init. Аргумент typep — это указатель на 8~разрядное поле, задающее тип параметра, за которым должно следовать 8-разрядное поле длины параметра и данные TLV. Вызывающий процесс устанавливает эти значения перед вызовом данной функции. Аргументы mul tx и plusy — это два слагаемых х и 弘 фигурирующие в условии выравнивания для данного параметра.
Описанная выше функция требует, чтобы вызывающий процесс составил данные TLV и передал указатель как аргумент typep, после чего параметр копируется в объект вспомогательных данных. Другой вариант — функция i net6_opt i on_a 11 ос возвращает указатель на объект вспомогательных данных, и вызывающий процесс затем сохраняет данные TVL для параметра как вспомогательные данные. Аргумент cmsg — это указатель на структуру cmsghdr, инициализированную функ・ цией 1 net6_opt 1 on_i ni t. Аргумент datalen — это значение поля длины для данного параметра. Этот аргумент необходим для расчета количества байтов заполнения, если таковые добавляются к параметру. Аргументы mul tx и pl usy — это два слагаемых % и 夕,фигурирующие в условии выравнивания для данного параметра.
Оставшиеся две функции обрабатывают полученный параметр.
finclude <netinet/in h>
int 1 net6_opt 1 on^next(const struct emsghdr *cmsg, uint8_t **tptrp).
Возвращает 0 в случае если имеется параметр для обработки. -1 в случае ошибки или если нет параметров для обработки
int 1net6_opt1on_f 1 nd(const struct emsghdr *cmsg, uint8_t *tptrp int type).
Возвращает 0 в случае успешного выполнения. -1 в случае ошибки
Функция 1 net6_opt 1 on_next обрабатывает следующий параметр в буфере. Аргумент cesgdr указывает на структуру cmsgdr, в которой поле cmsg_l evel должно иметь значение I PPROTO_I РV6, а поле cmsg_type — либо I PV6_H0P0PTS, либо IPV6一DSTOPTS. Когда эта функция впервые вызывается для конкретного объекта вспомогательных данных, аргумент *tptrp должен быть пустым указателем. Затем каждый раз при завершении этой функции аргумент *tptrp указывает на 8-разрядное поле типа для параметра, который будет обработан следующим. Значение аргумента *tptrp используется данной функцией для того, чтобы в промежутках между последовательными вызовами хранить информацию о том, в каком месте объекта вспомогательных данных она находилась. Когда обработан последний параметр, функция возвращает значение -1, a *tptrp становится пустым указателем. Если происходит ошибка, возвращаемое значение также-1, но *tptrp является непустым указателем.
Функция met6_option_find аналогична предыдущей функции, но позволяет вызывающему процессу задать тип параметра, который следует искать (аргумент type), вместо того чтобы каждый раз возвращать следующий параметр.
Заголовок маршрутизации 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 и возвращается в виде вспомогательных данных функцией reevmsg. Для отправки заголовка приложению не требуется выполнять какие-либо
07 815 1623 2431
Следующий заголовок
Длина заголовка расширения
Тип маршрутизации 二 0
Оставшиеся сегменты
Зарезервировано
0
1
2
3
4
5
6
7
8
9
1 0
1
1
1
2
1
3
1 4
1
5
1
6
1
7
1
8
1
9
2
0
2
1
2
2
2
3
—Адрес 1,-
--Адрес 2-
7
Адрес 23
Рис. 24.7. Заголовок маршрутизации IPv6
специальные действия — достаточно просто указать его при вызове функции sendmsg. Но для получения заголовка маршрутизации требуется, чтобы был включен параметр IPV6_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 <netmet/in h>
size_t inet6_rthdr_space(int type, int segments)
Возвращает положительное число равное количеству байтов 'в случаё успешного выполнения в случае ошибки
struct cmsghdr *inet6_rthdr_imt(void ★加Г int type).
Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки
int 1net6_rthdr_add(struct cmsghdr *cmsg const struct m6_addr *dddr unsigned mt flags). 一 一"
Возвращает 0 в случае успешного выполнения -1 в случае ошибки mt met6_rthdr_lasthop(struct cmsghdr ^cmsg unsigned mt flags). Возвращает 0 в случае успешного выполнения, -1 в случае ошибки
Функция met6_rthdr_space возвращает количество байтов, необходимое для размещения объекта вспомогательных данных, содержащего заголовок маршрутизации указанного типа (обычно это IPV6_RTHDR_TYPE_0) с заданным количеством сегментов. Этот размер включает структуру cmsghdr.
cmsghdr{}
cmsg_len
cmsg^level
IPPROTO_IPV6
cmsg_type
IPV6_RTHDR
Заголовок маршрутизации
Рис. 24.8. Объект вспомогательных данных для заголовка маршрутизации IPv6
ПРИМЕЧАНИЕ
Функции inet6_rthdr_space и inet6_option_space обе возвращают количество байтов, необходимое для размещения объекта вспомогательных данных заданного тина, но ни одна из них не занимается фактическим его размещением Это сделано потому, что вызывающему процессу, возможно, потребуется разместить в памяти буфер большего размера для размещения в нем еще и других объектов вспомогателыгых данных.
Функция inet6_trhdr_】nit инициализирует буфер, на который указывает аргумент buf, как содержащий объект вспомогательных данных с заголовком типа type. Возвращаемое значение этой функции — указатель на структуру cmsghdr, которая встраивается в буфер, после чего этот указатель используется как аргумент при вызове следующих двух функций. Инициализируются элементы cmsg_level и cmsg_type.
Функция met6_rthdr_add добавляет адрес IPv6, на который указывает аргумент addr, к концу составляемого заголовка маршрутизации. Аргумент fl ags — это либо IPV6_RTHDR_L00SE, либо IPV6_RTHDR_STRICT. В случае успешного выполнения обновляется значение элемента cmsg jen, чтобы учесть добавленный новый адрес.
Функция 1 net6_rthdM asthop задает значение аргумента fl ags (I PV6_RTHDR_L00SE или IPV6_RTHDR_STRICT) для последнего узла. Учтите, что заголовок маршрутизации с N адресами реально включает в себя N+1 узел. Это подразумевает N вызовов функции inet6_rthdr_add и один вызов функции i net6_rthdr_1 asthop.
Следующие четыре функции манипулируют полученным заголовком маршрутизации:
#include <netinet/in h>
mt inet6_rthdr_reverse(const struct cmsghdr *in struct cmsghdr *ouf)
Возвращает 0 в случае успешного выполнения. -1 в случае ошибки
int 1net6_rthdr_segments(const struct cmsghdr *cmsg)
Возвращает количество сегментов в заголовке маршрутизации в случае успешного выполнения. -1 в случае ошибки
struct in6_addr *inet6_rthdr_getaddr(struct cmsghdr *cmsg int index),
Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки int 1net6_rthdr_getf1ags(const struct cmsghdr *cmsg int index),
Возвращает флаг жесткой/гибкой маршрутизации в случае успешного выполнения, -1 в случае ошибки
Функция 1 net6__rthdr_reverse принимает в качестве аргумента заголовок маршрутизации, полученный в виде объекта вспомогательных данных (на который указывает аргумент in), и создает новый заголовок маршрутизации (в буфере, на который указывает аргумент out), отправляющий дейтаграммы по обратному маршруту. Указатели ш и out могут указывать на один н тот же буфер.
Функция 1 net6_rthdr_segments возвращает количество сегментов в заголовке маршрутизации, обозначенном как cmsg. В случае успешного выполнения функции возвращаемое значение лежит в диапазоне от 0 до 23 (включительно).
Функция 1 net6__rthdr_getaddr возвращает указатель на адрес IPv6, заданный через 1 ndex в заголовке маршрутизации cmsg. Аргумент i ndex должен лежать в пределах от 1 до значения, возвращенного функцией inet6_「thdr_segments, включительно.
Функция 1 net6_rthdr_getfl ags возвращает либо флаг IPV6一RTHDRLOOSE, либо флаг IРV6_RTHDR_STR I СТ, соответствующий адресу, на который указывает аргумент i ndex (его значение лежит в пределах от 1 до значения, возвращенного функцией 1 net6__rthdr_segments, включительно) в заголовке маршрутизации cmsg.
ПРИМЕЧАНИЕ
Адреса индексируются начиная с 1,а флаги жесткой/гибкой маршрутизации индексируются начиная с 0, как показано на рис 24.7. Такой способ индексации согласован с системой обозначений, описанной в RFC 1883 [25]
«Закрепленные» параметры IPv6
Мы рассмотрели использование вспомогательных данных с функциями sendmsg и recvmsg для отправки и получения следующих шести различных типов объектов вспомогательных данных:
Информация о пакете IPv6: структура i n6_pkti nfo, содержащая адрес получателя и индекс интерфейса для исходящих дейтаграмм либо адрес отправителя и индекс интерфейса для приходящих дейтаграмм (индекс принимающего интерфейса) (рис. 20.5).
Предельное количество транзитных узлов для исходящих или приходящих дейтаграмм (рис. 20.5)、
Адрес следующего транзитного узла (рис. 20.5).
Параметры транзитных узлов (рис. 24.6).
Параметры получателя (рис. 24.6).
Заголовок маршрутизации (рис. 24.8).
В табл. 13.4 приведены значения полей cmsg level и cmsg_type для этих объектов, а также значения для других объектов вспомогательных данных.
Вместо того чтобы отсылать эти параметры при каждом вызове функции sendmsg, мы можем установить параметр сокета IPV6_PKT0PTI0NS. При установке этого параметра четвертый аргумент функции указывает на буфер, содержащий все объекты вспомогательных данных, которые должны быть отправлены в каждом пакете, посылаемом с данного сокета. Формат этого буфера в точности совпадает с форматом буфера, используемого функцией sendmsg для хранения объектов вспо~ могательных данных. Заданные таким образом параметры называются закреп- леннымих {sticky)у поскольку, если они заданы один раз, они остаются действи* тельными, пока не будут явным образом отменены (для отмены требуется задать данный параметр сокета с нулевой длиной). Но закрепленные параметры могут быть заменены для конкретного пакета в случае сокета UDP или символьного сокета IPv6, если при вызове функции sendmsg задать какие-либо другие парамет- ры в качестве объектов вспомогательных данных. Если при вызове функции sendmsg указаны какие-либо вспомогательные данные, ни один из закрепленных параметров не будет послан с этим пакетом.
Концепция закрепленных параметров также может быть использована и в слу- чае ТСР, поскольку вспомогательные данные никогда не отсылаются и не принимаются с помощью функции sendmsg или recvmsg на сокете TCP. Вместо этого приложение TCP может установить параметр сокета IPV6_PKT0PTI0NS и указать любой из упомянутых в начале этого раздела шести объектов вспомогательных данных. Тогда эти параметры будут относиться ко всем пакетам, отсылаемым с данного сокета.
Приложение TCP может также вызвать функцию getsockopt, чтобы с помощью параметра сокета IPV6_PKT0PTI0NS получить эти объекты вспомогательных данных. В таком случае ядро будет хранить только те параметры из последнего полученного сегмента, которые явным образом запросило приложение (включив соответствующий параметр сокета).
Резюме
Из десяти определенных в IPv4 параметров наиболее часю используются параметры маршрутизации от отправителя, но в настоящее время их популярность падает из-за проблем, связанных с безопасностью. Доступ к параметрам заголовков IPv4 осуществляется с помощью параметра сокета IP_OPTIONS.
В IPv6 определены шесть заголовков расширения, хотя в настоящее время их поддержка минимальна. Доступ к заголовкам расширения IPv6 осуществляется с помощью функционального интерфейса, что освобождает нас от необходимости углубляться в детали фактического формата внутри пакета. Эги заголовки
Sticky — клейкий, липкий В данном случае подразумевается, чю параметры «приклеиваются» к сокету. — Примем перев.
расширения записываются как вспомогательные данные функцией sendmsg и возвращаются функцией recvmsg также в виде вспомогательных данных.
Упражнения
Что изменится на стороне сервера, если в нашем примере маршрута от отправителя, приведенном в конце раздела 24.3, мы вместо указания клиенту имени узла gw зададим другой IP-адрес этого маршрутизатора — 206.85.40.74, показанный на рис. 1.7?
Что изменится, если в нашем примере, приведенном в конце раздела 24.3, мы зададим каждый промежуточный узел с параметром -G вместо -д?
Размер буфера, указываемый в качестве аргумента функции setsockopt для параметра сокета IP OPTIONS, должен быть кратен 4 байтам. Что бы нам пришлось делать, если бы мы не поместили параметр NOP в начало буфера, как показано на рис. 24. Р
Каким образом программа pi ng получает маршрут от отправителя, когда используется параметр IP Record Route (запись маршрута), описанный в разделе 7.3 [105]?
Почему в примере кода для сервера rlogind, приведенном в конце раздела 24.3, который предназначен для удаления полученного маршрута от отправителя, дескриптор сокета (первый аргумент функций getsockopt и setsockopt) имеет нулевое значение?
В течение долгого времени для удаления маршрута использовался код, несколько отличающийся от приведенного в конце раздела 24.3. Он выглядел следующим образом:
optsize = О
setsockopt(0 ipproto IP_OPTIONS. NULL &optsize).
Что в этом фрагменте неправильно? Имеет ли это значение?
ГЛАВА 25
Символьные сокеты
Введение
Символьные, или неструктурированные, сокеты1 (raw sockets) обеспечивают три возможности, не предоставляемые обычными сокетами TCP и UDP.
Символьные сокеты позволяют читать и записывать пакеты ICMPv4, IGMPv4 и ICMPv6. Например, программа Ping посылает эхо-запросы ICMP и получает эхо-ответы ICMP. (Наша оригинальная версия программы Ping приведена в разделе 25.5.) Демон маршрутизации многоадресной передачи mrouted посылает и получает пакеты IGMPv4
Эта возможность также позволяет реализовывать как пользовательские процессы те приложения, которые построены с использованием протоколов ICMP и 1GMP, вместо того чтобы помещать большее количество кода в ядро. Например, подобным образом построен демон отыскания маршрута (i n rdi sc в системе Solaris 2.x. В приложении F книги [94] показано, как получить исходный код открытой версии). Этот демон обрабатывает два типа сообщений ICMP, о которых ядро ничего не знает (извещение маршрутизатора и запрос маршрутизатору).
С помощью символьных сокетов процесс может читать и записывать 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.
С помощью символьных сокетов процесс может построить собственный заголовок IPv4 при помощи параметра сокета IP_HDRINCL. Такую возможность можно использовать, например, для построения собственного пакета UDP или TCP. Подобный пример приведен в разделе 26.6.
Как уже отмечалось ранее, используется также термин «сырой сокет» — Примеч перев
В данной главе описывается создание символьных сокетов, а также их ввод и вывод. Далее приводятся версии программ Ping и Traceroute, работающие как с версией IPv4, так и с версией IPv6.
Создание символьных сокетов
При создании символьных сокетов выполняются следующие шаги:
Символьный сокет создается функцией socket со вторым аргументом SOCK RAW. Третий аргумент (протокол) обычно ненулевой. Например, для создания символьного сокета IPv4 следует написать:
int sockfd.
sockfd = socket(AF_INET, SOCK_RAW. protocol)
где protocol — одна из констант IPPR0T0_xxx, определенных в подключенном заголовочном файле <neti net/i n h>, например IPPROTO_ICMP. Имейте в виду, что хотя имя протокола может быть определено в данном заголовочном файле (например, IPPROTO_EGP), это не означает, что ядро его поддерживает.
Только привилегированный пользователь может создать символьный сокет. Такой подход предотвращает отправку IP-дейтаграмм в сеть обычными пользователями.
Параметр сокета IP_HDRINCL может быть установлен следующим образом:
const mt on = 1,
if (setsockopt(sockfd, IPPROTO_IP. IP_HDRINCL, &on. sizeof(on)) < 0) error
В следующем разделе описывается действие этого параметра.
На символьном сокете можно вызвать функцию bind, но это делается редко. Эта функция устанавливает только локальный адрес: на символьном сокете нет понятия порта. Что касается вывода, вызов функции bind устанавливает IP-адрес отправителя» который будет использоваться для дейтаграмм, отправляемых на символьном сокете (только если не установлен параметр сокета IP—HDRINCL). Если функция bind не вызывается, ядро использует в качестве IP- адреса отправителя IP-адрес исходящего интерфейса.
На символьном сокете можно вызвать функцию connect, но это делается редко. Эта функция устанавливает только внешний адрес, так как на символьном сокете нет понятия порта. О выводе можно сказать, что вызов функции connect позволяет нам вызвать функцию write или send вместо sendto, поскольку IP- адрес получателя уже определен.
Вывод на символьном сокете
Вывод на символьном сокете регулируется следующими правилами:
Стандартный вывод выполняется путем вызова функции sendto или sendmsg и определения IP-адреса получателя. Функции write, wntev и send также можно использовать, если сокет уже присоединен.
Если не установлен параметр сокета IP_HDRINCL, то начальный адрес данных, предназначенных для записи ядром, указывает на первый байт, следующий за
IP-заголовком, поскольку ядро будет строить IP-заголовок и добавлять его к началу данных из процесса. Ядро устанавливает значение третьего аргумента функции socket равным полю протокола создаваемого заголовка IPv4.
Если параметр сокета I P_HDRINCL установлен, то начальный адрес данных, предназначенных для записи ядром, указывает на первый байт IP-заголовка. Размер данных для записи должен включать размер IP-заголовка вызывающего процесса. Процесс полностью формирует IP-заголовок, за исключением того, что, во-первых, значение поля идентификации IPv4 может быть нулевым (что указывает ядру на необходимость самостоятельно установить это значение), и во- вторых, ядро всегда вычисляет и сохраняет контрольную сумму заголовка IPv4.
Ядро фрагментирует символьные пакеты, превышающие значение MTU исходящего интерфейса.
ПРИМЕЧАНИЕ
К сожалению, параметр сокета IP HDRINCL никогда не документ ировался, в том числе и в отношении порядка байтов для полей в заголовке IPv4. В Беркли-ядрах все тюля имеют порядок байтов сети, за исключением полей ip_lcn и 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 (включая дополнительные заголовки) не могут быть про-
читаны или записаны через символьный сокет IPv6. Приложения имеют доступ почти ко всем полям заголовка IPv6 и дополнительных заголовков через параметры сокета или вспомогательные данные (см. упражнение 25.1). Если приложению все же необходимо полностью считать или записать 1Ру6-дей- таграмму, необходимо использовать канальный доступ (о нем речь пойдет в главе 26).
Как вскоре будет показано, на символьном сокете IPv6 по-другому обрабатываются контрольные суммы.
Параметр сокета IPv6_CHECKSUM
Для символьного сокета IPv6 ядро всегда вычисляет и сохраняет контрольную сумму в заголовке ICMPv6, тогда как для символьного сокета ICMPv4 приложение должно выполнять данную операцию самостоятельно (сравните листинги 25.9 и 25.11). И ICMPv4, и ICMPv6 требуют от отправителя вычисления контрольной суммы, но ICMPv6 включает в свою контрольную сумму псевдозаголовок (понятие псевдозаголовка обсуждается при вычислении контрольной суммы UDP в листинге 26.9). Одно из полей этого псевдозаголовка представляет собой IPv6- адрес отправителя, и обычно приложение оставляет ядру возможность выбирать это значение. Чтобы приложение не пыталось отыскать этот адрес для вычисления контрольной суммы, проще разрешить вычислять контрольную сумму ядру.
В случае других символьных сокетов IPv6 (при создании которых третий аргумент функции socket отличен от I PPROTO_ICMPV6) параметр сокета сообщает ядру, вычислять ли контрольную сумму и сохранять ли ее в исходящих пакетах, а также следует ли проверять контрольную сумму в приходящих пакетах. По умолчанию этот параметр выключен, а включается он путем присваивания неотрицательного значения параметра, как в следующем примере: int offset = 2
if (setsockopt(sockfd IPPRDTDJPV6 IPV6_CHECKSUM, &offset sizeof(offset)) < 0)
error
Здесь не только разрешается вычисление контрольной суммы на данном сокете, но и сообщается ядру смещение 16-разряд ной контрольной суммы в байтах: в данном примере оно составляет 2 байта от начала данных приложения. Чтобы отключить данный параметр, ему нужно присвоить значение -1. Если он включен, ядро будет вычислять и сохранять контрольную сумму для исходящих пакетов, посланных на данном сокете» а также проверять контрольную сумму для пакетов, получаемых данным сокетом.
Ввод через символьный сокет
Первый вопрос, на который следует ответить, говоря о символьных сокетах, следующий: какие из полученных IP-дейтаграмм ядро передает символьному сокету? Применяются следующие правила:
Получаемые пакеты UDP и TCP никогда не передаются на символьный сокет. Если процесс хочет считать IP-дейтаграмму, содержащую пакеты UDP или TCP, пакеты должны считываться на канальном уровне, как показано в главе 26.
Большинство ICMP-пакетов передаются на символьный сокет, после того как ядро заканчивает обработку ICMP-сообщения. Беркли-реализации посылают все получаемые ICMP-пакеты на символьный сокет, кроме эхо-запроса, запроса отметки времени и запроса маски адреса [105, с. 302-303]. Эти три типа ICMP-сообщений полностью обрабатываются ядром.
Все IGMP-пакеты передаются на символьный сокет, после того как ядро заканчивает обработку IGMP-сообщения.
Все IP-дейтаграммы с таким значением поля протокола, которое не понимает ядро, передаются на символьный сокет. Для этих пакетов ядро выполняет только минимальную проверку некоторых полей IP-заголовка, таких как версия IP, контрольная сумма IPv4-заголовка, длина заголовка и IP-адрес получателя [105, с. 213-220].
Если дейтаграмма приходит фрагментами, символьному сокету ничего не передаются, до тех пор, пока все фрагменты не прибудут и не будут снова собраны вместе.
Если у ядра есть IP-дейтаграмма для пересылки символьному сокету, в поисках подходящих сокетов проверяются все символьные сокеты всех процессов. Копия IP-дейтаграммы доставляется каждому подходящему сокету. Для каждого символьного сокета выполняются три перечисленных ниже теста, и только в том случае, если все три теста дают положительный результат, дейтаграмма направляется данному сокету.
Если при создании символьного сокета определено ненулевое значение протокола (третий аргумент функции socket), то значение поля протокола полученной дейтаграммы должно совпадать с этим ненулевым значением, иначе дейтаграмма не будет доставлена на данный сокет.
Если локальный IP-адрес связан с символьным сокетом функцией bind, IP- адрес получателя в полученной дейтаграмме должен совпадать с этим адресом, иначе дейтаграмма не посылается данному сокету.
Если для символьного сокета был определен внешний адрес с помощью функции connect 1Р~адрес отправителя в полученной дейтаграмме должен совпадать с этим адресом, иначе дейтаграмма не посылается данному сокету.
Следует отметить, что если символьный сокет создан с нулевым полем протокола и не вызывается ни функция bind, ни функция connect, то сокет получает копии всех дейтаграмм, которые ядро направляет символьным сокетам.
Полная дейтаграмма, включая IP-заголовок, передается процессу всегда, когда полученная дейтаграмма направляется символьному сокету IPv4. В версии IPv6 символьному сокету передается все, кроме дополнительных заголовков (см., например, рис. 25.4 и рис. 25.6).
ПРИМЕЧАНИЕ
В заголовке IPv4, передаваемом приложению, для ip_len, ip off и ip_id установлен порядок байтов узла, а все остальные ноля имеют порядок байтов сети. В системе Linux все поля остаются в сетевом порядке байтов.
В предыдущем разделе уже отмечалось, что все поля символьного сокета IPv6 остаются в сетевом порядке байтов
Фильтрация ICMPv6
Символьный сокет ICMPv4 получает большинство сообщений ICMPv4, полученных ядром. Но ICMPv6 является расширением ICMPv4, включающим функциональные возможности ARP и IGMP (см. раздел 2.2). Следовательно, символьный сокет ICMPv6 потенциально может принимать гораздо больше пакетов по сравнению с символьным сокетом ICMPv4. Но большинство приложений, использующих символьные сокеты, заинтересованы только в небольшом подмножестве всех IС М Р-приложений.
Для уменьшения количества пакетов, передаваемых от ядра к приложению через символьный 1СМРу6-сокет, предусмотрен фильтр, связанный с приложением. Фильтр объявляется с типом данных struct icmp6_filter, который определяется путем подключения заголовочного файла <net met/i стрб .h> Для установки и получения текущего 1СМРу6-фильтра для символьного сокета ICMPv6 используются функции setsockopt и getsockopt с аргументом 】evel, равным IPPR0T0_ICMPV6, и аргументом optname, равным ICMP6一FILTER.
Со структурой icmp6__filter работают шесть макросов.
#include <net 1 net/1 стрб h>
void ICMP6_FILTER_SETPASSALL(struct icmp6_filter *f?7t).
void ICMP6[FILTER[SETBL0CKALL(struct icmp^filter *力7七).
void ICMP6_FILTER SETPASS(int msgtype. struct icmp6_fliter *f?It) void ICMP6_FILTER_SETBL0CK(int msgtype. struct icmp6_fiIter *f?7t).
int ICMP6_FILTER_WILLPASS(int msgtype, const struct icmp6_fliter *fi It).
1nt ICMP6_FILTER_WILLBLOCK(int msgtype const struct icmp6_fiIter *filt).
Обе возвращают 1. если фильтр пропускает (блокирует) сообщение данного типа 0 в 叩отивном случае
Аргумент fi 11 всех макрокоманд являем ся указателем на переменную i cmp6_fi 1ter, изменяемую первыми четырьмя макрокомандами и проверяемую последними двумя. Аргумент msgtype является значением в интервале от 0 до 255, определяющим тип ICMP-сообщения.
Макрокоманда SETPASSALL указывает, что все типы сообщений должны пересылаться приложению, а макрокоманда SETBLOCKALL — что никакие сообщения не должны посылаться приложениям. По умолчанию при создании символьного сокета ICMPv6 подразумевается, что все типы ICMP-сообщений пересылаются приложению.
Макрокоманда SETPASS определяет конкретный тип сообщений, который должен пересылаться приложению, а макрокоманда SETBLOCK блокирует один конкретный тип сообщений. Макрокоманда WILLPASS возвращает значение 1, если определенный тип пересылается фильтром. Макрокоманда WILLBLOCK возвращает значение 1, если определенный тип блокирован фильтром, и нуль в противном случае.
В качестве примера рассмотрим приложение, которое будет получать только ГСМРуб-извещения маршрутизатора:
struct 1cmp6_fiIter myfi1t,
fd = Socket(AF_INET6. SOCK_RAW. IPPR0T0_ICMPV6).
ICMP6JILTER_SETBLOCKALL(&myf1 It):
ICMP6]FILTER]SETPASS(ND_ROUTER一ADVERT. &myfilt),
Setsockopt(fd, IPPR0T0__ICMPV6. ICMP6_FILTER. &myf】]t. sizeof(myfiIt)).
Сначала мы блокируем все типы сообщений (поскольку по умолчанию все типы сообщений пересылаются), а затем разрешаем пересылать только извещения маршрутизатора.
Программа 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~6айтовая отметка времени отправки пакета. Правила ICMP-запроса требуют, чтобы идентификатор, порядковый номер и все дополнительные данные возвращались в зхо-ответе. Сохранение отметки времени отправки пакета позволяет вычислить RTT при получении ответа.
В листинге 25.1 приведены примеры работы нашей программы. В первом используется версия IPv4, а во втором IPv6. Обратите внимание па приглашение системы, заканчивающееся знаком #, обозначающим привилегированного польз。-
вателя, поскольку для создания символьного сокета нужны права привилегированного пользователя.
Листинг 25.1 L Примеры вывода программы Ping
sol “is # ping gennn、.tuc.no3o.edu
PING gennni tuc noao edu (140 252
4 54) 56
data bytes
64
bytes
from
140
252 4
54
seq=O
ttl=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
tt1=248.
rtt=29
204
ms
64
bytes
from
140
252 4
54
seq=3
tt1=248,
rtt=52
630
ms
solans # ping 6bone-router cisco.com
PING 6bone-router cisco
com 1
(5f00 6d00 cOlf 700
1 60 3ell 6770)
64
bytes
from
5f00
6d00
cOlf
700
1
60
3ell
6770
seq=0
hlim=255
64
bytes
from
5f00
6d00
cOlf
700
1
60
3ell
6770
seq=l
hl】m=255
64
bytes
from
5f00
6d00
cOlf
700
1
60
3ell
6770
seq=2
hl i佇255
64
bytes
from
5f00
6d00
cOlf
700
1
60
3ell
6770
seq=3
hlim=255
56 data bytes rtt=116 802 ms rtt=129 321 ms rtt=109 297 ms rtt=78 216 ms
На рис. 25 2 приведен обзор функций, составляющих программу Ping.
Бесконечный цикл полученияБесконечный цикл полученияРис. 25.2. Обзор функций программы PingРис. 25.2. Обзор функций программы Ping
Бесконечный цикл получения
Бесконечный цикл получения
Рис. 25.2. Обзор функций программы Ping
Рис. 25.2. Обзор функций программы Ping
Отправка эхоответа раз в секунду
Данная программа состоит из двух частей: одна половина читает все, что приходит на символьный сокет, и выводит эхо-ответы ICMP, а другая половина раз в секунду посылает эхо-запросы ICMP. Вторая половина запускается раз в секунду сигналом SIGALRM.
В листинге 25.2 приведен заголовочный файл ping.h, подключаемый во всех файлах программы.
Листинг 25.2. Заголовочный файл ping.h
//ping/pmg h
#include unp h
#include<netinet/m_systm h>
include<netinet/ip h>
Все исходные коды программ, опубл икованные в этой кнйге, вы можете найти по адресу nrtp:// www piter com /download
4 #1пс1ude<netinet/ip_icmp h>
5 #define BUFSIZE 1500
/* глобальные переменные */
charrecvbufEBUFSIZE]
charsendbufEBUFSIZE]
56789012111112225678901211111222d d d 010101 V V Vd d d 010101 V V Vd d d d 01010101 V V V Vd d d d 01010101 V V V V/* прототипы функции */ proc_v4(char * ssize_t proc_v6(char * ssize _t send_v4(void) send_v6(void) readToop(void) sig_alrm(mt) tv sub(struct timeval */* прототипы функции */ proc_v4(char * ssize_t proc_v6(char * ssize _t send_v4(void) send_v6(void) readToop(void) sig_alrm(mt) tv sub(struct timeval *struct timeval *) struct timeval *)struct timeval *) struct timeval *)struct timeval *)struct timeval *)
56789012
11111222
56789012
11111222
d d d 010101 V V V
d d d 010101 V V V
d d d d 01010101 V V V V
d d d d 01010101 V V V V
/* прототипы функции */ proc_v4(char * ssize_t proc_v6(char * ssize _t send_v4(void) send_v6(void) readToop(void) sig_alrm(mt) tv sub(struct timeval *
/* прототипы функции */ proc_v4(char * ssize_t proc_v6(char * ssize _t send_v4(void) send_v6(void) readToop(void) sig_alrm(mt) tv sub(struct timeval *
struct timeval *) struct timeval *)
struct timeval *) struct timeval *)
struct timeval *)
struct timeval *)
9
int
datalen
/* размер данных в байтах следующих за ICMP-загсловком */
10
char
*host
11
mt
nsent
/* добавляет 1 для каждого вызова sendto() */
12
pid_t
pid
/* идентификатор нашего процесса РЮ */
13
mt
sockfd
14
mt
verbose
23
struct proto {
24
void (*fproc) (char * ssize_t struct timeval *)
25
void (*fsend) (void)
26
struct sockaddr *sasend /* структура sockaddr{} для отправки, получена из getaddrinfo */
27
struct sockaddr *sarecv /* sockaddr{} для получения*/
28
socklen_t salen/* длина sockaddr{} */
29
inticmpproto/* значение IPPROTO_xxx для ICMP-сообщений*/
30
}*pr一
31
#ifdef IPV6
32
#include'ip6 h" /* должно быть <netinet/ip6 h> */
33
#include "icmp6 h" /* должно быть <netmet/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].
Функция mai п приведена в листинге 25.3.
Листинг 25.3. Функция main
//ping/mam с
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. IPPROTO_ICMPV6},
7
#endif
8
int datalen = 56./* данные передаваемые с эхо-запросом ICMP */
9
mt
10
main(int argc, char **argv)
11
{
12
int c.
13
struct addrinfo *ai,
14
opterr = 0/* чтобы функция getoptO не записывала в stderr */
15
while ( (с = getopt(argc. argv. "v*')) != -1) {
16
switch (c) {
17
case 'v'
18
verbose++.
19
break,
20
case ,?,
21
err quit("unrecognized option 紀二 c)
22
} -
23
}
24
if (optind != argc - 1)
25
err_quit("usage ping [ -v ] <hostname>").
26
host = argv[optind].
27
pid = getpidO,
28
Signal(SIGALRM. sigalrm).
29
ai = Host_serv(host. NULL. 0. 0).
30 pnntf(*'PING Ms (%s)紀 data bytes\en". ai->ai_canonname.
31Sock_ntop_host(ai->ai_addr, ai->ai_dddrlen). datalen).
32/* инициализация в зависимости от протокола */
33 if (ai->ai_family == AF_INET) {
34 pr = &proto_v4.
35 #ifdef IPV6 —
36} else if (ai->ai_famly == AF INET6) {
37pr = &proto_v6.
38if (IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_in6 *)
39ai ->ai_addr) ->sin6_adc!r)))一
40err_quit("cannot ping IPv4-mapped IPv6 address"),
41 #endif
42} else
43 err_quit("unknown address family %d". ai->a7_famly).
44pr->sasend = ai->ai_addr,
45pr->sarecv = Callocll, ai->ai_ac!clrlen),
46 pr->salen = ai->ai_addrlen.
47 readloopO,
48 exit(O).
49 }
Определение структуры proto для IPv4 и IPv6
2-7 Определяется структура proto для IPv4 и IPv6. Указатели структуры адреса сокета инициализируются как нулевые, поскольку еще не известно, какая из версий будет использоваться — IPv4 или IPv6.
Длина дополнительных данных
8 Устанавливается количество дополнительных данных (56 байт), которые будут посылаться с эхо-запросом ICMP. При этом полная IPv4-дейтаграмма будет иметь размер 84 байта (20 байт на IPv4 -заголовок и 8 байт на ICMP-заголовок), а 1Ру6-дейтаграмма будет иметь длину 104 байта. Все данные, посылаемые с эхо- запросом, должны быть возвращены в эхо-ответе. Время отправки эхо-запроса будет сохраняться в первых 8 байтах области данных, а затем, при получении эхо- ответа, будет использоваться для вычисления и вывода времени RTT.
Обработка параметров командной строки
14-28 Единственный параметр командной строки, поддерживаемый в нашей версии, это параметр -v, в результате использования которого большинство ICMP-сообщений будут выводиться на консоль. (Мы не выводим эхо-ответы, принадлежащие другой запущенной копии программы Ping.) Для сигнала SIGALRM установлен обработчик, и мы увидим, что этот сигнал генерируется раз в секунду и вызывает отправку эхо-запросов ICMP.
Обработка аргумента, содержащего имя узла
?9-4б Строка, содержащая имя узла или IP-адрес, является обязательным аргументом и обрабатывается функцией host_serv. Возвращаемая структура add门 nfo содержит семейство протоколов — либо AF_INET, либо AF_INET6. Глобальный указатель рг устанавливается на требуемую в конкретной ситуации структуру proto. Также с помощью вызова функции IN6_IS_ADDR_V4MApPED мы убеждаемся, что адрес IPv6 на самом деле не является адресом IPv4, преобразованным к виду IPv6, поскольку даже если возвращаемый адрес является адресом IPv6, узлу будет отправлен пакет IPv4. (Если такая ситуация возникнет, можно переключиться и использовать IPv4.) Структура адреса сокета, уже размещенная в памяти с помощью функции getaddn nfo, используется для отправки, а другая структура адреса сокета того же размера размещается в памяти для получения.
47 Как показано в листинге 25.4, обработка происходит в функции readl оор.
Создание сокета
10-11 Создается символьный сокет, соответствующий выбранному протоколу. В вызове функции setuid нашему эффективному идентификатору пользователя при-
сваивается фактический идентификатор пользователя. Для создания символьных сокетов программа должна иметь права привилегированного пользователя, но когда символьный сокет уже создан, от этих прав можно отказаться. Всегда разумнее отказаться от лишних прав, если в них нет необходимости, например на тот случай, если в программе есть скрытая ошибка, которой кто-либо может воспользоваться.
Листинг 25.4. Функция readloop //ping/readloop с
#include 'ping h"
void
readloop(void)
{
intsize
charrecvbuf[BUFSlZE]:
socklen_t len.
ssize_t n
struct timeval tval
sockfd = Socket(pr->sasend->sa_family. SOCK_RAW. pr->icmpproto).
setuid(getuidO)/* специальные права больше не требуются */
size = 60 * 1024/* ОК. если setsockopt не достигает успеха */
setsockopt(sockfd SOL_SOCKET SO_RCVBUF &size sizeof(size))
sig_alrm(SIGALRM)/* отправка первого пакета */
for ( ) {
暫 len = pr->salen.
n = recvfrom(sockfd recvbuf. sizeof(recvbuf), 0 pr->sarecv &len).
if (n < 0) {
if (errno == EINTR)
continue
else
err_sys(Mrecvfrom error”).
}~
Gettimeofday(&tval. NULL),
(*pr->fproc) (recvbuf n &tval):
}
}
Установка размера приемного буфера сокета
М3 Пытаемся установить размер приемного буфера сокета, равный 61 440 байт (60x1024) — этот размер больше задаваемого по умолчанию. Это делается в расчете на случай, когда пользователь проверяет качество связи с помощью программы Ping, используя либо широковещательный адрес IPv4, либо групповой адрес, каждый из которых генерирует большое количество ответов. Увеличивая размер буфера, мы уменьшаем вероятность того, что приемный буфер переполнится.
Отправка первого пакета
14 Запускаем обработчик сигнала, который, как мы увидим, посылает пакет и создает сигнал SIGALRM раз в секунду. Обычно обработчик сигналов не запускается
напрямую, как у нас, но это можно делать. Обработчик сигналов является функцией языка С, хотя обычно он асинхронно запускается ядром.
Бесконечный цикл для считывания всех 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-заголовок может содержать параметры.) Это позволяет нам установить указатель тетр так, чтобы он указывал на начало IС М P-заголовка. На рис. 25.3 приведены различные заголовки, указатели и длины, используемые в коде.
len
>
hlenl. icmplen
н<н
Заголовок Параметры Заголовок Данные IPv4IPv4J ICMPv4 ICMP
,20 байт 0-40 Т 8
ipicmp
Рис. 25.3. Заголовки, указатели и длина при обработке 1СМРу4-ответов
Проверка эхо-ответа ICMP
15-19 Если сообщение является эхо-ответом ICMP, то необходимо проверить поле идентификатора, чтобы выяснить, относится ли этот ответ к посланному данным процессом запросу. Если программа Ping запущена на одном узле несколько раз, каждый процесс получает копии всех полученных I СМ P-сообщен ий.
20-25 Путем вычитания времени отправки сообщения (содержащегося в части ICMP- ответа, отведенной под дополнительные данные) из текущего времени (на которое указывает аргумент функции tvreev) вычисляется значение RTT. Время RTT преобразуется из микросекунд в миллисекунды и выводится на экран вместе с полем порядкового номера и полученным значением TTL. Поле порядкового номера позволяет пользователю проследить, не были ли пакеты пропущены, переупорядочены или дублированы, а значение TTL показывает количество пере
сылок между двумя узлами.
Вывод всех полученных ICMP-сообщений при включении параметра verbose
30 Если пользователем указан параметр командной строки v, также выводятся поля типа и кода из всех других полученных I СМ Р-сообщений
В листинге 25 5 приведена функция tv_sub, вычисляющая разность двух структур tirneval и сохраняющая результат в первой из них
Листинг 25.5. Функция tv_sub вычитание двух структур tirneval
//lib tvsub с
#i ncTude unp h
2 3 4 5 6 7 82 3 4 5 6 7 8void
2 3 4 5 6 7 8
2 3 4 5 6 7 8
tv_sub(struct timevdl *out struct timevdl *in) { _
if ( (out >tv_usec = in >tv_usec) < 0) {/* out 亠 in */
out >tv_sec
out >tv_usec += 1000000
} 一
out >tv_sec = in >tv_sec
}--
Обработка сообщений ICMPv6 управляется функцией proc_v6, приведенной в листинге 25 7 Она аналогична функции p「oc_v4, представленной в листинге 25 6
Листинг 25.6. Функция proc_v4 обработка сообщений ICMPv4
//pmg/prov_v4 c
1 ^include ping h
2345678923456789void
23456789
23456789
proc_v4(char *ptr ssize_t len struct tirneval *tvrecv)
{--
int hlenl icmplen
double rtt
struct ip *ip
struct icmp *icmp
struct tirneval *tvsend
ip = (struct ip *) ptr/* начало IP заголовка */
hlenl = тр >ip_hl « 2/* длина IP заголовка */
icmp = (struct icmp *)(ptr+ hlenl) /* начало заголовкаICMP4 */
if ( (icmplen ^ len hlenl) < 8)
err_quit( icmplen (%d) < 8 icmplen)
if (icmp >icmp_type == ICMP_ECHOREPLY) {
if (icmp >icmp_id 』pid)
/ return/* ответ не на наш запрос ECHO_REQUEST */
if (icmplen < 16)一
err_quit( icmplen (%d) < 16 icmplen)
tvs end = (struct tirneval *) icmp >icmp_data
tv_sub(tvrecv tvsend)
rtt = tvrecv >tv_sec * 1000 0 + tvrecv >tv_usec / 1000 0
printfC %d bytes from Ms seq*Xu ttl^d rtt以 3f ms\en
icmplen Sock_ntop_host(pr >sarecv pr >salen)
25 icmp >icmp seq ip >ip ttl rtt)
26} else if (verbose) {
27 prmtf( %d bytes from %s type * Zd code =帝d\en
28 icmplen Sock_ntop_host(pr >sarecv pr >salen)
29 70np >icmp_type icmp >icmp_code)
30}——
31 }
Листинг 25.7. функция proc_v6 обработка сообщений ICMPv6
//ping/proc_v6 с
#include ping h
void
proc_v6(char *ptr ssize_t len struct timeval *tvrecv)
{一一
#ifdef IPV6
mthlenl icmp61 en
double rtt
structip6_hdr *ip6
structianp6_hdr *ianp6
structtimeval *tvsend
ip6 = (struct ip6_hdr *) ptr /* начало заголовка 1% */
hlenl == sizeof(struct ip6_hdr)
if (ip6 >ip6_nxt i= IPPR0T0_ICMPV6)
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 (%d) < 8 icmp61en)
18 if (icmp6 >icmp6_type == ICMP6_ECH0_REPLY) {
19 if (icmp6 >icmp6_id 『pid)
20 return/* ответ не на наш запрос ECHO一REQUEST */
21 if (icmp6Ien < 16)-
22 err_quit( icmp61en (%d) < 16 icmp61en)
23 tvsend = (struct timeval *) (icmp6 + 1)
24tv_sub(tvrecv tvsend)
25 rtt = tvrecv >tv_sec * 1000 0 + tvrecv >tv_usec / 1000 0
26 pnntf( Zd bytes from 先s seq^Xu 讣严紀 rtt=^ 3f ms\en
27】cmp61en Sock_ntop_host(pr >sarecv pr >salen)
28icmp6 >icmp6_seq ip6 >ip6_hlim rtt)
29} else if (verbose) {
30 printf( %d bytes from %s type = Zd code = Zd\en
31icmp6〕en Sock_ntop_host(pr >sarecv pr >salen)
32 тсгпрб >icmp6_type 7cmp6 >icmp6_code)
}一一
#endif /* IPV6 */
}
Извлечение указателя на заголовок ICMPv6
11 17 Размер заголовка фиксирован IPv6 (40 байт), и при этом известно, что следующий заголовок будет заголовком ICMPv6 (Напомним, что дополнительные заголовки, если они присутствуют, всегда возвращаются не как стандартные данные, а как вспомогательные ) На рис 25 4 приведены различные заголовки, указатели и длины, используемые в коде丄 ” 『,
len
hlenl. icmp61en
^►
Заголовок
IPv6
Заголовок
ICMPv6
Данные
ICMP
八40 байт8
ip6icmp6
Рис. 25.4. Заголовки, указатели и длина при обработке ответов ICMPv6
Проверка эхо=ответа ICMP
!-28 Если ICMP-сообщение является эхо-ответом, то чтобы убедиться, что ответ предназначен для нас, мы проверяем поле идентификатора. Если это подтверждается, то вычисляется значение RTT, которое затем выводится вместе с порядковым номером и предельным количеством транзитных узлов IPv4.
Вывод всех полученных ICMP-сообщений при включении параметра verbose
'-33 Если пользователь указал параметр командной строки -v, выводятся также поля типа и кода всех остальных получаемых 1СМР-сообщений.
ПРИМЕЧАНИЕ
Если параметр -v не включен, можно установить фильтр ICMPv6 (параметр сокета ICMP6 FILTER, описанный в разделе 25.4), чтобы ядро передавало нашему сокету только эхо-ответы.
Обработчиком сигнала SIGALRM является функция sig_d] гш, приведенная в листинге 25.8. В листинге 25.4 видно, что функция readloop вызывает обработчик сигнала один раз для отправки первого пакета. Эта функция в зависимости от протокола вызывает функцию send_v4 или send_v6 для отправки эхо-запроса ICMP и далее программирует запуск другого сигнала SIGALRM через 1 секунду.
Листинг 25.8. функция sig_alrm: обработчик сигнала SIGALRM //pmg/sig_alrm с
#mclude "ping h'*
void
sig_alrm(mt signo)
{ ~
(*pr->fsend)():
alarm(l):
return; /* вероятно, прерывает выполнение функции recvfromO */
}
Функция send_v4, приведенная в листинге 25.9, строит I CM Pv4 -сообщенйе^Хом запроса и записывает его в символьный сокет.
Формирование ICMP-сообщения
12 1СМРу4-сообщение сформировано. В поле идентификатора установлен идентификатор нашего процесса, а порядковый номер установлен как глобальная пе-
ременная nset, которая затем увеличивается на 1 для следующего пакета. Текущее время сохраняется в части данных ICMP-сообщения.
Вычисление контрольной суммы ICMP
13-15 Для вычисления контрольной суммы ICMP-значение поля контрольной суммы устанавливается равным 0, а затем вызывается функция т n_cksum, а результат сохраняется в поле контрольной суммы. Контрольная сумма ICMPv4 вычисляется по IС М Pv4-заголовку и всем следующим за ним данным.
Листинг 25.9. Функция send_v4: построение эхо-запроса ICMPv4 и его отправка //ping/send_v4 с
voidvoid2 3 4 5 62 3 4 5 6^include "ping.h"
void
void
2 3 4 5 6
2 3 4 5 6
send_v4(void)
{ 一
mt len.
struct icmp *ianp.
i5p = (struct icmp *) sendbuf:
icmp->icmp type = ICMP_ECH0:
ianp->icmp_code = 0,
icmp->icmp_id = pid.
icmp->icmp_seq = nsent++.
GettimeofdayC(struct timeval *) icmp->icmp_data. NULL).
len = 8 + datalen,/* контрольная сумма I CMP-за головка^ данных */
icmp->icmp_cksum = 0.
ionp->icmp_cksum = in_cksum((u_short *) icmp. len).
SendtoCsockfd. sendbuf. len, 0. pr->sasend, pr->salen),
}
Отправка дейтаграммы
ICMP-сообщение отправлено на символьный сокет. Поскольку параметр сокета IP_HDRINCL не установлен, ядро составляет заголовок IPv4 и добавляет его в начало нашего буфера.
Контрольная сумма Интернета является суммой обратных кодов 16-разряд- ных значений. Если длина данных является нечетным числом, то для вычисления контрольной суммы используется логическое сложение одного нулевого байта и данных, расположенных в конце. Такой алгоритм применяется для вычисления контрольных сумм IPv4, ICMPv4, IGMPv4, ICMPv6, UDP иТСР. В RFC 1071 [14] содержится дополнительная информация и несколько числовых примеров. В разделе 8.7 книги [105] более подробно рассказывается об этом алгоритме, а также приводится более эффективная его реализация. В нашем случае контрольную сумму вычисляет функция m_cksum, приведенная в листинге 25.10.
Листинг 25.10. Функция in_cksum: вычисление контрольной суммы Интернета //lWfree/in_cksuni с
unsigned short
in_cksum(unsigned short *addr int len)
{ ~
int nleft = len.продолжение 夕
Листинг 25.10 (продолжение}
mt sum = 0.
unsigned short *w = addr.
unsigned short answer = 0.
/* В нашем простом алгоритме используется 32-разрядный
* сумматор накапливающего типа, мы добавляем
* в него последовательно 16-разрядные слова,
* а в конце переносим старшие 16 бит в младшие
*/
while (nleft > 1) {
sum += *w++.
nleft -= 2.
}
/* если необходимо убираем добавочный байт */
if (nleft == 1) {
*(unsigned char *) (&answer) = *(unsigned char *) w:
sum += answer.
21}
22/* переносим старшие 16 бит в младшие */
23sum = (sum » 16) +(sum & Oxffff)./*добавляем старшие 16бит丛 младшим */
24sum += (sum » 16)./*перенос */
25answer = -sum./*усекаем до 16 бит */
return (answer).
}
Алгоритм вычисления контрольной суммы Интернета
27 Первый цикл while вычисляет сумму всех 16-битовых значений. Если длина нечетная, то к сумме добавляется конечный байт. Алгоритм, приведенный в листинге 25.10, является простым алгоритмом, подходящим для программы Ping, но неудовлетворительным для больших объемов вычислений контрольных сумм, производимых ядром.
ПРИМЕЧАНИЕ
Эта функция взята из общедоступной версии программы Ping.
Последней функцией нашей программы Ping является функция send_v6, приведенная в листинге 25.11, которая строит и посылает эхо-запросы ICMPv6.
Функция send_v6 аналогична функции send_v4, но обратите внимание, что она не вычисляет контрольную сумму. Как отмечалось ранее, поскольку для вычисления контрольной суммы ICMPv6 используется адрес отправителя из IPv6-3a- головка, данная контрольная сумма вычисляется для нас ядром, после того как ядро выяснит адрес отправителя.
Листинг 25.11. Функция send_v6: построение и отправка 1СМР7б-сообщения эхо- запроса
//ping/send_v6 с
#include"ping h"
void
send v6()
{-
#ifdef IPV6
int len,
struct ianp6_hdr *icmp6
icmp6 = (struct icmp6_hdr *) sendbuf.
icmp6->1cmp6_type = ICMP6_ECH0_REQUEST.
icmp6->icmp62code = 0,
icmp6->icmp6_id = pid,
icmp6~>icmp6[seq = nsent++.
Gettimeofday?(struct timeval*)(icmp6 + 1). NULL):
len = 8 + datalen,/* 8-байтовый заголовок ICMPv6 */
Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen).
/* ядро вычисляет и сохраняет контрольную сумму */
#endif /* IPV6 */
}
Программа 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 (Порт недоступен). Для этого U D Р-дейтаграмма посылается на случайный порт, который (как можно надеяться) не используется на данном узле.
Ранние версии программы Traceroute могли устанавливать поле TTL в заголовке IPv4 только с помощью параметра сокета I P_HDRINCL путем построения своего собственного заголовка. Однако современные системы поддерживают параметр сокета I P_TTL, позволяющий определить значение TTL для исходящих дейтаграм. (Данный параметр сокета впервые был представлен в выпуске 4.3BSD Reno.) Проше установить данный параметр сокета, чем полностью формировать IPv4- заголовок (хотя в разделе 26.6 показано, как строить собственные заголовки IPv4 и UDP). Параметр сокета IPv6 IРV6_UNICAST_HOPS позволяет контролировать поле предельного количества транзитных узлов (ограничения пересылок) в дейтаграммах IPv6.
В листинге 25.12 приведен заголовочный файл trace, h, подключаемый ко всем файлам нашей программы.
Листинг 25.12. Заголовочный файл trace.h
//traceroute/trace h
1
2
3
4
5
#include "unp h"
#i nclude <netinet/in_systm h>
#include <netinet/ip h>
#incl ude<netmet/ip_icmp h>
#mcl ude<netmet/udp h>
6
#define BUFSIZE 1500
7
8
9
10
11
struct rec {/* структура данных UDP */
u_short rec_seq,/* порядковый номер */
u_short rec^ttl./* значение TTL. с которым пакет отправляется */
struct timeval rec tv./* время от叩авкп пакета */
}.-
12
13
14
/* глобальные переменные */ charrecvbuf[BUFSIZE];
charsendbuf[BUFSIZE],
15
16
17
18
19
20
21
int datalen./* размер данных в байтах после заголовка ICMP */
char *host.
u_short sport. dport:
mtnsent./*добавляет 1 для каждого вызова sendto() */
pid_tpid./*идентификатор нашего 叩ouecca PID */
int probe, nprobes:
intsendfdrecvfd;/*посылает на сокет UDP. читает на символьном
сокете ICMP */
22
23
intttl, max_ttl.
intverbose「
24
25
26
27
28
29
30
31
/* прототппн функций */
char *icmpcode_v4(int).
char *icmpcode_v6(int).
intrecv_v4(Tnt. struct WflitMil *A
intrecv二v6(int structitimeral «*>j
voidsig_alrm(int).
voidtraceloop(void).,
voidtv_sub(struct timeval *. striict tlmeVtfl ★,:
32
33
34
35
struct proto {
char *(*icmpcode) (int).
■)nt(*recv) (int. struct timeval *).
struct sockaddr *sasend. /* структура sockaddr{} для отправки, получена М3 getaddrmfo */
36
37
38
struct sockaddr*sarecv./*структураsockaddr{} для получения */
struct sockaddr*salast,/*последняяструктура sockaddr{} для получения*/
struct sockaddr*sabind,/*структураsockaddr{} для связывания nopja
отправителя*/
39
40
41
socklen t salen/*длина структур sockaddr{}s */
mticmpproto,/*значение IPPR0T0_xxx для ICMP */
mt ttllevel./*значение аргумента level функцииsetsockoptO для
задания TTL */
42
int ttloptname/* значение аргумента name функции setsockoptO для
задания TTL */
} *pr.
#ifdef IPV6
#include”ip6 h"/* должно быть <netinet/ip6 h> */
#incl ude"чстрб h"/* должно быть <netmet/icmp6 h> */
#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 и вызывает нашу функцию trace loop.
Листинг 25.13. Функция main программы Traceroute //traceroute/mam с
1 #include"trace h"
7
{icmpcode v6. recv v6 NULL, NULL. NULL. NULL. 0.
8
IPPROTO_
_ICMPV6
IPPR0T0_IPV6. IPV6_UNICAST_HOPS}.
9
#endif
10
int
datalen
=sizeof(struct rec). /* значения no уиолчаййю */
11
mt
max_ttl
=30.■
12
int
nprobes
=3,
13
u_short
dport =
32768 + 666.
14
mt
2 struct proto proto_v4 =
3 {icmpcode_v4 recv_v4 NULL. NULL NULL. NULL. 0.
4 IPPROTO_ICMP. IPPROTO^IP. IPJTL}.
5 #ifdef IPV6
6 struct proto proto_v6
15 mainCmt argc. char **argv)
продолжение 少
Листинг 25Л3 (продолжение)
int с.
struct addnnfo *ai.
opterr = 0./* чтобы функция getopt() не записывала в stderr */
20 while ( (с = getopt(argc. argv, "m v")) != -1) {
21 switch (c) {
22case'm'
23if( (max_ttl = atoi(optarg)) <= 1)
24err_quit("invalid-mvalue"),
25break.
26case'v'
27verbose++.
28break.
29case'?'
30err_quit("unrecognizedoption Zc". c).
31} 一
32}
33 if (optind i= argc - 1)
34err_quit("usage traceroute [ -ni <maxttl> -v ] <hostname>*)
35 host = argv[optind],'
36 pid = getpidO,
37 Signal(SIGALRM. sig alrm)
38 ai = Host_serv(host. NULL. 0. 0);
39 printf("traceroute to %s (Ms) %d hops max. Zd data bytesXen".
40ai->ai_canonname.
41Sock_ntop_host(ai->ai_addr.ai->ai_addrlen).
42max_ttl, datalen),
43/* инициализация в зависимости от протокола*/
44 if (ai->ai_family == AF_INET) {
45 pr = &proto_v4,
46 #】fdef IPV6 -
47} else if (ai->ai_faniily == AF_INET6) {
48 pr = &proto_v6.
49 if (IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_m6 *) ai->ai_addr)->sin6_addr)))
50err_quit("cannot traceroute IPv4-mapped IPv6 address").
51 #endif
52} else
53 err_quit("unknown address family %d”. ai->ai_family),
54pr->sasend=ai->ai_addr. /* содержит адрес получателя */
55pr->sarecv=CallocTl.ai->ai_addrlen).
56pr->salast=CallocCl.ai->ai_addrlen):
57pr~>sabind=CallocCl,ai->ai_addrlen);
58 pr->salen = ai->ai_addrlen.
59traceloopO:
60exit(0).
61 1
Определение структуры 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, возвращающей указатель на структуру addnnfo. В зависимости от типа возвращенного адреса (IPv4 или IPv6) заканчивается инициализация структуры proto, сохраняется указатель в глобальной переменной рг, а также размещается в памяти дополнительная структура адреса сокета соответствующего размера.
59 Функция tracel оор, приведенная в листинге 25.14, отправляет дейтаграммы и читает вернувшиеся ICMP-сообщения. Это основной цикл программы.
Листинг 25.14. Функция traceloop: основной цикл обработки //traceroute/tracelоор с 1 #include"trace.h"
void
traceloop(void)
{
intseq. code,done;
doublertt.
structrec *rec.
structtimeval tvrecv.
recvfd = Socket(pr->sasend->sa_family. SOCK_RAW pr->icmpproto).
setuid(getuidO)/* специальные права больше не требуются */
И sendfd = Socket(pr->sasend->sa_fami1у. SOCK_DGRAM. 0).
pr->sabind->sa_family = pr->sasend->sa_fami ly
sport = (getpid() & Oxffff) | 0x8000./* номер порта UDP отправителя */
sock_set_port(pr->sabind, pr->sdlen. htons(sport)).
Bi nd (sendfd. pr->sabind. pr->salen)продолжение ^
Листинг 25-14 (продолжение)
16 sig_alrm(SIGALRM).
17seq = 0.
18done = 0
19 for (ttl = 1. ttl <= mdx_ttl && done = 0, ttl++) {
20Setsockopt(sendfd, pr->ttllevel. pr->ttloptname. &ttl. sizeof(int)):
21bzero(pr->salast, pr->salen),
22prmtf(^2d ”.ttl).
23fflush(stdout),
24for (probe 二 0,probe<nprobes, probe++) {
25rec = (struct rec *) sendbuf.
26rec->rec_seq = ++seq.
27rec->rec2ttl = ttl.
28Gettimeofday(&rec->rec_tv.NULL),
29sock_set_port(pr->sasend pr->salen htons(dport + seq))
30 Sendto(sendfd, sendbuf datalen. 0 pr->sasend pr->salen),
31 if ( (code = (*pr->recv) (seq Mvrecv)) = -3)
32pnntf(" *и)/* истечение времени ожидания. нет ответа */
33 else {
34char str[NI_MAXHOST].
35363738394041424344454647353637383940414243444546474849505152535455565748495051525354555657if (sock_cmp_addr(pr->sarecv. pr>>salast pr->salen) != 0) {if (getnameinfo(pr->sarecv pr~>salen str. sizeof(str).
35
36
37
38
39
40
41
42
43
44
45
46
47
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
48
49
50
51
52
53
54
55
56
57
NULL. 0. 0) к 0)
printfC %s (%s)“. str.
Sock_ntop__host(pr->sarecv. pr->salen)).
else一 —
printfC* 記”.
Sock_ntop_host(pr->sarecv pr->salen)).
memcpy(pr->saTast. pr->sarecv. pr->salen),
}
tv_sub(&tvrecv. &rec-=>rec_tv)
rtt = tvrecv tv__sec * 1000 0 + tvrecv tv_usec / 1000,0;
printfC % 3f ms" rtt).
if (code === -1) /* порт получателя недоступен */ done++
else if (code >= 0)
printfC (ICMP Zs)“. (*pr->icmpcode) (code))
}
fflush(stdout).
}
printfC"Xen").
Создание двух сокетов
-11 Нам необходимо два сокета: символьный сокет, на котором мы читаем все вернувшиеся ICMP-сообщения, и UDP-сокет, на который мы посылаем пробные пакеты с увеличивающимся значением поля TTL. После создания символьного сокета мы заменяем наш эффективный идентификатор пользователя на факти-
ческий, поскольку более нам не понадобятся права привилегированного пользователя.
Связывание порта отправителя UDP-сокета
12-15 Осуществляется связывание порта отправителя с UDP-сокетом, который используется для отправки пакетов. При этом берется 16 младших битов из идентификатора нашего процесса, а старшему биту присваивается 1. Поскольку несколько копий программы Traceroute могут работать одновременно, нам необходима возможность определить, относится ли поступившее I СМР-сообщение к одной из наших дейтаграмм, или оно пришло в ответ на дейтаграмму, посланную другой копией программы. Мы используем порт отправителя в UDP-заголовке для определения отправляющего процесса, поскольку возвращаемое ICMP- сообщение всегда содержит UDP-заголовок дейтаграммы, вызвавшей ICMP- ошибку.
Установка обработчика сигнала SIGALARM
16 Мы устанавливаем нашу функцию sig_alarm в качестве обработчика сигнала SIGALRM, поскольку каждый раз, когда мы посылаем UDP-дейтаграмму, мы ждем 3 секунды, прежде чем послать следующий пробный пакет.
Основной цикл: установка TTL или предельного количества транзитных узлов и отправка трех пробных пакетов
17-28 Основным циклом функции является двойной вложенный цикл for. Внешний цикл стартует со значения TTL или предельного количества транзитных узлов, равного 1, и увеличивает это значение на 1, в то время как внутренний цикл посылает три пробных пакета (UDP-дейтаграммы) получателю. Каждый раз, когда изменяется значение TTL, мы вызываем setsockopt для установки нового значения, используя параметр сокета IP_TTL или I PV6_UN ICAST_HOPS.
Каждый раз во внешнем цикле мы инициализируем нулем структуру адреса сокета, на которую указывает salast. Данная структура будет сравниваться со структурой адреса сокета, возвращенной функцией recvfrom, при считывании ICMP-сообщения, и если эти две структуры будут различны, иа экран будет выведен IP-адрес из новой структуры. При использовании этого метода для каждого значения TTL выводится IP-адрес, соответствующий первому пробному пакету, а если для данного значения TTL IP-адрес изменится (то есть во время работы программы изменится маршрут), то будет выведен новый IP-адрес.
Установка порта получателя и отправка UDP-дейтаграммы
29-30 Каждый раз, когда посылается пробный пакет, порт получателя в структуре адреса сокета sasend меняется с помощью вызова функции sock_set_port. Причина, по которой порт меняется для каждого пробного пакета, заключается в том, что когда мы достигаем конечного получателя, все три пробных пакета посылаются на разные порты, и есть надежда, что по крайней мере один из этих портов не используется. Функция sendto посылает UDP-дейтаграммы,
Чтение ICMP-сообщения
L-54 Одна из функций recv_v4 или recv_v6 вызывает функцию recvfrom для чтения и обработки вернувшихся ICMP-сообщений. Обе эти функции возвращают значение -3 в случае истечения времени ожидания (сообщая, что следует послать следующий пробный пакет, если для данного значения TTL еще не посланы все три пакета), значение -2, если приходит ICMP-ошибка о превышении времени передачи, и значение -1, если получена ICMP-ошибка о недоступности порта (port unreachable) (что означает, что достигнут конечный получатель). Если же приходит какая-либо другая ICMP-ошибка недоступности получателя (destination unreachable), эти функции возвращают неотрицательный 1СМР-код.
Вывод ответа
>53 Как отмечалось выше, в случае первого ответа для данного значения TTL, а также если для данного TTL меняется IP-адрес узла, посылающего 1СМР-соо6ще- ние, выводится имя узла и IP-адрес (или только IP-адрес, если вызов функции getnameinfo не возвращает имени узла). Время RTT вычисляется как разность между временем отправки пробного пакета н временем возвращения и вывода ICMP-сообщения.
Функция「ecv_v4 приведена в листинге 25.15.
Листинг 25.15. Функция recv_v4: чтение и обработка сообщений ICMPv4 //traceroute/recv_v4
#i nd ude "trace h"
/* Возвращает
* -3 в случае истечения времени ожидания.
* -2 в случае превышения времени передачи ,
* (вызывающая программа продолжает работать);
* -1 в случае недоступности порта工
* (вызывающая программа завершена).
* >= 0 означает другой ICMP-код недоступности
*/
int
recv_v4(int seq. struct timeval *tv)
io {-
int hlenl. hlenlicnplen.
socklen_t len.
ssize_t n:
struct ip *ip. *h】p.
struct icmp *icmp.
struct udphdr *udp.
a1arm(3).
for (..) {
len = pr->salen:
n = recvfromCrecvfd. recvbuf. sizeof(recvbuf). 0, pr->sarecv. &len);
if (n < 0) {
if (errno == EINTR)
return (-3)./* время ожидания истекло */
else
err_sys("recvfrom error"),
} 一
Gettimeofday(tv. NULL): /* получает время прибытия пакета*/
ip = (struct ip *) recvbuf, /* начало IP-заголовка */
hlenl = ip->ip_hl « 2. /* длина IP-заголовка */
icmp = (struct icmp *) (recvbuf + hlenl): /* начало ICMP-заголовка */
if ( (icmplen = n - hlenl) < 8)
err quitCicmplen (紀)< 8". icmplen).
if (ioiip->icnip_type = ICMP_TIMXCEED &&
icnip->icmp_code = ICMP_TIMXCEED^I NTRANS) {
if (icmplen <8 + 20 + 8)
36err_qint("icmplen (^d) <8 + 20 + 8". icmplen).
37 hip = (struct ip *) (recvbuf + hlenl + 8).
38 hlen2 = hip->ip_hl « 2.
39 udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2).
if (hip->ip_p == IPPROTO_UDP &&
udp->uh_sport = htonsTsport) &&
udp->uh_dport == htons(dport + seq))
43return 7-2). /* попали на промежуточный маршрутизатор */
44} else if (icmp->icmp_type == ICMP^UNREACH) {
45 if (icmplen <8 + 20 + 8)
46err_quit("icmplen (Zd) <8+20 + 8°. icmplen);
47 hip = (struct ip *) (recvbuf + hlenl + 8).
48' hlen2 = hip->ip_hl « 2.
49 udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2):
50 if (hip->ip_p == IPPROTO_UDP &&
51udp*>uh_sport == htonsTsport) &&
52udp->uh_dport == htons(dport + seq)) {
53if (icmp->icmp_code == ICMP_UNREACH_PORT)
54return (-1). /* достигли получателя */
55 I else
56return (icmp->icmp_code). /* 0. 1. 2 ... */
57}—
58} else if (verbose) {
59 printfC (from %s type = %d. code = ^d)\en".
60Sock_ntop_host(pr->sarecv, pr->salen).
61i cmp->i cmp_type. i cmp->i cmp code)-
62}——
63/* какая-либо другая ICMP-ошибка, снова вызываем recvfrom()*/
64}
65 }
Установка таймера и прочтение каждого ЮМР-сообщения
17-27 Таймер устанавливается на 3 секунды, и функция входит в цикл, вызывающий recvfrom, считывая каждое 1СМРу4-сообщение, возвращаемое на символьный сокет.
ПРИМЕЧАНИЕ
Данная функция приводит к той же ситуации гонок, которая была описана в разделе 18.5, когда обсуждалось, что сигнал SIGALRM прерывает операцию считывания.
Извлечение указателя на ICMP-заголовок
丄P丄P28-32 Указатель i р указывает на начало IPv4-заголовка (напомним, что операция чтения на символьном сокете всегда возвращает IP-заголовок), а указатель icmp указывает на начало IС М Р-заголовка. На рис. 25.5 показаны различные заголовки, указатели и длины, используемые в данном коде.
丄P
丄P
Заголовок
IPv4
Параметры IPv4
Заголовок
ICMPv4
Заголовок
IPv4
Параметры
IPv4
Параметры UDP
> icmplen Ч>
hlenlIhlen2.
H ]4►!
20 байт 0-40 八 8 八 200-40 八 8
udp
icmp
Дейтаграмма IPv4, которая сгенерировала ошибку ICMP
Рис. 25.5. Заголовки, указатели и длины при обработке ошибки ICMPv4
Обработка ICMP-сообщения о превышении времени передачи
33-43 Если ICMP-сообщение является сообщением Time exceeded (Превышено время передачи), вероятно, оно является ответом на один из наших пробных пакетов. Указатель hi р указывает на заголовок IPv4, который возвращается в 1СМР-сооб- щении и следует сразу за 8-байтовым ICMP-заголовком. Указатель udp указывает на следующий далее UDP-заголовок. Если ICMP-сообщение было сгенерировано UDP-дейтаграммой и если порты отправителя и получателя этой дейтаграммы совпадают с теми значениями, которые мы посылали, то тогда это ответ от промежуточного маршрутизатора на наш пробный пакет.
Обработка ICMP-сообщения о недоступности порта
44-57 Если IСМР-сообщение является сообщением Desti nation unreachablе (Получатель недоступен), тогда, чтобы узнать, является ли это сообщение ответом на наш пробный пакет, мы смотрим на UDP-заголовок, возвращенный в данном ICMP- сообщении. Если это так и код означает сообщение Port unreachable (Порт недоступен), то возвращается значение -1, поскольку достигнут конечный получатель. Если же I СМ P-сообщение является ответом на один из наших пробных пакетов, но не является сообщением типа Destination unreachable (Получатель недоступен), то тогда возвращается значение ICMP-кода. Обычным примером такого случая является ситуация, когда брандмауэр возвращает какой-либо другой код недоступности для получателя, на который посылается пробный пакет.
Обработка других ICMP-сообщений
58-62 Все остальные ICMP-сообщения выводятся, если был задан параметр -v.
Следующая функция, recv_v6, приведена в листинге 25.16 и является эквивалентом для IPv6 ранее описанной функции. Эта функция почти идентична функ-
ции recv_v4, за исключением различий в именах констант и элементов структур. Кроме того, размер заголовка IPv6 является фиксированным и составляет 40 байт, в то время как для получения IP-параметров в заголовке IPv4 необходимо получить поле длины заголовка и умножать его на 4. На рис. 25.6 приведены различные заголовки, указатели и длины, используемые в коде.
Рис. 25.6. Заголовки, указатели и длины, используемые при обработке ошибки ICMPv6
Мы определяем две функции, icmpcode_v4 и icmpcode_v6, которые можно вызывать в конце функции tracel оор для вывода строки описания, соответствующей ICMP-ошибке недоступности получателя. В листинге 25.17 приведена 1Ру6-функ- ция. 1Ру4-функция аналогична, хотя и длиннее, поскольку существует большее количество 1СМРу4-кодов недоступности получателя (табл. А.З).
Последней функцией в нашей программе Traceroute является обработчик сигнала SIGALRM — функция signal rm, приведенная в листинге 25.18. Эта функция лишь возвращает ошибку EINTR из функции reevfrom, как в случае функции recv_v4, так и в случае recv_v6.
Листинг 25.16. Функция recv_v6; чтение и обработка сообщений )CMPv6 //traceroute/recv_v6
#include 吒race h"
/* Возвращает
* -3 в случае истечения времени ожидания
* -2 в случее превышения времени передачи,
*(вызывающая программа продолжает работать)
* -1 в случае недоступности порта
*(вызывающая программа завершена),
* >= 0 означает другой ICMP-код недоступности
*/
int
「ecv_v6(int seq. struct timeval *tv)
io {—
#】fdef IPV6
int hlenl.hlen2.icmp61en,
ssize_t n
socklen_t len.
продолжение 〃продолжение 〃struct Tp6_hdr*ip6,*Mp6.
продолжение 〃
продолжение 〃
struct icmp6_hdr *icmp6.
48495051525354555657585960616263646566674849505152535455565758596061626364656667
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Листинг 25.16 (продолжение)
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"),
}-
28 GettimeofdayCtv. NULL)./* получаем время 叩нбытмя пакета */
29】р6 = (struct ip6_hdr *) recvbuf, /* начало заголовка IPv6 */
30hlenl = sizeof(struct 】p6_hdr)
31 icmp6 = (struct icmp6_hdr *) (recvbuf + hlenl). /* заголовок ICMP */
32 if ( (icmp61en = n - hlenl) < 8)
33err_quit(*'icmp61 en (^d) < 8". icmp61en),
34 if (icmp6->icmp6_type == ICMP6_TIME_EXCEEDED &&
35 icmp6->icnip6_code == ICMP6_TIME_EXCEED_TRANSIT) {
36 if (】cmp61en <8 + 40 + 8)
37err_quit("icmp61en (^d) < 8 + 40 + 8" icmp61en).
38 hip6 = (struct 】p6_hdr *) (recvbuf + hlenl + 8),
39, hlen2 二 sizeof(struct ip6_hdr),
40udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2),
if (hip6->ip6_nxt == IPPROTO_UDP &&
,udp->uh_sport r htons(sport) &&
'udp->uh_dport == htonsfdport + seq))
return (-2)/* попали на промежуточный маршрутизатор */
45} else if (icmp6->】cmp6_type == ICMP6_DST_UNREACH) {
46 if (icmp61en <8 + 40 + 8)
47err_qint(”icmp61en (%d) <8 + 40 + 8" icmp61en)
hip6 = (struct ip6_hdr *) (recvbuf + hlenl + 8), hlen2 = 40.
udp = (struct udphdr *) (recvbuf + hlenl + 8 + hlen2).
if (hip6->ip6_nxt == IPPROTO—UDP && udp->uh_sport == htons(sport) && udp->uh_dport == htons(dport + seq)) { if (】cmp6->icmp6_code ~ ICMP6_DST_UNREACH_N0P0RT) return (-1). 7* достигли получателя */
else
return (icmp6->icmp6_code). /* 0 1. 2*/
} else if (verbose) {
printfC (from Zs type 工気d. code = %d)\en". Sock_ntop_host(pr->sarecv pr->salen). icmp6->icmp6_type, iсшрб->icmp6_code).
}.
/* какая-либо другая ICMP-ошибка, снова вызываем recvfrom() */ )
#endif
}
Листинг 25.17, Возвращение строки, соответствующей коду недоступности ICMPv6
//traceroute/icmpcode_v6 с
#include "trace h"
char *
icmpcode_v6(int code)
{"
switch (code) {
case ICMP6_DST_UNREACH_N0R0UTE
return ("no route to host')
case ICMP6_DST_UNREACH_ADMIN
return ("administratively prohibited*);
case ICMP6_DST_UNREACH_N0TNEIGHBOR
return ('not a neighbor")
case ICMP6_DST_UNREACH_ADDR
return ("address unreachable').
case ICMP6_DST_UNREACH_N0P0RT
return ("port unreachable").
default
return ("[unknown code]").
}
}
Листинг 25.18. функция sig_alrm
//traceroute/sig_alrm c
include "trace h”
void
sig_alrm(mt signo)
{-
return /* прерывается работа функции recvfrom() */
}
Пример
Сначала приведем пример, использующий IPv4:
solans # traceroute gemini.tuc.noao.edu
traceroute to gemim tuc noao edu (140 252 3 54) 30 hops max 12 data bytes
gw kohala com (206 62 226 62) 3 839 ms 3 595 ms 3 722 ms
tuc-l-sl-9 rtd net (206 85 40 73) 42 014 ms 21 078 ms 18 826 ms
frame-gw ttn ep net (198 32 152 9) 39 283 ms 24 598 ms 50 037 ms
tucson-nap-1 arizona edu (198 32 152 248) 44 350 ms 78 109 ms 47 003 ms
Butch-ENET-BONE Tel com Arizona EDU (128 196 11 5) 29 849 ms 46 664 ms 83 571 ms
gateway tuc noao edu (140 252 104 1) 37 376 ms 36 430 ms 30 555 ms
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)
hops max. 12 data bytes
6bone-router cisco inner net (5f00 6d00 cOlf 700 1 60 3ell 6770)
185 869 ms * 127 082 ms
buzzcut ipv6 nrl navy mil (5f00 3000 84fa 5a00 5)
187 736 ms 199 455 ms 172 839 ms
ipng9 ipng nist gov (5f00 3100 8106 3300 0 cO 3302 5a)
206 762 ms * 441 081 ms
В данном примере время второй попытки с предельным количеством транзитных узлов, равным 1, истекло, так же как и во второй попытке, когда это значение было равно 3.
25.7. Демон сообщений ICMP
Получение асинхронных ошибок ICMP на сокет UDP всегда было и продолжает оставаться проблемой. Ядро получает сообщения об ошибках ICMP, но они редко доставляются приложениям, которым необходимо о них знать. Мы видели, что для получения этих ошибок в API сокетов требуется присоединение сокета UDP к одному IP-адресу (см. раздел 8.2). Причина такого ограничения заключается в том, что единственная ошибка, возвращаемая функцией reevfrom, является целым кодом еггпо, а если приложение посылает дейтаграммы по нескольким адресам, а затем вызывает reevfrom, то данная функция не может сообщить приложению, какая из дейтаграмм вызвала ошибку.
В разделе 31.4 мы увидим, что XTI усовершенствует такой подход (незначительно) путем возвращения ошибки своим эквивалентом функции reevfrom. После этого приложение должно вызвать другую функцию (t_rcvuderr) для получения фактической ошибки, адреса получателя и номера порта из дейтаграммы, вызвавшей эту ошибку. Однако проблема данного решения заключается в том, что ядро, вероятно, хранит одновременно информацию не более чем об одной из этих асинхронных ошибок. Если приложение посылает три дейтаграммы и две из них вызывают ICMP-ошибки, приложению возвращается только одна ошибка.
В данном разделе предлагается другое решение, не требующее никаких изменений в ядре Мы предлагаем демон ICMP-сообщений icmpd, который создает символьный сокет ICMPv4 и символьный сокет ICMPv6 и получает все ICMP- сообщеиия. направляемые к ним ядром. Он также создает потоковый сокет домена Unix, связывает его (при помощи функции bind) с полным именем /tmp/icmpd и прослушивает входящие соединения (устанавливаемые при помощи функции connect) клиента с этим сокетом. Этот пример приведен на рнс. 25.7.
Прослушиваемый потоковый сокет домена Unix, связанный с /tcp/icmpd
Рис. 25.7. Демон icmpd: создание сокетов
Приложение UDP (являющееся клиентом для демона) сначала создает сокет UDP — сокет, на который оно хочет получать асинхронные ошибки. Приложение должно связать (функция bi nd) динамически назначаемый порт с этим сокетом; для чего это делается, будет пояснено ниже. Далее оно создает потоковый
доменный сокет 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, полученной демоном в ответ на UDР-дейтаграмму, посланную с порта, который связан с UDP-сокетом приложения, демон посылает приложению сообщение (о котором мы рассказываем чуть ниже) через доменный сокет Unix. Тогда приложение должно использовать функцию select или poll, чтобы обеспечить ожидание прибытия данных либо на UDP-сокет, либо на доменный сокет Unix
Сначала рассмотрим исходный код приложения, использующего данный демон, а затем и сам демон. В листинге 25.19 приведен заголовочный файл, подключаемый и к приложению, и к демону.
Листинг 25.19. Заголовочный файл unpicmpd.h
//icmpd/unpicmpd h
1 #ifndef —unpicmp_h
2 #define —unpicmpji
3 include "unp h'*
4
#define ICMPD—PATH "/tmp/icmpd"
/* известное полное имя сервера */
5
struct
icmpd_err {
6
int
icmpd_errno.
/*
EHOSTUNREACH. EMSGSIZE. ECONNREFUSED */
7
char
icmpd_tyPe-
/*
действительный тип ICMPv[46] */
8
char
icmpd_code.
/*
действительный код ICMPv[46] */
9
socklen t icmpd_len.
/*
дана следующей ниже структуры адреса сокета
10
struct sockaddr icmpd dest.
/*
может быть больше */
11
12
char }
】cmpd_rill[MAXSOCKADDR -
-sizeof(struct sockaddr)];
13 #endi f /* —unpicmp_h */
-12 Определяются известное полное имя сервера и структура i cmpd_err, передаваемая от сервера приложению сразу, как только получено ICMP-сообщение, которое должно быть передано данному приложению.
)-8 Проблема в том, что типы сообщений ICMPv4 отличаются численно (а иногда и концептуально) от типов сообщений ICMPv6 (табл. А.З и А.4). Возвращаются реальные значения типа (type) и кода (code), но мы также отображаем соответствующие им значения errno (i cmpd_errno), взятые из последнего столбца табл. А.З и А.4. Приложение может использовать эти значения вместо зависящих от протокола значений ICMPv4 и ICMPv6. В табл. 25.1 показаны обрабатываемые сообщения ICMP и соответствующие им значения errno. Демон возвращает пять типов ошибок 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)
Port unreachabl е (Порт недоступен) означает, что сокет не связан с портом получателя на IP-адресе получателя.
Packet too bi g (Слишком большой пакет) используется при определении транспортной MTU. В настоящее время нет определенного API, позволяющего UDP-приложениям осуществлять поиск транспортной MTU. Если ядро поддерживает поиск транспортной MTU для UDP, то обычно получение данной ошибки ICMP заставляет ядро записать новое значение транспортной MTU в таблицу маршрутизации ядра, но UDP-приложение, пославшее дейтаграмму, не извещается. Вместо этого приложение должно завершиться по превышению времени ожидания и повторно послать дейтаграмму, и тогда ядро найдет новое (меньшее) значение MTU в своей таблице маршрутизации и фрагментирует дейтаграмму. Передача этой ошибки приложению позволяет ему ускорить повторную передачу дейтаграммы, и, возможно, приложение сможет уменьшить размер посылаемой дейтаграммы.
Ошибка Time exceeded (Превышено время передачи) обычно возникает с кодом 0 и означает, что либо значение поля TTL (в случае IPv4), либо предельное количество транзитных узлов (в случае IPv6) достигло нуля. Обычно это свидетельствует о зацикливании маршрута, что, возможно, является временной ошибкой.
Ошибка Source quench (Отключение отправителя) ICMPv4 хотя и рассматривается в RFC 1812 [5] как устаревшая, может быть послана маршрутизаторами (или неправильно сконфигурированными узлами, действующими как маршрутизаторы). Такие ошибки означают, что пакет отброшен, и поэтому обрабатываются как ошибки недоступности получателя. Следует отметить, что в версии IPv6 нет ошибки отключения отправителя.
Все остальные ошибки недоступности получателя (Destination unreachble) означают, что пакет отброшен.
Элемент icmpd_dest является структурой адреса сокета, содержащей IP-адрес получателя и порта дейтаграммы, сгенерировавшей ICMP-ошибку. Этот элемент может быть структурой sockaddr in для ICMPv4 либо структурой sockaddi^in6 для ICMPv6. Если приложение посылает дейтаграммы по нескольким адресам, оно, вероятно, имеет по одной структуре адреса сокета на каждый адрес. Возвращая эту информацию в структуре адреса сокета, приложение может сравнить ее со своими собственными структурами для поиска той, которая вызвала ошибку.
Элемент i cmpd_f 111 заполняет структуру i cmpd_err таким образом, чтобы можно было разместить структуру адреса сокета максимального размера.
Эхо-клиент UDP, использующий демон icmpd
Теперь модифицируем наш зхо-клнент UDP (функцию dg_cl 1) для использования нашего демона 1 cmpd. В листинге 25.20 приведена первая половина функции.
Листинг 25.20. Первая часть приложения dg ch
//icmpd/dgcliOl с
#include "unpicmpd h"
void
dg_cli(FILE *fp. int sockfd. const SA *pservaddr. socklen_t servlen)
{一一
int icmpfd, maxfdpl,
char sendline[MAXLINE], recvline[MAXLINE + 1].продолжение ^
Листинг 25.20 (продолжение)
fd_set rset.
ssize_t n.
struct timevaltv.
struct icmpd_err icmpd_err,
Sock_b】nd_wild(sockfd. pservaddr->sa_family).
icmpfd = Tcp_connect(”/unix", ICMPD_PATH).
Write_fd(icmpfd. и1". 1. sockfd),
n = Read(icmpfd. recvlin已 1):
15 if (n j 1 II recv1ine[0] != 'Г)
16err_quit("error creating icmp socket, n = %d. char * %c".
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 говорилось, что наша реализация getaddnnfo поддерживает потоковые доменные сокеты Unix.)
Отправка UDP-сокета демону, ожидание ответа от демона
3-17 Вызываем функцию write_fd, приведенную на рис. 14.11 для отправки копии UDP-сокета демону. Мы также посылаем одиночный байт данных — символ "1", поскольку некоторые реализации не передают дескриптор без данных. Демон посылает обратно одиночный байт данных, состоящий из символа Т", для обозначения успешного выполнения. Любой другой ответ означает ошибку.
18-19 Инициализируем набор дескрипторов и вычисляем первый аргумент для функции sei ect (максимальный из двух дескрипторов, увеличенный на единицу).
Последняя часть нашего клиента приведена в листинге 25.21. Это цикл, который считывает данные из стандартного ввода, посылает строку серверу, считывает ответ сервера и записывает ответ в стандартный вывод.
Листинг 25.21. Вторая часть приложения dg_cli
//icmpd/dgcliOl с
20 while (Fgets(sendline. MAXLINE, fp) >= NULL) {
21 SendtoCsockfd. sendline, strlen(sendiine). 0. pservaddr. servlen):
22tv tv_sec = 5.
23tv tv_usec = 0.
24FD_SEf(sockfd.&rset).
25FD_SET(icmpfd,Srset).
26 if) (n = Select(maxfdpl. &rset, NULL, NULL, &tv)) == 0) {
27fprintf(stderr, "socket timeout\en“):
28continue.
29}
30if (FD_ISSET(sockfd. &rset)) {
31 n = Recvfrom(sockfd. recvline, MAXLINE, 0, NULL. NULL).
32 recvline[n] = 0./* завершающий нуль */
33 FputsCrecvline. stdout).
34}
35if (FD_ISSET(icmpfd. &rset)) {
36 if (-(n = Read(icmpfd. &icnipd_err. sizeof(icmpd_err))) == 0)
37err_quit("ICMP daemon teпnlпated^^),
38 else if (n != sizeof(icmpd_err))
39err_quit(*'n = %d, expected %d". n sizeof(icmpd_err)).
40 prmtfC^ICMP error dest = %s %s type = %d, code = %d\en".
41Sock_ntop(&icmpd_err icmpd_dest. icmpd_err icmpdjen),
42strerror(icmpd_err icmpd_errno).
43icmpd_err icmpd_type. icmpd_er r.icmpd_code).
44}- 一
45}
46 }
Вызов функции select
22-29 Поскольку мы вызываем функцию select, мы можем легко установить время ожидания ответа от эхо-сервера. Задаем его равным 5 секундам, открываем оба дескриптора для чтения и вызываем функцию sei ect. Если происходит превышение времени, выводится соответствующее сообщение и осуществляется переход в начало цикла.
Вывод ответа сервера
30-34 Если дейтаграмма возвращается сервером, она выводится на стандартное устройство вывода.
Обработка ICMP-ошибки
35-44 Если наше соединение домена Unix с демоном icmpd готово для чтения, мы пытаемся прочитать структуру icmpd_err. Если это удается, выводится goOtWVCTSJ? ющая информация, возвращаемая демоном.
ПРИМЕЧАНИЕ
Функция strerror является примером простой, почти тривиальной функции, которая должна быть более переносимой, чем она есть. В ANSI С и стандарте Posix.l ничего не говорится об ошибках, возвращаемых функцией. В руководстве по операционной системе Solaris 2.5 говорится, что функция возвращает пустой указатель, если ее аргумент выходит за пределы допустимых значений. Это означает, что код наподобие следующего:
psntf(”%s“. strerror(arg)),
является некорректным, поскольку strerror может вернуть пустой указатель. Однако реализации UnixWare 2.1, так же как и все реализации исходного кода, когорые автор смог найти, обрабатывают неправильный аргумент, возвращая указатель на строку типа «Неизвестная ошибка». Это имеет смысл и означает, что приведенный выше код правильный. Но в документации к операционной системе Unix 98 говорится, что поскольку не предусмотрено значение, сигнализирующее об ошибке, связанной с выходом аргумента за допустимые пределы, функция присваивает переменной еггпо значение EIVAL. (Ничего не сказано об указателе, возвращаемом в случае ошибки.) Это означает, что полностью правильный код должен обнулить еггпо, вызвать функцию strerror, проверить, не равняется ли значение еггпо величине 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 gemini.tuc.noao.edu echo hello, world
ICMP error dest = 140 252 4 54 7, Connection refused type = 3 code = 3
Демон iempd
Начинаем описание нашего демона icnpd с заголовочного файла icmpd.h, приведенного в листинге 25.22.
Листинг 25.22. Заголовочный файл icmpd.h для демона icmpd
//icmpd/icmpd h
1 #include "unpicmpd h"
2 3 4 5 62 3 4 5 6struct client {
2 3 4 5 6
2 3 4 5 6
intconnfd/*соединение потокового доменного сокета Unix с клиентом*/
intfamily./*AF_INET или AF_INET6 */
int1 port,/*локальный порт связан с клиентским сокетом UDP*/
/*сетевой порядок байтов */
} clientEFD 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
readableconn(mt)
15 int
readable_listen(void).
16 mt
readable_v4(vo?d).
17 int
readable v6(void).
Массив client
2-17 Поскольку демон может обрабатывать любое количество клиентов, для сохранения информации о каждом клиенте используется массив структур client. Они аналогичны структурам данных, которые использовались в разделе 6.8. Кроме дескриптора для доменного сокета Unix, через который осуществляется связь с клиентом, сохраняется также семейство адресов клиентского UDP-сокета AF_INET нлн AF_INET6 и номер порта, связанного с сокетом. Далее объявляются прототипы функций и глобальные переменные, совместно используемые этими функциями.
В листинге 25.32 приведена первая часть функции main.
Листинг 25.23. Первая часть функции main: создание сокетов
//icmpd/icmpd с
2 3 4 52 3 4 5mtmt1 #1nclude"icmpd h"
2 3 4 5
2 3 4 5
mt
mt
main(mt argc char **argv) {
mt i, sockfd.
6 if (argc i= 1)
7err_quit("usage icmpd").
maxi = -1./* индекс массива client】]*/
for (i=0 i < FD_SETSIZE,i++)
10 client]】] connfd: -1. /* -1 обозначает доступный элемент */
11FD_ZERO(&allset).
12fd4 - Socket(AF_INETS0CK_RAW.IPPROTO__ICMP).
13FD_SET(fd4 &alTset),
14maxfd - fd4.
продолжение 夕продолжение 夕15 #ifdef IPV6
продолжение 夕
продолжение 夕
16 fd6 = Socket(AF INET6 SOCK RAW IPPROTO ICMPV6),
Листинг 25.23 (продолжение)
17 FD_SET(fd6 &dllset)
18 maxfd = max(maxfd fd6)
19 #endif
ICMPD_PATH &addrlen)ICMPD_PATH &addrlen)20 listenfd = Tcp_listen( /unix
ICMPD_PATH &addrlen)
ICMPD_PATH &addrlen)
21FD_SET(11stenfd &dl1 set)
22maxfd ^ max(maxfd listenfd)
23cliaddr = Malloc(addrlen)
Инициализация массива client
I 10 Инициализируется массив client путем присваивания значения -1 элементу присоединенного сокета
Создание сокетов
2 23 Создаются три сокета символьный сокет ICMPv4, символьный сокет ICMPv6 и потоковый доменный сокет Unix Для создания последнего вызывается функция tcp_l 1 sten, которая также связывает при помощи функции bi nd свое заранее известное полное имя с сокетом и вызывает функцию 11 sten Это сокет, к которому клиенты присоединяются с помощью функции connect Для функции select также вычисляется максимальный дескриптор, а для вызовов функции accept в памяти размещается структура адреса сокета
В листинге 25 24 приведена вторая часть функции mai п Она содержит бесконечный цикл, вызывающий функцию sei ect в ожидании, когда будет готов к чтению какой-либо из дескрипторов демона
Листинг 25.24. Вторая часть функции main обработка готового к чтению дескриптора
//icmpd/icmpd с
for ( ) {
rset = all set
nready = Seiect(maxfd + 1 &rset NULL NULL. NU«3
if (FD_ISSET(listenfd &rset))
if (readable_listen() <= 0)
continue
if (FD_ISSET(fd4 &rset))
if (readable_v4() <= 0)
continue
#】fdef IPV6
if (FD_ISSET(fd6 &rset))
if (readable_v6() <= 0)
continue
#endif
for (i = 0 i <- maxi i++) { /* все клиенты проверяются на наличие данных */
if ( (sockfd = dient[i] connfd) < 0)
continue
if (FD_ISSET(sockfd &rset))
if (readable_conn(】)<-0)
break /* больше нет дескрипторов готовых для чтения */
}
}
exit(O)
}
Проверка прослушиваемого доменного сокета Unix
27 29 Прослушиваемый доменный сокет Unix проверяется в первую очередь, и в случае, если он готов, запускается функция readabl е_11 sten Переменная nready — количество дескрипторов, которое функция sei ect возвращает как готовые к чтению —является глобальной Каждая из наших функций readabl е_ХХХ уменьшает ее значение на 1 и новое значение этой переменной является возвращаемым значением функции Когда ее значение достигает нуля, это говорит о том, что все готовые к чтению дескрипторы обработаны, и поэтому функция sei ect вызывается снова
Проверка символьных сокетов ICMP
30 37 Проверяется символьный сокет ICMPv4, а затем символьный сокет ICMPv6
Проверка присоединенных доменных сокетов Unix
38 44 Затем проверяется, готов ли для чтения какой-нибудь из присоединенных доменных сокетов Unix Готовнос1ь для чтения какого-либо из таких сокетов обозначает, что клиент отослал дескриптор или завершился
В листинге 25 25 приведена функция readabl е_11 sten, вызываемая, когда прослушиваемый сокет готов для чтения Это указывает на новое клиентское соединение
Листинг 25.25. Обработка нового соединения клиента
//icmpd/readablе_11sten с
include icmpd h
int
readable listen(void)
{-
int i connfd
socklen_t clilen
clilen = addrlen
connfd = Acceptsistenfd cliaddr &clilen)
/* найдена первая доступная структура client]] */
for (1=0 1 < FD_SETSIZE 1++)
if (client[i] connfd < 0) {
chent[i] connfd = connfd /* сохраняется дескриптор */
break
}
if (i == FD_SETSIZE)
err_quit( too many clients )
17printf( new connection i = $
18FD_SET(connfd &allset) /*
19 if (connfd > maxfd)
20maxfd 二 connfd/*
21 if (i > maxi)
22maxi = i/*
Nd connfd = %d\en i connfd)
в набор добавляется новый дескриптор */
для функции seiectО */
максимальным индекс в массиве client口 */、
продолжение
Листинг 25.25 (продолжение)
23 return (--nready).
24 }
7-23 Принимается соединение и используется первый доступный вход в массив с! 1 ent. Код данной функции скопирован из начала кода, приведенного в листинге 6.4.
Когда присоединенный сокет готов для чтения, вызывается функция readab】е_ conn (листинг 25.26), а ее аргументом является индекс данного клиента в массиве client.
Листинг 25.26. Считывание данных и, возможно, дескриптора от клиента
//icmpd/readable_conn с
include vicmpd h"
int
readable conn (mt i)
{_
intumxfd. recvfd;
charc.
ssize_tn.
socklen_t len:
union{
char buf[MAXSOCKADDR],
struct sockaddr sock.
} un.
urnxfd = clientfi] connfd.
recvfd = -1.
if ( (n = Read_fd(umxfd. &c. 1. &recvfd)) == 0) {
err_msg("client 紀 terminated, recvfd = ^d". i. recvfd);
goto clientdone. /* вероятно, клиент завершил выполнение */
}
19/* данные от клиента, должно быть. дескриптор */
20 if (recvfd < 0) {
21err_msg("read_fd did not return descriptor").
22goto clienterr.
23}
Считывание данных клиента и, возможно, дескриптора
L3-18 Вызываем функцию read_fd, приведенную в листинге 14.9, для считывания данных и, возможно, дескриптора. Если возвращаемое значение равно нулю, клиент закрыл свою часть соединения, вероятно, завершив свое выполнение.
ПРИМЕЧАНИЕ
При написании кода пришлось выбирать, что использовать для связи между приложением и демоном — либо потоковый доменный сокет Unix, либо дейтаграммный доменный сокет Unix. Дескриптор сокета UDP может быть передан через любой доменный соке г Unix. Причина, по которой предпочтение было отдано потоковому сокету, заключается в том, что он позволяет определить момент отключения клиента. Все его дескрипторы автоматически закрываются, когда клиент отключается, в том числе и доменный сокет Unix, используемый для связи с демоном, в результате чего данный клиент удаляется демоном из массива client. Если бы мы использовали сокет дейтаграмм, мы бы не узнали, когда клиент отключился.
19-23 Если клиент не закрыл соединение, мы ждем получения дескриптора. Вторая часть функции readable_conn приведена в листинге 25.27.
Листинг 25.27. Получение номера порта, который клиент связал с UDP-сокетом
//icmpd/readab! e_conn.с//icmpd/readab! e_conn.с24252627282930313233343536373839404142434445464748495051525354242526272829303132333435363738394041424344454647484950515253541en = sizeof(un buf).if (getsockname(recvfd. (SA *) un buf. &1en) < 0) { err_ret("getsockname error").goto di enter r,}dientti] family = un sock.sa_family,if ( (client[i] 1port = sock_get_port(&un sock. 1en)) == 0) { c1ient[i] Iport = sock_bind_wiTd(recvfd. c1ient[i] family), if (c1ient[i] Iport <= 0) {err_ret("error binding ephemera! port").goto dienterr:}1en = sizeof(un buf).if (getsockname(recvfd. (SA *) un buf. &1en) < 0) { err_ret("getsockname error").goto di enter r,}dientti] family = un sock.sa_family,if ( (client[i] 1port = sock_get_port(&un sock. 1en)) == 0) { c1ient[i] Iport = sock_bind_wiTd(recvfd. c1ient[i] family), if (c1ient[i] Iport <= 0) {err_ret("error binding ephemera! port").goto dienterr:}}Write(umxfd, "I", 1).FD_SET(umxfd. &a1lset). if (umxfd > maxfd) maxfd = umxfd.if (i > maxi)maxi = i.Close(recvfd), return (--nready). di enterrWnte(unixfd. "0". 1). clientdoneClose(unixfd).if (recvfd >= 0) Close(recvfd):FD_CLR(umxfd, &al 1set); client[i] connfd = -1, return (--nready),}Write(umxfd, "I", 1).FD_SET(umxfd. &a1lset). if (umxfd > maxfd) maxfd = umxfd.if (i > maxi)maxi = i.Close(recvfd), return (--nready). di enterrWnte(unixfd. "0". 1). clientdoneClose(unixfd).if (recvfd >= 0) Close(recvfd):FD_CLR(umxfd, &al 1set); client[i] connfd = -1, return (--nready),/* сообщение клиенту, ^то qce ОК *//* сообщение клиенту, ^то qce ОК *//* все сделано с UDP-сокетом клиента/* все сделано с UDP-сокетом клиента/* сообщение клиенту о возникновении/* сообщение клиенту о возникновении*/ошибки */*/ошибки */}}
//icmpd/readab! e_conn.с
//icmpd/readab! e_conn.с
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
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
1en = sizeof(un buf).
if (getsockname(recvfd. (SA *) un buf. &1en) < 0) { err_ret("getsockname error").
goto di enter r,
}
dientti] family = un sock.sa_family,
if ( (client[i] 1port = sock_get_port(&un sock. 1en)) == 0) { c1ient[i] Iport = sock_bind_wiTd(recvfd. c1ient[i] family), if (c1ient[i] Iport <= 0) {
err_ret("error binding ephemera! port").
goto dienterr:
}
1en = sizeof(un buf).
if (getsockname(recvfd. (SA *) un buf. &1en) < 0) { err_ret("getsockname error").
goto di enter r,
}
dientti] family = un sock.sa_family,
if ( (client[i] 1port = sock_get_port(&un sock. 1en)) == 0) { c1ient[i] Iport = sock_bind_wiTd(recvfd. c1ient[i] family), if (c1ient[i] Iport <= 0) {
err_ret("error binding ephemera! port").
goto dienterr:
}
}
Write(umxfd, "I", 1).
FD_SET(umxfd. &a1lset). if (umxfd > maxfd) maxfd = umxfd.
if (i > maxi)maxi = i.
Close(recvfd), return (--nready). di enterr
Wnte(unixfd. "0". 1). clientdone
Close(unixfd).
if (recvfd >= 0) Close(recvfd):
FD_CLR(umxfd, &al 1set); client[i] connfd = -1, return (--nready),
}
Write(umxfd, "I", 1).
FD_SET(umxfd. &a1lset). if (umxfd > maxfd) maxfd = umxfd.
if (i > maxi)maxi = i.
Close(recvfd), return (--nready). di enterr
Wnte(unixfd. "0". 1). clientdone
Close(unixfd).
if (recvfd >= 0) Close(recvfd):
FD_CLR(umxfd, &al 1set); client[i] connfd = -1, return (--nready),
/* сообщение клиенту, ^то qce ОК */
/* сообщение клиенту, ^то qce ОК */
/* все сделано с UDP-сокетом клиента
/* все сделано с UDP-сокетом клиента
/* сообщение клиенту о возникновении
/* сообщение клиенту о возникновении
*/
ошибки */
*/
ошибки */
}
}
Получение номера порта, связанного с сокетом UDP
24-28 Вызывается функция getsockname, так что демон может получить номер порта, связанного с сокетом. Поскольку неизвестно, каков размер буфера, необходимого для размещения структуры адреса сокета, мы объявляем объединение символьного массива и универсальной структуры адреса сокета. Такой подход, в отличие от отдельного объявления символьного массива, гарантирует, что символьный массив надлежащим образом выравнивается в соответствии со структурой адреса сокета. Эта проблема проиллюстрирована в листинге 11.3, а в данной программе для обеспечения выравнивания вызывается функция mal 1 ос.
29-36 Семейство адресов сокета вместе с номером порта сохраняется в структуре client. Если номер порта равен нулю, мы вызываем функцию sock_bind_wild для связывания универсального адреса и динамически назначаемого порта с сокетом, но, как отмечалось ранее, такой подход не работает в реализациях SVR4.
Сообщение клиенту, что все в порядке
37-42 Один байт, содержащий символ "1", отправляется обратно клиенту. Новый дескриптор добавляется к набору дескрипторов для функции select, а значения maxfd и maxi при необходимости обновляются. ,
Закрытие UDP-сокета клиента
43 Заканчиваем работу с UDP-сокетом клиента и закрываем его с помощью функции close. Дескриптор был переслан нам клиентом и, таким образом, является копией, следовательно, UDP-сокет все еще открыт на стороне клиента.
Обработка ошибок и завершение работы клиента
「5-53 Если происходит ошибка, клиент получает нулевой байт. Когда клиент завершается, наша часть доменного сокета Unix закрывается, и соответствующий дескриптор удаляется из набора дескрипторов для функции select. Полю connfd структуры с! 1 ent присваивается значение 一 1, что является указанием на ее недоступность.
Функция readable_v4 вызывается, когда символьный сокет ICMPv4 открыт для чтения. Первая часть данной функции приведена в листинге 25.28. Этот код аналогичен коду для ICMPv4, приведенному ранее в листингах 25.6 и 25.15.
Листинг 25.28. Обработка полученных дейтаграмм ICMPv4, первая часть
1
include
"icmpd h"
2
#i nc 1 tide
<netinet/in_systm h>
3
include
<netinet/ip h>
4
#inc1ude
<netinet/ip_icmp h>
5
#include
<netinet/udp h>
6
mt
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
ss】ze_t
:n
13
sock1 er
i_t len
14
struct
7P *ip. *hip
15
struct
icmp *icmp
16
struct
udphdr *udp.
17
struct
sockaddr in from, dest.
18
struct
icmpd_err icmpd_err.
19 1en = sizeof(from).
20 n = Recvfrom(fd4, buf, MAXLINE. О (SA *) &from. &len):
21p门ntf("%d bytes ICMPv4 from %s '.
22 n Sock_ntop_host((SA *) &from, len))
23ip = (struct ip *) buf,/* начало IP-заголовка */
24hlenl = ip->ip_hl « 2./* длина IP-за головка */
25 icmp = (struct icmp *) (buf + hlenl). /* начало ICMP-заголовка */
26 if ( (icmplen = n - hlenl) < 8)
27err_quit("icmplen (Zd) < 8' icmplen).
28 pri ntf('f type = Zd. code = %d\en", icmp->icmp_type icmp->icmp_code).
Функция выводит некоторую информацию о каждом получаемом сообщении ICMP. Это было сделано для отладки при разработке демона, и вывод управляется аргументом командной строки.
В листинге 25.29 приведена вторая часть функции readable_v4.
Листинг 25.29. Обработка полученных дейтаграмм ICMPv4, вторая часть
//1cmpd/readab!e_v4 с
29 if (icmp->icmp_type == ICMPJJNREACH ||
30icmp->】cmp一type == ICMP_TIMXCEED 11
31 icmp->icmp_type == ICMP_SOURCEQUENCH) {
32 if (icmplen <8 + 20 + 8)
33 err_quit("icmplen (Zd) < 8 + 20 + 8". icmplen);
34 hip = (struct ip *) (buf + hlenl + 8),
35 h1en2 = hip->ip_h1 « 2.
36 pnntf("\etsrcip = Zs. dstip = Zs. proto = ^d\en".
37Inet_ntop(AF_INET.&hip->ip_src.srcstrsizeof(srcstr)).
38Inet_ntop(AF_INET,&hip->ip_dst.dststr.sizeof(dststr)).
39hip->ip_p).
40 if (hip->ip 二 p == IPPROTOJJDP) {
41 udp = (struct udphdr *У (buf + hlenl + 8 + h1en2).
42 sport = udp->uh_SDort,
43/* найден доменный сокет клиента, отсылаем заголовки */
44 for (1=0 1 <= maxi. 1++) {
45if (c1ient[i] connfd >= 0 &&
46cl lent[i] family == AF_INET &&
47c1ient[i] 1port == sport) {
48bzero(&dest, sizeof(dest)).
49dest sm_fairnly = AF_INET.
50 #ifdef HAVE_SOCKADDR_SA_LEN -
51dest sin_1en = sizeof(dest).
52 #endif
53memcpy(&dest sin_addr. &hip->ipjdst.
54sizeof(struct in_addr))
55dest sm_port = udp->uh_dport.
56icmpd_err icmpd_type = icmp->icmp__type
57icmpd_err icmpd_code = icrnp->icmp_code.
58icmpd__err icmpd一1en = sizeof(struct sockaddr_m),
59memcpy(&icmpd_err icmpd_dest. test, sizeof(dest)).
606162636465666768697071727374606162636465666768697071727374}}/* тип и код преобразуются в корректное значение еггпо */ icmpd_err icmpd_errno = EHOSTUNREACH /* ло умолчанию */ if (icmp->icmp_type == ICMP_UNREACH) {
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
}
}
if (icmp->icrnp_code == ICMP_UNREACH_PORT) icmpd_err icmpd_errno = ECONNREFUSED
e!se if (icmp->icmp_code == ICMP_UNREACH_NEEDFRAG) icmpd_err icmpd_errno = EMSGSIZE
)--
Write(client[i] connfd. &icmpd_err, sizeof(icmpd_err));
}}}return (--nready):}}}return (--nready):}~-
}
}
}
return (--nready):
}
}
}
return (--nready):
Проверка типа сообщения, уведомление приложения
29-31 ICMP-сообщения, которые посылаются приложениям, — это сообщения о недоступности порта, превышения времени и завершении клиента (см. табл. 25.1).
Проверка ошибки 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, первая часть //1cmpd/readab!e_v6 с
1
2
3
4
5
include icmpd h
#indude<netinet/in_systm h>
#inc1ude<netinet/ip h>
#i nd ude <netinet/ip_icmp h>
#include <netinet/udp h>
6
7
8
9
#ifdef IPV6
#ind ude叩6 h /* должно быть <netinet/ip6 h> */
includeicmp6 h /* должно быть <netwet/icmp6 h> */
#endif
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mt
readable v6(void)
{ 一
#ifdef IPV6
mti hlenl hlen2 icmp61en sport
char buf[MAXLINE]
charsrcstr[INET6_ADDRSTRLEN] dststr[INtT6_A0DRSTRLEN]
ssize_t n-
sock1en_t 1en struct ip6_hdr *ip6 *hip6 struct icmp6_hdr *icmp6 struct udphdr *udp
struct sockaddr_in6 from dest struct i cmpd__err icmpd_err
24
25
len = sizeof(from)
n = Recvfrom(fd6 buf MAXLINE 0 (SA *) &from &len)
26
27
printf( Zd bytes ICMPv6 from Zs n Sock_ntop_host((SA *) &from len))
28
29
ip6 = (struct ip6_hdr *) buf /* начало IPv6-заголовка */ hlenl = sizeof(struct ip6_hdr)
30 if (ip6 >ip6_nxt '= IPPRDTD_ICMPV6)
31 err_qtnt( next header not IPPR0T0_ICMPV6 )
32 icmp6 = (struct icmp6_hdr *) (buf + hlenl)
33 if ( (ion師 1en = n hlenl) < 8)
34 err_quit( icmp61en (*d) < 8 icmp61 en)
35 pnntf( type = *d code = ^d\en icmp6 >icmp6_type icmp6 >icmp6_code)
Вторая часть функции readable_v6 приведена в листинге 25 31 Код аналогичен приведенному в листинге 25 29 мы проверяем тип ICMP-ошибки, убеждаемся, что дейтаграмма, вызвавшая ошибку, является UDP-дейтаграммой, а затем строим структуру 1 cmpd_err, которую отсылаем клиенту
Листинг 25.31. Обработка полученной дейтаграммы ICMPv6, вторая часть
//1cmpd/readable_v6 с
36 if (icmp6 >icmp6_type == ICMP6_DST_UNREACH ||
37тстрб >icmp6_type == ICMP6_PACKET_T00_BIG ||
38icmp6 >icmp6二type == ICMPGJIMEJXCEEDED) {
39 if (icmp61en <8 + 40 + 8)
40 err_quit( icmp61en (Zd) <8 + 40 + 8 icmp61en).
41 hip6 = (struct ip6_hdr *) (buf + hlenl + 8)
42 hlen2 = sizeof(struct ip6_hdr)
43 printf( \etsrcip =迎 dstip = Xs next hdr = Zd\en
44Inet_ntop(AF_INET6 &hip6 >ip6_s「c srcstr sizeof(srcstr))
45Inet[ntop(AF[lNET6 &hip6 >ip6[dst dststr sizeof(dststr))
46hip6 >ip6_nxt)
47 if (hip6 >ip6_nxt = IPPROTO_UDP) {
48 udp = (struct udphdr *) (buf + hlenl + 8 + hlen2)
49 sport = udp >uh_sport
50/* найден доменный сокет клиента посылаем заголовки*/
51 for (1 = 0 1 <= maxi i++) {
52if (client[i] connfd >= 0 &&
53c1ient[i] family = AF_INET6 &&
54client。] Iport =» sport) {
55bzero(&dest sizeof(dest))
56dest Sin6_fami7y = AF_INET6
57 #ifdef HAVE—SDCKA面R_SA_LEN~
58dest sin6_1en = sizeof(dest)
59 #endif
60memcpy(&dest sin6_addr &hip6 >ip6 dst,
61sizeof(struct in6_addr)) 「
62dest sin6_port = udp >uh_dport
63icmpd_err icmpd_type = icmp6 >icmp6_type
64icmpd_err icmpd_code = icmp6 >icmp6_code
65icmpd_err icmpd_1er)= sizeof(struct sockaddr_tn6)
66memcpy(&icmpd_err icmpd_dest &dest sizeof(dest))
67/* тип и код преобразуются в корректное значение errno */
68icmpd_err icmpd^ermo = EHOSTUNREACH /* no умолчанию */
69if (icmp6 >icmp6_type = ICMP6_DST_UNREACH) {
70if (icmp6 >icmp6_code == ICMP_UNREACH_PORT)
71icmpd_err icmpd_errno = ECONNREFUSED
продолжение 夕продолжение 夕72else i「(icmp6 >icmp6_code == ICMP_UNREACH_NEEDFRAG)
продолжение 夕
продолжение 夕
Листинг 25.31 (продолжение)
73icmpd_err icmpd_errn。 = EMSGSIZE,
74}——
75Write(chent[i] connfd. &icmpd_err. sizeof(icmpd_err)).
76}-一
77}
78}
79}
80 return (--nready).
81 #endif
82 }
Резюме
Символьные сокеты обеспечивают три возможности:
Чтение и запись пакетов ICMPv4, IGMPv4 и ICMPv6.
Чтение и запись IР-дейтаграммы с полем протокола, которое не обрабатывается ядром.
Формирование своих собственных заголовков IPv4, обычно используемых в диагностических целях (или, к сожалению, хакерами).
Два традиционных диагностических средства — программы Ping и Traceroute — используют символьные сокеты. Мы разработали наши собственные версии этих программ, поддерживающих обе версии протокола — и IPv4, и IPv6. Также нами разработан наш собственный демон i cmpd, который обеспечивает доступ к сообщениям об ошибках ICMP через сокет UDP. Данный пример также иллюстрирует передачу дескриптора через доменный сокет Unix между неродственными клиентом и сервером.
Упражнения
В этой главе говорилось, что почти все поля заголовка IPv6 и все дополнительные заголовки доступны приложению через параметры сокета или вспомогательные данные. Какая информация из дейтаграммы IPv6 недоступна приложению?
Что произойдет в листинге 25,29, если по какой-либо причине клиент перестанет производить считывание из своего соединения домена Unix в свой демон icmpd, и множество ошибок ICMP поступят к клиенту? В чем заключается простейшее решение данной проблемы?
Если задать нашей программе Ping адрес широковещательной передачи, направленный в подсеть, она будет работать, то есть широковещательный эхо- запрос ICMP посылается как широковещательный запрос канального уровня, даже если мы не установим параметр сокета SO_BROADCAST. Почему?
Что произойдет с программой Ping, если мы запустим ее на узле с несколькими интерфейсами, а в качестве аргумента имени узла возьмем групповой адрес 224.0.0.1?
ГЛАВА 26
Доступ к канальному уровню
Введение
В настоящее время большинство операционных систем позволяют приложению получать доступ к канальному уровню. Это свойство подразумевает следующие возможности:
Отслеживание пакетов, принимаемых на канальном уровне, что, в свою очередь, позволяет запускать такие программы, как tcpdump, на обычных компьютерных системах (а не только на специальных аппаратных устройствах для отслеживания пакетов). Если добавить к этому способность сетевого интерфейса работать в смешанном режиме (promiscuous mode), это позволит приложению отслеживать все пакеты, проходящие по локальному кабелю, а не только предназначенные для того узла, на котором работает эта программа.
Возможность запуска определенных программ как обычных приложений, а не как частей ядра. Например, большинство версий Unix сервера RARP — это обычные приложения, которые считывают запросы RARP с канального уровня (запросы RARP не являются дейтаграммами IP), а затем передают ответы также на канальный уровень.
Три наиболее распространенных средства получения доступа к канальному уровню в Unix — это пакетный фильтр BSD (BPF, BSD Packet Filter), DLPI в SVR4 (Datalink Provider Interface — интерфейс поставщика канального уровня) и интерфейс пакетных сокетов Linux (SOCK_PACKET). Мы приводим в этой главе обзор перечисленных средств, а затем описываем 11 bcap — открытую для свободного доступа библиотеку, содержащую функции для захвата пакетов. Эта библиотека работает со всеми тремя перечисленными средствами, и использование библиотеки позволяет сделать наши программы не зависящими от фактического способа обеспечения доступа к канальному уровню, применяемому в данной операционной системе. Мы описываем эту библиотеку, разрабатывая программу, которая посылает запросы серверу имен DNS (мы составляем свои собственные дейтаграммы UDP и записываем их в символьный сокет) и считывает ответ при помощи libcap, чтобы определить, добавляет ли сервер имен контрольную сумму в дейтаграммы UDP.
BPF: пакетный фильтр BSD
4.4BSD и многие другие Беркли-реализации поддерживают BPF — пакетный фильтр BSD (BSD Packet Filter). Реализация BPF описана в главе 31 [105].
История 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 (такие, как только что показанная строка,
начинающаяся с tcp) в машинный язык с помощью функции pcap_compi 1 е, о которой мы рассказываем в разделе 26.6.
В технологии BPF применяются три метода, позволяющие уменьшить накладные расходы на ее использование.
Фильтрация BPF происходит внутри ядра, за счет чего минимизируется количество данных, которые нужно копировать из ядра в приложение. Копирование из пространства ядра в пользовательское пространство является довольно дорогостоящим. Если бы приходилось копировать каждый пакет, у BPF могли бы возникнуть проблемы при попытке взаимодействия с быстрыми каналами.
BPF передает приложению только часть каждого пакета. Здесь речь идет о длине захвата (capture length). Большинству приложений требуется только заголовок пакета, а не содержащиеся в нем данные. Это также уменьшает количество данных, которые BPF должен скопировать в приложение. В программе tcpdump, например, по умолчанию это значение равно 68 байт, и этого достаточно для размещения 14-байтового заголовка Ethernet, 20-байтового заголовка IP, 20-байтового заголовка TCP и 14 байт данных. Но для вывода дополнительной информации по другим протоколам (например, DNS или NFS) требуется, чтобы пользователь увеличил это значение при запуске программы tcpdump.
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 можно только с одной целью — чтобы отослать
наши собственные сетевые пакеты, не являющиеся дейтаграммами IP. Например, демон RARP делает это для отправки ответов RARP, которые не являются дейтаграммами IP.
Для получения доступа к BPF необходимо открыть (вызвав функцию open) еще не открытое каким-либо другим процессом устройство BPF. Например, можно попробовать /dev/bpfO, и если будет возвращена ошибка EBUSY, то нужно попробовать /dev/bpfl, и т. д. Когда устройство будет открыто, потребуется выполнить примерно 12 команд ioctl для задания характеристик устройства, таких как загрузка фильтра, время ожидания для считывания (read timeout), размер буфера, присоединение канального уровня к устройству BPF, включение смешанного режима и т. д. Затем с помощью функций read и write осуществляется ввод и вывод.
DLPI: интерфейс поставщика канального уровня
SVR4 обеспечивает доступ к канальному уровню через DLPI (Data Link Provider Interface — интерфейс поставщика канального уровня). DLPI — это не зависящий от протокола интерфейс, разработанный в AT&T, который служит средством связи с сервисами, обеспечиваемыми канальным уровнем [101]. Доступ к DLPI осуществляется посредством отправки и получения сообщений через потоки.
Для подсоединения к канальному уровню приложение просто открывает устройство (например, 1е0) с помощью команды open и использует запрос DL_ATTACH_REQ. Но для эффективной работы используются два дополнительных модуля: pfmod, который осуществляет фильтрацию внутри ядра, и bufmod, буферизующий данные, предназначенные для приложения. Это показано на рис. 26.2.
Рис. 26.2. Захват пакета с использованием DLPI, pfmod и bufmod
Концептуально DLPI аналогичен BPF. pfmod поддерживает фильтрацию внутри ядра, используя псевдопроцессор, a bufmod сокращает количество данных и системных вызовов, поддерживая длину захвата и время ожидания для считывания.
Одно интересное различие, тем не менее, заключается в том, что для BPF и фильтров pfmod используются разные типы псевдопроцессоров. Фильтр BPF — это ориентированный ациклический граф управления потоком (acyclic control flow graph, CFG), в то время как pfmod использует дерево булевых выражений. В первом случае естественным является отображение в код для вычислительной машины с регистровой организацией, а во втором — в код для машины со стековой организацией [63]. В статье [63] показано, что реализация CFG, используемая в BPF, обычно работает быстрее, чем дерево булевых выражений, в 3-20 раз в зависимости от сложности фильтра.
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))
Другие константы, которые могут использоваться в качестве последнего аргумента, —это, например, ЕТН_Р一ARP и ETH_P_IPV6.
Указывая протокол ЕТН_Р_ххх, мы тем самым сообщаем канальному уровню, какой тип из получаемых канальным уровнем кадров передавать сокету. Если канальный уровень поддерживает смешанный режим (например, Ehternet), то устройство тоже должно работать в смешанном режиме. Для этого нужно вызвать функцию ioctl с запросом SIOCGIFFLAGS для получения флагов, установить флаг IFF_PROMISC и далее сохранить флаг с помощью SIOCSIFFLAGS.
Сравнивая это средство Linux с BPF и DLPI, мы можем отметить некоторые различия.
В Linux не обеспечивается буферизация и фильтрация для ядра. Существует обычный буфер приема сокета, но отсутствует возможность буферизации и отправки приложению нескольких кадров с помощью одной операции считывания. Это увеличивает накладные расходы, связанные с копированием потенциально возможных больших объемов данных из ядра в приложение.
В Linux не предусмотрена фильтрация на уровне устройства. Если в вызове функции socket указан аргумент ЕТН_Р_1Р, то все пакеты IPv4 со всех устройств (например, Ethernet, каналы РРР, каналы SLIP и закольцовка) будут переданы на сокет. Функция recvfrom возвращает общую структуру адреса сокета, а элемент sa_data содержит имя устройства (например, ethO). Тогда приложение само должно игнорировать данные с тех устройств, которые не представ-
ляют для него интереса. Здесь мы сталкиваемся, фактически, с той же проблемой: возможно, что приложение будет получать слишком много данных, особенно в случае наблюдения за высокоскоростной сетью.
Libcap: библиотека для захвата пакетов
Библиотека захвата пакетов libcap обеспечивает не зависящий от реализации доступ к средствам операционной системы, с помощью которых осуществляется этот захват. В настоящее время поддерживается только чтение пакетов (хотя добавление нескольких строк кода позволяет также записывать пакеты).
Сейчас осуществляется поддержка BPF для Беркли-ядер, DLPI для Solaris 2.x, NIT для SunOS 4.1.x, пакетных сокетов (SOCK PACKET) в Linux и нескольких других операционных системах. Библиотека 11 Ьсар используется программой tcpdump. Всего в библиотеке насчитывается порядка 25 функций, но вместо того чтобы просто описывать их, мы продемонстрируем их фактическое использование на примере, рассматриваемом в следующем разделе. Названия всех функций начинаются с pcsp_. В руководстве по использованию функций рсар они описаны более подробно.
ПРИМЕЧАНИЕ
Библиотека libcap находится в свободном доступе по адресу ftp://ftp.ee.lbl.gov/ 一 libcap tar.z.
Анализ поля контрольной суммы UDP
Теперь мы приступаем к рассмотрению примера, в котором отсылается дейтаграмма UDP, содержащая запрос UDP к серверу имен, а затем считывается ответ с помощью библиотеки захвата пакетов. Цель данного примера — установить, вычисляется на сервере имен контрольная сумма UDP или нет. В случае IPv4 вычисление контрольной суммы не является обязательным. В большинстве систем в настоящее время вычисление контрольных сумм по умолчанию включено, но, к сожалению, в более старых системах, в частности SunOS 4.1.x, вычисление контрольных сумм по умолчанию отключено. В настоящее время все системы, а особенно система, в которой работает сервер имен, всегда должны работать с включенными контрольными суммами UDP, поскольку поврежденные (содержащие ошибки) дейтаграмма могут повредить базу данных сервера.
ПРИМЕЧАНИЕ
Включение и выключение контрольных сумм обычно осуществляется сразу для всей системы, как показано в приложении Е [94].
Мы формируем дейтаграмму UDP (запрос DNS) и записываем ее в символьный сокет. Для отправки запроса мы могли бы использовать обычный сокет UDP, но мы хотим показать, как использовать параметр сокета IP_HDRINCL для создания полной дейтаграммы IP. Нет возможности получить контрольную сумму UDP при чтении из обычного сокета UDP, а также считывать пакеты UDP или TCP,
используя символьный сокет (см. раздел 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-адрес, в предположении, что посылающий узел не является маршрутизатором.
ореп_рсарореп_рсарtest udptest udpmainmainВызывает: рсар_1。。kupdev pcap_open_live pcap_lookupnet pcap_compile pcap_setfilter pcap__datalink
ореп_рсар
ореп_рсар
test udp
test udp
main
main
VVРис. 26.4. Функции, которые используются в программе udpcksum
V
V
На рис. 26.4 приведены функции, используемые в нашей программе.
В листинге 26.11 показан заголовочный файл udpcksum.h, в который включен наш базовн访 заголовочный файл unp.h, а также различные системные заголовки, необходимые для получения доступа к определениям структур для заголовков пакетов IP и UDP.
Листинг 26.1. Заголовочный файл udpcksum.h
//udpcksum/udpcksum h
include"unp h"
#include#i nd ude/include#includeinclude#include#includeinclude#include#i nd ude/include#includeinclude#include#includeinclude<netwet/in_systm h><netinet/in h><netinet/ip h><netinet/ip_var h><netinet/udp h><netinet/udp_var h><net/if h>vnetinet/ijether h><netwet/in_systm h><netinet/in h><netinet/ip h><netinet/ip_var h><netinet/udp h><netinet/udp_var h><net/if h>vnetinet/ijether h>/* требуется ip h *//* требуется ip h */include<pc叩 h>
#include
#i nd ude
/include
#include
include
#include
#include
include
#include
#i nd ude
/include
#include
include
#include
#include
include
<netwet/in_systm h>
<netinet/in h>
<netinet/ip h>
<netinet/ip_var h>
<netinet/udp h>
<netinet/udp_var h>
<net/if h>
vnetinet/ijether h>
<netwet/in_systm h>
<netinet/in h>
<netinet/ip h>
<netinet/ip_var h>
<netinet/udp h>
<netinet/udp_var h>
<net/if h>
vnetinet/ijether h>
/* требуется ip h */
/* требуется ip h */
#define LOCALPORT"39123"/* порт отправителя (заданный по умолчанию) */
#define TTL_OUT64/* исходящее TTL */
/* объявление глобальных переменных */
extern struct sockaddr *dest *local.
extern socklen_t destlen. locallen.
extern mt datalink.
extern char *device.
extern pcap_t *pd.
extern int rawfd,
extern int snaplen,
extern int verbose
extern int zerosum.
23/* прототипы функций */
24 void clednup(int)
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http // www piter com/download
25 char *next_pcap(int *),
voidopen_pcap(void).
voidtest_udp(void).
voidudp_write(char *,int)
29 struct udpiphdr *udp_read(void).
3-10 Для работы с полями заголовков IP и UDP требуются дополнительные заголовочные файлы Интернета.
11-29 Мы определяем некоторые глобальные переменные и прототипы для своих собственных функций, которые вскоре покажем.
Первая часть функции msm показана в листинге 26.2.
Листинг 26.2. Функция main: определения
//udpcksum/main с
1 #include “udpcksum h"
2
3
4
/* определения глобальных переменных */ struct sockaddr *dest. *local.
socklen_t destlen locallen.
5
6
7
8
9
10
11
12
intdatai ink,/*из pcap_datalink(). в <net/bpf h>*/
char "device./* устройство pcap */
intfddipad./*HACK для 1ibpcap если определеноFDDI*/
pcap_t *pd./* указатель на структуру устройства для захвата пакетов */
int - rawfd,/*символьный сокет для записи */
intsnaplen = 200:/*количество данных, которые будут захвачены */
int verbose,
int zerosum./* отправка запроса LIDP без контрольной суммы */
13
static void usage(const char *),
14
15
16
17
18
19
int
main(int argc. char *argv[])
{
int c. on = 1.
char*ptr 1 ocalname[1024]. *localport;
struct addnnfo *aip
20
21
if (argc < 2) usage (,,M).
22
23
24
25
/* Нужен локальный IP-адрес для задания IP-адреса * отправителя дейтаграммы UDP Нельзя задать его * равным 0 и оставить выбор за IP. так как он * нужен для вычисления контрольной суммы * учитывающей псевдозаголовок
26
* Локальное имя и локальный порт можно заменить
* с помощью параметра -1
27
*/
28
29
30
if (gethostname(local name sizeof(local name)) < 0) err_sys("gethostname error").
localport 二 LOCALPORT.
Проверка количества аргументов командной строки
20-21 Программа требует как минимум двух аргументов: имя или IP-адрес узла, на котором запущен сервер DNS, и имя службы (doma i п) или номер порта (53) серве-
ра. Мы не показываем функцию 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 ( (с = getopt(argc. argv. "01 1 v")) != -1) {
33switch (c){、,
34case 'O'
35zerosum = 1.、
36break“
37case i'
38device = optarg; /* устройство pcap */
39break,
40 case '1'/* локальный IP-адрес и порт a b c d p */
41 if ( (ptr = strrchr(optarg. ' ')) == NULL)
42 usage("invalid -1 option").
43 *ptr++ = 0/* 0 заменяет последнюю точку */
44 local port = ptr, /* имя службы или номер гюрта */
45strncpy(local name, optarg sizeof(localname)),
46break,
47case'v'
48verbose= 1.
49break.
50case,?,
51usage("unrecognized option"):
52}
53}
Обработка аргументов командной строки
11-36 Мы вызываем функцию getopt для обработки аргументов командной строки. С помощью параметра -0 мы посылаем запросы UDP без контрольной суммы UDP, чтобы выяснить, обрабатываются ли эти дейтап)аммы сервером иначе, чем дейтаграммы с контрольной суммой.
37-39 Параметр -1 позволяет нам задать интерфейс, на котором будут приниматься ответы сервера. Если этот интерфейс не будет задан, библиотека для захвата па-
кетов выберет какой-либо интерфейс самостоятельно, но в случае узла с несколькими сетевыми интерфейсами этот выбор может оказаться некорректным. В этом заключается одно из различий между считыванием из обычного сокета и из устройства для захвата пакетов: в первом случае мы можем указать универсальный локальный адрес, что позволяет получать пакеты, прибывающие на любой из сетевых интерфейсов. Но во втором случае при работе с устройством для захвата пакетов мы можем получать пакеты, прибывающие только на конкретный интерфейс.
ПРИМЕЧАНИЕ
Можно отметить, что для пакетных сокетов Linux захват пакетов не ограничен одним устройством. Тем не менее библиотека hbcap обеспечивает фильтрацию либо по умолчанию, либо согласно заданному нами параметру т.
40-46 Параметр -1 позволяет нам задать IP-адрес отправителя и номер порта. В качестве номера порта (или названия службы) берется строка, следующая за последней точкой, а IP-адресом является все, что расположено перед последней точкой.
Последняя часть функции mai п показана в листинге 26.4.
Листинг 26.4. Функция main: преобразование имен узлов и названий служб, создание сокета
//udpcksum/mdin с
54
55
if (opti nd i= а где - 2) usage(°nnssing <host> and/or <serv>").
56
57
58
59
/* Преобразование имени узла получателя и названия службы */
aip = host_serv(argv[optind] argv[optind + 1]. AF_INET, SOCK_DGRAM). dest = aip->ai_addr/* не вызываем функцию freeaddrinfo?) */
destlen = aip->ai_addrlen.
60
61
62
63
/* Преобразуем локальное имя и название службы */
aip = host_serv(local name local port AF_INET. SOCK_DGRAM).
local = aip->ai_addr/* не вызываем функцию freeaddrinfoO */
locallen = aip->ai_addrlen.
64
65
/*
* Нужен символьный сокет для записи наших
* дейтаграмм IP
66
* Процесс должен иметь права 叩ивмле「ировзнно「о
* пользователя для создания этого сокета
67
* Нужно установить параметр IP_HDRINCL чтобы
* записывать свои собственные заголовки IP
68
*/
69
rawfd = Socket(dest->sa_fаплly. SOCK_RAW. 0).
70
Setsockopt(rawfd. IPPROTO^IP. IP_HDRINCL &on. sizeof(on)):
71
open_pcap(). /* открываем устройство для захвата пакетов */
72
setuid(getuidO) /* больше не нужны права привилегированного ПбльзойтеЛя W
73
74
75
Signal(SIGTERM cleanup) Signal(SIGINT, cleanup). Signal(SIGHUP. cleanup).
76
test_udp().продолжение 挨
Листинг 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 показана функция ореп_рсар, которую мы вызвали из функции msm, чтобы открыть устройство для захвата пакетов.
Листинг 26.5. Функция ореп_рсар. открытие и инициализация устройства для захвата пакетов
//udpcksum/pcap с
#include "udpcksum h"
#define CMD "udp and src host %s and src port %d"
open_pcap(void)
5 6 7 8 95 6 7 8 9{一
5 6 7 8 9
5 6 7 8 9
tn nt32_t local net netmask.
char - cmd[MAXLINEJ. errbuf[PCAP ERRBUF SIZE].显rfftNET_ADDRSTRLEN].
str2[INET_ADDRSTRLEN]
struct bpf program fcode.
if (device == NULL) {
if ( (device = pcap_lookupdev(errbuf)) == NULL)
err_quit(Mpcap_lookup 案s". errbuf).
} 一一
pnntfC'device = %s\en". device).
/* жестко задано promisc=0 to_nis=500 */
if ( (pd = pcap_open_live(device. snaplen. 0. 500 errbuf)) == NULL)
err_quit(ирсар_орепJive tsM. errbuf)
if (pcap_lookupnet(device. &localnet &netmask, errbuf) < 0)
err_quit( "pcapjookupnet %s", errbuf)
if (verbose)
pnntf(°localnet = %s netmask = %s\en”.
Inet_ntop(AF_INET &localnet strl. sizeof(strl)),
Inet_ntop(AF_INET. &netmask str2. sizeof(str2))),
snprintf(cmd sizeof(cmd). CMD
Sock_ntop_host(dest destlon),
ntohs(sock_get_port(dest destlen))):
if (verbose)
printfCcmd = %s\en" cmd).
if (pcap_compile(pd &fcode cmd. 0 netmask) < OJ
err^quit(Mpcap_compi1e Zs” pcap_geterr(pd)),'
if (pcap_setfilter(pd &fcode) < 0)
err_qui t (f pcap^setf11 ter 毋"pcap_geterr(pd))
if ( (datalink = pcap_datalink(pd)) < 0)
err_qu11("pcsp^dataTink %s" pcap_geterr(pd)).
if (verbose)
printf("datalink = %d\en” datalink):
}
Выбор устройства для захвата пакетов
10-14 Если устройство для захвата пакетов не было задано (с помощью параметра командной строки -】),то выбор этого устройства осуществляется с помощью функции рсзр_1 ookupdev. С помощью запроса SIOCGIFCONF функции ioctl выбирается включенное устройство с минимальным порядковым номером, но только не устройство обратной связи. Многие из библиотечных функций рсар возвращают сообщения об ошибках в виде строк. Единственным аргументом функции рсар_1 ookupdev является массив, в который записывается строка с сообщением об ошибке.
Открываем устройство
5-17 Функция pcap_open_live открывает устройство. СпЬво live П{Жсугсту№В 1Ш^ звании функции потому, что здесь имеется в виду фактиче(жое уст卩而TS 期#
захвата пакетов, а не файл, содержащий предыдущие сохраненные пакеты. Первым аргументом функции является имя устройства, вторым — количество байтов, которое нужно сохранять для каждого пакета (значение shaplen, которое мы инициализировали числом 200 в листинге 26.2), а третий аргумент — это флаг, указывающий на смешанный режим. Четвертый аргумент — это значение времени ожидания в миллисекундах, а пятый — указатель на массив, содержащий сообщения об ошибках.
Если установлен флаг смешанного режима, интерфейс переходит в этот режим, в результате чего он принимает все пакеты, проходящие по кабелю. Для программы tcpdump это нормальный режим. Тем ие менее в нашем примере ответы сервера DNS будут посланы непосредственно на наш узел (то есть можно обойтись без смешанного режима).
Четвертый аргумент (время ожидания) — это аргумент для считывания. Вместо того чтобы возвращать пакет процессу каждый раз, когда приходит очередной пакет (что может быть весьма неэффективно, так как в этом случае потребуется выполнять множество операций копирования отдельных пакетов из ядра в процесс), это делается, либо когда считывающий буфер устройства оказывается заполненным, либо когда истекает время ожидания. Если время ожидания равно нулю, то каждый пакет будет переправляться процессу, как только будет получен.
Получение сетевого адреса и маски подсети
18-23 Функция рсар_1 ookupnet возвращает сетевой адрес и маску подсети для устройства захвата пакетов. При вызове функции pcap^compile, которая будет вызвана следующей, нужно задать маску подсети, поскольку с помощью маски фильтр пакетов определяет, является ли IP-адрес адресом широковещательной передачи для данной подсети.
Компиляция фильтра пакетов
24-30 Функция рсар_соп1р11 е получает строку, построенную нами как массив cmd, и компилирует ее, создавая тем самым программу для фильтрации (записывая ее в code). Эта программа будет отбирать те пакеты, которые мы хотим получить.
Загрузка программы фильтрации
31-32 Функция pcap_setfi 1 ter получает только что скомпилированную программу фильтрации и загружает ее в устройство для захвата пакетов. Таким образом инициируется захват пакетов, выбранных нами путем настройки фильтра.
Определение типа канального уровня
33*36 Функция pcap_datahnk возвращает тип канального уровня для устройства захвата пакетов. Эта информация нужна нам при захвате пакетов для того, чтобы определить размер заголовка канального уровня, который будет добавлен в начало каждого считываемого нами пакета (листинг 26.10).
После вызова функции ореп_рсар функция main вызывает функцию test_udp, показанную в листинге 26.6. Эта функция посылает запрос DNS и считывает ответ сервера.
Листинг 26.6. Функция test_udp: отправка запросов и считывание ответов //udpcksum/udpcksum с 47 void 48 test__udp(void) 49 { 一 50 volatile int nsent = 0. timeout = 3. 51struct udpiphdr*ui,
52Signal(SIGALRM,sig_dlrm).
53if (sigsetjmptjmpbuf.D) {
54if (nsent >= 3)
55err_quit("no response").
56pnntf ("timeoutXen"),
57 timeout *=2./* экспоненциальное увеличение задержки 3 6. 12 */
58}
59 canjump =1./* теперь siglongjmp OK */
60 send_dns_query().
61 nsent++ -
62 alarm(timeout).
63 in = udp_read().
64 canjump 二 0.
65 alarm(O).
66 if (ui->ui_sum == 0)
67p门ntf(,Udp checksums off\en"):
68 else
69printfC'UDP checksums on\en"),
70 if (verbose)
71 pnntf("recevied UDP checksum = Xx\en"« ntohs(ui->ui_sum)):
72 }-
Переменные volatile
50 Нам нужно, чтобы две динамические локальные переменные nsent и timeout сохраняли свои значения после возвращения siglongjmp из обработчика сигнала в нашу функцию. Реализация допускает восстановление значений динамических локальных переменных, предшествовавших вызову функции sigset jump [93, с. 178], но добавление спецификатора vol at】1 е предотвращает это восстановление.
Установление обработчика сигналов и буфера перехода
52-53 Для сигнала SIGALRM устанавливается обработчик сигнала, а функция sigsetjmp устанавливает буфер перехода для функции siglongjmp. (Эти две функции подробно описаны в разделе 10.15 [93].) Значение 1 во втором аргументе функции sigsetjmp указывает, что требуется сохранить текущую маску сигнала, так как мы будем вызывать функцию siglongjmp из нашего обработчика сигнала.
Функция siglongjinp
;4~58 Этот фрагмент кода выполняется, только когда функция siglongjmp вызывается из нашего обработчика сигнала. Это указывает на возникновение условий, при которых мы входим в состояние ожидания: мы отправили запрос, на который не пришло никакого ответа. Если после того, как мы отправим три запроса, ответа
не будет, мы прекращаем выполнение кода. По истечении времени ожидания, отведенного на получение ответа, мы выводим соответствующее сообщение и увеличиваем значение времени ожидания в два раза, то есть задаем экспоненциальное смещение {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_redd считывает ответ. Мы вызываем функцию al arm для предотвращения «вечной» блокировки функции read. Если истекает заданное (в секундах) время ожидания, генерируется сигнал SIGALRM, и наш обработчик сигнала вызывает функцию siglongjmp.
Анализ полученной контрольной суммы UDP
66-71 Если значение полученной контрольной суммы UDP равно нулю, это значит, что сервер не вычислил и не отправил контрольную сумму.
В листинге 26.7 показана наша функция sig_alrm — обработчик сигнала SIGALRM.
Листинг 267 Функция sig_alrm: обработка сигнала SIGALRM
//udpcksum/udpcksum с
include"udpcksum h"
#include<setjmp h>
static sigjmp^buf jmpbuf.
static wt canjump.
void
S7g_a1rffl(?nt signo)
{-
if (canjump == 0)
return.
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)
18
19
20
21
size_t nbytes.
charbuf[sizeof(struct udpiphdr) + 100] *ptr,
short one.
22
ptr = buf + sizeof(struct udpiphdr). /* оставляем место для заголовков ПЩ(|Р */
23
24
25
26
27
28
29
30
31
*((u~sho「t *) ptr) = htons(1234)./* идентификация */
ptr += 2
*((u_short *) ptr) = htons(OxO), /* флаги */
ptr += 2.
*((u_short *) ptr) = htons(l)./* количество запросов */
ptr += 2
*((u_short *) ptr) = 0./* количество записей о ресурсах посланных в ответ */
ptr += 2"
*((u_short *) ptr) = 0 •/* количество записей о ресурсах, определяющих
полномочия */
32
33
34
ptr += 2.
*((u_short *) ptr) = 0./* количество дополнительных записей о ресурсах */
ptr += 2
35
36
37
38
39
40
41
memcpy(ptr "\e001a\e014root-servers\e003net\e000" 20).
ptr += 20
one = htons(l)
memcpy(ptr. &one. 2)./* тип запроса = A */
ptr += 2.
memcpytptr &one 2)./* класс запроса = 1 (IP addr) */
ptr += 2
42
43
44
45
46
nbytes = 36
udp_write(buf. nbytes).
if Tverbose)
pnntf("sent 紀 bytes of data\en". nbytes).
Инициализация указателя на буфер
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 байтами данных можно манипулировать столь же легко, не прибегая к использованию специального типа данных.
35-41 Затем мы формируем простой запрос, который располагается после заголовка: запрос типа А IP-адреса узла a root-servers net. Это доменное имя занимает 20 байт и состоит из 4 фрагментов: однобайтовая часть а, 12-байтовая часть rootservers, 3-байтовая часть net и корневая часть, длина которой занимает 0 байт. Тип запроса 1 (так называемый запрос типа А), и класс запроса также 1.
Запись дейтаграммы UDP
42-45 Это сообщение состоит из 36 байт пользовательских данных (восемь 2-байтовых полей и 20-байтовое доменное имя). Мы вызываем нашу функцию udp_wnte для формирования заголовков UDP и IP и последующей записи дейтаграмма UDP в наш символьный сокет.
В листинге 26.9 показана наша функция udp_write, которая формирует заголовки IP и UDP, а затем записывает дейтаграмму в символьный сокет.
Листинг 26.9. Функция udp_write: формирование заголовков UDP и IP и запись дейтаграммы IP в символьный сокет
Z/udpcksum/udpwrite с
void
udp_write(char *buf. int userlen)
{-
struct udpiphdr *ui,
struct ip *ip
/* Заполняем контрольную сумму UDP */
ip = (struct ip *) buf.
in = (struct udpiphdr *) buf:
/* добавляем 8 к userlen - длину псевдозаголовка */
ui->uijen = htons((u_short) (sizeof(struct udphdr) + userlen)):
/* затем добавляем 28 - длин^ дейтаграммы IP */
userlen += sizeof(struct udpiphdr).
ui->ui_next = 0,
ui~>ui__prev = 0.
ui->ui_xl = 0.
ui->u<pr = IPPRDTDJDP.
ui->ui_src s_ddd「 = ((struct sockaddrin *) local)->sin_addr s_addrr
ui->ui_dst s_add「= ((struct sockaddjin *) dest)->sin_addr s_addr.
ui->ui_sport = ((struct sockaddr_in *) local)->sin_port.
ui->ui_dport = ((struct sockaddrjn *) dest)->sin_port.
ui->ui_ulen ^ ui->ui_len.
ui->ui_sum = 0.
if (zerosum == 0) {
#ifdef notdef /* изменяем ifndef для Solans 2 x. x < 6 */
if ( (ui->ui_sum = in_cksum((u_short *) in. userlen)) =« 0)
ui->ui_sum = Oxffff.
#else 一
ui->ui_sum = ui->ui Jen
#endif 一一
}
/* Заполняем остальную часть заголовка IP */
/* ip_output() вычисляет и записывает контрольную сумму заголовка IP */
ip->ip_v = IPVERSION,
ip->ip_hl = sizeoftstruct ip) » 2.
ip->ip_tos = 0.
#ifdef linux
42
ip->ip_len
=htons(userlen)
/*
сетевой порядок байтов */
43
#else
44
45
ipepjen
#endif
=userlen
/*
порядок байтов узла */
46
ip->ip_id =
=0.
/*
пусть IP устанавливает зто значение */
47
48
ip->ip_off ip->ip_ttl
=0.
=「TL—OUT.
/*
сдвиг, флаги MF и DF */
49
50
SendtoCrawfd. buf userlen. 0 }
dest
:.destlen).
Инициализация указателей на заголовки пакетов
11-13 Указатель ip указывает на начало заголовка IP (структуру ip), а указатель ui указывает на то же место, но структура udpi phdr является объединением заголовков IP и UDP.
Обновление значений длины
14-17 Переменная ui_len — это длина дейтаграммы UDP: количество байтов пользовательских данных плюс размер заголовка UDP (8 байт). Переменная useri еп (количество байтов пользовательских данных, которые следуют за заголовком 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.
Заполнение заголовка IP
36-49 Поскольку мы установили параметр сокета IP_HORINCL, нам следует заполнить большую часть полей в заголовке IP. (В разделе 25.3 обсуждается запись в символьный сокет при включенном параметре IP_HDRINCL.) Мы присваиваем полю идентификации нуль (ip_】d), что указывает IP на необходимость задания значения этого поля. IP также вычисляет контрольную сумму IP, а функция sendto записывает дейтаграмму IP.
Следующая функция — это udp redd, показанная в листинге 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 %x 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
}
14-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 Мбит/с.
Наша функция udp_check (см. листинг 26.12) исследует пакет и проверяет поля в заголовках IP и UDP.
В листинге 26.11 показана функция next_pcap, возвращающая следующий пакет из устройства захвата пакетов.
Листинг 26.11. функция next_pcap: возвращает следующий пакет //udpcksum/pcap с 38 char *
next_pcap(int *1en)
{—
char *ptr.
struct pcap_pkthdr hdr.
/* продолжаем следить, пока пакет не будет готов */
while ( (ptг = (char *) pcap_next(pd &hd「)) == NULL):
*len == hdr caplen. /* длина захваченного пакета */
return (ptr)
}
43-44 Мы вызываем библиотечную функцию pcap_next, возвращающую следующий пакет. Указатель на пакет является возвращаемым значением данной функции, а второй аргумент указывает на структуру pcap_pkthdr, которая тоже возвращается заполненной:
struct pcapjkthdr {
struct timeval ts /* временная метка */
bpf_u_int32 caplen /* длина захваченного фрагмента */
bpf_u_】nt32 len. /* полная длина пакета находящегося в канале */
}—-
Временная отметка относится к тому моменту, когда пакет был считан устройством захвата пакетов, в противоположность моменту фактической передачи пакета процессу, которая может произойти чуть позже Переменная caplen содержит длину захваченных данных (вспомним, что в листинге 26.2 нашей переменной shaplen было присвоено значение 200 и она являлась вторым аргументом функции рсар_ореп_11 ve в листинге 26.5). Назначение устройства захвата пакетов состоит в захвате заголовков, а не всего содержимого каждого пакета. Переменная 1 еп — это полная длина пакета, находящегося в канале. Значение cap] еп будет всегда меньше или равно значению len.
45-46 Перехваченная часть пакета возвращается через указатель (аргумент функции), и возвращаемым значением функции является указатель на пакет. Следует помнить, что указатель на пакет указывает фактически на заголовок канального уровня, который представляет собой 14-байтовый заголовок Ethernet в случае кадра Ethernet или 4-байтовый псевдоканальиый (pseudo-link) заголовок в случае за- кольцовки на себя.
Если мы посмотрим на библиотечную реализацию функции pcap_next, мы увидим, что между различными функциями существует некоторое «разделение труда», схематически изображенное на рис. 26.5. Наше приложение вызывает функции рсар_, среди которых есть как зависящие, так и не зависящие от устройства захвата пакетов. Например, мы показываем, что реализация BPF вызывает функцию read, в то время как реализация DLPI вызывает функцию getmsg, а реализа* ция Linux вызывает resvfrom.
udp_readudp_readnext_pcapnext_pcap■ Приложение■ Приложениеpcap_nextpcap_nextpcap_dispatchpcap_dispatchpcap_readpcap_readЗависит от устройстваЗависит от устройстваНе зависит от устройстваНе зависит от устройства-Библиотека захвата пакетов libpcad-Библиотека захвата пакетов libpcadПроцессЯдроПроцессЯдро
udp_read
udp_read
next_pcap
next_pcap
■ Приложение
■ Приложение
pcap_next
pcap_next
pcap_dispatch
pcap_dispatch
pcap_read
pcap_read
Зависит от устройства
Зависит от устройства
Не зависит от устройства
Не зависит от устройства
-Библиотека захвата пакетов libpcad
-Библиотека захвата пакетов 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_setfilter в листинге 26 5, не должен возвращать пакеты никакого другого типа
Листинг 26.12. Функция udp_check проверка полей в заголовках IP и UDP //udpcksum/udpread с 38 struct udpiphdr * 39 udp_check(char *ptr int len) 40 {- 41 int hlen 42struct ip *ip
43struct udpiphdr *ui
44 if (len < sizeofCstruct ip) + sizeof(struct udphdr))
45 err_quit( len = %d len)
46/* минимальная проверка заголовка IP */
47 ip = (struct ip *) ptr
48 if (ip >ip_v i= IPVERSION)
49 err_quit? ip_v = Zd ip >ip_v)
50 hlen = ip >ip_hl « 2
51 if (hlen < sizeofCstruct ip))
52 err_quit( ip hl = Zd ip >ip_hl)
53 if (len < hlen + sizeofCstruct udphdr))
54 err_quit( len = %d hlen =紀 len hlen)
55 if ( (ip >ip_sum = in_cksum{(u_short *) ip Men)) •- 0)
56err_quit( ipchecksumerror 5
57 if (ip >ip_p == IPPR0T0_UDP) {
58ui = (structudpiphdr*)ip
59return (ui)
60} else
61 err_quit( not a UDP packet )
62 } 一
Функция cleanup, показанная в листинге 26 13, вызывается из функции mam непосредственно перед тем, как программа завершается, а также вызывается в качестве обработчика сигнала в случае, если пользователь прерывает выполнение программы (см листинг 26 4)
Листинг 26.13. Функция cleanup
Z/udpcksum/cleanup с
void
cleanup(int signo)
{
struct pcap_stat stat
fflush(stdout)
putc( \en stdout)
if (verbose) {
if (pcap_stats(pd &stat) < 0)
err_quit( pcap_stats Zs\en pcap_geterr(pd))
printf( %d packets received by filter\en stat ps nec^yj
printf( %d packets dropped by kernel\en stat ps_drop),
}-
exit(0)
}
Получение и вывод статистики по захвату пакетов
М3 Функция pcap_stats получает статистику захвата пакетов общее количество полученных фильтром пакетов и количество пакетов, переданных ядру
Пример
Сначала мы запустим нашу программу с аргументом командной строки 0 и убедимся, что сервер имен отвечает на приходящие дейтаграммы, не содержащие контрольной суммы Мы также задаем флаг -v.
solans # udpcksum 0 v connix 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
Затем мы запускаем нашу программу на сервере имен, в котором отключен подсчет контрольных сумм 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
packets received by filter 0 packets dropped by kernel
Резюме
Символьные сокеты предоставляют возможность записывать и считывать 1Р-дей- таграммы, которые могут быть не поняты ядром, а доступ к канальному уровню позволяет считывать и записывать кадры канального уровня любых типов (не только дейтаграммы IP) Программа tcpdump — это, вероятно, наиболее широко используемая программа, предоставляющая непосредственный доступ к канальному уровню
В различных операционных системах применяются различные способы доступа к канальному уровню Мы рассмотрели пакетный фильтр Беркли, DLPI SVR4 и пакетные сокеты Linux (SOCK_PACKET) Но у нас имеется возможность, не вникая в различия перечисленных способов, использовать находящуюся в свободном доступе переносимую библиотеку захвата пакетов libcap
Упражнения
Каково назначение флага can jump в листинге 26 7?
При работе программы udpcksum наиболее распространенным сообщением об ошибке является сообщение о недоступности порта ICMP (в пункте назначения не работает сервер имен) или недоступности узла ICMP В обоих случаях нам не нужно ждать истечения времени ожидания, заданного функцией udp_read в листинге 26 6, так как сообщение о подобной ошибке фактически является ответом на наш запрос DNS Модифицируйте программу таким образом, чтобы она перехватывала эти ошибки ICMP
ГЛАВА 27
Альтернативное устройство клиента и сервера
Введение
При написании сервера под Unix мы можем выбирать из следующих вариантов управления процессом
Наш первый сервер, показанный в листинге 1 5, был последовательным (iteratwe). но количество сценариев, для которых этот вариант является предпочтительным, весьма ограничено, поскольку последовательный сервер не может начать обработку очередного клиентского запроса, не закончив полностью обработку текущего запроса
綏 В листинге 5 1 показан первый в данной книге параллельный (concurrent) сервер, который для обработки каждого клиентского запроса порождал дочерний процесс с помощью функции fork Традиционно большинство серверов, работающих под Unix, попадают в эту категорию
В разделе 6 8 мы разработали другую версию сервера TCP, в котором имеется только один процесс, обрабатывающий любое количество клиентских запросов с помощью функции select
В листинге 23 2 мы модифицировали параллельный сервер, создав для каждого клиента по одному потоку вместо одного процесса
В этой главе мы рассмотрим два других способа модификации устройства параллельного сервера
Предварительное создание дочерних процессов (preforking) В этом случае при запуске сервера выполняется функция fork, которая создает определенное количество (пул) дочерних процессов Обработкой очередного клиентского запроса занимается процесс, взятый из этого набора
緩 Предварительное создание потоков (prethreading) При запуске сервера создается некоторое количество (пул) потоков, и для обработки каждого клиента используется поток из данного набора
В данной главе мы будем рассматривать множество вопросов, связанных с предварительным созданием потоков и процессов Например, что произойдет, если в пуле окажется недостаточное количество процессов или потоков? А если их будет слишком много? Как родительский и дочерние процессы (или потоки) синхоонизиоуют свои действия?
Обычно написать клиент легче, чем сервер, за счет простоты управления процессом клиента. Тем не менее мы уже исследовали различные способы написания простого эхо-клиента, которые вкратце изложены в разделе 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
дескриптор сокета
Описание сервера
Время центрального процессора на управление процессом:
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
6,2
1,8
25,2
10,0
2,7
8,6
30
7,8
3,5
27,3
11,2
5,6
10,0
45
8,9
5,5
29,7
13,1
8,7
19,6
60
10,1
6,9
34,2
14,3
11,2
28,6
75
И,4
8,7
39,8
16,0
13,7
29,3
90
12,6
10,9
130,1
17,6
15,5
28,6
105
13,2
12,0
19,7
17,6
30,4
120
15,7
13,5
22,0
19,2
29,4
Все приведенные выше значения времени были получены путем запуска клиента, показанного в листинге 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
Альтернативы для клиента TCP
Мы уже обсуждали различные способы устройства клиентов, но стоит тем не менее еще раз обратить внимание на относительные достоинства и недостатки этих способов.
В листинге 5.4 показан основной способ устройства клиента TCP. С этой программой были связаны две проблемы. Во-первых, когда она блокируется в ожидании ввода пользователя, она не замечает происходящих в сети событий, например отключения собеседника от соединения. Во-вторых, она действует в режиме остановки и ожидания, что неэффективно в случае пакетной обработки.
Листинг 6.1 содержит следующую, модифицированную версию клиента. С помощью функции select клиент получает информацию о событиях в сети во время ожидания ввода пользователя. Однако проблема этой версии заключается в том, что программа не способна корректно работать в пакетном режиме. В листинге 6.2 эта проблема решается путем применения функции shutdown.
С листинга 15.1 начинается рассмотрение клиентов, использующих неблокируемый ввод-вывод.
Первым из рассмотренных нами клиентов, вышедшим за пределы ограничений, связанных с наличием единственного процесса или потока для обслуживания всех запросов, является клиент, изображенный на рис. 15.4. В этом случае использовалась функция fork, и один процесс обрабатывал передачу данных от клиента к серверу, а другой — передачу данных в обратном направлении.
В листинге 23.1 используются два потока вместо двух процессов.
В конце раздела 15.2 мы резюмируем различия между перечисленными версиями. Как мы отметили, хотя версия с неблокируемым вводом-выводом является самой быстродействующей, ее код слишком сложен, а применение двух потоков или двух процессов упрощает код.
Тестовый клиент TCP
В листинге 27.11 показан клиент, который будет использоваться для тестирования всех вариаций нашего сервера.
Листинг 27.1. Код клиента TCP для проверки различных версий сервера //server/client с
include "unp h"
#define MAXN 16384 /* максимальное количество байтов которые могут быть запрошены клиентом от сервера */
int
main(int argc, char **argv)
{продолжение 少
Все исходные коды программ, опубликование в ЭТОЙ ^и^%т М0Же%е йайти по адресу http // www piter com/downJoad
Листинг 27.1 (продолжение)
int1. jfd nchildren.nloops,nbytes:
pid_tpid.
ssize_tn.
char - request[MAXLINE]. reply[MAXN]
if (argc i= 6)
err_quit('*usage client <hostname or IPaddr> <port> <#children>
- ”<#loops/child> <#bytes/request>rt).
nchildren = atoi(argv[3])
nloops = atoi(argv[4]).
nbytes = atoi(argv[5]).
snprintf(request. sizeof(request), ,,^d\en,^ nbytes), /* в конце символ новой строки */
for (1 = 0. 1 V nchildren. i++) {
if ( (pid = ForkO) == 0) { /* дочерний процесс */
for (j = 0. j < nloops, j++) {
fd м Tcp_connect(argv[l]. argv[2]),
Wnte(fd request strlen(request)).
if ( (n = Readn(fd. reply, nbytes)) J nbytes)
err_quit("server returned Zd bytes" n)
Close(fd). /* состояние TIME_WAIT на стороне клиента, а не сервера */
}—
printf("child Yd done\enw D.
exit(0)
}
/* родительский процесс снова вызывает функцию fork */
}
31 while (wait(NULL) > 0) /* теперь родитель ждет завершения вс献 дочерних 而бцессов */
32
33 if (еггпо i= ECHILD)
34 err_sys("wait error"):
35 exit(0),
36 }
)-12 Каждый раз при запуске клиента мы задаем имя узла или IP-адрес сервера, порт сервера, количество дочерних процессов, порождаемых функцией fork (что позволяет нам инициировать несколько соединений с одним и тем же сервером одновременно), количество запросов, которое каждый дочерний процесс должен посылать серверу, и количество байтов, отправляемых сервером в ответ на каждый запрос.
'-30 Родительский процесс вызывает функцию fork для порождения каждого дочернего процесса, и каждый дочерний процесс устанавливает указанное количество соединений с сервером. По каждому соединению дочерний процесс посылает запрос, задавая количество байтов, которое должен вернуть сервер, а затем дочерний процесс считывает это количество данных с сервера. Родительский процесс просто ждет (wait) завершения выполнения всех дочерних процессов. Обратите внимание, что клиент закрывает каждое соединение TCP, таким образом состояние TCP TIME_WAIT имеет место на стороне клиента, а не на стороне сервера. Это отличает наше клиент-серверное соединение от обычного соединения HTTP.
При тестировнии различных серверов из этой главы мы запускали клиент следующим образом:
% client 206.62.226.36 8888 5 500 4000
Таким образом создается 2500 соединений TCP с сервером: по 500 соединений от каждого из 5 дочерних процессов. По каждому соединению от клиента к серверу посылается 5 байт, а от сервера клиенту передается 4000 байт ('ЧОООХп*'). Мы запускаем клиент на двух различных узлах, соединяясь с одним и тем же сервером, что дает в сумме 5000 соединений TCP, причем максимальное количество одновременных соединений с сервером в любой момент времени равно 10.
ПРИМЕЧАНИЕ
Для проверки различных web-серверов существуют изощренные контрольные тесты Один из них называется WebStone Информация о нем находится в свободном доступе по адресу http //www mindcraft Для общего сравнения различных альтернативных устройств сервера, которые мы рассматриваем в этой главе, нам не нужны столь сложные тесты.
Теперь мы представим девять различных вариантов устройства сервера.
Последовательный сервер TCP
Последовательный сервер TCP полностью обрабатывает запрос каждого клиента, прежде чем перейти к следующему клиенту. Последовательные серверы редко используются, но один из них, простой сервер для определения времени и даты, мы показали в листинге 1.5.
Тем не менее у нас имеется область, в которой желательно применение именно последовательного сервера — это сравнение характеристик других серверов. Если мы запустим клиент следующим образом:
% client 206.62.226.36 8888 1 5000 4000
и соединимся с последовательным сервером, то получим такое же количество соединений TCP (5000) и такое же количество данных, передаваемых по одному соединению. Но поскольку сервер является последовательным, на нем не осуществляется никакого управления процессами. Это дает нам возможность получить базовое значение времени, затрачиваемого центральным процессором на обработку указанного количества запросов, которое потом мы можем вычесть из результатов измерений для других серверов. С точки зрения управления процессами последовательный сервер является самым быстрым, поскольку он вовсе не занимается этим управлением. Взяв последовательный сервер за точку отсчета, мы можем сравнивать результаты измерений быстродействия различных других серверов, показанные в табл. 27.1.
Мы не приводим код для последовательного сервера, так как он представляет собой тривиальную модификацию параллельного сервера, показанного в следующем разделе.
Параллельный сервер TCP: один дочерний процесс для каждого клиента
Традиционно параллельный сервер TCP вызывает функцию fork для порождения нового дочернего процесса, который будет выполнять обработку очередного к пирнтгкпгп запппса Зтс позвппярт rpnRpnv п^па^атыиать нргкппькп запппсов
одновременно, выделяя по одному дочернему процессу для каждого клиента Единственным ограничением на количество одновременно обрабатываемых клиентских запросов является ограничение операционной системы на количество дочерних процессов, допустимое для пользователя, в сеансе которого работает сервер Листинг 5 9 содержит пример параллельного сервера, и большинство серверов TCP написаны в том же духе
Проблема с параллельными серверами заключается в количестве времени, которое тратит центральный процессор на выполнение функции fork для порождения нового дочернего процесса для каждого клиента Еще десять лет назад, в конце 80-х, когда наиболее загруженные серверы обрабатывали сотни или тысячи клиентов за день, это было приемлемо Но расширение Сети изменило требования Теперь загруженными считаются серверы, обрабатывающие миллионы соединений TCP в день Сказанное относится лишь к одиночным узлам, но наиболее загруженные сайты используют несколько узлов, распределяя нагрузку между ними (в разделе 14 2 [95] рассказывается об общепринятом способе распределения этой нагрузки, называемом циклическим обслуживанием DNS — DNS round гоЬгп) В последующих разделах описаны различные способы, позволяющие избежать вызова функции fork для каждого клиентского запроса, но тем не менее параллельные серверы остаются широко распространенными
В листинге 27 2 показана функция mai п для нашего параллельного сервера ТСР
Листинг 27.2. Функция main для параллельного сервера TCP //server/servOl с
#include unp h
int
main(int argc char **argv)
{
intlistenfd connfd
pid__tchildpid
voidsig chld(wt) sig_int(lnD-web childUnti
socklen_t cTilen addrlen
struct sockaddr *cliaddr
if (argc == 2)
listenfd = Tcp_listen(NULL argv[l] &addrlen)
else if (argc == 3)
listenfd = Tcp_listen(argv[l] argv[2] &addrlen)
else
err_quit( usage servOl [ <host> ] <port#> )
cliaddr = Malloc(addrlen)
Signal(SIGCHLD sig_chld)
Signal(SIGINT sig_int)
192021222324252627192021222324252627for ( ) {
19
20
21
22
23
24
25
26
27
19
20
21
22
23
24
25
26
27
cl ilen = addrlen
if ( (connfd = accept(listenfd cliaddr &clilen)) < 0)
if (errno == EINTR)
continue /* назад к forО */
else
err_sys( accept error )
} ~
】f Г frhiIdmrl = Fcrkr〉、== П、 I /* псиапнмм пппнргг */
Close(listenfd) /* закрываем прослушпваеммн сокет */
web_chiId(connfd) /* обрабатываем запрос */
exit(0)
}
Close(connfd)/* родительский npuecc закрывает присоединенный сокет */
}
}
Эта функция аналогична функции, показанной в листинге 5 9 она вызывает функцию fork для каждого клиентского соединения и обрабатывает сигналы SIGCHLD, приходящие от закончивших свое выполнение дочерних процессов Тем не менее мы сделали эту функцию не зависящей от протокола за счет вызова функции tcp_listen Мы не показываем обработчик сигнала sig_chld он совпадает с показанным в листинге 5 8, но только без функции pnntf
Мы также перехватываем сигнал SIGINT, который генерируется при вводе символа прерывания Мы вводим этот символ после завершения работы клиента, чтобы было выведено время, потраченное центральным процессором на выполнение данной программы В листинге 27 3 показан обработчик сигнала Это пример обработчика сигнала, который никогда не возвращает управление
Листинг 27.3. Обработчик сигнала SIGINT
//server/servOl с
void
sig_int(int signo)
{ 一
voidpr_cpu_t 1^( void)
pr_cpu_time()
exit(07
}
В листинге 27 4 показана функция pr_cpu_time, вызываемая из обработчика сигнала
Листинг 27.4. Функция pr_cpu』me вывод полного времени центрального процессора
//server/pr_cpu_tiте с
#include unp h
#include<sys/resource h>
#ifndef HAVE一GETRUSAGE一PROTO
int getrusage(int ~struct rusage *).
#enth f
void
pr_cpu_time(void)
{一一
double user sys
struct rusage myusage childusage
И if (getrusage(RUSAGE_SELF &myusage) < 0)
err_sys( getrusage error )
if (getrusage(RUSAGE_CHILDREN &childusage) < Q)
err_sys( getrusage~error )
15 user = (double) myusage ru_utime tv_sec +
16 myusage ru_utime tv__usec / 1000000 0
Листинг 27.4 (продолжение)
user += (double) chi1dusage.ru_utime.tv_sec +
chi1dusage ru_utime tv_usec / 1000000 0,
sys = (double) myusage ru_stime tv_sec +
myusage.ru_stime tv_usec / 1000000.0,
sys += (double) chi 1 dusage.runtime tv_sec +
chi 1dusage.ru_stime tv_usec / 1000000 0:
pnntf("\enuser time ^ Zg. sys time = Zg\erT. user, sys),
}
Функция getrusage вызывается дважды: она позволяет получить данные об использовании ресурсов вызывающим процессом (RUSAGE一SELF)и всеми его дочерними процессами, которые завершили свое выполнение (RUSAGE CHILDREN). Выводится время, затраченное центральным процессором на выполнение пользовательского процесса (общее пользовательское время, total user time), и время, которое центральный процессор затратил внутри ядра на выполнение задач, заданных вызывающим процессом (общее системное время, total system time).
Возвращаясь к листингу 27.2, мы видим, что функция web_child вызывается для обработки каждого клиентского запроса. Эта функция показана в листинге 27.5.
Листинг 27.5. Функция web_child: обработка каждого клиентского запроса
//server/web_chi 1 d с
#include "unp h"
#define MAXN 16384 /* максимальное количество байтов. которое клиент может запросить */
void
web_child(int sockfd)
{ ~
intntowrite,
ssize_t nread,
char~ line[MAXLINE]. result[MAXN],
for (,.) {
if ( (nread = Readline(sockfd, line, MAXLINE)) == 0)
return./* соединение закрыто другим концом */
/* line задает, сколько байтов следует от叩авлять обратно */
ntowrite = atol(line).
if ( (ntowrite <= 0) 11 (ntowrite > MAXN))
err_quit("c1ient request for Zd bytes", ntowrite):
Writen(sockfd, result, ntowrite),
}
}
Установив соединение с сервером, клиент записывает одну строку, задающую количество байтов, которое сервер должен вернуть. Это отчасти похоже на HTTP: клиент отправляет небольшой запрос, а сервер в ответ отправляет требуемую информацию (часто это файл HTML или изображение G圧).В случае HTTP сервер обычно закрывает соединение после отправки клиенту затребованных данных, хотя более новые версии используют постоянные соединения {persistent connection), оставляя соединения TCP открытыми для лополнительных клиент-
ских запросов. В нашей функции 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. Предварительное создание дочерних процессов сервером
Преимущество этой технологии заключается в том, что обслуживание нового клиента не требует вызова функции fork родительским процессом, тем самым стоимость этого обслуживания понижается. Недостатком же является необходимость угадать, сколько дочерних процессов нужно создать при запуске. Если в некоторый момент времени количество имеющихся дочерних процессов будет равно количеству обслуживаемых клиентов, то дополнительные клиентские запросы будут игнорироваться до того момента, когда освободится какой-либо дочерний процесс. Но, как сказано в разделе 4.5, клиентские запросы в такой ситуации игнорируются не полностью. Для каждого из этих дополнительных клиентов ядро выполнит трехэтапное рукопожатие (при этом общее количество соединений не может превышать значения аргумента backlog функции 1 тsten), и при вызове функции accept установленные соединения будут переданы серверу. При этом, однако, приложение-клиент может заметить некоторое ухудшение в скорости получения ответа, так как, хотя функция connect может быть выполнена сразу же, запрос может не поступать на обработку еще некоторое время.
За счет некоторого дополнительного усложнения кода всегда можно добиться того, что сервер справится со всеми клиентскими запросами. От родительского процесса требуется постоянно отслеживать количество свободных дочерних процессов, и если это количество падает ниже некоторого минимального предела, родитель должен вызвать функцию fork и создать недостающее количество дочерних процессов Аналогично, если количество свободных дочерних процессов превосходит некоторую максимальную величину, некоторые из этих процессов могут быть завершены родителем, так как, согласно табл. 27.2, излишнее количество свободных дочерних процессов также отрицательно влияет на производительность.
Но прежде чем углубляться в детали, исследуем основную структуру этого типа сервера. В листинге 27.6 показана функция main для первой версии нашего сервера с предварительным порождением дочерних процессов.
Листинг 27.6, Функция mam сервера с предварительным порождением дочерних процессов
//server/serv02 с
#include Munp h"
static int nchildren
static pid_t *pids
int
main(int argc char **argv)
{
int listenfd i.
socklen_t addrlen.
void sig_int(int).
pid_t chiTd_make(int int int)
if (argc == 3)
listenfd = Tcp_listen(NULL argv[l] &addrlen)
else if (argc ==一4)
1istenfd = Tcp_listen(argv[l] argv[2]. &addrlen)
else
err_quit( "usage serv02 [ <host> ] <port#> <#children>'*).
nchildren 二 atoi(argv[argc - 1])
pids = Calloc(nchildren sizeof(pid_t))
for (i=0 i < nchildren. i++)
pids[i] = child_make(i listenfd. addrlen) /* возвращение родительского 叩ouecca */
Signal(SIGINT sig_int)
for (.)
pauseO./* дочерние процессы завершились */
}
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 { ~ 28mti
voidpr_cpu_time(void)
/* завершаем все дочерниепроиессн */
for (1=0 1 < nchildreni++)
ki】l(pids[i] SIGTERM).
while (wait(NULL) > 0)/* ждем завершения всех дочерних процессов */
34 35 if (errno i= ECHILD) 36 err_sys("wait error")
37 pr_cpu_time()
38 exit(0T 39 }
30-34 Функция getrusage сообщает об использовании ресурсов всеми дочерними процессами, завершившими свое выполнение, поэтому мы должны завершить все дочерние процессы к моменту вызова функции pr_cpu_time. Для этого дочерним процессам посылается сигнал SIGTERM, после чего мы вызываем функцию wait и ждем завершения выполнения дочерних процессов.
В листинге 27.8 показана функция child_make, вызываемая из функции main для порождения очередного дочернего процесса.
Листинг 27・8. Функция child_make: создание очередного дочернего процесса //server/child02 с 1 include "unp h"
pid_t
chiTd_make(int i mt 1istenfd int addrlen)
Листинг 27.8 (продолжение)
{
pid_t pid,
void child_main(int. int,int).
if ( (pid = ForkO) > 0)
return (pid) /* родительский процесс */
child main(i, 1istenfd. addrlen). /* никогда не завершается */
Ю -
-9 Функция fork создает очередной дочерний процесс и возвращает родителю идентификатор дочернего процесса. Дочерний процесс вызывает функцию chi 1 d_mai n, показанную в листинге 27.9, которая представляет собой бесконечный цикл.
Листинг 27.9. Функция child一main: бесконечный цикл, выполняемый каждым дочерним процессом
//server/child02 с
void
child_main(int i, int 11stenfd int addrlen)
{-
intconnfd,
voidweb_child(int).
sockl ent cTilen,
17 struct sockaddr *cliaddr,
18 cliaddr = Malloc(addrlen),
19 pnntfC'child Zld start!пд\епи, (long) getpidO)
20 for ( .) {
21clilen / addrlen
22connfd = AcceptClistenfd cliaddr &clilen).
23web_chiId(connfd):/* обработка запроса */
24Close(connfd).
25}
26 }
-25 Каждый дочерний процесс вызывает функцию accept, и когда она завершается, функция web chi 1С (см. листинг 27.5) обрабатывает клиентский запрос. Дочерний процесс продолжает выполнение цикла, пока родительский процесс не завершит его.
Реализация 4.4BSD
Если вы никогда ранее не сталкивались с таким типом устройства сервера (несколько процессов, вызывающих функцию accept на одном и том же прослушиваемом сокете), вас, вероятно, удивляет, что это вообще может работать. Пожалуй, здесь уместен краткий экскурс, описывающий реализацию этого механизма в Беркли-ядрах (более подробную информацию вы найдете в [105]).
Родитель сначала создает прослушиваемый сокет, а затем ——дочерние процессы. Напомним, что каждый раз при вызове функции fork происходит копирование всех дескрипторов в каждый дочерний процесс. На рис. 27.2 показана организация структур proc (по одной структуре на процесс), одна структура file для прослушиваемого дескриптора и одна структура socket.
proc{}proc{}proc{}proc{}
Рис. 27.2- Организация структур proc, file и socket
Дескрипторы — это просто индексы в массиве, содержащемся в структуре proc, который ссылается на структуру file. Одна из целей дублирования дескрипторов в дочерних процессах, осуществляемого функцией fork, заключается в том, чтобы данный дескриптор в дочернем процессе ссылался на ту же структуру f 11 е, на которую этот дескриптор ссылается в родительском процессе. Каждая структура fi 1 е содержит счетчик ссылок, который начинается с единицы, когда открывается первый файл или сокет, и увеличивается на единицу при каждом вызове функции fork и при каждом дублировании дескриптора (с помощью функции dup). В нашем примере с N дочерними процессами счетчик ссылок в структуре fi 1 е будет содержать значение N+1 (учитывая родительский процесс, у которого по-прежнему открыт прослушиваемый дескриптор, хотя родительский процесс никогда не вызывает функцию accept).
При запуске программы создается N дочерних процессов, каждый из которых может вызывать функцию accept, и все они переводятся родительским процессом в состояние ожидания [105, с. 458]. Когда от клиента прибывает первый запрос на соединение, все N дочерних процессов «просыпаются», так как все они были переведены в состояние ожидания по одному и тому же «каналу ожидания» — полю so_timeo структуры socket, как совместно использующие один и тот же прослушиваемый дескриптор, указывающий на одну и ту же структуру socket. Хотя «проснулись» все N дочерних процессов, только один из них будет связан с клиентом. Остальные N-1 снова перейдут в состояние ожидания, так как длина очереди клиентских запросов снова станет равна нулю, после того как первый из дочерних процессов займется обработкой поступившего запроса.
Такая ситуация иногда называется thundering herd — более или менее дословный перевод будет звучать как «общая побудка», так как все N процессов должны быть выведены из спящего состояния, хотя нужен всего один процесс, и остальные потом снова «засыпают». Тем не менее этот код работает, хотя и имеет побочный эффект — необходимость «будить» слишком много дочерних процессов каждый раз, когда требуется принять (accept) очередное клиентское соединение. В следующем разделе мы исследуем, как это влияет на производительность в целом.
Эффект наличия слишком большого количества дочерних процессов
В табл 27 1 (строка 2) указано время (1,8), затрачиваемое центральным процессором в случае наличия 15 дочерних процессов, облуживающих не более 10 клиентов. Мы можем оценить эффект «общей побудки», увеличивая количество дочерних процессов и оставляя то же максимальное значение количества обслуживаемых клиентов (10), В табл. 27.2 показаны значения времени, затрачиваемого центральным процессором для данного примера и для двух других примеров, которые обсуждаются далее. Здесь мы обсудим только блокировку функции accept, а значения, приведенные в остальных четырех колонках, рассматриваются в следующих разделах.
Каждый раз при добавлении очередных (неиспользуемых) 15 дочерних процессов мы отмечаем увеличение времени, затрачиваемого центральным процессором Итак, чтобы избежать эффекта «общей побудки», нам следует отказаться от избыточного количества дочерних процессов.
ПРИМЕЧАНИЕ
Некоторые ядра Unix снабжены функцией, часто с именем wakeup one, которая выводит из состояния ожидания только один процесс для обработки одного клиентского
,запроса [91J Ядра BSD/OS не имеют такой функции
Распределение клиентских соединений между дочерними процессами
Следующей темой обсуждения является распределение клиентских соединений между свободными дочерними процессами, блокированными в вызове функции accept. Для получения этой информации мы модифицируем функцию main, размещая в совместно используемой области памяти массив счетчиков, которые представляют собой длинные целые числа (один счетчик на каждый дочерний процесс) Это делается следующим образом.
long *cptr *mete「(int) /* для подсчета количества клиентов на один дочерний процесс */
cptr = meter(nchildren) /* перед порождением дочернего процесса */ В листинге 27 10 показана функция meter.
Листинг 27.10. функция meter; которая размещает массив в совместно используемой памяти
//server/meter с
#include unp h'
#inc1ude<sys/mman h>
/* Размещаем массив 'nchiIdren" длинных целых чисел
* в совместно используемой области памяти
* Эти числа используются как счетчики количества
* клиентов обслуженных данным дочерним процессом.
* см с 467-470 книги [93]"
*/
long *
meter(int nchiIdren)
Ю {
mtfd
long *ptr
#i 旬ef MAP_ANON
ptr = Mmap(0. nchildren * sizeof(long). PROT_READ | PROT_WRITE.
MAP_AN0N I MAPJHARED -1 0)--
#else
fd = Open(M/dev/zero,' O_RDWR 0)
ptr = Mmap(0 nchildren * sizeof(long) PR0T_READ | PROT.WRITE.
MAP一SHARED, fd, 0).-一
Close(fd).
#endif
return (ptr)
}
Мы используем неименованное отображение в память, если оно поддерживается (например, в 4 4BSD), или отображение файла /dev/zero (например, SVR4). Поскольку массив создается функцией гплпар до того, как родительский процесс порождает дочерние, этот массив затем используется совместно родительским и всеми дочерними процессами, созданными функцией fork.
Затем мы модифицируем нашу функцию chi 1 domain (см. листинг 27.9) таким образом, чтобы каждый дочерний процесс увеличивал значение соответствующего счетчика на единицу при завершении функции accept, а после завершения выполнения всех дочерних процессов обработчик сигнала SIGINT выводил бы упомянутый массив счетчиков.
В табл. 27.3 показано распределение нагрузки по дочерним процессам. Когда свободные дочерние процессы блокированы вызовом функции accept, имеющийся в ядре алгоритм планирования равномерно распределяет нагрузку, так что в результате все дочерние процессы обслуживают примерно одинаковое количество клиентских запросов.
Коллизии при вызове функции select
Рассматривая данный пример в 4 4BSD, мы можем исследовать еще одну проблему, которая встречается довольно редко и поэтому часто остается непонятой до конца. В разделе 16 13 [105] говорится о коллизиях {colhstons), возникающих при вызове функции sei ect несколькими процессами на одном и том же дескрипторе, и о том, каким образом ядро решает эту проблему. Суть проблемы в том, что в структуре socket предусмотрено место только для одного идентификатора процесса, который выводится из состояния ожидания по готовности дескриптора. Если же имеется несколько процессов, ожидающих, когда будет готов данный дескриптор, то ядро должно вывести из состояния ожидания все процессы, блокированные в вызове функции select, так как ядро не знает, какие именно процессы ожидают готовности данного дескриптора.
Коллизии при вызове функции sei ect в нашем примере можно форсировать, предваряя вызов функции accept из листинга 27.9 вызовом функции select в ожидании готовности к чтению на прослушиваемом сокете. Дочерние процессы будут теперь блокированы в вызове функции select, а не в вызове функции accept. В листинге 27 11 показана изменяемая часть функции chi 1 d_mam, при этом измененные по отношению к листингу 27.9 строки отмечены знаками
Листинг 27,11. Модификация листинга 27 9 блокирование в вызове select вместо блокирования в вызове accept
pnntf( child Zld starting\en (long) getpidO)
+ FD_ZERO(&rset) for () {
+ FD SETdi stenfd &rset)
+ Select(listenfd+1 &rset NULL NULL NULL)
+ if (FDJSSEKlistenfd &rset) == 0)
+err_quit( listenfd readable )
+
clilen = addrlen
connfd = Accept(listenfd cliaddr &clilen)
web_child(connfd) /* обработка запроса */
Close(connfd)
}
Если, проделав это изменение, мы проверим значение счетчика ядра BSD/OS nselcoll, мы увидим, что в первом случае при запуске сервера произошло 1814 коллизий, а во втором случае — 2045 Так как при каждом запуске сервера два клнен- та создают в сумме 5000 соединений, приведенные выше значения указывают, что примерно в 35-40% случаев вызовы функции select приводят к коллизиям
Если сравнить значения времени, затраченного центральным процессором в этом примере, то получится, что при добавлении вызова функции select это значение увеличивается с 1,8 до 2,9 Частично это объясняется, вероятно, добавлением системного вызова (так как теперь мы вызываем не только accept, но еще и select), а частично — накладными расходами, связанными с коллизиями
Из этого примера следует вывод, что, когда несколько процессов блокируются на одном и том же дескрипторе, лучше, чтобы эта блокировка была связана с функцией accept, а не с функцией select
27.7. Сервер TCP с предварительным порождением процессов и защитой вызова accept блокировкой файла
Описанная выше реализация, позволяющая нескольким процессам вызывать функцию accept на одном и том же прослушиваемом дескрипторе, возможна только для систем 4 4BSD, в которых функция accept реализована внутри ядра Ядра системы SVR4, в которых accept реализована как библиотечная функция, не допускают этого В самом деле, если мы запустим сервер из предыдущего раздела, в котором имеется несколько дочерних процессов, в Solans 2 5 (система SVR4), то вскоре после того, как клиенты начнут соединяться с сервером, вызов функции accept в одном из дочерних процессов вызовет ошибку EPROTO, что свидетельствует об ошибке протокола
ПРИМЕЧАНИЕ
Причины возникновения этой проблемы с библиотечной версией функции accept в SVR4 связаны с реализацией потоков и тем фактом что библиотечная функция accept не является атомарной операцией В Solans 2 6 эта проблема решена, но в большинстве реализаций SVR4 она остается
Решением этой проблемы является защита вызова функции accept при помощи блокировки (lock), так что в данный момент времени только один процесс может быть блокирован в вызове этой функции Другие процессы также будут блокированы, так как они будут стремиться установить блокировку для вызова функции accept
Существует несколько способов реализации защиты вызова функции accept, о которых рассказывается во втором томе1 данной серии В этом разделе мы используем блокировку файла функцией fcntl согласно стандарту Posix
Единственным изменением в функции main (см листинг 27 6) будет добавление вызова функции my_l ock_imt перед началом цикла, в котором создаются дочерние процессы + my_lock_init( /tmp/lock ХХХХХХ ) /* один файл для всех дочерних процессов */ for (1=0 1 < nchildren i++)
pids[i] = child_make(i 1istenfd addrlen) /* возвращение родительского процесса */
Функция childjnake остается такой же, как в листинге 27 8 Единственным изменением функции chi 1 d_mam (см листинг 27 9) является блокирование перед вызовом функции accept и снятие блокировки после завершения этой функции for () {
clilen = addrlen +my_lock_wait()
connfd =~Accept(listenfd cliaddr &clilen) +my_l ock_rel ease()
web_chiId(connfd)/* обработка запроса */
Close(connfd)
В листинге 27 12 показана только функция my_lock_imt, в которой используется блокировка файла согласно стандарту Posix
Листинг 27.12« Функция myjockjnit блокировка файла согласно стандарту
Posix 1--
//server/1 ock_fcntl с
include unp h
static struct flock lock_it unlock_it
static int lock_fd = 1 ~
/* fcntl () не выполнится если не будет вызвана функция fl^^lt^Q'InltC) */
void
niy_lock_imt(char *pathname)
~ 一
char lock_file[1024]
/* нужно скопировать строку вызывающего проиесса на случай< еелм ат© HOHGTdW'V
strncpy(lock_file pathname sizeof(lock_file))
Mktemp(1ock_fi1e)
lock_fd = Opendockjile O_CREAT | O_WRONLY FILE_MODE)
Unlink(lock_file)/* но iock_fd остается открытые */
lockjt l_type = F—WRLCKпродолжение 少
Листинг 27.12 (продолжение)
lock_itl_whence =SEEK_SET;
lockjitl_start = 0.
lock_itl_len = 0.
unlock_it l_type = F_UNLCK,
unlock_it l_whence = SEEK_SET;
unlock一it l_start = 0.
unlock_it l_len = 0.
}一—
-13 Вызывающий процесс задает шаблон для имени файла в качестве аргумента функции my_lock_mit, и функция mktemp на основе этого шаблона создает уникальное имя файла. Затем создается файл с этим именем и сразу же вызывается функция unlink, в результате чего имя файла удаляется из каталога. Если в программе впоследствии произойдет сбой, то файл исчезнет безвозвратно. Но пока он остается открытым в одном или нескольких процессах (иными словами, пока счетчик ссылок для этого файла больше нуля), сам файл не будет удален. (Отметим, что между удалением имени файла из каталога и закрытием открытого файла существует фундаментальная разница.)
4-21 Инициализируются две структуры fl ock: одна для блокирования файла, другая для снятия блокировки. Блокируемый диапазон начинается с нуля (]_whence= SEEK SET, l_std「t=0). Значение 1_1 еп равно нулю, то есть блокирован весь файл, В этот файл ничего не записывается (его длина всегда равна нулю), но такой тип блокировки в любом случае будет правильно обрабатываться ядром.
ПРИМЕЧАНИЕ
Сначала автор инициализировал эти структуры при объявлении:
static struct flock lock_it = { F_WRLCK, 0. 0. 0. 0 }.
static struct flock unlock_it = { F_UNLCK, 0 0. 0 0 }.
но тут возникли две проблемы: у нас нет гарантии, что константа SEEK SET равна нулю, но, что более важно, стандарт Posix не регламентирует порядок расположения полей этой структуры. В Solans и Digital Unix поле I type всегда расположено первым, но в случае BSD/OS это не так. Posix гарантирует только то, что требуемые поля присутствуют в структуре. Posix не гарантирует какого-либо порядка следования полей структуры, а также допускает наличие в ней полей, не относящихся к стандарту Posix. Поэтому когда требуется инициализировать эту структуру (если только не нужно инициализировать все поля нулями), это приходится делать через фактический код С, а не с помощью инициализатора при объявлении структуры.
Исключением из этого правила является ситуация, когда инициализатор структуры обеспечивается реализацией. Например, при инициализации взаимного исключения в Posix в главе 23 мы писали:
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER.
Тип данных pthread_mutex_t — это некая структура, но инициализатор предоставляется реализацией и может быть различным для разных реализаций.
В листинге 27.13 показаны две функции, которые устанавливают и снимают блокировку с файла. Они представляют собой вызовы функции fcntl, использующие структуры, инициализированные в листинге 27.12.
Листинг 27.13. функции my_lock_wait (установление блокировки файла) и myjock release (снятие блокировки файла)
//server/1ock_fcntl с
void
my_lock wait О
{一 一
int rc.
while ( (rc = fcntl (lock_fd, F_SETLKW. &lock」t )) < 0 {
if (errno == EINTR)
continue.
else
errsys ("fcntl error for щу lock wait").
}——
}
void
my_lock_release ()
{-~
if (fcntl (1 ock_fd F SETLKW &unlock_it )) < 0)
errsys ("fcntT error for my_lock_reTease').
}一 —
Новая версия нашего сервера с предварительным порождением процессов работает теперь под SVR4, гарантируя, что в данный момент времени только один дочерний процесс блокирован в вызове функции accept. Сравнивая строки 2 и 3 в табл. 27.1 (результаты для серверов Digital Unix и BSD/OS), мы видим, что такой тип блокировки увеличивает время, затрачиваемое центральным процессором на узле сервера.
ПРИМЕЧАНИЕ
В выпуске 1.1 wcb-ссрвера Apache (http7/w\wv apache org) использована технология предварительного порождения процессов, причем если позволяет реализация, все дочерние процессы блокируются в вызове функции accept, иначе используется блокировка файла для защиты вызова accept
Эффект наличия слишком большого количества дочерних процессов
Мы можем проверить, возникает ли в данной версии сервера эффект «обшей побудки», рассмотренный в предыдущем разделе. В табл. 27.2 показаны результаты излишнего увеличения количества дочерних процессов. В колонке, отведенной под данную версию сервера в системе Solaris, мы видим, что имеет смысл увеличивать количество дочерних процессов до 75, так как при следующем увеличении (до 90) происходит нечто, приводящее к значительному увеличению времени центрального процессора. Возможно, причина в том, что система исчерпывает свои ресурсы памяти из-за слишком большого количества дочерних процессов и начинается интенсивная работа с файлом подкачки (swapping).
Распределение клиентских соединений между дочерними процессами
Используя функцию, показанную в листинге 27.10, мы можем исследовать распределение клиентских запросов между свободными дочерними процес-
сами. Результат показан в табл. 27.3. Для всех трех систем оказывается, что все клиентские запросы распределяются в пуле дочерних процессов равномерно.
Сервер TCP с предварительным порождением процессов и защитой вызова accept при помощи взаимного исключения
Как мы уже говорили, существует несколько способов синхронизации процессов путем блокирования. Блокировка файла по стандарту Posix, рассмотренная в предыдущем разделе, переносится на все Posix-совместимые системы, но она подразумевает некоторые операции с файловой системой, которые могут потребовать времени. В этом разделе мы будем использовать блокировку при помощи взаимного исключения, обладающую тем преимуществом, что ее можно применять для синхронизации не только потоков внутри одного процесса, но и потоков, относящихся к различным процессам.
Функция main остается такой же, как и в предыдущем разделе, то же относится к функциям child_make и child_niam. Меняются только три функции, осуществляющие блокировку. Чтобы использовать взаимное исключение между различными процессами, во-первых, требуется хранить это взаимное исключение в разделяемой процессами области памяти, а во-вторых, библиотека потоков должна получить указание о том, что взаимное исключение совместно используется различными процессами.
ПРИМЕЧАНИЕ
Требуется также, чтобы библиотека потоков поддерживала атрибут PTHREAD_PRO- CESS_SHARED Например, в Digital Unix 4 0b этот а грибут не поддержи вается, так что мы нс можем запустить этот сервер в данной операционной системе.
Существует несколько способов разделения памяти между различными процессами, что мы подробно описываем во втором томе1 данной серии. В этом примере мы используем функцию mmap с устройством /dev/zero, которое работает с ядрами Solaris и другими ядрами SVR4. В листинге 27.14 показана только функция my_l ock_imt.
Листинг 27.14. Функция myjockjnit: использование взаимного исключения потоками, относящимися к различным процессам (технология Pthread)
//server/lock_pthread с
include unpthread h
^include <sys/mman h>
static pthread_mutex_t *mptr /* фактически взаимное иск^оченме ..Ovflej. в "$qbместно используемой памяти */
void
my Jock initCchar *pathname)
-
int fd,
pthread_mutexattr_t mattr,
fd = OpenC/dev/zero". O_RDWR. 0):
mptr * Mmap(0. sizeof(pthread__mutex_t) PR0T_READ | PROT WRITE,
MAP SHARED, fd, 0)—』-
Close(fd).
Pthread_mutexattr_in7t(&iTiattr)
Pthread_mutexattr_setpshared(&mattr PTHREAD_PROCESS_SHARED)
Pthread_mutex init(mptr &mattr)
} 一一
9-12 Мы открываем (open) файл /dev/zero, а затем вызываем mmap. Количество байтов (второй аргумент этой функции) — это размер переменной pthread_mutex_t. Затем дескриптор закрывается, но для нас это пе имеет значения, так как файл уже отображен в память.
13-15 В приведенных ранее примерах взаимных исключений Pthread мы инициализировали глобальные статические взаимные исключения, используя константу PTHREAD_MUTEX_INITIALIZER (см., например, листинг 27.13). Но располагая взаимное исключение в совместно используемой памяти, мы должны вызвать некоторые библиотечные функции Pthread, чтобы сообщить библиотеке о наличии семафора в совместно используемой памяти и о том, что он будет применяться для синхронизации потоков, относящихся к различным процессам. Мы должны инициализировать структуру pth「edd_mutexatt「_t задаваемыми по умолчанию атрибутами взаимного исключения, а затем установить значение атрибута PTHREAD_PROCESS_ SHARED. (По умолчанию значением этого атрибута должно быть PTHREAD_PROCESS_ PRIVATE, что подразумевает использование взаимного исключения только в пределах одного процесса.) Затем вызов pthread_mutех_чn11 инипиализпрует взаимное исключение указанными атрибутами.
В листинге 27.15 показаны только функции my_l ock_wait и ггу_1 ock_re] ease. Они содержат вызовы функций Pthread, предназначенных для блокирования и разблокирования взаимного исключения.
Листинг 27.15. Функции my_lock_wait и myjock^release: использование блокировок Pthread
//server/lock_pthread с
void
my_lock_wait()
{一 一
Pthread_mutex_lock(mptr);
}-~
void
my lock releaseC)
{ "~
Pthread_mutex_unlock(mptr)
} 一 一
Сравнивая строки Зи4 табл. 27 1 для сервера Solaris, можно заметить, что версия, использующая синхронизацию процессов при помощи взаимного исключения, характеризуется более высоким быстродействием, чем версия с блокировкой файла
Сервер TCP с предварительным порождением процессов: передача дескриптора
Последней модификацией нашего сервера с предварительным порождением процессов является версия, в которой только родительский процесс вызывает функцию accept, а затем «передает» присоединенный сокет какому-либо одному дочернему процессу. Это помогает обойти необходимость защиты вызова accept, но требует некоторого способа передачи дескриптора между родительским и дочерним процессами. Эта техника также несколько усложняет код, поскольку родительскому процессу приходится отслеживать, какие из дочерних процессов заняты, а какие свободны, чтобы передавать дескриптор только свободным дочерним процессам.
В предыдущих примерах сервера с предварительным порождением процессов родительскому процессу не приходилось беспокоиться о том, какой дочерний процесс принимает соединение с клиентом. Этим занималась операционная система, организуя вызов функции accept од1пш из свободных дочерних процессов или блокировку файла или взаимного исключения. Из первых пяти строк табл. 27.3 видно, что все три рассматриваемые нами операционные системы осуществляют равномерную циклическую загрузку свободных процессов клиентскими соединениями.
В данном примере для каждого дочернего процесса нам нужна некая структура, содержащая информацию о нем. Заголовочный файл child.h, в котором определяется структура Child, показан в листинге 27.16.
2
pid_t
child_pid
/* ID пооцесса */
3
1nt
child^pipefd;
/* программный (неименованный) канал иежду родительским
и дочерним процессами */
4
wt
child_status;
/★ 0 = готово */
5
long
child_count.
/* количество обрабатываемых соединений */
6
} Child.
7
Child *cptr.
/* массив структур Child */
Листинг 27.16. Структура Child
//server/child h
1 typedef struct {
Мы записываем идентификатор дочернего процесса, дескриптор программного канала (pipe) родительского процесса, связанного с дочерним, статус дочернего процесса и количество обрабатываемых дочерним процессом клиентских соединений. Это количество выводится обработчиком сигнала SIGI NT и позволяет нам отслеживать распределение клиентских запросов между дочерними процессами.
Рассмотрим сначала функцию child_make, которая приведена в листинге 27.17. Мы создаем канал и доменный сокет Unix (см. главу 14) перед вызовом функции fork. После того как создан дочерний процесс, родительский процесс закрывает один дескриптор (sockfd[l]), а дочерний процесс закрывает другой дескриптор (sockfd[0]). Более того, дочерний процесс подключает свой дескриптор канала (sockfd[l]) к стандартному потоку сообщений об ошибках, так что каждый до-
черний процесс просто использует это устройство для связи с родительским процессом. Этот механизм проиллюстрирован схемой, приведенной на рис. 27.3.
Листинг 27.17. Функция child一make: передача дескриптора в сервере с предварительным порождением дочерних процессов
//server/child05 с
#include"unp h"
^include"child h"
pid_t
child make(int i. int 1istenfd, int addrlen)
{-
intsockfd[2].
pid_tpid,
void child^mamCwt. int, int).
Socketpair(AF_LOCAL. SOCK_STREAM 0, sockfd):
1011121314151617181920212210111213141516171819202122if ( (pid = ForkO) > 0) { Close(sockfd[l]). cptr[i] child_pid = pid cptr[i] child「pipefd = sockfd[0], cptr[i] child_status = 0.return (pid)./*}Dup2(sockfd[l]. STDERR_FILENO)./*Close(sockfd[OJ).Close(sockfd[l]).Closed! stenfd)./*if ( (pid = ForkO) > 0) { Close(sockfd[l]). cptr[i] child_pid = pid cptr[i] child「pipefd = sockfd[0], cptr[i] child_status = 0.return (pid)./*}Dup2(sockfd[l]. STDERR_FILENO)./*Close(sockfd[OJ).Close(sockfd[l]).Closed! stenfd)./*родительский процесс */канал от дочернего процесса к родительское */родительский процесс */канал от дочернего процесса к родительское */}}child_(nair)(i 11stenfd. addrlen). /*child_(nair)(i 11stenfd. addrlen). /*дочернему проиессу не требуется. чтобы он был открыт */никогда не завершается */дочернему проиессу не требуется. чтобы он был открыт */никогда не завершается */
10
11
12
13
14
15
16
17
18
19
20
21
22
10
11
12
13
14
15
16
17
18
19
20
21
22
if ( (pid = ForkO) > 0) { Close(sockfd[l]). cptr[i] child_pid = pid cptr[i] child「pipefd = sockfd[0], cptr[i] child_status = 0.
return (pid)./*
}
Dup2(sockfd[l]. STDERR_FILENO)./*
Close(sockfd[OJ).
Close(sockfd[l]).
Closed! stenfd)./*
if ( (pid = ForkO) > 0) { Close(sockfd[l]). cptr[i] child_pid = pid cptr[i] child「pipefd = sockfd[0], cptr[i] child_status = 0.
return (pid)./*
}
Dup2(sockfd[l]. STDERR_FILENO)./*
Close(sockfd[OJ).
Close(sockfd[l]).
Closed! stenfd)./*
родительский процесс */
канал от дочернего процесса к родительское */
родительский процесс */
канал от дочернего процесса к родительское */
}
}
child_(nair)(i 11stenfd. addrlen). /*
child_(nair)(i 11stenfd. addrlen). /*
дочернему проиессу не требуется. чтобы он был открыт */
никогда не завершается */
дочернему проиессу не требуется. чтобы он был открыт */
никогда не завершается */
РодительскийДочерний
процесспроцесс 1
,・/ Потоковый канал 、、—痴с
stderr ,YA,sockfd[O]
Рис. 27.3. Канал после того, как дочерний и родительский процесс закрыли один конец
После создания всех дочерних процессов мы получаем схему, показанную на рис. 27.4. Мы закрываем прослушиваемый сокет в каждом дочернем процессе, поскольку только родительский процесс вызывает функцию accept. Мы показываем на рисунке, что родительский процесс должен обрабатывать прослушиваемый сокет, а также все доменные сокеты. Как можно догадаться, родительский процесс использует функцию sei ect для мультиплексирования всех дескрипторов.
В листинге 27.18 показана функция main. В отличие от предыдущих версий этой функции, в данном случае в памяти размещаются все наборы дескрипторов и в каждом наборе включены все биты, соответствующие прослушиваемому сокету и каналу каждого дочернего процесса. Вычисляется также максимальное значение дескриптора и выделяется память для массива структур Child. Основной цикл запускается при вызове функции select.
Рис. 27.4. Каналы после создания всех дочерних процессов
Листинг 27.18. Функция main, использующая передачу дескриптора
//server/serv05 с
include"unp h"
include"child h"
static mt ncmldren.
4 int
5
main(mt argc. char **argv)
6
{
7
int
listenfd, 1 navai 1
maxfd. nsel, connfd. rc
8
void
sig_int(int)
9
pid_t
chiTd_make(int int.
int)
10
ssize_t
n
И
fd_set
rset masterset.
12
socklen
_t addrlen, clilen.
13
struct
sockaddr "cliaddr.
if (argc == 3)
listenfd = Tcp_listen(NULL argv[l]. &addrlen).
else if (argc == 4)
listenfd = Tcp_listen(argv[l], argv[2] &addrlen),
else
19errequite*usage serv05 [ <host> ] <port#> <#children>”).
FD_ZERO(Smasterset)
FD_SET(listenfd. tasterset),
maxfd = listenfd.
cliaddr = MaHoc(addrlen)
nchildren = atoi(argv[argc - 1]).
navai1 = nchildren.
eptr = Cal1oc(nchildren sizeof(Child)):
27/* предварительное создание дочерних
28 for (i =0. i < nchildren, i++) {
29 child_make(i, listenfd. addrlen).
процессов */
/* родительский процесс завершается */
46474849504647484950515253545556575859606162636465666768515253545556575859606162636465666768
46
47
48
49
50
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
30FD_SET(cptr[i] childjnpefd. &masterset),
31maxfd = max(maxfd cptr[i] child_pipefd),
32}~
33 Signal(SIGINT sig_int).
34 for (..) {
35rset = masterset.
36if (navai1 <= 0)
37 FD_CLR(】7Stenfd. &rset) /* выключаем если нет свободных дочерних процессов */
38 nsel = Select(maxfd 宀 1. &rset NULL NULL. NULL).
39/* проверка новых соединений */
40if (FD_ISSET(l]Stenfd, &rset)) {
41chlen = addrlen.
42connfd = Accept(listenfd cliaddr, &clilen).
43 for (i = 0 i < nchildren 】++)
44if (cptr[i] child_status = 0)
45break./* свободный */
if (1 == nchildren)
err_quit("no available children").
cptr[T] child_status = 1/* отмечаем этот дочерний процесс как занятый *У
cptr[i] child_count++
navai1--
п = Write__fd(cptr[i] child_pipefd, "" 1 connfd). Close(connfd) if (--nsel == 0)
continue. /* с результатами selectO закончено */ )
/* поиск освободившихся дочерних процессов */
for (1 = 0. 1 < nchildren. i++) {
if (FD_ISSET(cptr[i] ch71d_pipefd &rset)) {
if「(ri = Read(cptr[i] child_pipefd &rc 1)) == 0) err_qint("child 紀 terminated unexpected!у,r i)
cptr[i] chi]d_status = 0,
navai1++
if (--nsel == 0)
break. /* с результатами selectO закончено */ }
Отключение прослушиваемого сокета в случае отсутствия свободных дочерних процессов
36-37 Счетчик navai 1 отслеживает количество свободных дочерних процессов. Если его значение становится равным нулю, прослушиваемый сокет в наборе дескрипторов функции select выключается. Это предотвращает прием нового соединения в тот момент, когда нет ни одного свободного дочернего процесса. Ядро по-прежнему устанавливает эти соединения в очередь, пока их количество не превысит значения аргумента back] од функции 1ч sten, заданного для прослушиваемого сокета, но мы пе хотим их принимать, пока у нас не появится свободный дочерний процесс, готовый обрабатывать клиентский запрос.
Прием нового соединения
39-55 Если прослушиваемый сокет готов для считывания, можно принимать (accept) новое соединение. Мы находим первый свободный дочерний процесс и передаем ему присоединенный сокет с помощью функции w门 te_fd, приведенной в листинге 14.11. Вместе с дескриптором мы передаем 1 байт, но получатель ие интересуется содержимым этого байта. Родитель закрывает присоединенный сокет.
Мы всегда начинаем поиск свободного дочернего процесса с первого элемен- га массива структур Child. Это означает, что новое соединение для обработки поступившего клиентского запроса всегда получает первый элемент этого массива. Этот факт мы проверим при рассмотрении табл. 27.3 и значения счетчика child_ count после завершения работы сервера. Если мы не хотим оказывать такое предпочтение первому элементу массива, мы можем запомнить, какой дочерний процесс получил последнее клиентское соединение, и каждый раз начинать поиск свободного дочернего процесса со следующего за ним, а по достижении конца массива переходить снова к первому элементу. В этом нет особого смысла (иа самом деле все равно, какой дочерний процесс обрабатывает очередное соединение, если имеется несколько свободных дочерних процессов), если только планировочный алгоритм операционной системы не накладывает санкций на процессы, которые требуют относительно больших временных затрат центрального процессора.
Обработка вновь освободившихся дочерних процессов
56-66 Когда дочерний процесс заканчивает обработку клиентского запроса, наша функция child_mam записывает одни байт в канал для родительского процесса. Тем самым родительский конец канала становится доступным для чтения. Упомянутый байт считывается (по его значение при этом игнорируется), а дочерний процесс помечается как свободный. Если же дочерний процесс завершит свое выполнение неожиданно, его конец канала будет закрыт, а операция чтения (read) возвратит нулевое значение. Это значение перехватывается и дочерний процесс завершается, но более удачным решением было бы записать ошибку и создать новый дочерний процесс для замены завершенного.
Функция chi 1 d_mai п показана в листинге 27.19.
Листинг 27.19. Функция child_main: передача дескриптора в сервере с предварительным порождением дочерних процессов //server/child05 с 23 void
24 child ma?n(?nt i. int listenfd int addrlen)
25 {"
charc.
intconnfd,
ssize_t n.
voidweb_child(int),
pnntfCchiTd Zld starting\en". (long) getpidO)
for ( ,) {
32 if ( (n = Read_fd(STDERR_FILENO. &c 1 &connfd)) == 0)
err_quit("read_fd returned 0").
if (connfd < 0) ~
err_quit(Mno descriptor from read fd").
web_chiId(connfd),/* обработка запроса */
Close(connfd).
Write(STDERR_FILENO "". 1)./* сообщаем родительскому 叩oueccy о том.
что дочерний освободился */
}
}
Ожидание дескриптора от родительского процесса
32-33 Эга функция отличается от аналогичных функций из двух предыдущих разделов, так как дочерний процесс не вызывает более функцию accept. Вместо этого дочерний процесс блокируется в вызове функции read_fd, ожидая, когда родительский процесс передаст ему дескриптор присоединенного сокета.
Сообщение родительскому процессу о готовности дочернего к приему новых запросов
38 Закончив обработку очередного клиешского запроса, мы записываем (write) 1 байт в канал, чтобы сообщить» что данный дочерний процесс освободился.
В табл. 27.1 при сравнении строк 4 и 5 для сервера Solaris мы видим, что данный сервер медленнее, чем версия, рассмотренная нами в предыдущем разделе, которая использовала блокировку потоками взаимного исключения. Сравнивая строки 3 и 5 для серверов Digital Unix и BSD/OS, мы приходим к тому же заключению: передача дескриптора по каналу от родительского процесса к дочернему и запись одного ба^та в канал для сообщения родительскому процессу о завершении обработки клиентского запроса занимает больше времени, чем блокирование II разблокирование взаимного исключения или файла.
В табл. 27.3 показаны знаисинясчетчиков chi ld_count из структуры Child, которые выводятся обработчиком сигнала SIGINT по завершении работы сервера. Дочерние процессы, расположенные ближе к началу массива, обрабатывают большее количество клиентских запросов, как было указано при обсуждении листинга 27.18.
Параллельный сервер TCP: один поток для каждого клиента
Предыдущие пять разделов были посвящены рассмотрению серверов, в которых для обработки клиентских запросов используются дочерние процессы, либо заранее порождаемые с помощью функции fork, либо требующие вызова этой функции для каждого вновь поступившего клиентского запроса. Если же сервер поддерживает потоки, мы можем применить потоки вместо дочерних процессов.
Наша первая версия сервера с использованием потоков показана в листинге 27.20. Это модификация листинга 27.2: в ней создается один поток для каждого клиента вместо одного дочернего процесса для каждого клиента. Эта версия во многом похожа на сервер, представленный в листинге 23.2.
Листинг 27.20. функция mam для сервера TCR использующего потоки /Zserver/serv06 с
2 int2 int#include"unpthread h"
2 int
2 int
продолжение 按
Листинг 27.20 (продолжение)
3
main(int
argc. char **argv)
4
{
5
int
1istenfd. connfd.
6
void
sig_int(int)
7
void
*doit(void *).
8pthread_t tid,
9socklen t clilen. addrlen:
10 struct sockaddr *cliaddr.
if (argc == 2)
listenfd = Tcp_listen(NULL, argv[l], &addrlen).
else if (argc == 3)
listenfd = Tcp_listen(argv[l] argv[2]. &addrlen):
else
16err_quit("usage serv06 [ <host> ] <po「t#>“),
cliaddr = Malloc(addrlen),
Signal(SIGINT, sig_int)
for (. ) {
clilen = addrlen.
21 connfd = Acc叩t(listenfd. cliaddr. &clilen),
22Pthread_create(&tid NULL &doit (void *) connfd).
23}-
24 }
25 void *
26 doit(void *arg)
27 {
28 void web_child(int)
29Pthread_detach(ptnread_self());
web_chiTd((mt) arg).
Close((int) arg).
return (NULL).
}
Цикл основного потока
19-23 Основной поток блокируется в вызове функции accept, п каждый раз, когда прибывает новое клиентское соединение, функцией pthread_create создается новый поток. Функция, выполняемая новым потоком, — это функция doit, а ее ар- 1ументом является присоединенный сокет.
Функция прочих потоков
?5-33 Функция doit выполняется как неприсоединенный поток, так что основному потоку не приходится ждать ее завершения. Doit вызывает функцию web_child (см. листинг 27.5). Когда эта функция возвращает упправление, присоединенный сокет закрывается.
В табл. 27.1 мы видим, что эта простая версия с использованием потоков является более быстродействующей, чем даже самая быстрая из версий с предварительным порождением процессов — как для Solaris, так и для Digital Unix. Кроме того, эта версия, в которой каждый клиент обслуживается одним потоком, во много
раз быстрее версии, в которой каждый клиент обслуживается специально созданным для него дочерним процессом (первая строка табл. 27.1).
ПРИМЕЧАНИЕ
В разделе 23.5 мы упомянули о трех вариантах преобразования функции, которая не является безопасной в многопоточной среде, в функцию, обсспечивающую требуемую безопасность. Функция wcb_child вызывает функцию readlinc, и версия, показанная в листинге 3.11, не является безопасной в многопоточной среде. На примере, приведенном в листинге 27.20, были испробованы вторая и грет ья альтернативы из раздела 23.5. Увеличение быстродействия при переходе от альтернативы 3 к альтернативе 2 составило менее одного процент а, вероятно, потому, что функция readlinc использовалась лишь для считывания значения счетчика (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
typedef struct {
pthread_t thread_tid:/* идентификатор потока */
long thread_count./* количество обработанных запросов */
} Thread. 一
Thread *tptr:/* массив структур Thread */
mt listenfd, nthreads ;
socklen_t addrlen.
pthread_mutex_t mlock;
Мы также объявляем несколько глобальных переменных, таких как дескриптор прослушиваемого сокета и взаимное исключение, Которые должны совместно использоваться всеми потоками.
В листинге 27.22 показана функция main.
Листинг 27.22. Функция main для сервера TCP с предварительным порождением потоков
//server/serv07 с
findude"unpthread h"
findudeMpthread07 h"
pthread_mutex_t mlock = PTHREAD_MUTEX_INI7IALIZER.
int
main(int argc. char **argv)
{
inti.
voidsig_int(int). thread_make(mt),
if (argc == 3)
1istenfd = Tcp_listen(NULL, argv[l], &addrlen).
else if (argc == 4)
11stenfd = rcp_listen(argv[l], argv[2], &addrlen);
13 else
14 err_quit("usage serv07 [ <host> ] <port#> 〈批hreads>").
15 nthreads = atoi(argv[argc - 1])
16 tptr = Cal1oc(nthreads. sizeof(Thredd)).
17 for (i = 0. i < nthreads. i++)
thread_make(i)./* завершается только основной поток */
Signal(SIGINT. sig_mt).
for (..)
pauseO./* потоки все выполнили */
}
Функции thread__make и threadjnain показаны в листинге 27.23.
Листинг 27.23. Функции thread_make и thread_main
//server/pthread07 с
include,,unpthread.ht,
include"pthread07 h”
void
thread_make(int i)
{ 一
void *thread__main(void *).
Pthread_create(&tptr[i] thread_tid. NULL, &threadjnain, (void *) i):
return./* завершается основной поток */
}
void *
thread__mai nCvoid *arg)
{ 一
intconnfd,
voidweb_child(int),
socklen_t cTilen,
struct sockaddr *cliaddr.
cliaddr = Malloc(addrlen).
printf("thread %d start!ng\en". (int) arg):
19 for (,.) {
20clilen = addrl еп
21Pthread_mutexJ ock(&mlock)
22connfd = Accept(1 istenfd, cliaddr. Schlen).
23Pthread_mutex_unlock(&mlock).
24tptr[(mt) arg] thread_count++.
25web_chiId(connfd). /* обработка запроса */
26Close(connfd).
27}
28 }
Создание потоков
Создаются потоки, каждый из которых выполняет функцию pthread main. Единственным аргументом этой функции является порядковый номер потока.
21-23 Функция thread_mai п вызывает функции pthread_mutex_l ock и pthread_mutex_unl ock соответственно до и после вызова функции accept.
Сравнивая строки би 7в табл. 27.1, можно заметить, что эта последняя версия нашего сервера быстрее, чем версия с созданием нового потока для каждого клиентского запроса как для Solaris, так и для Digital Unix. Этого можно было соки- дать, так как в данной версии мы сразу создаем пул потоков п не тратим время на создание новых потоков по мере поступления клиентских запросов. На самом деле эта версия сервера — самая быстродействующая для обеих систем.
В табл. 27.3 показано распределение значений счетчика th「ead_count структуры Thread, которые мы выводим с помощью обработчика сигнала SIGINT по завершении работы сервера. Равномерность этого распределения объясняется тем, что при выборе потока, который будет блокировать взаимное исключение, алгоритм планирования загрузки потоков последовательно перебирает все потоки в цикле.
ПРИМЕЧАНИЕ
В Беркли-ядрах (например, Digital Unix) нам не нужна блокировка при вызове функции accept, так что мы можем использовать версию, представленную в листинге 27.23, без взаимных исключений. Но в результате этого время, затрачиваемое центральным процессором, увеличится с 3,5 секунды (строка 7 в табл. 27.1) до 3,9 секунды. Если рассмотреть два компонента, из которых складывается время централыюх о процессора — иол ьзоватсльское и системное время. — то окажется, что первый комiioiichtуменьв!ается при отсутствии блокировки (поскольку блокирование осуществляется в библиотеке потоков, входящей в пользовательское нространсгво), но системное время возрастает (за счет эффекта «общей побудки», возникающего, когда все потоки, блокированные в вызове функции accept, выходят из состояния ожидания при появлении нового клиентского соединения). Для того чтобы каждое соединение передавалось только одному потоку, необходима некая разновидность взаимного исключения, и оказывается, что быстрее это делают сами потоки, а не ядро.
7.12. Сервер с предварительным порождением потоков: основной поток вызывает функцию accept
Последняя рассматриваемая нами версия сервера устроена следующим образом: главньп! поток создает пул потоков при запуске сервера, после чего он же вызывает функцию accept и передает каждое клиентское соединение какому-либо из
свободных на данный момент потоков. Это аналогично передаче дескриптора в версии, рассмотренной нами в разделе 27.9.
При таком устройстве сервера необходимо решить, каким именно образом должна осуществляться передача присоединенного дескриптора одному из потоков в пуле. Существует несколько способов решения этой задачи. Можно, как н прежде, использовать передачу дескриптора, но при этом не требуется передавать дескриптор от одного потока к другому, так как все они, в том числе и главный поток, принадлежат одному и тому же процессу. Все, что требуется знать потоку, получающему дескриптор, —- это номер дескриптора. В листинге 27.24 показан заголовочный файл pth「ead08 h, определяющий структуру Thread, аналогичный файлу, показанному в листинге 27.21.
Листинг 27.24. Заголовочный файл pthreadO8.h //server/pthread08 h
typedef struct {
/* идентификатор потока *//* количество обработанных запросов *//* массив структур Thread */iput./* идентификатор потока *//* количество обработанных запросов *//* массив структур Thread */iput.pthread_t thread_tid.
/* идентификатор потока */
/* количество обработанных запросов */
/* массив структур Thread */
iput.
/* идентификатор потока */
/* количество обработанных запросов */
/* массив структур Thread */
iput.
1ong ~thread_count,
} Thread
Thread *tptr
#define MAXNCLI 32
mt clifd[MAXNCLI] iget.
pthread_mutex_t clifd_niutex.
pthread^cond^t clifd_cond.
Определение массива для записи дескрипторов присоединенных сокетов
6-9 Мы определяем массив cli fd, в который главный поток записывает дескрипторы присоединенных сокетов. Свободные потоки из пула получают по одному дескриптору из этого массива и обрабатывают соответствующий запрос.】put — это индекс в данном массиве для очередного элемента, записываемого в иего главным потоком, a iget — это индекс очередного элемента массива, передаваемого свободному потоку для обработки. Разумеется, эта структура данных, совместно используемая всеми потоками, должна быть защищена, и поэтому мы используем условную переменную и взаимное исключение.
В листинге 27,25 показана функция main.
Листинг 27.25. Функция mam для сервера с предварительным порождением потоков
//server/serv08 с
include"unpthread h”
#i nclude"pthredd08 h“
static int nthreads.
pthread_mutex_t clifd_mutex = PTHREAD_MUTEX_INITIALIZER,
pthread^condj clifd_cond = PTHREAD_COND_INITIALIZER;
int
main(int argc char **argv)
{
inti. listenfd. connfd
voidsig_int(int) thread_make(int).
socklen_t addrlen. clilen
struct sockdd" *cliaddr.
if (argc == 3)
listenfd 二 Tcp_listen(NULL argv[l] Addrlen).
else 】f (argc =一4)
listenfd = Tcpjisten(argv[l], argv[2]. &addrlen).
else一
18 err_quit("usage serv08 [ <host> ] <port#> <#threads>"):
cliaddr = Mal1oc(addrlen).
nthreads = atoi(argv[argc - 1]).
tptr = Calloc(nthreads. sizeof(Thread)).
iget = iput = 0,
/* создание всех потоков */
for (i = 0. i < nthreads: i++)
thread_make(i)./* завершается только основной поток */
Signal(SIGINT. sig_int).
for (・.){
clilen = addrlen.
connfd = Accept(listenfd cliaddr. &clilen)
Pthread_mutex_lock(&clifd_mutex).
clifd[iput] = connfd
if (++】put к MAXNCLI)
iput = 0
if (iput == iget)
err_quit(*iput = iget = ^d", iput),
Pthread_cond_signa1(&clif d_cond).
Pthread_mutex_unlock(&clifd^mutex),
}--~
}
Создание пула потоков
23-25 Функция thread^make создает все потоки.
Ожидание прихода клиентского соединения
27-38 Основной поток блокируется в вызове функции accept, ожидая появления нового соединения. При появлении этого соединения дескриптор присоединенного сокета записывается в следующий элемент массива clifd после блокирования взаимного исключения. Мы также следим, чтобы индекс iget не совпал со значением индекса iput, что укажет на недостаточно большой размер массива. Условная переменная сигнализирует о прибытии нового запроса, и взаимное исключение разблокируется, позволяя одному из потоков пула обслужить прибывший запрос.
Функции thread_make и thread__mam показаны в листинге 27.26. Первая из них идентична функции, приведенной в листинге 27.23.
Листинг 27“26. Функции thread_make и thread_main
//server/pthread08 с
#include'unpthread h"
^include"pthreacl08 h"
void ,q 八Jтпродочжение 身
Листинг 27.26 (продолжение)
thread makeCint i)
{-
void *thread_main(void *)
Pthread_create(&tptr[i] thread_tid NULL &thread_niain. (void *) i).
return,/* завершается основной поток */
}
void *
thread_main(void *arg)
{"
intconnfd,
voidweb_child(int),
printfC*thread Zd starting'en". (int) arg)
for ( .) {
Pthread_mutexJock(&clifd_mutex)
while (iget == iput)
Pthread_cond_wait(&clifd_cond &clifd_mutex):
connfd = clifdbget] /* лрнсоедмненн而 сокет, который требуется обслужить */
if (++iget == MAXNCLI)
iget = 0
Pthread_mutex_unlock(&clifdjnutex),
tptr[(int) arg] thread_count++
web ch】ld(connfd). /* обработка запроса */
Close(connfd).
}
}
Ожидание присоединенного сокета, который требует обслуживания
17-26 Каждый поток из пула пытается блокировать взаимное исключение, блокирующее доступ к массиву clifd. Если после того, как взаимное исключение заблокировано, оказывается, что индексы iput и iget равны, то вызывается функция pthread_cond_wait, и поток переходит в состояние ожидания, так как ему пока нечего делать. После прибытия очередного клиентского запроса основной поток вызывает функцию pth「ead_cond_signal, выводя тем самым из состояния ожидания поток, заблокировавший взаимное исключение. Когда этот поток получает соединение, он вызывает функцию web chi 1 d.
Значения времени центрального процессора, приведенные в табл. 27.1, показывают, что эта версия сервера медленнее рассмотренной в предыдущем разделе (когда каждый поток из пула сам вызывал функцию accept). Причина заключается в том, что рассматриваемая в данном разделе версия использует как взаимное исключение, так и условную переменную, тогда как в предыдущем случае (см. листинг 27.23) применялось только взаимное исключение.
Если мы рассмотрим гистограмму количества клиентов, обслуживаемых каждым потоком из пула, то окажется, что распределение клиентских запросов по потокам будет таким же, как показано в последнем столбце табл. 27.3. Это означает, что если основной поток вызывает функцию pth「ead_cond_si gnal, то при выборе очередного потока, который будет выведен из состояния ожидания для обслуживания клиентского запроса, осуществляется последовательный перебор всех имеющихся свободных потоков.
Резюме
В этой главе мы рассмотрели 9 различных версий сервера и их работу с одним и тем же web-клиентом, чтобы сравнить значения времени центрального процессора, затраченного на управление процессом.
0. Последовательный сервер (точка отсчета — управление процессом отсутствует). 1. Параллельный сервер, по одному вызову функции fork для каждого клиента.
Предварительное порождение дочерних процессов, каждый из которых вызывает функцию accept.
Предварительное порождение дочерних процессов с блокировкой файла для защиты функции accept.
Предварительное порождение дочерних процессов с блокировкой взаимного исключения дочерними процессами для защиты функции accept.
Предварительное порождение дочерних процессов с передачей дескриптора от родительского процесса дочернему.
Параллельный сервер, поочередное создание потоков по мере поступления клиентских запросов.
Предварител ыюе порождение потоков с блокировкой взаимного исключения потоками для защиты функции accept.
Предварительное порождение потоков, основной поток вызывает функцию accept.
Резюмируя материал этой главы, можно сделать несколько комментариев.
+ Если сервер не слишком загружен, хорошо работает традиционная модель параллельного сервера, в которой при поступлении очередного клиентского запроса вызывается функция fork для создания нового дочернего процесса. Этот вариант допускает комбинирование с демоном i netd, принимающим все клиентские запросы. Остальные версии применимы в случае загруженных серверов, таких как web-серверы.
塞 Создание пула дочерних процессов или потоков сокращает временные затраты центрального процессора по сравнению с традиционной моделью (один вызов функции fork для каждого запроса) в 10 и более раз. При этом не слишком усложняется код, но становится необходимо (как говорилось при обсуждении примеров) отслеживать количество свободных дочерних процессов и корректировать его по мере необходимости, так как количество клиентских запросов, которые требуется обслужить, динамически изменяется.
叢 Некоторые реализации допускают блокирование нескольких потоков или дочерних процессов в вызове функции accept, в то время как другие реализации требуют использования блокировки того или иного типа для защиты accept. Можно использовать для этого либо блокировку файла, либо блокировку взаимного исключения Pthread.
裔 Как правило, версия, в которой каждый поток или дочерний процесс вызывает функцию accept, проще и быстрее, чем версия, где вызов функции accept осуществляется только основным потоком (или родительским процессом), впоследствии передающим дескриптор присоединенного сокета другому потоку или дочернему процессу.
Блокировка всех дочерних процессов или программных потоков в вызове функции accept предпочтительнее, чем блокировка в вызове функции select, что объясняется возможностью появления коллизий при вызове функции sei ect. Использование потоков, как правило, дает больший выигрыш во времени, чем использование процессов. Но выбор между версиями 1 и 6 (один дочерний процесс на каждый запрос и один поток на каждый запрос) зависит от свойств операционной системы и от того, какие еще программы задействованы в обслуживании клиентских запросов. Например, если сервер, принимающий клиентское соединение, вызывает функции fork и ехес, то может оказаться быстрее породить с помощью функции fork процесс с одним потоком, чем процесс с несколькими потоками.
Упражнения
Почему на рис. 27.2 родительский процесс оставляет присоединенный сокет открытым, вместо того чтобы закрыть его, когда созданы все дочерние процессы?
Попробуйте изменить сервер из раздела 27.9 таким образом, чтобы использовать дейтаграммный доменный сокет Unix вместо потокового сокета домена Unix. Что при этом изменяется?
Запустите клиент и те серверы из рассмотренных в этой главе, которые позволяет запустить конфшурация вашей системы, и сравните полученные результаты с приведенными в тексте.
ЧАСТЬ 4
XTI: ТРАНСПОРТНЫЙ ИНТЕРФЕЙС X/OPEN
ГЛАВА 28
XTI: TCP-клиенты
Введение
В первой главе книги на рис. 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 ( Transpoit Layer Interface — интерфейс транспортного уровня). Хотя между TLI и сокетами имеется большое сходство, TLI был создан в соответствии с Определением транспортной службы модели взаимодействия открытых систем ( OSI Transport Service Definition). Система 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 — Х/Ореп Transpoit Interface {транспортный интерфейс группы Х/Ореп). Интерфейс XTI, по существу, является расширенным вариантом протокола TLI, и в своем развитии он прошел несколько версий. Мы описываем XTI вместо ТЫ, потому что стандарт Posix.lg стартовал от XTI, а не от TLL В нескольких последующих главах мы описываем XTI в определении для Unix 98 [74], почти идентичный XTI из Posix.lg.
Для описания реализации протокола в XTI используется термин поставщик службы связи {communications provider). Наиболее распространенные поставщик ки службы связи для интернет-протоколов — это TCP и UDP. Термин точка йо- ступа службы связи {communications endpoint) относится к объекту, создаваемому и поддерживаемому поставщиком службы связи и затем используемому приложением. Ссылка на эти точки доступа службы связи производится при помощи файловых дескрипторов. Мы часто будем сокращать эти два термина, просто употребляя слова поставищк и точка доступа.
ПРИМЕЧАНИЕ
В протоколе ТЫ эти понятия носят названия «поставщик транспортной службы» (transport provider) и «точка доступа транспортной службы» (transport endpoint).
Названия всех функций XTI начинаются с t_. Заголовочный файл, включаемый в приложение, в котором требуются все определения XTI, называется <xti h> Некоторые специфичные для Интернета определения становятся доступными после включения файла <xti_inet h>.
Мы обсуждаем XTI в следующем порядке:
TCP-клиенты.
Функции имени и адреса.
ТСР-серверы.
Клиенты и серверы UDP.
Параметры.
Потоки.
Дополнительные функции.
Наше обсуждение XTI будет короче, чем обсуждение сокетов, поскольку для XTI применяются те же приемы сетевого программирования. Изменения сводятся к именам функций, аргументам функций и некоторым практически важным деталям (например, принятию соединений TCP), поэтому нет необходимости повторять каждый из примеров сокетов с использованием XTI.
Функция t_open
Первый шаг в создании конечной точки связи состоит в том, чтобы открыть устройство Unix, идентифицирующее конкретного поставщика связи. Эта функция возвращает дескриптор (короткое целое), который используется другими функциями XTL
#i nc 1 tide <xti h>
#include <fcntl h>
int t_open(const char *pathname. mt of lag struct t_info *info)
Возвращает 0 в случае успешного выполнения -1 в случае ошибки
Используемое текущее значение аргумента pathname зависит от реализации, но типичными значениями для точек доступа TCP/IP являются /dev/tep, /dev/ udp н /dev/iemp. Типичные значения для точек доступа обратной связи (loopback endpoints) — это /dey/ticots, /dev/ticotsord и /dev/ticlts.
Аргумент ofl ag задает флаги открытия. Его значение — O RDWR、Для неблокируемой точки доступа этот флаг объединен с флагом O_NONBLOCK путем логического сложения.
ПРИМЕЧАНИЕ —
Эта функция XTI аналогична функции socket. Обе возвращают файловый дескриптор, который связан с протоколом, заданным пользователем.
Структура t_info — это совокупность целых значений, описывающих зависящие от протокола свойства поставщика службы связи. Значение структуры возвращается посредством указателя info, если эго не пустой указатель. Это наша
первая встреча с одной из структур XTI, название которой начинается с t_. Всего имеется семь таких структур, о которых мы расскажем подробнее в разделе 28.4. struct t_info {t_scalar_t addr, /* максимальное число байтов в адресе коммуникационного протокола */первая встреча с одной из структур XTI, название которой начинается с t_. Всего имеется семь таких структур, о которых мы расскажем подробнее в разделе 28.4. struct t_info {t_scalar_t addr, /* максимальное число байтов в адресе коммуникационного протокола */t_scalar_t t_scalar_toptions./* максимальное число байтов для транспортннх параметров */tsdu./* максимальное число байтов в обслуживаемом блоке данныхтранспортного уровня (TSDU-transport service data unit) */t_scalar_t t_scalar_tetsdu,/* максимальное число байтов в срочном TSDU (ETSDU) */connect./* максимальное число байтов данных, которые можно передатьпри запросе соединения */t_scalar_tdiscon. /* максимальное число байтов данных которые можно передать при разрыве соединения */t_sca1ar_t t_sca1ar_tservtype, /* тип поддерживаемой службы */f1ags./* другая информация (нововведение XTI) */t_scalar_t t_scalar_toptions./* максимальное число байтов для транспортннх параметров */tsdu./* максимальное число байтов в обслуживаемом блоке данныхтранспортного уровня (TSDU-transport service data unit) */t_scalar_t t_scalar_tetsdu,/* максимальное число байтов в срочном TSDU (ETSDU) */connect./* максимальное число байтов данных, которые можно передатьпри запросе соединения */t_scalar_tdiscon. /* максимальное число байтов данных которые можно передать при разрыве соединения */t_sca1ar_t t_sca1ar_tservtype, /* тип поддерживаемой службы */f1ags./* другая информация (нововведение XTI) */}.ПРИМЕЧАНИЕЭто паша первая встреча с переменной нового типа t_scalar_t datatype, который впервые появился в Unix 98. В предшествующих реализациях все эти элементы относились к типу long integer, что создает проблему в 64-разрядной архитектуре, которую мы уже обсуждали в разделе 1.11. Поэтому типы t_scalar_t и t_uscalar_t определены соответственно как int32_t и uint32_t.}.ПРИМЕЧАНИЕЭто паша первая встреча с переменной нового типа t_scalar_t datatype, который впервые появился в Unix 98. В предшествующих реализациях все эти элементы относились к типу long integer, что создает проблему в 64-разрядной архитектуре, которую мы уже обсуждали в разделе 1.11. Поэтому типы t_scalar_t и t_uscalar_t определены соответственно как int32_t и uint32_t.
первая встреча с одной из структур XTI, название которой начинается с t_. Всего имеется семь таких структур, о которых мы расскажем подробнее в разделе 28.4. struct t_info {
t_scalar_t addr, /* максимальное число байтов в адресе коммуникационного протокола */
первая встреча с одной из структур 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_sca1ar_t t_sca1ar_t
servtype, /* тип поддерживаемой службы */
f1ags./* другая информация (нововведение XTI) */
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_sca1ar_t t_sca1ar_t
servtype, /* тип поддерживаемой службы */
f1ags./* другая информация (нововведение XTI) */
}.
ПРИМЕЧАНИЕ
Это паша первая встреча с переменной нового типа t_scalar_t datatype, который впервые появился в Unix 98. В предшествующих реализациях все эти элементы относились к типу long integer, что создает проблему в 64-разрядной архитектуре, которую мы уже обсуждали в разделе 1.11. Поэтому типы t_scalar_t и t_uscalar_t определены соответственно как int32_t и uint32_t.
}.
ПРИМЕЧАНИЕ
Это паша первая встреча с переменной нового типа 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
KWW«»M»S«1S^^
AIX4.2
Dunix4.0B
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
T_COTS_ORD
T_COTS_ORD
T_COTS_ORD
T_COTS_ORD
Таблица 28.2. Значения полей структуры tjnfo для UDP
AIX4.2
Dunix 4.0B
HP-UX 10.30
Solaris 2.6
UnixWare^. 1.2
addr
16
16
16
16
16
options
512
768
256
468
328
tsdu
8192
9216
65508
65508
65508
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_INFINITE) и -2 (также называемое T_INVALID).
addr. Переменная задает максимальный размер (в байтах) адреса, специфичного для данного протокола. Значение -1 указывает,1! го нет ограничения размера, -2 — что у пользователя нет доступа к адресам протокола.
Значение 16, показанное в табл. 28.1 и 28.2 для TCP и UDP, — это длина структуры sockaddr_m. Для точки доступа протокола IPv6 это значение, вероятно, будет длиной структуры sockaddr_m6.
options. Указывает максимальное количество байтов для транспортных параметров. Значение -1 указывает, что нет ограничения размера, а значение -2 — что у пользователя нет доступа к параметрам. Мы поговорим подробнее о параметрах XTI в главе 32.
Как мы можем видеть из примеров, между различными реализациями имеется мало общего — значения изменяется в диапазоне ог 256 до 4096.
繆 tsdu. TSDU обозначает блок данных транспортного уровня. Эта переменная указывает максимальное количество байтов в записи, размер которой зафиксирован при передаче от одной точки доступа к другой. Нулевое значение указывает, что поставщик службы связи не поддерживает концепцию передачи данных по транспортному уровню в виде блоков, хотя он поддерживает потоковую передачу байтов данных (то есть нет границ записей). Значение -1 указывает, что нет ограничения размера блоков данных, а значение -2 — что не поддерживается передача обыкновенных данных (редкая ситуация).
Для протокола TCP эта переменная всегда имеет нулевое значение, потому что этот протокол предоставляет службу потоковой передачи данных без ка~ ких・лн6о границ записей. Преобладающее значение для протокола UDP, как видно из табл. 28.2, — 65 508, что является неверным. Поясним это подробнее. Максимальное значение дейтаграммы для протокола IP равно 65 535 байт (16-разрядная полная длина поля на рис. А.1), таким образом, максимальный размер дейтаграммы протокола UDP равен 65 535 минус 2 0 (длипа 1Р-заго~ ловка) и минус 8 (для UDP-заголовка), что дает 65 507.
■ etsdu. ETSDU обозначает блок срочных данных транспортного уровня, а пере~ менная etsdu задает максимальное количество байтов в отправленном блоке данных транспортного уровня. В главе 21 мы называли их внеполосными (。〃£~ of-band data), или срочными (expedited data), данными. Нулевое значение указывает, что поставщик службы связи пе поддерживает концепцию ETSDU, хотя он поддерживает потоковую передачу внеполосных данных (то есть границы записей во внеполосных данных не сохраняются). Значение -1 указывает, что нет ограничения на размер данных. Значение -2 говорит о том, что передача срочных данных не поддерживается.
Как мы и предполагали, протокол UDP не поддерживает никакой формы внеполосных данных. Протокол TCP поддерживает эту концепцию, но в нем нет ограничения на размер внеполосных данных, которые может послать приложение. В связи с этим вспомните наше обсуждение срочного режима (urgent mode) в разделе 21.2.
經 connect. Некоторые ориентированные на установление соединения протоколы поддерживают передачу данных пользователя вместе с запросом на соеди*- нение. Эта переменная указывает максимальный размер таких данных. Значение -1 указывает, что нет ограничения па размер данных, а значение ~2 —что поставщик службы связи не поддерживает эту возможность.
Протокол TCP не поддерживает эту возможность, поэтому для него значение данной переменной всегда -2, а так как протокол UDP не является протоколом, ориентированным на установление соединения, то для него эта переменная также имеет значение -2 Протокол транспортно! о уровня модели OSI ориентирован на установление соединения и поддерживает такую возможность
ПРИМЕЧАНИЕ
Заметим что про 】окон TCP разрешает носы пать данные bmcci е с cci мен ] ом SYN как показано на с 14-16 киши (95J Сокеты и XTI однако, не предостав 1як)1 способа заставить TCP послать данные вмес】е с cei мен 】ом SYN
Тем не менее этот элемент структуры t info относится к чему-то дру!ому (например к возможности, нредос!аш1 ясмои протоколом Tpanciiopi hoi о уровня модели OSI)
discon Некоторые ориентированные на установление соединения протоколы поддерживают передачу данных пользователя вместе с запросом на разрыв соединения Мы увидим эту возможность, когда будем обсуждать функции t_snddis и t_rcvdis далее в этой главе Эта переменная указывает наибольший размер таких данных Значение ~1 указывает, чго нет ограничения па этот размер Значение -2 говорит о том, что поставщик службы связи не поддерживает такую возможность Эта переменная также указывает размер данных пользователя, которые мотут быть посланы при нормальном завершении (о/ deily release) с использованием функции t_sndre】datd и t__rcvreldata, о которых рассказывается в разделе 34 10
Протокол TCP не поддерживает это свойство, но оно поддерживается протоколами транспортного уровня модели OSI
servtype Эта переменная указывает гип службы, предоставляемой поставщиком службы связи Она может принимать одно из трех значении, показанных в табл 28 3
Таблица 28.3. Типы служб, предоставляемых поставщиками службы связи
'«<"'«WWWW««<»fr»W4TOK.'04MMWSM««w».'«»MWWM««0««WMW«W««W«l4««««'»«»WW<WWaSK«ejHW««W««««»'*'» •ИеФИмЫИ»»»»**' 4y«W^WXMMC»0W«»W»««^?K<«»?«»^SSW»WW<W««««№>x«WX«<i«»»SCeoe«» servtypeОписание
T COTSОриентированная на установление соединения служба без нормальною
завершения (orderly ukase)
T_COTS_ORD Ориентированная ла установление соединения служба с нормальным завершением (orderly release)
T CLTSСлужба без поддержки устаиовления соединения
Протокол TCP является ориеш ированным на установление соединения с поддержкой нормального завершения, а протокол UDP относится к службам без поддержки установления соединения
fl ags Этот элемент структуры является новым для XTI и задает дополнительные флаги для поставщика службы связи Через этот элемент moi ут быть переданы (возвращены) две константы, представленные в табл 28 4 и определяемые путем включения заголовочного фаила <xti h>
Протокол TCP не поддерживает записи нулевой длины а протокол UDP под держивает (получается 28-байтовая 1Р-дейта1 рамма, содержащая 1Р-заголо- вок и UDP-заголовок и не содержащая никаких данных) Протокол TCP также не поддерживает флаг T_ORDRELDATA
Таблица 28.4. Значения элемента flags структуры tjnfo
•X3:q5C3WH»MW«*,*"K«*K8W»*WWX«W«WW8g»«*«3夕 7W««W®W«*»»»*«*»«««»»»'v<«««"»£*'N«'W«»««»SW«««W»«;XM«««W«W»MSW
Flag
Описание
T_SENDZERO
T_ORDRELDATA
Поставщик поддерживает записи нулевой длины
Поставщик поддерживает передачу данных при завершении соединения константы t_sndrcldata и t_rcvreldata
Функции t_error и t_strerror
Напомним, что большинство функции сокетов (например, socket, bind, connect и другие) возвращают значение —1, koi да встречают ошибку, и присваивают то или иное значение переменной errno, чтобы дать дополнительную информацию об ошибке Функции протокола XTI обычно возвращают значение -1 в случае ошибки и задают некоторое значение переменной t_errno Напомним наше обсуждение переменной errno в разделе 23 1 и, в частности, то, что для каждого потока есть своя переменная errno (per thead-vanable) Каждый поток также имеет свою переменную t_errno Переменная t_errno аналогична переменной errno в том, что ей присваивается значение, только если имеет место ошибка, и ее значение не сбрасывается при успешном завершении вызова функции
Все коды ошибок протокола XTI определяются во включаемом заголовочном файле <xti h> и начинаются с буквы Т, как, например, TBADADDR (неправильный ^юрмат адреса), TBADF (недопустимое значение транспортного дескриптора) ит д
Существует одно специальное значение ошибки — TSYSERR Koi да переменная t_errno возвращается с этим значением, это указывает приложению, что надо посмотреть назначение переменной errno, чтобы получить значение системной ошибки
Две функции, t_error и t_strerror предназначены для того, чтобы задать формат сообщения об ошибках, получаемых ог функции протокола XTI
#1 nd ude <xti h>
mt t_error (const char *msg)
Возвращает 0 const char *t_strerror (mt errnum)
Возвращает указатель на сообщение
Функция t_error посылает сообщение на стандартное устройство вывода сообщений об ошибках Это сообщение состоит из строки, на которую указывает указатель msg (предполагается, что этот указатель непустой), заканчивающейся двоеточием и пробелом За ней идет строка сообщения, соответствующая текущему значению переменной t_errno Если значение переменной t_errno равно TSYSERR, то выводится еще строка сообщения, соответствующая текущему значению переменной errno В заключение выводится символ перевода строки
Функция t_strerror возвращает строку, описывающую значение переменной errnum, которое, как предполагается, принимает одно из возможных для t_errno значений В отличие от функции t_error, функция t_strerror не делает ничего особенного, если это значение — TSYSERR
Программа в листинге 28 11 показывает применение этих двух функций вывода сообщений об ошибках протокола XTI вместе с нашей функцией err_xti (Последнюю функцию мы рассматриваем в разделе Г4 )
Все исходные коды npoi рамм опубликованные в эюи книге вы можете нантн по адресу http // uvvw piter com /download
Листинг 28.1. Пример использования функции t_erro и t_strerro //xti1ntro/strerror с
1234567891234567890 12 3 4 50 12 3 4 5#】nclude "unpxti h“ mt
123456789
123456789
0 12 3 4 5
0 12 3 4 5
main(int argc char **argv) {
printf(”%s\en' t_strerror(TPROTO)) errno = ETIMEDOUT”
printf('記\en' t_strerror(TSYSERR)). t_errno = TSYSERR errno = ETIMEDDUT
t_error(и1_еггог5ау5и)
t_errno = TSYSERR errno = ETIMEDOUT.
err_xt1(” err_xt1 says") exit(O)
}
Эта программа выводит следующее:
aix * strerror
XTI protocol error
system error
t_error says system error Connection timed out err_xti says system error Connection timed out
Структуры netbuf и структуры протокола XTI
Протокол XTI определяет семь структур, используемых для передачи информации между приложением и функциями протокола XTL Одна из иих, структура t info, которую мы описали в разделе 28 2, представляет собой набор целых значений, описывающих зависящие от протокола свойства поставщика службы связи. Оставшиеся шесть структур, в свою очередь, содержат каждая от двух до трех структур netbuf. Структура netbuf определяет буфер, используемый для передачи данных от приложения в функции XTI или в обратном направлении.
struct netbuf {
unsigned mt maxlen. /* максимальный размер буфера buf */
unsigned int len /* фактическое количество данных в буфере buf */
void *buf /* данные (до введения Posix 1g имели тип char*) */ }
В табл. 28.5 приведены шесть структур XTI, содержащие одну или более структуру netbuf и другие элементы структур XTI.
Эти шесть структур протокола XTI, содержащие внутри себя структуры netbuf, всегда передаются между приложением и функциями XTI по ссылке. Это значит, чю в качестве аргумента функции XTI мы передаем адрес структуры XTL Поэтому функции XTI всегда могут прочесть и изменить любой из трех элементов структуры netbuf (хотя ни одна из функций ие меняет элемент maxlen).
Использование трех структур netbuf зависит от направления, в котором передается информация: от приложения к функциям XTI или наоборот. Это отражено в табл, 28.6, в которой также отмечено, читает функция XTI значение элемента или записывает его значение.
Таблица 28.5. Шесть структур протокола XTI и их элементы
Тип данных
Структуры протокола XTI
t_bind
t_call
t_discon
t_optmgmt
t_uderr
t_u nitdata
Struct netbuf
addr
addr
addr
addr
Struct netbuf
opt
opt
opt
opt
Struct netbuf
udata
udata
udata
t scalar t
error
Jscalar t
flags
unsigned mt
qlen
mt
reason
mt
sequence
sequence
Таблица 28.6. Обработка трех элементов структуры netbuf
Элемент
Передача данных от приложения к XTI
Передача данных от XTI к приложению
maxlen
Игнорируется
Только читается Размер буфера, иа который указывает buf Функция XTI поместит в буфер данные не более указанного количества Если значение равно нулю, то ничего не возвращается и значения len и buf игнорируются
len
Только читается Приложение присваивает этому элементу количество данных, иа которые указывает buf
Только записывается Функция XTI присваивает этому элементу фактическое количество данных находящихся в буфере, и оно всегда должно быть меньше или равно maxlen
buf
Указатель на данные, приготовленные приложением, которые будут обрабатываться функцией XTI
Указатель на данные, приготовленные функцией XTI, которые будут обрабатываться приложением
Если XTI-функция возвращает больше данных, чем позволяет переменная maxlen, то вызов функции завершается аварийно и переменная t_errno принимает значение TBUFOVFLW.
Так как адрес структуры netbuf всегда передается функции XTI и структура содержит и размер (maxi еп) буфера, и количество данных (1 еп), фактически находящихся в буфере, то функциям XTI не обязательно иметь аргументы типа «значение-результат» (value-result), как это имеет место для функций сокетов.
Функция t_bind
Эта функция присваивает локальный адрес точке доступа и активизирует ее. В случае протоколов TCP или UDP локальный адрес — это IP-адрес и номер порта. #wclude <xti h> mt t_bind (mt fd const struct t_bind ^request struct t_bind ^return) Возвращает 0 в случае успешного выполнения -1 в случае ошибки
Второй и третий аргументы указывают на структуры функции t_bi nd:
struct t_bind {
struct netbuf addr /* адрес, зависящий от протокола*/
unsigned int qlen /* максимальное число возможных соединении (если сервер) */ }
Точка доступа указывается переменной fd Для аргумента request следует рассмотреть три случая
request == NULL Вызывающий процесс не заботится о том, какой локальный адрес присваивается точке доступа Поставщик службы связи сам выбирает адрес Элементу qlen присваивается нулевое значение (подробнее мы поговорим об этом далее)
request ,= NULL и request->addr 1еп == 0 Вызывающий процесс не заботится о том, какой локальный адрес присваивается точке доступа, и поставщик службы связи сам выбирает адрес Но в отличие от предыдущего случая, вызывающий процесс может задать ненулевое значение для элемента qlen структуры request
毛 request 丨=NULL и request >addr len > 0 Вызывающая программа указывает поставщику службы связи, какой локальный адрес присвоить точке доступа службы связи
В обоих случаях (адрес указывается приложением или выбирается поставщиком связи) поставщик связи возвращает адрес, который он присвоил точке доступа, в структуре return Если аргумент return является пустым указателем, поставщик связи не возвращает фактический адрес
Значение элемента ql еп имеет смысл только для сервера, ориентированного на установление соединения оно указывает максимальное количество соединений, которое может быть установлено в очередь для этой точки доступа Это значение может быть изменено поставщиком службы связи, и в таком случае элемент q〕en структуры return указывает фактическое число соединений, поддерживаемых поставщиком Мы расскажем подробнее о значениях элемента q〕еп и об измерении фактического числа соединений, поставленных в очередь, обсуждая табл 30 2
ПРИМЕЧАНИЕ
Если протокол XTI не может связать (присвоить) запрошенный адрес, то возвращается ошибка TADDRBUSY Если протокоч ТЫ сталкивается с згой пробчемой, го он может присвоить другой локальный адрес точке доступа, что приводит к необходимости сравнивать присвоенный адрес с запрошенным
Используемый в протоколе XTI метод с указанием поставщику на необходимость выбора подходящего адреса является более общим, чем метод связывания с адресом (bind) Например, используя протоколы TCP и UDP в IPv4, мы должны указать поставщику службы связи семейство адресов INADDR_ANY и нулевой порт для выбора локального адреса Этот подход специфичен для протокола IPv4 и является недостаточно общим
Значение элемента qlen соответствует аргументу backlog, задаваемому для функции listen Для сервера, ориентированного на установление соединения, функция t_bind выполняет то же деист вие, что функции bind и listen
Клиент XTI ориентированный на установление соединения должен вызывать функцию t_bind перед вызовом функции t_connect (которую мы опишем далее) В этом заключается отличие от функции connect, которая вызывает функцию bind самостоятельно, если сокет не был связан предварительным вызовом bind
Заметим, что элемент addr структуры t bind — это действительно структура netbuf, а не указатель на одну из этих структур Мы увидим, что это общее свойство для рассматриваемых структур XTI большинство из них содержат одну или более структур netbuf внутри структуры t_XXX
Функция t_connect
Клиент, ориентированный на установление соединения, инициирует соединение с сервером путем вызова функции t_connect Клиент задает адрес протокола сервера (например, IP-адрес и порт для ТСР-сервера) include <xti h>
mt t_connect (int 加 const struct t_call *sendcall struct t_call *recvcdll} Возвращает 0 в случае успешного выполнения -1 в случае ошибки
Второй и третий аргументы функции указывают на структуру t eal】
struct t call {
struct
netbuf
addr
/*
адрес определяемый типом протокола 永/
struct
netbuf
opt
/*
параметры опрепеллемме типом протокола */
struct
netbuf
udata
/*
данные пользователя которне должны сопровождать запрос на соединение */
mt
sequence
/*
для функции t_listen() и t_accept() */
}
Структура t_call, на которую указывает аргумент sendcal】,задает информацию, необходимую поставщику транспортных служб для установления соединения
Структура dddr задает адрес сервера
Структура opt задает параметры, специфичные для данного протокола, по желанию пользова1еля
Структура udatа содержит любые данные поль зова геля, которые должны быть переданы серверу при установлении соединения (Напомним, что, как показано в табл 28 1, протокол TCP ие поддерживает передачу данных пользователя при запросе на соединение )
Элемент sequence не имеет значения для этой функции, но он используется, когда структура t_call применяется с функцией t_accept
При возврате из этой функции структура t_ca〕l, на которую указывает аргумент recvcsl 1, содержит информацию о соединении, возвращаемую пользователю поставщиком службы связи
Структура addr содержит адрес процесса-собеседника (pee? process)
Структура opt содержит любые параметры, специфичные для данного протокола связанные с соединением
Структура uGdta содержит любые данные пользователя возвращенные поставщиком транспортной службы процесса-собеседника при установлении соединения
Элемент sequence не имеет значения
Содержание структуры opt зависит oi протокола Пользователь может задать нулевое значение поля 1 еп этой структуры, указывая тем самым поставщику службы связи, что для всех параметров соединения надо использовать значения
заданные по умолчанию. Мы обсудим параметры протокола XTI подробнее в главе 32.
Пользователь может задать аргумент recvcall как пустой указатель, если он не заинтересован в возвращении информации о соединении.
По умолчанию эта функция не возвращает управление, пока не будет установлено соединение или пока не произойдет ошибка. Мы обсудим, как установить неблокируемое соединение, в разделе 34.3.
Мы видели в разделе 4.3, что наиболее распространенные ошибки при установлении соединения — это получение сегмента RST, получение сообщения ICMP Destination unreachable (Получатель недоступен) и отсутствие ответа в течение определенного интервала времени. К сожалению, когда возникает одна из этих ошибок, функция Jconnect возвращает значение-1,а переменная t_errno принимает значение TLOOK, что требует дополнительных данных для определения точной причины. Мы обсудим эту проблему в разделах 28.9 и 28.10 и рассмотрим пример в листинге 28.2.
ПРИМЕЧАНИЕ
Функция t_conncct протокола XTI а (логична функции connect протокола TCP.
Функции t_rcv и t_snd
По умолчанию приложения ХТ1 не могут вызывать обычные функции read и write (до тех пор, пока модуль 11 rdwr не помещен в поток, как мы покажем в разделе 28.12). Вместо этих функций приложения XTI должны вызывать функции t_rcv и t_snd. include <xti h> int t_rcv (int fd void *buff. unsigned mt nbytes, mt *flagsp).
int t_snd (int fd const void *buff, unsigned mt nttytes. mt r%gs),
Обе функции возвращают число прочитанных или записанных байтов в случае успешного выполнения, -1 в случае ошибки
Первые три аргумента аналогичны первым трем аргументам функций read и write: дескриптор, указатель на буфер и число байтов, которые надо прочитать пли записать.
ПРИМЕЧАНИЕ
Вес функции ввода и вывода API сокетов попользуют тип данных s】zc_t для размера буфера и тип данных ssizc_t для возвращаемого значения. Фупкции XTI используют соответственно unsigned int и int.
Значение аргумента (флага) fl ags функции t_snd либо равно пулю, либо является комбинацией констант, приведенных в табл. 28.7.
Таблица 28.7. Значения флагов flags и flagsp для функций t_rcv и t_snd
»3№fr«iS>e«ss^»ia>assfc8sssisBaaj>»>BW<s»8S««*!Sft«8SC№w>i»»e<5»?«^^
ФлагОписание
T_EXPEDITEDПосла!ь или принять внеполосные данные
Т MOREЕше есть данные для от правки или приема
Константа T_EXPEDITED используется с функцие!! t_snd для отправки внеполосных данных (см. раздел 34.12). Этот флаг устанавливается при возврате из функ- нмк t rrv кпгтгя nonvwenbi внеполосные данные.
Константа T_MORE используется для того, чтобы при множественных вызовах функции t_「cv или t_snd могли читать или писать то, что протокол рассматривает как логическую запись. Это свойство применимо только для протоколов с поддержкой записей. Мы покажем пример использования этого флага с функцией t__rcvudata и ориентированным на записи протоколом UDP в листинге 31.6. Этот флаг также используется с протоколом TCP при чтении внеполосных данных, как мы покажем в разделе 34.12, но с обычными данными протокола TCP он никогда не используется.
ПРИМЕЧАНИЕ
Протокол XTI определяет 巾刀ar 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_e「rno принимает значение TLOOK. Затем должна быть вызвана функция t_l ook протокола XTI, которая вернет значение T_ORDREL. Это называется признаком нормального завершения (orderly release indication).
飾 Если сегмент RST протокола TCP получен в точке доступа протокола XTI, то функция t_rcv возвращает значение -1, а переменная t_e「rno принимает значение TLOOK. Затем должна быть вызвана функция t_l ook протокола XTI, которая вернет значение T_DISCONNECT. Это называется разрывом соединения {disconnect) или аварийным завершением (abortive release).
Сначала мы обсудим функцию t_look, а затем функции нормального и аварийного завершения.
Функция tjook
В точке доступа протокола XTI могут происходить различные события (events). Эти события могут происходить асинхронно {asynchronously). Под этим подразумевается, что приложение может быть занято выполнением некоторого задания, когда в точке доступа происходит не связанное с этим заданием событие. Некоторые события указывают на возникновение аварийной ситуации (например, T_UDERR сообщает об ошибке в уже отправленной дейтаграмме), в то время как другие события не являются ошибками (T_EXDATA сообщает о приходе срочных данных).
Например, представим себе, что приложение вызывает функцию t_snd, чтобы отправить данные собеседнику, но непосредственно перед этим на узле собеседника что-то происходит, и процесс на узле собседника посылает сегмент RST и завершается. Информация о неожиданном событии (получение сегмента RST, когда приложение вызывает функцию t_snd) передается приложению путем присваивания значения ~1 коду возврата функции t_snd и присваивания значения TLOOK переменной t_errno. Приложение вызывает функцию t_look, чтобы определить, какое событие произошло в точке доступа. В рассмотренном примере этим событием будет T_DISCONNECT — прием извещения о разрыве соединения (RST). include <xti h>
int t_look(int M),
Возвращает константу обозначающую событие (табл 28 8) в случае успешного выполнения, -1 в случае ошибки
Целочисленное значение, возвращаемое этой функцией, соответствует одному из девяти событий, приведенных в табл. 28.8.
Таблица 28.8. События, возникающие в точке доступа протокола XTI
Событие
Описание
T_CONNECT
Получено подтвержден!!e об установлении соединения
T_DATA
Получены обычные данные
T_DISCONNECT
Получено сообщение о разрыве соединения
T—EXDATA
Получены срочные данные
T—GODATA
Сняты ограничения управ刀снм夕 потоком для обычных данных
T_GOEXDATA
Сняты ограничения управления потоком для срочных данных
T—LISTEN
Получено сообщение о соединении
T_ORDREL
Получен признак нормального завершения
T_UDERR
Ошибка в ранее посланной дейтаграмме
Когда в точке доступа протокола XTI происходит событие, оно считается ожидающим обработки (outstanding), до тех пор пока не будет обработано {consumed). В табл. 28.9 приведены функции обработки событий XTI, а также показаны два события, обрабатывамые вызовом функции t_look.
Таблица 28.9. События протокола XTJ и функции, которые их обрабатывают
Событие
Сбрасывается функцией tjook
Функции обработки
T_CONNECT
t_connect, L_rcvconnect
Т DATA
1 rev, t rcu, t revudata, t rewudata
Событие
Сбрасывается функцией tjook
Функции обработки
^DISCONNECT
t_rcvdis
T_EXDATA
t_rcv, t_rcw
T—GODATA
Да
L_snd, t_sndv, jsndudata, l_sndvudata
T_GOEXDATA
Да
t_snd, t_sndv
T—LISTEN
t_listen
T„ORDREL
t_rcvrel
T_UDERR
t_rcvuderr
Для функции t_connect в блокируемой точке доступа (по умолчанию) событие T_CONNECT обрабатывается самой функцией и невидимо для приложения. Применительно к рассмотренному примеру (получение сегмента FIN при вызове функции t_snd) приведенная таблица показывает, что для обработки события мы должны вызвать функцию t_rcvrel.
ПРИМЕЧАНИЕ —■——
В начале этого раздела мы показали, как получение сегмента RST генерируег сообщение T_DISCONNECT для точки доступа. До появления Unix 98 получение сегмента FIN генерировало событие T_ORDREL для гочки дос1упа. В Unix 98 делать это стало нс обязательно.
Функции t_sndrel и t_rcvrel
Протокол XTI поддерживает два способа завершения соединения: нормальное завершение {orderly release) и аварийное завершение {abortive release). Разница заключается в том, что аварийное завершение не гарантирует доставку необработанных данных, в то время как нормальное завершение гарантирует это. Все поставщики служб связи должны поддерживать аварийное завершение, а поддержка нормального завершения не является обязательной. В связи с этим напомним, что, как следует из табл. 28.1, протокол TCP предоставляет возможность нормального завершения. Мы можем посылать и принимать сообщение о нормальном завершении с помощью следующих функций:
#include <xti.h>
int t_sndrel(int Ш)
mt OcvreKint Ш).
Обе функции возвращают 0 в случае успешного выполнения. -1 в случае ошибки
Для того чтобы понять семантику нормального завершения, следует вспомнить, что ориентированные на установление соединения протоколы обычно обеспечивают двустороннее соединение между двумя процессами. Передача данных в одном направлении происходит независимо от другого направления. На рис. 28.1 показано одно из применений этих функций с протоколом TCP, использующее преимущество техники половинного закрытия (half-close), обеспечиваемой этим протоколом.
Процесс инициирует нормальное завершение вызовом функции t_sndrel. Тем самым поставщик связи извещается о том, что приложение больше не имеет данных для отправки через эту точку доступа. В точке доступа TCP протокол посылает сегмент FIN процессу-собеседнику после отправки ему данных, находивпшхея
Рис. 28.1. Использование протоколом XTI техники половинного закрытия протокола TCP
в очереди. Процесс, вызвавший функцию t_sndrel, может продолжать прием данных —он все еще может читать из сокета, но уже не может записывать в него.
ПРИМЕЧАНИЕ
Эта функция выполняет то же действие, что и функция shutdown со вторым аргументом SHUT WR на сокете TCP.
Процесс-собеседник подтверждает получение сообщения о завершении соединения вызовом функции t_rcvrel. Этот процесс все еще может записывать в сокет, но уже не может читать из него.
ПРИМЕЧАНИЕ
Дли API сокетов нет аналогов функции t rcvrel. Информация о получении сегмента FIN передав гея процессу в виде признака конца файла (то есть функция read возвращает пулевое значение).
Это свойство XTI заставляет приложение работать с двусюрошшм нормальным завершением, даже если приложение нс заинтсресовано в использовании этого свойства, как мы увидим в примере из листинга 282
Функции t_snddis и t_rcvdis
Следующие две функции обрабатывают аварийное завершение (разрыв соединения):
include <xti h>
int t_snddis(int fd. const struct t_cal 1 *ca77)
int t_rcvdis(int fd. struct t_discon *discon).
Обе функции возвращают 0 в случае успешного выполнения. -1 в случае ошибки
Функция t_snddis используется с двумя целями:
для выполнения аварийного завершения существующего соединения что в терминах TCP вызывает отправку сегмента RST;
для отказа при получении запроса на соединение.
Для аварийного завершения существующего соединения аргумент call может быть пустым указателем — в этом случае информация процессу-собеседнику не посылается. Интерпретация полей структуры t eal 1 для противоположного случая приведена в табл. 28.10.
Таблица 28.10. Использование элементов структуры t_call для функции t_snddis
Элемент
Разрыв существующего соединения
Отказ от нового соединения
addr
Игнорируется
И【норируется
opt
Итерируется
Игнорируется
udata
Необязательный
Необязательный
sequence
Игнорируется
Обязательный
Необязательный элемент udata указывает данные пользователя, которые должны пересылаться при разрыве соединения. Но в табл. 28.1 мы видели (речь идет об элементе di scon структуры t_i nfo), что это не поддерживается протоколом TCP.
ПРИМЕЧАНИЕ
Напомним, что для генерации аварийного завершения приложение сокета должно установить параметр сокета SO_LINGER, присвоить элсмешу l_onoff ненулевое значение, обнулить элемент l linger, а затем закрыть сокет (см главу 7).
Когда в точке доступа протокола XTI происходит событие ^DISCONNECT (протокол TCP получает сегмент RST), то приложение должно принять аварийное завершение соединения путем вызова функции t_rcvdis. Если аргумент di scon является непустым указателем, то структура t_discon должна содержать код причины возникновения аварийного завершения.
struct tjjiscon {
struct netbuf udata /* данные пользователя */
int reason, /* специфичный для протокола код причины завершения соединения */ int sequence.
}.
Элемент udata содержит необязательные данные пользователя, которые сопровождают разрыв соединения, элемент reason — зависящий от протокола код причины разрыва соединения, а элемент sequence применим только для серверов, принимающих соединения.
ПРИМЕЧАНИЕ
Среди функций API сокетов нет аналогов функции t_rcvdis. Для них получение сегмента RST передается процессу как ошибка ввода (например, функция read возвращает значение -1) с переменной еп по, установленной в ECONNRESET. Запись в сокет, получивший сегмент RST, генерирует возникновение сигнала SIGPIPE.
Клиент времени и даты для протоколов XT! и TCP
Сейчас мы перепишем нашу программу клиента для протокола TCP, приведенную в листинге 1.1, используя протокол XTI. Результат приведен в листинге 28.2.
Листинг 28.2. Клиент времени и даты, использующий протокол XTI
//xti1ntro/daytimecli01 с
1 #include "unpxti h"
2 int
34567891013456789101main(int argc. char **argv)
3456789101
3456789101
{
mttfd. n. flags
charrecvline[MAXLINE + 1].
struct sockaddr_in servaddr.
struct t call teal 1.
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.
16 servaddr sin[port = htons(13). /* сервер времени м даты */
17Inet_pton(AF_INET, argv[l] &servaddr sin_addr)
18tcalladdr maxlen = sizeof(servaddr)
19tcalladdr len = sizeof(servaddr)
20tcalladdr buf = &servaddr
21tcallopt len = 0./* нет параметровприсоединении*/
22tcalludata len = 0./* нет данныхпользователяприсоедин^ии */
23 if (t_connect(tfd &tcall NULL) < 0) {
24 if ?t_errno == TLOOK) {
25 if 7 (n = TJook(tfd)) == T_DISCONNECT) {
26tdiscon udata maxien = 0
27T_rcvdis(tfd &tdiscon)
28errno = tdiscon reason,
29err_sys("t_connect error").
30} else
31err_quit(иunexpected event after
t_connect Zd" n).
32} else —
33 err_xti("t_connect error").
34}——
35for (,.) {
36 if ( (n = t_rcv(tfd. recvline MAXLINE. &flags)) < Q) {
37, if (t_errno == TLOOK) {
38if T (n = TJook(tfd)) == TJDRDREL) {
39T_rcvrel(tfd).
40break.
41} else if (n == T_DISCONNECT) {
42tdiscon udata maxlen =0,
43T_rcvdis(tfd. &tdiscon).
44errno = tdiscon reason/* вероятно. ECONNRESET 皆
45err_sys(hserever termimated prematurely").
46} else
47err_quit( *'unexpected event after
t_rcv 帝d" n).
48} else
49err xt“"t rev error")
50}- 一
51recvline[n] =0/* завершающий нуль */
52fputs(recvline. stdout),
53}
54 exit(0).
55 }
Заголовочный файл unpxti.h
Мы определили наш собственный заголовочный файл, включаемый во все наши программы директивой #i nclude. Этот файл приведен в разделе Г.З.
Создание точки доступа и связывание ее с локальным адресом
12-13 Функция t_open создает точку доступа протокола XTL Мы разрешаем системе выбрать локальный адрес протокола, вызывая функцию t_bi nd с нулевым значением второго аргумента.
Указание адреса и порта сервера
14-22 Мы заполняем структуру адреса сокета Интернета значениями IP-адреса и порта сервера аналогично тому, как это делалось в листинге 1.1. Затем мы заполняем структуру t_cal 1, чтобы передать указатель назначение адреса этого сокета, а также задаем нулевые значения элементов 1еп для структур opt и udata, указывая, что параметры и данные пользователя отсутствуют.
ПРИМЕЧАНИЕ
В протоколе XTI не требуется задавать в структуре t_call указатель на структуру sockaddг_ш для IPv4. Тем не менее почти все реализации Unix воплощают XTI с протоколами Интернета, используя структуру 8оскас1с1г_1пдля передачи адреса протокола между приложением и поставщиком службы связи. В коде программы, ие зависящем от протокола, использование этой структуры должно быть скрыто о г приложения. В следующей главе мы покажем, как это сделать.
Установление соединения
23-34 Функция t connect устанавливает соединение, которое в данном случае выполняется по схеме трехэтапного рукопожатия TCP. Как упоминалось ранее, если установление соединения заканчивается неудачно с одной из стандартных ошибок, то функция t_connect возвращает значение TLOOK. Далее мы вызываем функцию t_look, чтобы определить произошедшее событие, и если это событае T_DISCONNECT, дополнительно вызываем функцию t_「cvdis. В этом случае мы также сохраняем код разрыва соединения в переменной errno и вызываем нашу функцию err_sys, чтобы вывести соответствующее сообщение об ошибке.
Чтение с сервера и копирование на устройство стандартного вывода
35-53 Мы считываем данные с сервера при помощи функции t_rcv и внводим^да屮 стандартное устройство вывода до тех пор, пока не встретим конец соедммпц1Ш
Обработка нормального завершения соединения и разрыва соединения
37-47 Если функция t_rcv возвращает ошибку и t_errno равна TLOOK, то мы вызываем функцию t_look, чтобы получить значение текущего события для точки доступа. Если это событие — T_ORDREL, мы вызываем функцию t_rcvrel, а если 7_DISC0NNECT — функцию t_「cvd?s.
Если мы запустим эту программу на таком же наборе узлов, что и в разделе 4.3, то получим следующие результаты. Сначала обратимся к узлу, на котором работает наш сервер времени и даты, и увидим обычный вывод:
umxware % 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 Z 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. Функция xti_^dwr
Как показано в примере предыдущего раздела, мы не можем по умолчанию использовать функции read и write для дескриптора, который ссылается на точку доступа протокола XTL Чтобы увидеть, что произойдет, если мы модифицируем листинг 28.2 и используем функции read и write вместо функций t_rcv и t_snd,
скопируем цикл for из листинга 1.1. Тогда мы получим следующую ошибку после установления соединения:
umxware % daytimecli03 206 62 226 35
read read Not a data message
Мы получим такие же результаты в AIX, а под управлением HP-UX или Solaris ответ сервера читается, но затем следует ошибка:
hpux % daytimecli03 198 69 10 4
Wed Apr 2 18 59 40 1997 read error Bad message
Мы объясним разницу между этими сценариями при обсуждении листинга 33.5.
К счастью, есть обходной путь для этой проблемы. Если мы имеем дело с реализацией XTL использующей потоковый ввод-вывод, а так обычно и бывает, то мы можем поместить в поток потоковый модуль tirdwr и после этого использовать функции read и write. На рис. 33.3 показано использование потокового модуля. Но так как эта возможность зависит от реализации, то вместо того чтобы помещать команду ioctl в нашу программу, мы определили свою собственную функцию. Таким образом, для функционирования программы в других реализациях достаточно будет заменить только эту библиотечную функцию.
include “unpxti h"
int xti_rdwr(int fd) •
Возвращает 0 в случае успешного выполнения -1 в случае ошибки
В листинге 28.3 показана тривиальная реализация этой функции, которая просто помещает потоковый модуль tirdwr в поток
Листинг 28.3. Функция xti_rdwr. помещает потоковый модуль tirdwr в поток
#include 1unpxti h"
int
xti rdwr(int fd)
{"
return (ioctl(fd I PUSH "tirdwr**)).
}-
При использовании этого модуля нужно учитывать следующие особенности.
Этот потоковый модуль может быть использован, только если точка доступа находится в фазе передачи данных (data i?ansferphase). В нашем примере в листинге 28.2 мы помещаем обращение к функции xti_rdwr после того, как возвращает управление функция t_connect.
С того момента, как этот модуль помещен в поток, ни одна из функций XTI (это функции, начинающиеся с t_) не может быть вызвана.
Этот модуль не может быть вызван приложениями, использующими внеполосные данные, так как приход внеполосных данных не может быть обработан функцией read.
Если получено сообщение о нормальном завершении соединения, то оно заставляет функцию read вернуть нулевое значение (то есть признак конца файла).
К сожалению, если получено сообщение о разрыве соединения, то оно тоже заставляет функцию read вернуть нулевое значение. Это делает невозможным отличить стандартное получение сегмента FIN от исключительного получе-
ния сегмента RST. Получение сегмента RST также ведет к тому, что любые следующие обращения к функции write потерпят неудачу.
ПРИМЕЧАНИЕ
Напомним, что у сокетов получение сегмента RST заставляет функцию read вернуть значение -1 (ошибку ECONNRESET).
Резюме
Клиенты XTI аналогичны клиентам сокетов. Они вызывают функцию t_open вместо функции socket и функцию t connect вместо connect. В обоих случаях используется одна и та же структура адреса сокета Интернета для указания адреса протокола сервера, хотя в XTI эта структура описывается при помощи структуры netbuf. По умолчанию в XTI используются функции t_rcv и t_snd вместо функций read и write, хотя в зависимости от окружения могут использоваться и две последние. Мы видели, что для работы с функциями read и wn te в поток должен быть помещен специальный потоковый модуль.
Протокол XTI определяет девять событий, которые могут происходить в точке доступа. Когда одно из них происходит, функция XTI возвращает ошибку TLOOK, и мы должны вызвать функцию t_】ook , чтобы определить, что именно произошло, после чего обработать это событие. Обработка этих событий часто увеличивает размер кода по сравнению с аналогичным сценарием для сокетов.
Упражнения
Во втором сценарии для функции t_bi nd мы говорили, что аргумент request имеет непустое значение, а элемент addr len этой структуры равен нулю, что позволяет вызывающему процессу задать ненулевое значение элемента qlen. Является ли такой сценарий полезным?
Когда мы запускали программу из листинга 28.2 для недоступного узла 192.3.4.5, мы сказали, что получили сообщение ICMP о недоступности узла в ответ на наши сегменты SYN. Почему мы говорили именно о сегментах (во множественном числе)?
В начале раздела 28.8 мы описали сценарий, в котором приложение вызывает функцию t_snd и получает по этому соединению сегмент RST. Сравните это с тем, как сокеты обрабатывают функцию write, когда по данному соединению получен сегмент RST.
Напишите функцию с именем xti_「ead, которая имеет те же аргументы, что и функция read, но вызывает функцию t_rcv и обрабатывает два сценария из листинга 28.2: если получено сообщение о нормальном завершении связи, то функция возвращает нулевое значение; если же получено сообщение о разрыве соединения, то функция возвращает значение -1, а переменной errno присваивается код причины разрыва соединения.
ГЛАВА 29
XTI: функции имен и адресов
Введение
Протокол XTI ничего не говорит о преобразовании имен и адресов. В Unix 98 для решения этих вопросов требуются только функции, рассмотренные в главе 9: gethostbyname, gethostbyaddr, getservbyname и им подобные. Тем ие менее, так как многие реализации протокола XTI выполнены в системах, производных от SVR4, большинство из них предоставляют функции имени и адреса, берущие начало от SVR4. Это функции netconfig и netdir. В SVR4 эти функции называются средствами выбора сети и преобразования имен в адреса {network selection and пате- to-address mapping facility).
В нашем клиенте из раздела 28.11 мы заполняли структуру адреса сокета Интернета значением IP-адреса и номером порта Это решение зависит от протокола. Наша же цель в этой главе состоит в том, чтобы избегать необходимости знания конкретного содержимого структуры netbuf, оперируя этой структурой, как «черным ящиком». Мы начнем с имени узла (hostname) и имени службы (service пате), вызовем некоторые функции и в результате получим структуру netbuf, готовую к использованию в вызове функции t_connect, например для клиента TCP. Это напоминает применение функции getaddnnfo в разделе 11.2.
ПРИМЕЧАНИЕ
Существует одна проблема В связи с тем, что рассматриваемые нами функции пс входят ни в какие стандарты, для этих функций отсутствуют определения способа их функционирования Например, большинство реализаций функции netdir^getbyname принимают в качестве аргумента либо имя, либо десятичный номер порта для имени службы TCP или UDP, а в других реализациях принимается только имя, а не номер порта.
Файл /etc/netconfig и функции netconfig
Отправной точкой для рассмотрения преобразования имен и адресов в протоколе XTI является файл /etc/netconfig. Это текстовый файл, содержащий по одной строке для каждого поддерживаемого протокола. Некоторые типичные значения полей файла для каждого протокола показаны в табл. 29.1.
Фактически в каждой строке фа11ла имеется семь полей, но в таблице мы не показали последнее поле, которое задает одну или несколько библиотек поиска по каталогам {directory lookups) для данной системы. Типичные значения этого
последнего поля для протоколов Интернета — /usr/lib/tcpip so или /usr/lib/ resol v. so. Обычно это динамически загружаемые библиотеки, которые содержат специфические для каждого типа сети фрагменты преобразования имен в адреса.
Таблица 29.1. Типичные значения компонентов файла /etc/netconfig
Идентификатор сети
Семантика
Флаги
Семейство протоколов
Имя протокола
Устройство
tep
tpi_cots_ord
V
met
tep
/dev/tep
udp
tpi_cks
V
met
udp
/dev/udp
icmp
tpi_raw
-
met
icmp
/dev/icmp
rawip
tpi_raw
-
met
—
/dev/rawip
ticks
tpi_cks
V
loopback
-
/dev/ticlts
ticots
tpi_cots
V
loopback
-
/dev/ticots
ticotsord
tpi_ets_ord
V
loopback
-
/dev/ticotsord
spx
tpi_cots_ord
V
netware
spx
/dev/nspx2
ipx
tpi_clts
V
netware
ipx
/dev/ipx
Идентификаторы сети для четырех протоколов Интернета в таблице такие, как мы ожидали. Следующие три строки таблицы соответствуют записям интерфейсов закольцовки (loopback entries). Здесь ti обозначает «транспортно-независимый» (transport independent). Последние две строки предназначены для протоколов сетевой операционной системы Nowell Netware, которые мы не обсуждаем в этой книге.
Показанные значения сетевой семантики для протоколов Интернета соответствуют типам служб, указанным в табл. 28.1, за исключением tp】_raw, которое используется для протокола ICMP и неструктурированного IP. Заметим, что, как следует из табл. 28.1, протокол TCP предоставляет сервис, ориентированный на установление соединения, с поддержкой нормального завершения связи.
В настоящее время определен единственный флаг — v. Он указывает, что поле видимо для библиотечных подпрограмм NETPATH, описываемых ниже.
Имя устройства используется как аргумент функции t open.
Библиотека сетевых служб предоставляет многочисленные функции для чтения файла netconfi g. Функция setnetconfi g открывает файл, а функция getnetconfi g затем читает следующую запись из файла. Функция endnetconfig закрывает файл и освобождает память, выделенную для работы.
ПРИМЕЧАНИЕ
Термин «библиотека сетевых служб» (network services library) взят из операционной системы System V и обычно относится к библиотеке, которая указывает ся при вызове компоновщика с помощью параметра -Insl. Эта библиотека, например /usr/hb/libnsl.so, содержит вес библиотечные функции про юкола XTI, а также функции, которые мы собираемся описать.
void *setnetconf1g(vold).
Возвращает непустой указатель в случае успешного выполнения нулевое значение в случае ошибки
struct netconfig *getnetconfig (void "handle}.
Возвращает непустой указатель в случае успешного выполнения. нулевое значение в случае конца файла (end-of-file)
mt endnetconfig(void * handle}.
Возвращает 0 в случае успешного выполнения -1 в случае ошибки
struct netconfig { char*nc_netid.unsigned long nc_semantics. unsigned long nc_flag, char*nc_protofmly.char*nc_protochar*nc_deviceunsigned 10ng nc_nlookups.char**nc_lookups.unsigned long nc_unused[8]struct netconfig { char*nc_netid.unsigned long nc_semantics. unsigned long nc_flag, char*nc_protofmly.char*nc_protochar*nc_deviceunsigned 10ng nc_nlookups.char**nc_lookups.unsigned long nc_unused[8]Указатель, возвращаемый функцией setnetconfig (абстрактный идентификатор —handle), используется далее как аргумент для следующих двух функций. Значение каждой записи (строки) файла возвращается в виде структуры netconfig*
struct netconfig { char*nc_netid.
unsigned long nc_semantics. unsigned long nc_flag, char*nc_protofmly.
char*nc_proto
char*nc_device
unsigned 10ng nc_nlookups.
char**nc_lookups.
unsigned long nc_unused[8]
struct netconfig { char*nc_netid.
unsigned long nc_semantics. unsigned long nc_flag, char*nc_protofmly.
char*nc_proto
char*nc_device
unsigned 10ng nc_nlookups.
char**nc_lookups.
unsigned long nc_unused[8]
/* ”tcp'. ”udp” и T д */
/* NC_TPI_CLTS и т д */
/* NC[VISfBLE и т д */
/★ "met" "loopback' и т д */
/★ °tcp * "dp” и т д */
/* имя устройства соответствующее идентифякатору Сети */
/* число записей в nc_lookups */,
/* список библиотек поиска */
Первые шесть элементов в этой структуре соответствуют шести колонкам табл. 29.1. Если мы напишем примерно следующую программу:
void * handle.
struct netconfig *nc.
handle = setnetconfigO,
while ( (nc ^ getnetconfig(handle)) '= NULL) {
/* выводим структуру netconfig */
}
endnetconfig(handle).
и предположим, что файл /etc/netconfig такой, как показано в табл. 29.1, то программа выведет девять структур netconfig, по одной на каждую строку таблицы в том же порядке.
Переменная NETPATH и функция netpath
Функция getnetconf ig возвращает очередную запись файла, давая нам возможность прочитать весь файл по строкам. Но для интерактивных программ,которыми являются клиенты, нужна возможность ограничивать поиск определенным образом, то есть искать только те протоколы, которые требуются пользователю. Это реализуется за счет того, что пользователю разрешается установить значение переменной окружения NETPATH и далее вместо функций netconfi g, описанных в предыдущем разделе, применяются следующие функции:
#шс1 ude <netconfig h> void *setnetpath(void).
Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки struct netconfig *getnetpath(void ★handle).
Возвращает непустой указатель в случае успешного выполнения, NULL в случае конца файла int endnetpath(void * handle)
Возвращает 0 в случае успешного выполнения. -1 в случае ошибки
Например, мы можем установить значение переменной окружения, используя KornShell, таким образом:
export NETPATH=udp tcp
Если, используя это значение, мы напишем следующую программу:
void * handle struct netconfig *nc handle = setnetpathO while ( (nc = getnetpath(handle)) i= NULL) {
/* выводим структуру netconfig */
}
endnetpath(handle)
будут выведены только две строки — одна для протокола UDP и следующая за ней для протокола TCP. Последовательность возвращенных нам структур в этом случае соответствует последовательности протоколов в переменной окружения, а пе последовательности в файле netconfig.
Если переменная окружения NETPATH не установлена, все видимые записи будут возвращены в том порядке, который определен в файле netconfig.
Функции netdir
Функции netconfig и netpath позволяют найти нужный протокол. Нам также необходима возможность поиска имени узла и имени службы на основе протокола, выбранного нами с помощью функции netconfig или netpath. Это позволяет сделать функция netch r_getbyname.
#include <netdir h>
mt netdir_getbyname(const struct netconfig *ncp const struct nd_hostserv *hsp struct nd^addrlist **d『pp)
Возвращает 0 в случае успешного выполнения ненулевое значение в случае ошибки void netdir_fгее (void *ptr int type)
Первая функция преобразует имя узла и имя службы в адрес. Ее аргумент пер указывает на структуру netconfig, которая была возвращена функцией getnetconfig или getnetpath. Мы также должны заполнить структуру nd_hostse「v значениями имени узла и имени службы, а затем передать указатель на эту структуру в качестве второго аргумента.
struct nd_hostserv {
char *h_host./* имя узла */
char *h_serv, /* имя службы */
}• 一
Третий аргумент указывает на указатель на структуру nd_addrlist, и в случае успешного завершения функции указатель *а!рр содержит указатель на одну из структур следующего типа: struct nd_addrlist {
intn_cnt /* число буферов netbufs */
struct netbuf *n_addrs /* массив netbufs содержащий адреса */ }_
Заметим, что структура nd_addrli st указывает на массив из одной или нескольких структур netbuf, каждая из которых содержит один из адресов узла. Напомним, что узел может быть многоинтерфейсным.
Рассмотрим пример, аналогичный представленному па рис. 11.1, где имя узла — bsdi (он имеет два IP-адреса), а имя службы — domain (для TCP и UDP порт 53). На рис. 29.1 показана информация, возвращаемая функцией netdi r_getbyname, в предположении, что используемая в качестве первого аргумента функции структура netconfig содержит информацию для протокола TCP.
ПРИМЕЧАНИЕ—
Мы опять предполагаем, чю формат, использованный поставщиком услуг связи для представления адреса Интернета, определяется структурой sockaddr in Х01Я эго достаточно расиросграненный вариант, он, тем не менее, не является обязательным.
Последний аргумент функции netdi r_getbyname в этом примере будет указателем на нашу переменную alp.
Закончив работу с этими динамически создаваемыми структурами, мы вызываем функцию netdi「free с аргументом ptr, указывающим на структуру nd_addrl i st, и аргументом type, равным ND_ADDRLIST.
Обратное преобразование, когда задана структура netbuf, содержащая адрес, а нужно получить имя узла и имя службы, осуществляется функцией netdi г_ getbyaddr.
#include <netdiг h>
int netdir_qetbydddr(const struct netconfig *〃cp struct nd_hostservlist **hslpp const struct netbuf *addr}
Возвращает 0 в случае успешного выполнения ненулевое значение в случае ошибки
Первый и третий аргументы — это входные параметры: указатель на структуру netconfig и указатель на структуру netbuf. Результатом является указатель на структуру nd_hostsevl i st, и этот указатель записывается в *hsl рр.
struct nd_hostservlist {
inth_cnt /* число nd_hostservs */
struct ndjostserv *h_hostservs /* пары имя узпа / имя службы */ }-
Эта структура, в свою очередь, указывает на массив из одной или нескольких структур nd_hostserv. Память для структуры nd_hostservlist, для массива струк-
тур nd_hostserv, на который указывает первая структура, и для строк, содержащих имена узла и службы, на которые указывает вторая структура (или структуры), выделяется динамически. Эта память освобождается вызовом функции netch「_ free, параметру type которой присваивается значение ND HOSTSERVLIST.
Функции t_alloc и t_free
Одно из требований независимости системы функций API от используемого протокола сводится к тому, чтобы функции умели определять размер адреса протокола, не зная точно формата адреса. Для функций сокетов этот размер задается элементом ai_addrl еп структуры addrinfo, значение которой возвращается функцией getaddri nfo (см. раздел 11.2). Для протокола XTI это значение дает элемент addr структуры t_info, которую возвращает функция t_open.
Следующим шагом после получения этого размера является динамическое размещение необходимых структур в памяти. В случае сокетов мы должны заботиться только о структурах адресов сокетов» и мы просто вызываем функцию та 11 ос, когда это необходимо (см„ например, листинг 27.2). А вот для XTI существует шесть структур (см. табл. 28 5), каждая из которых содержит одну или несколько структур netbuf. Структуры netbuf указывают на буфер, размер которого зависит от размера адреса протокола (например, элемент addr структуры t_call в разделе 28.6). Для упрощения динамического размещения в памяти структур XTI и содержащихся в них структур netbuf служат функции t_alloc и t_f「ee.
void *t_dlloc(int fd mt struct type mt fields)
Возвращает непустой указатель в случае успешного выполнения NULL в случае ошибки mt t_f гее (void *ptr mt structtype)
Возвращает 0 в случае успешного выполнения -1 в случае ошибки
Аргумент structtype указывает, для какой из семи структур XTI память должна быть динамически выделена или освобождена. Он должен принимать значение одной из констант, приведенных в табл. 29.2.
Таблица 29.2. Значения аргумента structtype функций t_alloc и t__free для разных структур
structtype
Тип структуры
TJBIND
struct t bind
T_CALL
struct tcall
T_DIS
struct t discon
T_INFO
struct t_info
T—OPTMGMT
struct t_optnigmt
T_UDERR
struct t_uderr
T_UNITDATA
struct t—Unitdata
Аргумент fields дает нам возможность указать, что память под одну или несколько структур netbuf тоже должна быть выделена и инициализирована соответствующим образом. Его значение определяется применением побитового ИЛИ к константам, выбранным из табл. 29.3. Напомним, что, согласно табл. 28.5, объекты структурного типа netbuf всегда имеют имена addr, opt или udata.
Таблица 29.3. Значения аргумента fields функций t_alloc
fields
Выделить память и инициализировать1
T_ALL
Все поля заданной структуры
T_ADDR
Поле add г структур tbind, t_call, tuderr или t_umtdata
торт
Поле opt структур t_optmgmt, t_call, t_uderr или t_u nitdata
T_UDATA
Поле udata структур t_call, t_discon или t_umtdata
Причина, по которой введены эти значения аргумента fields, в том, что некоторые структуры XTI содержат более чем одну структуру netbuf, и для нас может оказаться лишним выделять память под все буферы. Например, структура t_cal 】, которую мы показали в разделе 28.6, содержит три структуры типа netbuf: struct t_call {
struct netbuf
addr
/* адрес специфичный для протокола */
struct netbuf
opt
/* параметры специфичные для протокола*/
struct netbuf
udata.
/* пользовательские данные сопровождающие за叩ос на соединение */
wt
sequence
/* для функций t_listenО и t_accept() */
}
Возможность задания некоторой комбинации значений T_ADDR, Т_ОРТ и TJJDATA дает нам полное управление процессом выделения памяти. Обычно в наших примерах мы будем использовать значение T_ALL как простейшее решение. (См. также упражнение 29.2.)
Если вызвать функцию t_alloc, задав аргумент fd соответствующим точке доступа протокола TCP, аргумент structtype равным T_CALL, а аргумент f 】el ds — равным T_ALL, и учесть значения, приведенные в табл. 28.1 для AIX 4.2, то в результате мы получим распределение памяти, показанное на рис. 29.2.
t_call{}
addr.maxien
16
netbu£{}■
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
Этот вызов функции t_alloc выделяет память для структуры t eal 1, содержащей три структуры типа netbuf. Один буфер выделен под зависящий от протокола адрес (элемент addr), другой под зависящие от протокола параметры (элемент opt). Два указателя на буферы инициализированы вместе со значениями элементов maxlen, а два значения элементов 1еп равны нулю. Третья структура netbuf не используется для протокола TCP (данные пользователя, сопровождающие запрос на соединение), следовательно, две длины в элементе udata нулевые, а указатель на буфер имеет значение пустого указателя.
Функция t_free освобождает память, которая была предварительно выделена функцией t_al 1ос. Аргумент structtype задает тип структуры, для чего используются константы, показанные в табл. 29.2. Функция t_free не только освобождает память, которая была выделена для структуры, заданной параметром structtype, но и проверяет предварительно все структуры netbuf, содержащиеся внутри нее, и освобождает память, используемую под буферы. В нашем примере на рис. 29.2 функция t_f гее сначала освободит память, отведенную для двух буферов, а затем уже память, занятую под структуру t_ca】1.
Функции t_getprotaddr
Функция t_getprotaddr возвращает локальный и удаленный адреса протокола, связанные с точкой доступа.
include <xti h>
int t_getprotaddr(int fd, struct t_bind ★localdddr. struct t_bind *peeraddr),
Возвращает 0 в случае успешного выполнения• -1 в случае ошибки
Эта функция использует элементы addr структур netbuf, соответствующие двум структурам t_bi nd. Когда функция вызывается, элементы maxlen и buf структур netbuf определяют, куда результат должен быть записан. Значение maxi еп, равное нулю, указывает, что значение соответствующего адреса не должно возвращаться. При завершении функции элементы 】еп структур netbuf содержат размер адресов, которые были сохранены в элементах buf. Это значение будет равно нулю для локального адреса, если он до сих пор не был связан, и будет равно нулю для адреса собеседника, если точка доступа еще не присоединена.
ПРИМЕЧАНИЕ
Протокол TLI имел недокументированную функцию t getname, которая могла возвращать оба адреса протокола — локальный и удаленный.
Функция t gctprotaddr является комбинацией двух функций: getsockname и getpeername. Если мы интересуемся только одним из двух адресов, мы все равно должны выделить память для структуры t_bind второго адреса и установить для нее нулевое значение элемента maxlen. Более простое решение позволило бы задавать пустой указатель в качестве значения любого из двух аргументов функции, если мы нс хотим, чтобы значение этого адреса возвращалось функцией.
Функция xti_ntop
Нам необходим простой способ для вывода адреса протокола XTI (более простой, чем использование функции netdi rjgetbyaddr). Для этого мы напишем нашу собственную функцию xti_ntop, аналогичную нашей функции sock_ntop из раздела 3.8. Большинство реализаций протокола XTI предоставляют две функции taddr2uaddr и uaddr2taddr. Аббревиатура taddr соответствует транспортному адресу (transport address^ содержащемуся в структуре netbuf, a uaddr — универсальному адресу (universal address), то есть текстовой строке, хранящейся в виде заканчивающейся нулем строки С. Эти реализации выводят универсальные адреса IPv4 в виде шести десятичных чисел, разделенных пятью десятичными точками,
где первые четыре числа — зто адрес IPv4 в точечно-десятичной записи, а последние два числа — это 2-байтовый номер порта протоколов TCP или UDP.
Тем не менее с применением этих двух функций связана проблема, состоящая в том, что они требуют передачи аргумента для структуры netconfig, дающего информацию о протоколе, адрес которого преобразуется. Но адреса протокола XTI для IPv4 и IPv6 являются самоопределяющимися. Например, когда адреса IPv4 хранятся в структуре netbuf, адрес на самом деле представляет собой настоящую структуру адреса сокета, первым элементом которой является имя семейства адресов — AF_INET. Длина этой структуры netbuf составляет 16 байт (см., например, строку addr в табл. 28.1 и 28.2). Мы предполагаем, что адреса в IPv6 будут храниться как структуры sockaddr i пб, с именем семейства AF_I NET6 и длиной 24 байта. У нас нет гарантии, что все адреса XTI, записанные в структуры netbuf, будут самоопределяющимися, но адреса IPv4 и IPv6 отвечают этому требованию.
ПРИМЕЧАНИЕ
Термин «самоопредсляюшиеся», может быть, слишком сильный. Для некоторых других протоколов возможно использование 16-байтового адреса, первые 2 байта которого окажутся равными константе AF NET. Но на практике это не будет составлять проблемы.
Нам следует выбрать, каким образом передать нашей функции адрес протокола. Сначала рассмотрим возможность использовать одну из структур вида t_XXX протокола XTL Но для клиентов адрес протокола сервера находится в элементе 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. Мы нс приводим исходный код, но он находится в свободном доступе (см. предисловие).
Функция tcp_connect
Теперь мы можем скомбинировать функцию getnetpath, которая возвращает информацию об одном или нескольких протоколах, с функцией netdi r_getbyname, 1ГПТППЙ«Г LflTIPT ЫМРНЯ vsna и службы гпстаи Ы ПРПРПРПйТЬ H2】nv rbVHRnWTO trn ccnnuct
из раздела 118 так, чтобы использовать протокол XTI вместо сокетов и функции getaddrinfo Мы показываем получившуюся функцию в листинге 29 I1
Листинг 29.1 ・ функция tcp_connect для протокола XTI
//11bxti/tcp_connect с
include unpxti h
int
3456789101112131415161718192021222324252627282930313233343536373839404142434434567891011121314151617181920212223242526272829303132333435363738394041424344tcp_connect(const char *host const char *serv)
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
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
int tfd 1 void *handle struct t_call tcall struct t_discon tdiscon struct netconfig *ncp struct njhostserv hs struct nd_add「l】st *d1p struct netbuf *np handle = Setnetpath() hs h_host = (char *) host hs h_serv = (char *) serv while ( (nep = getnetpath(handle)) ,= NULL) { if (strcmp(ncp >nc_proto tep ) ,= 0) continue
if (netdir_getbyname(nep &hs &alp) ,= 0) continue
/* 4 проверки для каждого адреса сервера */
for (1=0 пр = alp >n_addrs i < alp >n_cnt i++ np++) { tfd - T_open(ncp >ncjdevice O_RDWR NULL) T_bind(tfd NULL NULL) " tcall addr len = np >len tcall addr buf = np >buf /* копия указателя */ tcall opt len = 0/* нет параметров */
tcall udata len = 0/* нет данных
пользователя при соединении */
if (t_connect(tfd &tcall NULL) == 0) { endnetpath(handle)/* успешное
соединение с сервером */ netdir_free(alp ND_ADDRLIST) return (tfd)
}
if (t_e「「no == TLOOK && t_look(tfd)==
- T_DISCUNNECT)-{ t_rcvdis(tfd &tdiscon) errno = tdiscon reason } t_close(tfd)
netdir_free(alp ND_ADDRLIST)
}- 一
endnetpath(handle)
return ( 1)
Все исходные коды программ опубликованные в этой книге вы можете найти по адресу http // www niter ccm /download
Инициализация
13 15 Функция setnetpath открывает файл net config Структура nd_hostserv инициализируется значениями указателей на имя узла и имя службы
Получение следующей строки из файла netconfig
16-18 Функция getnetpath ищет в файле netconfig следующий протокол из переменной окружения NETPATH Если это не протокол TCP, мы игнорируем строку Так как мы ищем только строки для протокола TCP, мы могли бы вызвать функцию таким образом
nep = getnetconfigent( tep )
чтобы определить именно эту строку Последующий вызов функции
freenetconfigent(nep)
освободит память, выделенную функцией getnetconfigent Но так как мы хотели бы, чтобы этот код работал и с IPv6, мы организуем цикл, который будет просматривать каждую структуру netconfig В настоящий момент неизвестно, как будет выглядеть строка в файле netconfig для протокола TCP в версии IPv6 и как XTI-функции определения имени будут работать в случае IPv6
Поиск имен узла и службы
19 20 Функция netdi r_getbyname ищет имена узла и службы, используя структуру netconfig, возвращенную функцией getnetpath
Перебор всех адресов серверов
21 28 В этом цикле проверяется каждый возвращенный адрес сервера посредством вызовов функций t_open, t_b】nd и t_connect до тех пор, пока не будет установлено соединение или не будут перебраны все адреса Структура t_cal 1 инициализируется значением из структуры типа netbuf, возвращаемой функцией netdi r_getbyname
Соединение установлено
29 33 Если соединение успешно установлено, то производится очистка памяти и возвращается значение дескриптора соединения Функция endnetpath освобождает память, выделенную для структуры netconfi g, и закрывает файл netconfi g, а функция netdi「_free освобождает все области памяти, начиная со структуры nd_addrlist (см рис 为 1)
Обработка ошибок функции t_connect
34 38 Если функция t_connect завершается аварийно, то мы проверяем значение TLOOK кода ошибки и при отказе в соединении вызываем функцию t_rcvdis Мы присваиваем переменной еггпо значение, зависящее от используемого протокола, чтобы дать возможность пользователю диагностировать ситуацию Точка доступа закрыта
Завершение работы со всеми адресами
40-41 После того как все адреса проверены, память, занимаемая структурой nd_addrl i st и массивом структур типа netbuf, на которые структура указывает, освобождается функ ией netdiг free Цикл while будет продолжать работать, продвига-
ясь по файлу netconfig и, возможно, находя дополнительные протоколы для проверки.
ПРИМЕЧАНИЕ ——
Функция getaddnnfo объсдиняег вызов функции gctnetpath (поиск соо гве j сi вующего протокола или семантики) с вызовом функции netclii^getbynamc.
Точка доступа протокола XTI, которой не удалось установить соединение, все же может быть использована в другом вызове функции t_connect, то есть мы можем вынести обращения к функциям t_call и t bind из цикла for, обращаясь к этим двум функциям по одному разу в каждом проходе тела цикла while. Конечно, ири этом мы также вынесем вызов функции t_closc из цикла for. Но когда вызов функции connect завершается неудачно при использовании сокетов, сокет не может более использоваться и должен быть закрыт (см., например, листинг tl 2)
Но в таком подходе к XTI есть одна коварная проблема. Проблема возникает в том случае, когда узел, с которым мы пытаемся соединиться, имеет несколько адресов, а попытка установить соединение с ним завершается неудачно. В этом сценарии значение локального порта никогда не изменяется, и каждое новое обращение к функции t_connect с целью получения следующего адреса задерживается на величину экспоненциального смещения относительно времени предшествующего обращения, так как все эти попытки установления соединения делаю 1ся из одной i【юй же локалыю11 точки доступа. Таким образом, если первое обращение к t call пе сработало, то следующее обращение может быть задержано на одну секунду, а если и эта попытка будет псудач- пой, то следующее может быть задержано уже иа две секунды, и т д. Чтобы избежать этой трудности, мы будем закрывать точку доступа функцией t closc, когда функция t_connect завершается неудачно, а за гем создавать новую точку доступа для следующего обращения к функции t connect.
Пример
Сейчас мы воспользуемся нашей функцией t_connect и переделаем программу клиента времени и даты из листинга 11.3, независимого от протокола, применяя технику XTI вместо сокетов. Наша ХТ1-версия представлена в листинге 29.2.
Листинг 29.2. Не зависящий от протокола клиент времени и даты
//xtiintro/daytimecli02 с
1 #include "unpxti h”
2 3 4 5 6 7 82 3 4 5 6 7 8int
2 3 4 5 6 7 8
2 3 4 5 6 7 8
main(int argc, char **argv)
f
inttfd. n, flags
charrecvline[MAXLINE + 1):
struct t_bind *bound *peer. struct t_discon tdiscon.
9 if (argc i= 3)
10err_qmt( "usage daytimecli02 <hostname/1Paddress>
<service/port#>").
11 tfd = Tcp^connect(argv[1], drgv[2]).
12 bound = T alloc(tfd T BIND T ALL).
14 T_getp「otaddr(tfd bound, peer),
15 pnntf("connected to %s\en". Xti_ntop(&peer->addr)),
for (..) {
if ( (n = t_rcv(tfd. recvline, MAXLINE &flags)) < 0) {
18 if (t_errno == TLOOK) {
19if ? (n = T_look(tfd)) — TJRDREL) {
20T_rcvrel (tfd).
21break
22} else if (n == T_DISCONNECT) {
23T_rcvdis(tfd, &Ediscon),
24errno = tdisconreason /* вероятно.
ECONNRESET */
25err_sys(Mserver terminated prematurely"):
26} else
27err_quit("unexpected event after t_rcv %d". n):
28} else-
29err_xti("trcv error").
30} 一 —
31 recvline[n] = 0/* завершающий нуль */
32fputs(recvline stdout).
33}
34 exit(0)
35 }
Установление соединения
И Вызываем нашу функцию tcp^connect из листинга 29.1 для поиска имени узла, имени службы и установления соединения.
Вывод адреса протокола собеседника
12-15 Выделяем память для двух структур t_bi nd и вызываем функцию t_getprotaddr, чтобы получить локальный адрес протокола и адрес протокола собеседника. Выводим адрес собеседника, вызывая нашу функцию xti_ntop.
Чтение данных с сервера до получения EOF
16-33 Чтение данных с сервера выполняется идентично коду в разделе 28.11.
Мы можем запустить программу следующим образом:
unixware % daytimeci102 aix daytime
connected to 206 62 226 43 13
Fri Feb 7 13 28 24 1997
Резюме
В реализациях протокола XTI для SVR4 выбор сетевого соединения обычно реализуется с помощью файла /etc/netconfig с последующим поиском функцией netdi r_getbyname имен узла и службы, в результате чего получается массив структур типа netbuf, по одной на адрес и службу. Это аналогично использованию функции getaddnnfo в главе И. Обратное преобразование от адреса протокола к формату представления осуществляется функцией netdi r_getbyaddr, которая аналогична функции getnameinfo,
В связи с тем, что в XTI используется так много структур ——семь структур типа t_XXX со структурами netbuf, содержащимися в них, — в API протокола ХТ1
предоставляются две функции для динамического выделения и освобождения памяти для этих структур: t_al 1ос и t_free.
Упражнения
Функция getnetconf 1 g возвращает указатель на структуру, которую она заполняет, аналогично функции gethostbyname. Но мы сказали, что последняя функция не является безопасной в многопоточной среде. Является ли функция getnetconfig безопасной, и если да, то как она достигает этого?
Напишите программу, которая вызывает функцию t_a И ос дважды для структуры t_cdl 1 для точки доступа TCP. При первом обращении задайте третьему аргументу значение T_ALL, а при втором — значение T_ADDR | Т_ОРТ | T_UDATA. Что произойдет?
Почему функция t_free требует аргумента structtype?
Почему в листинге 29.1 мы не инициализировали структуру nd_hostserv следующим образом:
struct nd_hostserv hs = { host, serv }.
ГЛАВА 30
XTI: TCP-серверы
Введение
Несомненно, наиболее сложным аспектом 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_bindLISTEN (пассивное открытие) t_listen (блокировка)t_listen завершаетсяСервер принимает соединение t_open, t_bind, t_acceptt_open, t_bindLISTEN (пассивное открытие) t_listen (блокировка)t_listen завершаетсяСервер принимает соединение t_open, t_bind, t_accept
Клиент
Клиент
t_open, t_bind
LISTEN (пассивное открытие) t_listen (блокировка)
t_listen завершается
Сервер принимает соединение t_open, t_bind, t_accept
t_open, t_bind
LISTEN (пассивное открытие) t_listen (блокировка)
t_listen завершается
Сервер принимает соединение t_open, t_bind, t_accept
Рис. ЗОЛ, Предполагаемая модель XTI принятия запроса на соединение
Обратите внимание на вызовы функций на стороне сервера: первый вызов функции t_bi nd (с ненулевым аргументом qlen) указывает, что точка доступа будет принимать входящие соединения, функция t_listen завершается, когда соединение становится «доступно» (об этом мы поговорим чуть позже), а затем сер-
вер должен вызывать функции t_open, t_bind и t_accept, чтобы принять соединение. С точки зрения TCP точка доступа находится в состоянии LISTEN (см. рис. 2.4).
Когда процесс сервера получает извещение о запросе на соединение, он имеет возможность не принимать это соединение, вызвав функцию t snddis для отклонения запроса. Этот вариант проиллюстрирован на рис. 30.2. Здесь сервер информируется о прибытии сегмента SYN (сообщение о соединении), в результате чего сервер решает не принимать это соединение (возможно, основываясь на IP- адресе клиента, на номере порта или на пользовательских данных, присланных вместе с запросом на соединение, если этот параметр поддерживается протоколом). Затем приложение вызывает функцию t_snddis, в результате чего вместо завершения трехэтапного рукопожатия отсылается сегмент RST. В итоге вызов функции t_connect на стороне клиента возвратит ошибку.
t_open, t_bind t_connect (блокировка)t_open, t_bind t_connect (блокировка)t_connect завершаетсяt_connect завершаетсяt_open, t_bindLISTEN (пассивное открыуие) t_listen (блокировка)t_open, t_bindLISTEN (пассивное открыуие) t_listen (блокировка)Клиент
t_open, t_bind t_connect (блокировка)
t_open, t_bind t_connect (блокировка)
t_connect завершается
t_connect завершается
t_open, t_bind
LISTEN (пассивное открыуие) t_listen (блокировка)
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) использовалась техника отложепиого приема.
В обоих случаях был изменен фактически ставший стандартным метод возвращения из функции t_listen по завершении трехэтаиного рукопожатия. Одной из причин изменения было то, что отложенный прием вызывает сбой в большинстве реализаций протокола FTP.
В Posix.lg также требуется, <1тобы успешное завершение функции t listcn для точки доступа TCP указывало бы на установление соединения, а нс служило бы сообщением о соединении.
В [44] говорится, что в 4.4BSD предполагалось включить параметр, который позволял бы каждому сокету в отдельности осуществлять отложенный прием для TCP, по это не было реализовано. 4.4BSD поддерживает отложенный прием для протоколов OS1.
Функция tjisten
Обычным сценарием для XTI-сервера, ориентированного на соединение, является вызов следующих функций:
listenfd = t_open( ).t_bind(listenfd. ).listenfd = t_open( ).t_bind(listenfd. ).for ( . . ) {t_listen(listenfd. ): connfd ^ t_open( ),t_bind(connfd, NULL. NULL);t[accept(listenfd. connfd.for ( . . ) {t_listen(listenfd. ): connfd ^ t_open( ),t_bind(connfd, NULL. NULL);t[accept(listenfd. connfd.\&\&/* создание прослушиваемой точки доступа *//* t_tnnd qlen > 0 *//* блокируется в ожидании соединения *//* создаем новый дескриптор fd для присоединенной точки доступа *//* любой локальный адрес */)./* прием на новом дескрипторе fd *//* обработка прмсое口иненно讶 точки доступа *//* создание прослушиваемой точки доступа *//* t_tnnd qlen > 0 *//* блокируется в ожидании соединения *//* создаем новый дескриптор fd для присоединенной точки доступа *//* любой локальный адрес */)./* прием на новом дескрипторе fd *//* обработка прмсое口иненно讶 точки доступа */
listenfd = t_open( ).
t_bind(listenfd. ).
listenfd = t_open( ).
t_bind(listenfd. ).
for ( . . ) {
t_listen(listenfd. ): connfd ^ t_open( ),
t_bind(connfd, NULL. NULL);
t[accept(listenfd. connfd.
for ( . . ) {
t_listen(listenfd. ): connfd ^ t_open( ),
t_bind(connfd, NULL. NULL);
t[accept(listenfd. connfd.
\&
\&
/* создание прослушиваемой точки доступа */
/* t_tnnd qlen > 0 */
/* блокируется в ожидании соединения */
/* создаем новый дескриптор fd для присоединенной точки доступа */
/* любой локальный адрес */
)./* прием на новом дескрипторе fd */
/* обработка прмсое口иненно讶 точки доступа */
/* создание прослушиваемой точки доступа */
/* t_tnnd qlen > 0 */
/* блокируется в ожидании соединения */
/* создаем новый дескриптор fd для присоединенной точки доступа */
/* любой локальный адрес */
)./* прием на новом дескрипторе fd */
/* обработка прмсое口иненно讶 точки доступа */
t_close(connfd),
}一
Функция t_li sten обычно блокируется в ожидании соединения от клиента.
#include <xti h>
int t_listen(int fd. struct t_call *call),
Возвращает 0 в случае успешного выполнения -1 в случае ошибки
Мы уже описывали структуру t call, обсуждая функцию t_connect, йо пока
жем ее здесь снова:
struct t_call {
struct netbuf
addr.
/* адрес. специфичный для протокола */
struct netbuf
opt.
/* параметры, специфичные для протокола*/
struct netbuf
udata,
/* пользовательские данные, сопровождающие запрос на соединение */
int
sequence.
/* для функций t listen() и t_accept() */
}
Структура, возвращаемая через указатель call, содержит интересующие нас параметры соединения: addr — это адрес протокола клиента, opt может содержать любые специфичные для протокола параметры, a udata — любые пользовательские данные, которые были посланы вместе с запросом на соединение (эта возможность не поддерживается протоколом TCP). Переменная sequence содержит некоторое уникальное значение, идентифицирующее данный запрос на соедине-
нне. Это значение будет использовано при вызове функции t_accept (или t_snddis) для идентификации принимаемого (или отклоняемого) соединения.
ПРИМЕЧАНИЕ
Хотя эта функция кажется аналогичной функции accept, она на самом деле отличается от нее, так как функция t_listen только ждет прибытия соединения и не занимается приемом соединения. Для приема соединения следует вызывать функцию t_accept.
Переменная sequence является петюимслешю宓,но в некоторых реализациях это поле структуры t call используется для записи адреса. Не стоит предполагать, что значение этой переменной — небольшое целое число типа дескриптора.
Функция tcpjisten
Теперь мы переходим к созданию нашей собственной функции (листинг ЗОЛ1), создающей прослушиваемую точку доступа, через которую можно было бы принимать входящие соединения. Вызывающая последовательность идентична той, которая имела место для функции с тем же именем, показанной в листинге 11.4.
Листинг 30.1. Функция XTI tcpjisten: создание прослушиваемой точки доступа
//libxti/tcpjisten с
include "unpxti h"
#mcl ude <limits h>/* PATH_MAX */
charxti_serv_dev[PATH_MAX + 1].
int
tcpjisten(const char *host const char *serv. sock1en_t *add「1enp)
( ~~
int listenfd.
void*handle.
char*ptr.
structtjbindtbind
structt_】nf。tinfo.
structnetconfig *ncp.
structnd_hostse「v hs.
structnd_add「1ist *alp;
structnetbuf *np.
handle= SetnetconfigO:
hs h_host = (host == NULL) ? HOST_SELF ; (char *) host;
hs h_serv = (char *) serv.
19 while ( (nep = getnetconfig(hand1e)) !■ NULL &&
20 strcmp(ncp->nc_proto. utcpH) != 0):
21 if (nep == NULL)
22 return (-1).
23 if (netdir_getbyname(nep. &hs. &dlp) >= 0) {
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу Ifttp:〃 www.piter.com/download.
24endnetconfig(handle),
25return (-2).
26}
np = alp->n_addrs. /* используем первый адрес */
bstenfd = T_open(nep->nc_device. O_RDWR, &tinf。).
29st rnepy(xti _serv_dev. ncp->nc_device, sizeof(xti_serv_dev)).
30 tbind add「 = *np,/* копируем всю структуру netbuf{} */
31/* можем заменить константу LISTENQ переменной окружения */
32 if ( (ptr = getenvCLISTENQ")) '= NULL)
33tbind qlen = atoi(ptr).
34 else
35tbind qlen = LISTENQ,
36T_bind(listenfd. &tbind NULL).
37netdir_free(alp ND_ADDRLIST).
38 endnetconfig(handleT,
39 if (addrlenp)
40 *addrlenp = tinfo addr. /* размер адреса протокола */
41 return (listenfd)
42 }
Инициализация
16-18 Функция setnetconfig открывает файл /etc/netconfi g. Если аргумент host является пустым указателем, мы передаем функции netdi rjetbyname специальную строку HOST_SELF. В результате этого прослушиваемый сокет связывается с универсальным адресом (в случае IPv4 это 0.0.0.0).
Поиск подходящего протокола
19-22 Мы обрабатываем каждую строку файла /etc/netconfig в поисках протокола TCP. Обратите внимание, что в данном случае мы используем функцию get- netconfig для сервера, в то время как в листинге 29.1 мы вызывали функцию getnetpath для клиента. Это делается для того, чтобы сервер не предположил, что переменная окружения NETPATH имеет какое-либо осмысленное значение, так как сервер может быть запущен через сценарий инициализации или из командной строки. Клиенты, напротив, обычно запускаются в интерактивном режиме из интерпретатора команд от имени пользователя, поэтому им можно позволить предположить, что эта переменная устанавливается пользователем.
Поиск имени узла и имени службы
23-27 Функция netdi г jetbyname ищет имя узла и имя службы, используя указатель на структуру netconfig для искомого протокола.
Открытие устройства
?8-29 Функция t_open открывает соответствующее устройство (например, /dev/tep), и копия имени этого устройства сохраняется во внешней переменной xti_serv_dev. Это делается потому, что процессу, вызывающему функцию tcp_l i sten, необходимо снова вызывать функцию t_open для каждого соединения, и ему потребуется имя этого устройства для поддержания независимости от протокола. В случае
сокетной версии функции tcp_l i sten нам не нужно было делать ничего подобного, так как функция accept (а не процесс) автоматически создавала новый сокет для каждого соединения.
ПРИМЕЧАНИЕ
Описанная технология не является безопасной в многопо!очной среде. Этот нежелательный побочный эффект является результатом требования о сохранении приложением информации о состоянии (имени устройства) в промежутке между вызовом функции t open для прослушиваемого дескриптора и последующими вызовами функции t_open для каждого присоединенного дескриптора. Одним из способов сделать эту операцию безопасной в многопоточной среде является вызов функции strdup, которая скопирует имя устройства в динамически выделенную облаегь памяти, после чего указатель возвратится через другой аргумент функции tcp listen, с тем чтобы вызывающий процесс мог вызвать функцию free и освободить выделенную ранее память
Переход в состояние TCP LISTEN
)-36 Мы вызываем функцию t_bi nd, связывая адрес, возвращенный функцией netdi г_ getbyname, с точкой доступа. Устанавливая поле qlen структуры t_bind в ненулевое значение, мы указываем, что это — прослушиваемая точка доступа, и в случае TCP эта точка доступа входит в состояние LISTEN. (В данном случае мы говорим о состоянии LISTEN протокола TCP. В случае XTI это состояние называется T IDLE.) Теперь входящие соединения будут приниматься поставщиком транспортных служб. Мы заменяем значением переменной окружения LISTENQ значение этой константы из нашего заголовочного файла unp.h, заданное по умолчанию. Аналогичный код можно найти в листинге 4Д, где приводится функция- обертка Li sten для функции сокетов 11 sten.
Освобождение памяти, возвращение значений
7-41 Мы вызываем функции netdi r_free и endnetconfig для освобождения выделенной области памяти. Мы возвращаем размер адресов протокола (если требуется), а возвращаемым значением функции является дескриптор прослушиваемой точки доступа.
Обратите внимание, что мы не вызываем функцию t_] i sten, так как это привело бы к блокированию сервера в ожидании входящего соединения.
Функция t_accept
Когда функция t_li sten сообщает о прибытии соединения, мы можем выбрать, принимать это соединение или отказаться от него. Если мы решаем принять это соединение, следует вызать функцию t_accept.
#include <xti h>
int t_accept(int hstenfd int connfd struct t_cal 1 *ca77)
Аргумент li stenfd указывает точку доступа, на которую прибыло соединение. Иначе говоря, это та точка доступа, которая была аргументом функции t_li sten. Аргумент connfd указывает точку доступа, в которой должно быть установлено соединение. Обычно сервер создаст новую точку доступа connfd для приема соединения.
Аргумент cal 1 указывает, какое соединение принимается в данный момент (это актуально в случае, когда принятия ожидают несколько соединений, о чем мы вскоре расскажем), а его значение — это то, что было возвращено функцией t_l i sten.
Обратите внимание на то, чю за создание точки доступа на стороне сервера отвечает сам сервер. Обычно это делается с помощью функции t_open в промежутке между вызовами функций t_li sten и t_dccept.
ПРИМЕЧАНИЕ
У нас также есть возможность задавать один и тот же дескриптор для listenfd и для connfcl. Иными словами, мы можем принимать новое соединение па прослушиваемой точке доступа. Но в таком случае мы получаем последовательный сервер — никакие другие соединения не могут быть приняты до тех нор, пока не будет закончена работ а с при ня гым соединением. В таком сценарии единственное осмысленное значение qlen — это 1. С учетом ограничений, свойственных этому сценарию, и необходимости для серверов в большинстве случаев работать одновременно с несколькими клиентскими соединениями, мы не будем исследовать примеры этого сценария.
Функция xti_accept
Теперь мы напишем простую функцию с именем xti_dccept для выполнения шагов, необходимых для приема соединения с использованием XTI. В общем случае нужно было бы написать для серверных приложений XTI код, подобный следующему:
11stenfd = Tcp_listen( )./* создаем прослушиваемую точку доступа */
for ( .) {
connfd = Xti_accept(11stenfd, ... ). /* блокируемся. затем принимаем соединение */ /* обрабатываем connfd */
t_close(connfd)
} 一
Это похоже на код, применявшийся в случае использования сокетов, с той лишь разницей, что вместо функции accept используется xti_accept.
include "unpxti h"
mt xti_accept(mt listenfd struct netbuf ★cliaddr. int rdwr).
Возвращает неотрицательный дескриптор в случае успешного выполнения -1 в случае ошибки
В случае успешного выполнения возвращается новый присоединенный дескриптор. Адрес клиента возвращается в структуре netbuf, на которую указывает cl iaddr, и если аргумент「dwr ненулевой, для присоединенной точки доступа вызывается наша функция xti_「dw「.
В листинге 30.2 мы показываем простую версию пашей функции xti_accept. Мы называем ее простой, поскольку, как мы увидим вскоре, она может не сработать в случае, когда одновременно готовы несколько соединений. В разделе 30.8 мы увидим, как можно решить эту проблему.
Листинг 30.2. Простая версия функции xti_accept //libxti/xti_accept_simple с
2 int2 int#include "unpxti h"
2 int
2 int
продолжение 按
Листинг 30.2 (продолжение)
xti_accept(int listenfd. struct netbuf "cliaddr. int rdwr)
{-
intconnfd.
u_intn
structt_cal1 *tcallp.
teal Ip=~T_alloc(nstenfd. T_CALL. T_ALL).
T_listen(listenfd. teal Ip). /* blocks */
/* дальнейшее предполагает, что вызывающий процесс вызвал функцию tcp_listen() */
connfd = T_open(xti_se「v_dev. O_RDWR NULL).
T_bmd(connfd. NULL? NULL) —
T]accept(listenfd. connfd tealIp)
if (rdwr)
Xti_rdwr(connfd),
if (cliaddr) {/* возвращает адрес протокола клиента */
n = min(cl1addr->maxlen.tcallp->addrlen).
memcpyCcliaddr->buf.tcallp->addrbuf.n).
cliaddr->1en = n
}
T_free(tealIp T CALL).
return (connfd).
}
Ожидание соединения
卜9 В памяти выделяется место для структуры t_call, в которой будет храниться информация о соединении с клиентом. Функция t_l isten блокируется в ожида- пии соединения.и
Создание новой точки доступа и связывание с локальным адресом
D-12 Функция t_open создает новую точку доступа, используя полное имя, записанное во внешней переменной xti_serv_dev функцией tcp_l т sten. С этой точкой доступа связывается любой локальный адрес. Этот вызов функции t_bind является необязательным. Если мы не свяжем с точкой доступа никакого локального адреса, она останется неприсоединенной на момент вызова функции t_accept, и поставщик службы связи автоматически свяжет с ней какой-либо подходящий адрес.
Принятие соединения
13 Функция t_accept принимает соединение. Опа узнает, какое соединение нужно принимать, на основании значения элемента sequence структуры t_cdl】,которая была заполнена функцией t_li sten с целью идентификации этого конкретного соединения.
При необходимости даем разрешение на использование функций read и write
М5 Если вызывающий процесс задает ненулевое значение аргумента rdwr, наша функция xti_rdwr помещает модуль tirdwr в поток, позволяя тем самым использовать функции read и write вместо t_rcv и t_snd.
Возвращение адреса протокола клиента
16-20 Вызывающий процесс может задать непустой аргумент cliaddr, который указывает на структуру netbuf. Эта структура должна быть инициализирована вызывающим процессом так, чтобы указывать на буфер, в который записывается возвращаемый адрес протокола клиента. Мы проверяем, не переполним ли мы буфер вызывающего процесса, а затем присваиваем полю len размер возвоашае- мого адреса.
Освобождение занятой памяти и возвращение присоединенного дескриптора
21-22 Память, занимаемая структурой t_cal 1, освобождается, и возвращается*Ьрйсо- единенный дескриптор.
Простой сервер времени и даты
Перепишем наш простой сервер времени и даты, приведенный в листинге 11.6, с использованием XTI, вызывая функции tcp_listen и xti accept.
Листинг 30.3. TCP-сервер времени и даты, написанный с использованием XTI
//xtnntro/daytimesrvOl с
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include "unpxti h" int
main(int argc. char **argv)
{
intlistenfd. connfd.
charbuff[MAXLINE].
time__t ticks.
socklen_t addrlen.
struct netbuf clidddr.
if (argc == 2)
listenfd = Tcp_listen(NULL. argv[l], &addrlen).
else if (argc ==一3)
listenfd = Tcp_listen(argv[l]. argv[2], &addrlen), else
err_quit(Musage daytimetcpsrvOl [ <host> ] <service or port〉”): cliaddr buf = Malloc(addrlen), cliaddr maxlen = addrlen.
18
19
20
for (. ) {
connfd = Xti_accept(l1stenfd. &cliaddr, 0), pnntf("connection from 記\en". Xti_ntop(&clladdr)):
21
22
23
ticks = time(NULL).
snpnntf(buff. sizeof(buff), "^ 24s\er\en”. ct1me(&ticks)):
T_snd(connfd. buff, strlen(buff). 0):
24
25
26
T close(connfd).
}" }
Создание точки доступа
10-17 Функция tcp_l 1 sten создает прослушиваемую точку доступа. Мы выделяем память для адреса протокола клиента и для этого инициализируем структуру netbuf.
Ожидание соединения и его прием
19-20 Наша функция xti_accept ждет прибытия соединения, создает новую точку доступа, возвращает присоединенный дескриптор и возвращает IP-адрес клиента и номер порта. Мы выводим адрес протокола клиента, используя нашу функцию xti_ntop.
Генерируем вывод времени и даты
?1-24 Вызов функции time, а затем ctime генерирует получение значения времени и даты в текстовом формате, а функция t_snd отсылает эти данные обратно клиенту по установленному соединению. Точка доступа закрывается посредством вызова функции t_close.
Обратите внимание на то, что мы просто вызываем функцию t_close по завершении отправки данных. Так как в TCP имеется возможность нормального завершения, то отсылается сегмент FIN и осуществляется обычная последовательность закрытия соединения посредством обмена четырьмя пакетами (см. рис. 2.5), но функция t_close завершается сразу же.
ПРИМЕЧАНИЕ
Здесь имеется полная аналогия с вызовом функции close на сокете TCP.
Если бы мы хотели дождаться получения данных собеседником и прихода отправленного им сегмента FIN, мы должны были бы вызвать функцию t_snd「el для отправки своего сегмента FIN, а затем ждать сегмент FIN, который отправит собеседник, с помощью функции t_rcvrel. Тогда пришлось бы заменить вызов функции gelose в конце листинга 30.3 следующим кодом:
Tsndrel(connfd)
while ( (n = t_「cv(connfd buff MAXLINE &flags)) >= 0)
if (t_errno == TLOOK) {
if ( (n = TJook(connfd)) == T_ORDREL) {
I_rcvrel(connfd)
} eTse if (n == T_DISCONNECT) {
T_「cvdis(connfd. NULL)
} eTse
err_quit(1 unexpected event after t_rcv Xd". n):
} el se
err_xti('trev error')
T_close?connfdT.
Функция t_sndrel отсылает сегмент FIN, и далее мы должны ждать сообщения о нормальном завершении (orderly release indication), после чего можно вызвать функцию t_rcvrel. Для этого мы вызываем функцию t_rcv, игнорируя любые данные, которые могут прибыть.
ПРИМЕЧАНИЕ
Этот сценарий аналогичен вызову функции shutdown для сокета, после которого мы ждем, когда функция read возвратит конец файла (см. рис. 7.3).
В случае XTI мы также можем вызвать задержку выполнения функций t close или close, если в очереди для отправки клиенту остаются какие-либо данные, вместо того чтобы немедленно завершить соединение. Это осуществляется путем установки параметра XTI LINGER, который мы описываем в разделе 32 3. Он аналогичен параметру сокетов SOLINGER.
Несколько соединений, ожидающих обработки
Мы уже упоминали о сложностях, возникающих в случае, когда приблизительно в одно и то же время на прослушиваемую точку доступа прибывает несколько соединений. Для того чтобы продемонстрировать эти проблемы, мы вернемся к нашему TCP-серверу, представленному в листинге 27.2, Мы использовали этот сервер для измерения времени, затрачиваемого на управление процессом, для разных типов серверов. Мы можем запустить клиент, написанный для этого сервера (см. листинг 27.1), и задать количество дочерних процессов, которые будут порождаться функцией fork, устанавливая тем самым несколько соединений с сервером.
В листинге 30.4 показан сервер, который представляет собой повторение сервера из листинга 27.2, но использует XTI вместо сокетов.
Листинг 30.4. Параллельный сервер TCR демонстрирующий проблемы с несколькими одновременными соединениями
//xtiserver/servOl с
1
#】nclude "unpxti h"
2
mt
3
main(int argc. char **argv)
4
{
5
mt listenfd. connfd
6
pid_t childpid
7
void sig_chld(int) sig_mt(int). web_child(1nt>:
8
sockl en_t addrlen.
9
struct netbuf cliaddr
10
】f (argc == 2)
И
listenfd = Tcp_listen(NULL. argv[l] &addrlen).
12
else if (argc == 3)
13
listenfd = Tcp_listen(argv[l] argv[2]. &addrlen):
14
el se
15
err_quit("usage servOl [ <host> ] <Dort#>”)
16
cliaddr buf = Malloc(addrlen)
17
cliaddr maxien = addrlen
18
Signal(SIGCHLD sig_chld).
19
Signal(SIGINT. sig_int).
20
for (. ) {
21
connfd = Xti_accept(listenfd &cl?addr. 1),
22
pnntf("connection from Zs\en". Xti_ntop(&cl 1 addr))
23
if ( (childpid = ForkO) == 0) {/* дочерний процесс */
24
Close(listenfd). /* закрываем прослушиваемую точку доступа */
25
web_child(connfd). /* обрабатываем запрос */
26
exit(O)
27
}
28
Close(connfd) /* родительский процесс закрывает присоединенную точку доступа */
29
}■
30
}продолжение 少
Листинг 30.4 (продолжение)
void
sig_int(int signo)
зз {-
void xti_accept_dump(void);
xti_accept_dump().
exit(O),
}
-22 Мы вызываем наши функции tcp_1isten и xtl.accept, которые мы описали ранее в этой главе.
Обработчик сигнала SIGINT
-37 Наш обработчик сигнала вызывает внутреннюю функцию xti_accept_dump, и мы используем ее для получения значений счетчиков, показанных в табл. 30.1 (с. 872). Эта функция выводит значение элемента count каждой структуры cli (рис. 30.3).
Если мы запустим этот сервер для прослушивания на порте TCP 9999:
unixware 右 servOl 9999
и запустим клиент с другого узла приведенной ниже командой:
solans % 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_dccept. Когда по первому соединению завершено трехэтапное рукопожатие, завершается функция t_listen, а затем вызываются функции t_open и t_accept. Но когда для этого первого соединения вызывается функция t_dccept, для второго соединения завершается трехэтапное рукопожатие, и оно также готово к тому, чтобы быть принятым. Правила ХТ1 указывают, что в таком случае вместо завершения установления первого соединения функция t_accept возвращает ошибку со значением переменной t_errno, равным TLOOK (Event requi res attention — Событие требует внимания). Событие, ожидающее обработки, — это T_LISTEN (сообщение о подключении требует обработки), поскольку имеется еще одно соединение, ожидающее обработки (второе соединение).
Здесь происходит следующее: функция t_accept всегда возвращает ошибку, если имеется другое соединение, готовое к приему. В таком случае нужно вызвать функцию t_l 1 sten для получения всех сообщений о соединении, сохранить структуру t_call для каждого соединения, а затем для каждого соединения вызвать функцию t_accept.
ПРИМЕЧАНИЕ
Почему в XTI прием соединения осуществляется таким странным образом? Если функция t listen на самом деле завершается в момент прибытия сегмента SYN от клиента (см. рис. 30.1), то вызов функции t_listen сервером для всех прибывающих сегментов SYN, предшествующий вызову функции t_acccpt для какого-либо одного из прибывших соединений, дает процессу сервера возможность выбирать порядок приема ожидающих соединений. Сервер может задать этот порядок, например, на основании 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 не срабатывает, когда по соединению, ожидающему обработки, прибыл cerMeiirRST (функция t_l ook возвращает событие T_DISCONNECT). Для обработки описанных сценариев нам нужно поддерживать очередь событий, ожидающих обработки. Возможная длина этой очереди определяется значением qlim при вызове функции t_bind для прослушиваемой точки доступа. Существует множество структур данных, которые мы можем использовать для отслеживания соединений, ожидающих обработки. Для простоты мы будем использовать простой стек (массив) структур cli. Каждая структура содержит присоединенный дескриптор, диагностический счетчик для определения того, как часто используется данная структура, п указатель на структуру t_cal 1.
Предположим, что три клиента устанавливают соединения с нашим сервером примерно в одно и то же время. TCP сервера завершит все три последовательности трехэташюго рукопожатия, и когда функция t_listen возвратит первое из установленных соединений, мы используем для него структуру cl i [0] из нашего массива, как показано на рис. 30.3.
connfd — это новый дескриптор, созданный путем вызова функции t open* На нем будет осуществляться прием данного соединения, count — это диагностический счетчик, который мы вскоре исследуем с помощью тестовой программы, tcall ар — это указатель на структуру t_call, которая заполняется с помощью функции t_l 1 sten и затем передается функции t_accept. Она содержит адрес протокола клиента (поле add г) и идентификатор соединения (поле sequence). У нас также
cli[0]-cli[0]-ncl工=1ncl工=1Рис. 30.3. Структуры данных после того, как первое соединение с клиентом возвращено функцией tjistenРис. 30.3. Структуры данных после того, как первое соединение с клиентом возвращено функцией tjisten
cli[0]-
cli[0]-
ncl工=1
ncl工=1
Рис. 30.3. Структуры данных после того, как первое соединение с клиентом возвращено функцией tjisten
Рис. 30.3. Структуры данных после того, как первое соединение с клиентом возвращено функцией tjisten
имеется счетчик (ncli) количества элементов нашего массива структур cli, и его значение пока равно 1.
Предоложим, что мы вызываем функцию t_dccept для приема первого соединения, по она возвращает ошибку TLOOK, a t_l ook возвращает T.LISTEN. Тогда мы должны вызвать t_l i sten еще раз для получения нового соединения. Мы просто добавляем еще один элемент в наш массив структур cl 1 и увеличиваем на 1 значение счетчика ncl 1. Эта ситуация проиллюстрирована рис. 30.4.
cli[0]-cl±[l]-cli[0]-cl±[l]-・ncli«2・ncli«2Рис. 30.4. Структуры данных после того, как второе соединение с клиентом возвращено функцией t_listenРис. 30.4. Структуры данных после того, как второе соединение с клиентом возвращено функцией t_listen
cli[0]-
cl±[l]-
cli[0]-
cl±[l]-
・ncli«2
・ncli«2
Рис. 30.4. Структуры данных после того, как второе соединение с клиентом возвращено функцией t_listen
Рис. 30.4. Структуры данных после того, как второе соединение с клиентом возвращено функцией t_listen
Мы всегда вызываем функцию t_dccept для «последнего» элемента массива, индекс которого равен ncli-1. В результате мы обрабатываем соединения в порядке LIFO (Last In, First Out — последним пришел, первым обслужен), а не в порядке FIFO, как можно было предположить. Порядок обработки можно изменить, но это потребует некоторых усложнений.
ПРИМЕЧАНИЕ
В табл 4 5 показано, чю даже на умеренно загруженном web-ссрвере редко случаегся так, чго для приема приложением в одно и то же время гоюво более одного соединения. Следовательно, простота нашей схемы может счinaiься оправданной
В этот момент мы вызываем функцию t_accept для элемента массива cli[l], но предполагаем, что эта функция также возвращает ошибку TLOOK, a t_l ook возвращает T_LISTEN. Мы должны добавить в наш массив другой элемент, cli [2], как показано на рис. 30.5.
На данный момент у нас имеется три соединения, ожидающих приема, и теперь мы вызываем функцию t_dccept для элемента с11[2]. Но теперь предположим, чю первый клиент ([cliO]) только что разорвал соединение, отослав сегмент RST. Снова вызов функции t_accept не срабатывает и возвращается ошибка TLOOK, но на этот раз функция t_l ook возвращает ошибку T_DISCONNECT. Теперь нам нужно вызвать функцию t_rcvdi s для закрытия соединения. Вспомним, что одним
ncli=3ncli=3Рис. 30.5. Структуры данных после того, как третье соединение с клиентом возвращено функцией tjistenРис. 30.5. Структуры данных после того, как третье соединение с клиентом возвращено функцией tjisten
ncli=3
ncli=3
Рис. 30.5. Структуры данных после того, как третье соединение с клиентом возвращено функцией tjisten
Рис. 30.5. Структуры данных после того, как третье соединение с клиентом возвращено функцией tjisten
из компонентов структуры t_di scon, которую заполняет данная функция, является sequence ~ идентификатор разорванного соединения (см. раздел 28.10). Его следует использовать для поиска в нашем массиве структур cl i того элемента, у которого в структуре t_call имеется такой же идентификатор sequence. Затем мы сдвигаем элементы массива «вверх» один за другим, заменяя cli[0] Hacli [1], aclifl] на cl 1 [2]. Кроме того, мы уменьшаем на 1 значение счетчика ncli и получаем структуры данных, представленные на рис. 30.6.
cli[0]-cli[l] -cli[0]-cli[l] -
cli[0]-
cli[l] -
cli[0]-
cli[l] -
ncli=2
Рис. 30.6. Структуры данных после того, как первое соединение было разорвано
Мы снова вызываем функцию t_accept для элемента cl i [1], но предполагаем, что на этот раз сбоя не произойдет и функция выполнится успешно. Затем мы убираем элемент cli[l] из массива, уменьшаем значение счетчика ncli на 1 (он становится равным 1) и передаем третье соединение процессу, вызвавшему xti_ accept. Напомним, что все, о чем здесь шла речь, начиная с вызова функции tji Sten, происходило в рамках нашего вызова функции xti_accept・ Теперь настал момент, когда эта функция в первый раз возвращает вызывающему процессу присоединенный дескриптор.
В следующий раз при вызове функции xti .accept значение счетчика nd i будет равно 1, а функция t_accept будет вызвана для приема соединения, соответствующего элементу с11[0]. Если предположить, что она будет успешно выполнена, вызывающему процессу будет возвращен второй присоединенный дескриптор.
Теперь мы покажем исходный код для нашей функции xti^accept, первая часть которого содержится в листинге 30.5.
Листинг 30.5. Функция xti_accept: первая часть
//libxti/xti_accept с
1 #include~ "unpxti h"
2 3 4 5 6 72 3 4 5 6 7static int ncli = -1, ndisconn,
2 3 4 5 6 7
2 3 4 5 6 7
static struct cli {
inf connfd /* присоединенный дескриптор fd или -1. если соединение разорвано */
1 nt count
struct t_call *tcallp /* указатель на структуру размещенную в памяти */
} *cli/* используются элементы cli[O]. cli[l] cli[ncli-l] */
int
xti_accept(int listenfd struct netbuf *cliaddr int rdwr) 1。{「
int 1 event.
u_int n.
char*ptr
14struct t_discon tdiscon
15 if (ncli == -1) {/* вначале сквозная инициализация */
16 if (cli '» NULL)
17 err_quit("al ready initialized").
18 if ( 7ptr - getenv(,•LISTENQ,^)) »» NULL)
19•n=atoi(ptr),
20else
21n»LISTENQ.
22cli=Calloc(n sueof(structcli)).
23for (1 » 0, i < n i++)
24 cli[i] teal Ip » T_alloc(listenfd T_CALL. T_ALL)
25 ncli = 0.一“-
26 )
Объявление статических переменных
•7 Счетчик rich, диагностический счетчик разорванных соединений ndisconn и указатель на наш массив структур cl i объявляются как статические переменные.
Инициализация при первом вызове
■26 При первом вызове мы размещаем в памяти массив структур cli, количество элементов в котором равно константе LISTENQ или значению переменной окружения с тем же именем. Это будет то же значение, что использовалось в функции tcp_l 1 sten при вызове функции t_bind. Тогда для каждого элемента массива мы вызываем функцию t_alloc для размещения структуры t_Cdl 1 в памяти и записываем возвращенный указатель в нашей структуре с!ъ
ПРИМЕЧАНИЕ
В случае использования потоков эта копия массива cli и его счетчик ncli должны быть защищены, чтобы позволить нескольким потокам вызывать функцию xti_accept одновременно.
Вторая часть этой функции показана в листинге 30.6.
Листинг 30.6. Функция xti_accept: вторая часть //libxti/xti_accept с 27 for ( 7) {
28 if (ncli == 0) {/* нужно ждать соединения */
29 T_listen(listenfd cli[ncli] tcalip). /* блокируемся здесь */
30313233343536373839403031323334353637383940/* дальнейшее предполагает что вызываюший процесс вызвал функцию tcp_listen() */ cli[ncli] connfd = T_open(xti_serv_dev. O_RDWR. NULL),一
30
31
32
33
34
35
36
37
38
39
40
30
31
32
33
34
35
36
37
38
39
40
T_bind(cli[ncli] connfd NULL NULL).
cTi[ncli] count++
ПС1Т++.
}
if (t_accept(1istenfd. cli[ncli - 1] connfd
cli[ncli - 1] teal Ip) == 0) {
ncli--, /* успех */
if (rdwr)
Xti_rdwr(cli[ncli] connfd).
41 if (cliaddr) {/* возвращаем адрес протокола клиента */
42n = пиn(cl 1addr->maxlen cli[nc1i] tcallp->addr len):
43memcpy(cliaddr->buf. cli[ncli] tcallp->addr buf n).
44cliaddr->len = n.
45}
46return (cli[ncli] connfd),
47} else if (t_errno == TLOOK) {
48if ( (event » TJook(listenfd))»= T_LISTEN) {
49T_h stendi stenfd. cli[ncli] teal Ip)./* не блокируется */
50cTi[ncli] connfd » T_open(xti_serv_dev O_RDWR NULL),
51 T_bind(ch[ncli] connfd, NULL: NULL), 一
52cTi[ncli] count++.
53ncli++.
5455565758596061626364656667686970717254555657585960616263646566676869707172} else if (event »» T_DISCONNECT) {
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
T_rcvdis(listenfd &tdiscon);
for (i - 0. i < ncli. i++) {
if (cli[i].teal 1p->sequence == tdisepn.sequence) { T_close(cli[i] connfd) ndisconn++.
ncl1 --,
if ( (n н ncli - i) > 0)
“ memmove(&cli[i]P &cli[i + 1], n * sizeof(struct cli)).
break.
}
} else
err quite"unexpected t look event ^d". event).
} else ~-
err_xti("unexpected t^accept error").
Ожидание соединения
28-35 Если наш массив пуст, мы должны вызвать функцию t_li sten и ждать прибытия соединения. В этом состоянии прослушивающий сервер проводит большую часть времени. Когда функция tjisten возвращает управление, адрес протокола клиента и идентификатор соединения записываются функцией t_l i sten в структуру t_ca11. Затем мы вызываем функцию t_open для создания новой точки доступа, на которой это соединение будет принято, и связываем эту точку доступа с любым локальным адресом.
Вызов функции t_accept, возвращение в случае успешного выполнения
46 Мы вызываем функцию t_accept для приема соединения» заданного элементом массива cli[ncli -l], и если этот прием успешно выполняется, мы возвращаем вызывающему процессу дескриптор присоединенной точки доступа. Мы также вызываем нашу функцию xti_rdwr, если вызывающий процесс должен использовать функцию read или write, и при необходимости возвращаем адрес протокола клиента.
Обработка дополнительных соединений из очереди
53 Если функция t_accept возвращает константу TLOOK, a t_l ook возвращает TJ.ISTEN, это означает, что имеется еще одно соединение, ожидающее обработки, и следует вызвать функцию t_l isten для получения информации о нем. Обратите внимание, что этот вызов не будет блокирован, так как в точке доступа имеется собы- гие T L1STEN, ожидающее обработки. Мы также вызываем функции t open и t_bi nd и сохраняем полученную информацию в элементе массива cl НпсЪ].
Обработка разрыва соединения, ожидающего приема
66 Если функция t_accept возвращает константу TLOOK, a t_l ook возвращает ^DISCONNECT, это означает, что одно из соединений, которое уже было возвращено функцией t_l 1 sten, было впоследствии разорвано клиентом. Для получения информации о разорванном соединении (а точнее, значения его идентификатора sequence) мы вызываем функцию t_rcvdi s и просматриваем наш массив в поисках соответствующего элемента. Найдя этот элемент, мы вычисляем количество соединений, расположенных в массиве после разорванного (п), и вызываем функцию memmove для того, чтобы сдвинугь все последующие элементы. Мы вызываем функцию memniove, а не memepy, так как первая корректно обрабатывает перекрывающиеся области памяти, которые могут появиться в данном сценарии (см. упражнение 30.3).
Таблица 30.1. Сколько раз функция t^accept возвращает константу T_USTEN
>*<««>**4«*<Л«*«*4««+>ММ«*+»««*»Я***4«*««*й<«»Ш<»>ИФ<ФМ«<*<4ИЯЖ««^««4«»М«4И^»Ж*Й«<Ш««««>ЧШ^WX«MW«««*«^«^-*X<**i^
Счетчик на стороне сервера Количество дочерних процессов на стороне клиента
1
2
3
4
cli[OJ.count
600
309
95
102
cli[l],count
291
286
121
cli[2].count
219
235
ch [3].count
142
Всего
600
GOO
600
600
Мы можем протестировать сервер, приведенный в листинге 30.4, используя эту новую версию функции xti_accept. Опа корректно работает при наличии нескольких соединений. Мы также хотели бы знать, как часто функция t_accept возвращает ошибку в результате прибытия дополнительных соединешиг Для этого мы напишем простую функцию, которая выводит значение счетчика count структуры cli и значение счетчика ndi sconn (который мы вскоре опишем), а затем вызовем эту функцию из родительского обработчика сигнала SIGINT (см. листинг 30.4). В табл. 30.1 показаны результаты, полученные для сервера, работаю-
щего под управлением 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(argvL1], argv[2]):
+ if (t == 2 && (j Z 3) == 0) {
+struct linger ling.
+ling l_onoff = 1
+ling l_linger = 0.
+SetsockoptCfd. SDL_SOCKETSO_LINGER. &lmg. sizeofdwg)).
+Close(fd),
+/* и продолжаем далее для этого клиентского соединения */
+fd = rcp_connect(argv[l], argv[2]).
+ } 一
Wnte(fd. request. str〕en(request))
if ( (n = ReadnCfd reply. nbytes)) '= nbytes) err_qirt("server returned 忠d bytes" nj.
Close(fd). /* состояние TIME_WAIT на стороне клиенга а не сервера */
Строки, перед которыми стоит знак +, мы добавили. В результате этой модификации третий дочерний процесс клиента (для которого 1 равно 2) разрывает каждое третье голько что установленное соединение. Для отправки сегмента RST мы устанавливаем параметр сокета SO_LINGER и закрываем (cl ose) сокет. Затем мы
создаем другое соединение и продолжаем цикл. Воздействие этой модификации на работу сервера зависит от временных соотношений: некоторые ссшенты RST могут прибыть к серверу в промежутках между вызовами функции t_li sten и функции t_dccept (эго подсчитывается нашим счетчиком ndisconn для проверки выполнения кода). Другие сегменты могут прибыть еще до начала выполнения функции t_listen либо после завершения выполнения функции taccept, то есть когда соединение уже установлено.
Длина очереди XTI и аргумент backlog функции listen
Длина очереди XTI и аргумент backlog функции 11 sten похожи, но не одинаковы. Начнем с того, что для аргумента backl од функции 11 sten отсутствует точное определение. В табл. 4.6 показано, что реализация этого аргумента различна в системах, используемых в настоящее время.
В Posix.lg сказано, что длина очереди XTI задает то количество сообщений о соединении, ожидающих обработки, которое поставщик поддерживает для данной точки доступа. Сообщение о соединении, ожидающее обрабо пш, — это такое сообщение, которое было передано приложению поставщиком, но еще не было принято (или отклонено). Поставщик может устанавливать в очередь большее количество сообщений о соединении, чем задано, но при этом он должен следить за тем, чтобы количество доставленных приложению п ожидающих обработки сообщений не превышало величины ql еп в каждый конкретный момент времени.
Если бы реализация была такова, что сообщения о соединении передавались бы приложению сразу же по прибытии соединения (то есть по прибытии сегмента SYN к серверу), чо такой тип формирования очереди мог бы иметь смысл. Но поскольку приложение получает извещение о соединении TCP только тогда, когда это соединение уже полностью установлено, то на самом деле приложению не требуется устанавливать эти соединения в очередь.
Как обычно, чтобы разобраться со стандартами, нам нужно посмотреть, каковы реальные значения ql еп в различных системах. Мы модш|)ицировали листинг Д.9 для работы с XTI вместо сокеюв и запустили нашу программу в пяти различных системах, поддерживающих XTL Результаты показаны в табл. 30.2.
Таблица 30.2. Фактическое количество соединений в очереди для различных значений параметра XTI qlen
.«««vwwwwwta
запр。-
MMWeWW R :»9«М9ММЯС*>»9» 帆
AIX4.2
«mwemavwMmMeMPsMMh ж^Ж'
Dunix 4.0В
w»4o&«.4»sov» и«<чем№ «хнеммммчмь»1
HP-UX 10.30
««uMMMK'K^evWtt^CWWWfCWWSWXifSW*
Solaris 2.6
、>#. *Zj»w Ч4*оиа№4дв. V ЧМКФХа*
Uware2.1.2
ный qlen
возврат щен- ный 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
5
10
5
5
5
5
5
«WWWWXflWW запро- шейный qlen
ЖМШФХК4«4М4К«««М«<4К<4<*тМ4MM<№
AIX 4.2
MmvMWt«^w^^
Dunix4.0B
ктс<^»»»ттт^мткт№А^^>^
HP-UX 10.30
Solaris 2.6
Uware 2.1.2
возвращенный 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
11
И
И
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 значение не возвращается вовсе.
Ни в одной системе не допускаются соединения, если значение ql en равно нулю (в отличие от аргумента backlog функции listen, пулевое значение которого не подразумевает непременного отсутствия соединений, см. табл. 4.6). Две системы вернули ошибку при выполнении функции t_connect (HP-UX и Solaris). Некою- рые реализации устанавливают в очередь большее количество соединений, чем задано значением qlen (AIX и Digital Unix), но остальные три не делают этого.
ПРИМЕЧАНИЕ
Здесь мы подсчитывали количество соединений, установленных в очередь поставщиком, а не приложением, так как нас интересует именно та очередь, которая формируется пос1авщиком
Установка сервером единичной длины очереди
Одним из способов избежать сложностей, связанных с приемом соединений XTI, является установка значения поля qlen структуры t_bind равным 1. Но с этим решением связана следующая проблема: многие реализации будут устанавливать в очередь только одно соединение, а все остальные приходящие сегменты SYN будут игнорироваться до тех пор, пока стоящее в очереди соединение не будет принято.
Мы можем проверить это свойство с помощью нашего сервера из данного раздела. Мы снова запустим сервер в системе UnixWare 2.1.2, в которой при единичном значении ql еп в очередь устанавливается только одно соединение. Как и в табл. 30.1, мы варьируем количество дочерних клиентских процессов, отправляющих запросы на соединение, от 1 до 4, но на этот раз мы измеряем время
(в секундах), необходимое для установления фиксированного количества соединений (600).
Таблица 30.3. Время в секундах, необходимое для установления 600 соединений, в зависимости от количества дочерних процессов
f^mmvx№>№tiKmv^^^»• ■»> fV>^<Sivi<e^miXiv»M'MU»^^ vtaMMi«M«x»w»twiw<«w'KKiKU^«ttimsMX<<>tciKm«^wimvvt<>wtm»vf>s»»ie^^
Длина очередиКоличество дочерних процессов
1234
110,612,215,613,2
102410,610,210,310,4
Б случае, когда длина очереди (qlen) равна 1, время увеличивается пропорционально количеству дочерних процессов. Это объясняется гем, что с увеличением количества дочерних процессов все больше клиентских сегментов SYN игнорируется сервером, поскольку в очереди может находиться одновременно только одно соединение, а все сегменты SYN, пришедшие в это время, клиент должен будет передавать повторно. Но если длина очереди (qlen) больше, чем максимально возможное количество одновременно устанавливаемых соединений, то затрачиваемое на обработку соединений время уменьшается, а потом возрастает (в зависимости от количества дочерних процессов)
Приведенные результаты подтверждают то, что мы говорили ранее: на прак- гике для сервера значение qlen, равное 1, является нереальным.
Резюме
Прием клиентского соединения с использованием технологии XTI гораздо сложнее, чем с использованием сокетов. Как мы говорили, оправданием этой сложности является возможность осуществления отложенного приема, при котором приложение получает извещение в момент прибытия запроса на соединение, а не когда это соединение уже установлено. В реализациях TCP отложенный прием ие поддерживается, а в Unix 98 снято требование, заставляющее функцию t_dccept возвращать событие T LISTEN, когда другое соединение жде1 обработки, но в целях обеспечения обратной совместимости от серверов XTI требуется умение обрабатывать этот сценарий.
Упражнения
В разделе 30.2 мы отмечали, что в некоторых реализациях в поле sequence структуры t eal 1 (заполняемой функцией t_li sten) хранился указатель. Что происходит в 64-разрядных архитектурах?
Почему в листинге 30 1 в объявлении xti_serv_dev к РАТН_МАХ добавлена единица?
В листинге 30.6 мы вызываем функцию memmove и объясняем это тем, что происходит перекрывание копируемых областей (исходной и конечной). Предположим, что имеется 4-баптовый массив с элементами от х[0] до х[3] (индекс возрастает слева направо) и нам нужно удалить элемент х[1], сдвинув при этом следующие два элемента «влево» на 1 байт и оставив 3 элемента.
Нарисуйте массив с исходной и конечной областями. Затем опишите, что происходит, если копирование осуществляется из начала области исходной в начало конечной (справа налево) Затем опишите, что произойдет, если операция копирования осуществляется из конца исходной области в конец конечной области (слева направо). Гарантирует ли использование функции memepy какое-либо определенное направление копирования?
Модифицируйте листинг Д 9 таким образом, чтобы использовать XTI вместо сокетов,
Модисрицируйте листинги 30.5 и 30.6 таким образом, чтобы использовать связный список структур cl 1 вместо массива фиксированного размера, который применялся в этой главе для простоты. При этом память под структуры еле- дует выделять динамически.
ГЛАВА 31
XTI: клиенты и серверы UDP
Введение
XTI предоставляет три функции для серверов и клиентов, не ориентированных на установление соединения: t_sndudata для отправки дейтаграммы, t_rcvudata для получения дейтаграммы и t_rcvuderr для получения информации об асинхронных ошибках. При использовании сокетов у нас имеется возможность вызвать функцию connect для приложения UDP, но в XTI этой возможности нет.
Функции t_rcvudata и t_sndudata
Эти две функции используются в протоколах, не ориентированных на установление соединения (например, UDP), для получения и отправки дейтаграмм.
#1 nc 1 tide <xti h>
mt t_rcvudata(int fd, struct t^unitdata *umtddtd, int ★fldgsp),
int t_sndudata(mt fd. struct t_unitdata *umtddtd).
Обе функции возвращают 0 в случае успешного выполнения. -1 в случае ошибки
Для функции t_sndudatd структура t_umdata задает адрес получателя, параметры и фактические отсылаемые данные.
struct t_umtdata {
struct netbuf addr./* адрес специфичный для протокола */
struct netbuf opt./* параметры специфичные для протокола */
struct netbuf udata, /* пользовательские данные */ }.
Для функции t_rcvudata эта структура определяет, где хранится адрес протокола отправителя, а также содержит полученные параметры и фактические данные.
Обе эти функции возвращают 0 в случае успешного выполнения или -1 в случае ошибки. В этом заключается отличие от большинства функшп! чтения и записи, которые обычно возвращают количество переданных байтов. При выполнении функции t_rcvudata размер полученной дейтаграммы возвращается как поле udata len структуры t_umdata. Функция t_sndudata вовсе не возвращает количество записанных байтов — она просто возвращает 0 в случае успешного выполнения (это означает, что вся дейтаграмма была скопирована в буферы ядра).
Целочисленное значение, на которое указывает аргумент flagsp, аналогично последнему аргументу функции t_rcv: это не аргумент типа «значение-результат», поскольку его значение не анализируется функцией, а только задается при
ее завершении. Если требуется еще раз вызвать функцию t_rcvuddta для чтения оставшейся части дейтаграммы (то есть если размер дейтаграммы больше размера буфера приема), то возвращается флаг T_MORE. Подобный пример мы показываем в разделе 31.6.
ПРИМЕЧАНИЕ
Эти две функции XTI соответствуют функциям sendto и reevfrom.
Функция udp_client
Перед тем как приступать к рассмотрению примера использования XTI для UDP, мы напишем функцию udp_cl т ent, которая создает точку доступа XTI для клиента UDP, с той же последовательностью вызовов, что была показана в разделе 11.10. Эта функция, приведенная в листинге 31.1 \ выполняет преобразования имени узла и имени службы, описанные в главе 29.
Листинг 31.1. Функция udp_client для XTI
//1ibxti/udp_cllent с
1 #include"unpxti h"
23456789012345678901int
2345678901
2345678901
udp_cl nent(const char *host. const char *serv, MJtd *?vptr「sdcl<?l球i:t ■喲
int tfd.
void *handle.
struct netconfig *ncp:
struct njhostserv hs:
struct nd_add门ist *alp.
struct netbuf *np.
struct t unitdata *tudptr:
handle = SetnetpathC).
hs h_host = (char *) host:
hs h_serv = (char *) serv:
while ( (nep = getnetpath(handle)) *= NULL) {
if (strcmp(ncp->nc_proto. "udp") l= 0)
continue
if (netdir_getbyname(nep, &hs, &alp) '= 0)
continue.
tfd = T_open(nep->nc_device. O_RDWR. NULL);
TJnnd(tfd. NULL. NULL).
tudptr = T_alloc(tfd. T_UNITDATA. T_ADDR).
np = alp->n_addrs./* используем первый адрес */
tudptr->addr len = mm(tudptr->addr maxlen np->len),продолжение ^
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:// www piter.com/download.
Листинг 31.1 (продолжение)
memcpy(tudptr->addr buf np->buf tudptr->addr len).
endnetpatn(handle)
netdir_free(alp. ND_ADDRLIST).
*vptr = tudptr./* возвращаем указатель на t_un1tdata{) */
*】enp = tudptr->addr maxlen. /* и размер адресов */
return (tfd)
}
endnetpath(handle).
return (-1)
}
Поиск имени узла и имени службы
2-19 Вызовы функций getnetpath и netdi r_byname аналогичны вызовам в листинге 29Д.
Открытие устройства, связывание его с произвольным локальным адресом
J-21 Функция t open открывает соответствующее устройство, и функция t_bi nd связывает произвольный локальный адрес с данной точкой доступа.
Размещение структуры t_unidata в памяти
22 Функция t_alloc размещает в памяти структуру t_unidata, содержащую внутри себя только структуру addr, но не opt или udata. Мы не размещаем в памяти структуру opt, поскольку в случае UDP редко используются параметры, относящиеся только к одной дейтаграмме (см. главу 32). Мы не размещаем структуру udata, так как размеры дейтаграмм UDP довольно велики и дос гигают 65 507 байт, как показано в табл. 28.2, но немногие приложения используют дейтаграммы UDP максимального размера. Поскольку большинство приложений имеют дело с меньшими по размеру дейтаграммами (максимум до нескольких тысяч байтов), то более разумно разрешить приложению самостоятельно выделять память для буфера данных того размера, который требуется в данный момент.
Использование первого возвращенного адреса
3-25 Структура addr заполняется данными по первому адресу, возвращенному для данного сервера. На рис. 31.1 показано, какие структуры данных при этом используются, в предположении, что две структуры netbuf возвращаются для указанного имени узла и имени службы и что в данной реализации используется структура sockaddr_i п для представления адреса IPv4.
Значение addr maxi еп должно быть таким же» как значение maxi еп в структуре, возвращенной функцией netdi r_getbyname (16 для IPv4), но мы используем макрос mi п, чтобы удостовериться, что мы не переполняем область памяти (приемный буфер) при вызове memepy. Структуры opt и udata содержат четыре нулевых поля длины и два пустых указателя, поскольку функция t__alloc инициализировала соответствующие поля этих структур в соответствии с нашим указанием разместить в памяти и инициализировать только структуру addr с аргументом T_ADDR・
tudptr:
netbuf{}
t unitdata{}
netbuf{}-
netbuf{}*
! Динамически размещается
;в памяти функцией t alloc
Рис. 31.1. Структуры данных при вызове функции udp.client
Освобождение занятой памяти и возвращение управления
26-33 Функция endnetpath освобождает выделенную под структуру netconfig память, а функция netdi r_fгее освобождает память, которая была выделена функцией netdi r_getbyname (рис. 31.1). Указатель на структуру t_unidata возвращается вызывающему процессу вместе с размером адреса протокола и дескриптором точки доступа. Мы возвращаем размер адреса протокола как addr maxlen вместо addr len, поскольку это значение возвращается вызывающему процессу 〃刀я использования при вызове функции malloc. Если бы адреса имели переменную дли-
."*
ну, нам следовало бы возвращать максимальную длину, а не просто размер данного адреса.
Пример: клиент времени и даты
С помощью нашей функции udp_cl lent мы модифицируем код не зависящего от протокола клиента времени и даты из листинга 11.8 таким образом, чтобы использовать XTI вместо сокетов. Результат приведен в листинге 31.2.
Листинг 31.2. UDP-клиент времени и даты, использующий XTI и функцию udp_client
//xtiudp/daytimeudpcli1 с
include "unpxti h”
int
main(int argc, char **argv)
{
mttfd. flags
charrecvline[MAXLINE + 1].
socklen_t addrlen,
struct t_unitdata *sndptr, *rcvptr.
if (argcTJ 3)
errequiteusage daytimeudpcli <hostname> 〈service〉").
tfd = Udp_client(argv[l]. argv[2], (void **) &sndptr Saddrlen),
rcvptr - T_alloc(tfd. T_UNITDATA・ T_ADDR).
printfCsending to 纪\en". Xti__ntop_host(&sndptr->addr)),
sndptr・>udatamaxlen = MAXLINE
sndptr->udatalen =1
sndptr->udatabuf *recvline,
recvline[0] -0./*однобайтоваядейтаграмма, содержащая нулевой байт */
T_sndudata(tfd. sndptr).
rcvptr->udata maxlen = MAXLINE.
rcvptr->udata buf = recvlme
T rcvudataCtfd. rcvptr &flags).
22recvline[rcvptr->udata len] - 0,/* завершающий нуль */
23 pnntf(,,from 纪 %s" Xti_ntop_host(&rcvptr->addr), reevhne);
24 exit(0),
25 }
Создание точки доступа
-13 Мы вызываем нашу функцию udp_client для создания точки доступа XTI и для размещения структуры t_umdata, которую мы будем использовать для отправки дейтаграмм. Мы размещаем в памяти еще одну структуру t_umdata, которая будет использоваться для приема ответов. Для вывода IP-адресов сервера мы вызываем функцию xti_ntop_host.
Отправка дейтаграммы
-18 Мы инициализируем структуру udata, входящую в структуру t_uniddta, задавая указатель на буфер recvl i ne и однобайтовую дейтаграмму, содержащую нулевой байт. Функция t_snduddtd отправляет дейтаграмму серверу.
ПРИМЕЧАНИЕ
Вообще говоря, мы должны иметь возможность отправлят ь U DP-дейтаграмм ы, содержащие 0 байт (см. пояснения к табл. 28.4), но многие реализации XTI нс позволяют это делать.
Чтение ответа
19-23 Мы инициализируем структуру udata для принимающей структуры t^umdata и вызываем функцию t_rcvudata для чтения ответа сервера. Ответ сервера заканчивается нулевым байтом (символом конца строки) и выводится в стандартный поток вывода вместе с адресом сервера.
В этом примере свойство клиента UDP (отсутствие надежности) останется таким же, как и в сокетной версии этого клиента из листинга 11.8: при отсутствии ответа клиент навсегда заблокируется в вызове функции t_rcvudatd・
Если мы запустим этот клиент на том же узле, на котором работает сервер, мы получим следующий результат: umxware % daytimeudpclil bsdi daytime sending to 206 62 226 35 from 206 62 226 35 Fn 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^revudata возвращает ошибку TLOOK, и чтобы определить, какая фактически произошла ошибка, следует вызвать функцию t_rcvudata. Это обсуждается в следующем разделе.
Функция t_rcvuderr: асинхронные ошибки
Для протокола, не ориентированного на установление соединения, ошибки могут возвращаться асинхронно. Это означает, что, возможно, дейтаграмма будет корректно передана стеком протоколов, н только впоследствии при ее передаче по сети будет обнаружена ошибка. Распространенными для дейтаграмм UDP являются сообщения ICMP о недоступности порта (port unreachable) получателя или недоступности узла (host unreachable) для какого-либо промежуточного маршрутизатора. Когда подобное сообщение ICMP приходит поставщику спустя некоторое время, требуется, чтобы поставщик каким-либо образом уведомил процесс об этой ошибке и чтобы процесс попытался выяснить фактический характер ошибки Как показано в конце предыдущего раздела, в XTI подобное уведомление организовано посредством присваивания переменной t_errno значения
TLOOK при вызове функции t_rcvudata. Тем самым мы указываем, что в уже отправленной дейтаграмме обнаружена ошибка. Затем мы вызываем функцию t_rcvuderr, чтобы определить, что же произошло на самом деле, и сбросить значение статуса ошибки.
#】nc】ude <xti h>
int t_rcvuderr(int fd. struct t_uderr ^uderr),
Возврашает 0 в случае успешного выполнения. -1 в случае ошибки
struct t_uderr { struct netbuf addr. struct netbuf opt. t_scalar_t errorstruct t_uderr { struct netbuf addr. struct netbuf opt. t_scalar_t errorЕсли указатель uderr непустой, структура t_uderr заполняется в соответствии с информацией об этой ошибке.
struct t_uderr { struct netbuf addr. struct netbuf opt. t_scalar_t error
struct t_uderr { struct netbuf addr. struct netbuf opt. t_scalar_t error
/* адрес, специфичный для протокола */
/* параметры, специфичные для протокола */
/* ошибка. специфичная для протокола */
Структура addr содержит адрес получателя дейтаграммы, которая вызвала ошибку, структура opt — специфичные для протокола параметры из этой дейтаграммы, a error содержит специфичный для данного протокола код ошибки. Для UDP значение error обычно является одним из значений errno из файла <sys /errno h>.
Если uderr является пустым указателем, то статус ошибки сбрасывается, причем никакой информации не возвращается.
Пример: сообщение ICMP о недоступности порта
Теперь мы изменим наш клиент из листинга 31.2 так, чтобы он мог обрабатывать асинхронные ошибки. Результат показан в листинге 31.3.
Листинг 31.3. Клиент UDR использующий XTI, способный обрабатывать асинхронные ошибки
//xtiudp/da.ytinieudpcli2 с
#】nclude "unpxti h"
int
mam(int argc, char **argv)
{
int tfd. flags.
char recvhne[MAXLINE + 1],
socklen_t sddrlen,
struct t_unitdata *sndptr, *rcvptr,
struct tjjderr *uderr,
if (argc 『3)
err_quit("usage a out <hostname or IPaddress> <service or port#>M):
tfd = Udp_client(argv[l], argv[2]. (void **) &sndptr. &addrlen),
rcvptr = T_dlloc(tfd T UNITDATA T_ADDR).
uder「= T_alloc(tfd. TJDERROR T_a6dR)
pnntf("sending to ^s\enM. Xti_ntop_host(&sn^)tr->addr)):
sndptr->udata maxien = MAXLINE.
^ndptr->udata len = 1
jntr->'^^ta buf = recvline;
recvline[O] =0./* однобайтовая дейтаграмма содержащая нулевой байт */
T_sndudata(tfd. sndptr)
rcvptr->udata maxlen = MAXLINE.
rcvptr->udata buf = recvlme,
if (t_rcvudata(tfd. rcvptr &flag5) == 0) {
recvhne[rcvptr->udata len] = 0./* завершающий нуль */
printfC"from Ms Ms' Xti_ntop_host(&rcvptr->dddr) recvline):
} else {一 —
if (t_errno == TLOOK) {
T_rcvuderr(tfd. uderr).
printfC 'error Mid for datagram sent to ^s\en
uderr->error. Xti_ntop_host(&uderr->addr)).
} else
err xt】(”t rcvudata error").
}- 一
exit(0).
}
Обработка асинхронных ошибк
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/1ndude/sys/errno.h
#define ECONNREFUSED 146/* Отказано в соединении */
Мы видим, что значение error, возвращенное в структуре t_uder「, 一 это значение errno, соответствующее ошибке ICMP (см. табл. А.З).
ПРИМЕЧАНИЕ
К сожалению, несмотря паю, что это свойство весьма удобно (возвращение опгибок ICMP для точек доступа UDP XTI), все же с ним связаны некогорые проблемы. Во-первых, отсутствует требование, заставляющее поставщика извещать приложение, когда возникает ошибка. В системе UnixWare 2.1.2, например, сообщения ICMP о недосгуппости порта возвращаются приложению, по для сообщений ICMP о недоступности узла это не так. Во-вторых, если мы модифицируем наш клиент таким образом, чтобы три дейтаграммы отправлялись на три различных сервера, а затем прочитаем все ответы и два из них будут содержать сообщения ICMP о недоступности порта, то только первое из этих двух сообщений будет возвращено приложению функцией t_rcvudcrr. Это объясняется тем, что поставщик может принять только одно сообщение об ошибке для каждой точки доступа. Все эти проблемы стали причиной того, что мы разработали независимый способ извещения приложения, работающего с дейтаграммами, об асинхронных ошибках: зто демон icmpd, описанный в разделе 25.7.
Обратите внимание па то, чю мы получаем лишь код ошибки и адрес получателя дейтаграммы, вызвавшей ошибку. Мы не получаем такую информацию, как, например, адрес отправителя сообщения ICMP об ошибке.
Функция udp_server
Мы также можем модифицировать нашу функцию udp_server из листинга 11.10, чтобы использовать XTI Результат приведен в листинге 31 4
Листинг 31.4. Функция udp_sever, использующая XTI
//Ъbxti/udp^server с
#】nclude unpxti h
int
udp_server(ccnst char *host const char *serv socklen_t *addrlenp)
{一一
mt tfd
void*hand】e
structt_bind tbind
structt_info tinfo
structnetconfig *ncp
structnd^hostserv hs
structnd_addHist *a】p
structnetbuf *np
handle = SetnetconfigO
hs h_host = (host == NULL) 7 HOST_SELF (char *) host
hs h_serv = (char *) serv
while ( (nep = getnetconfig(handle)) '= NULL &&
strcnp(ncp >nc_proto udp ) ,= 0)
if (nep == NULL)
return ( 1)
if (netdir_getbyname(ncp &hs Salp) '= 0)
return ( 2)
np = alp >n_addrs/*используем первый адрес */
tfd = T_open(ncp >nc_dev】ce O_RDWR &tinfo)
tbind addr = *np/*копируем всю структуру netbuf{}*/
tbind qlen = 0/*не используется для сервера неориентированного
на установление соединения */
T_bwd(tfd &tb】nd NULL)
endnetconfigChandle)
netdir_free(alp ND_ADDRLIST)
if (addrlenp)
*addrlenp = tinfo addr /* размер адресов протоколов */
return (tfd)
}
Поиск протокола, имени узла и имени службы
L3 22 Начало этой функции аналогично нашей функции tcp_l i sten (см листинг 30 1): с помощью вызова функции getnetconfig отыскивается протокол, а затем вызывается функция netdi「_getbyname для поиска имени узла и имени службы
Открытие устройства, связывание IP-адреса сервера и порта
23 28 Функция t_open открывает требуемое устройство, а функция t_bi nd связывает IP-адрес сервера (универсальный адрес, если аргумент host является пустым указателем) и порт Память, выделяемая функцией netdi r_getbyname, освобождается функцией netdi r_f гее
Получение длины адреса и дескриптора
29 31 Размер адреса протокола возвращается в том случае если последний аргумент является непустым указателем, а возвращаемым значением функции является дескриптор для точки доступа
Пример: сервер времени и даты
Теперь мы можем переписать наш простой сервер времени и даты из листинга И Ис использованием XTI Полученный результат приведен в листинге 31 5
Листинг 31.5. UDP-сервер времени и даты с использованием XTI
//xtiudp/daytimeudpsrv2 с
#inc】ude unpxti h
include 〈time h>
int
main(int argc char **argv)
{
mttfd flags
charbuffEMAXLINE]
timetticks
struct t_unitdata *tud
if (argc == 2)
tfd = Udp_server(NULL argv[l] NULL)
else if (argc == 3)
tfd = Udp_server(argv[l] argv[2] NULL)
else
err_quit( usage daytimeudpsrv [ <host> ]〈service or port>#,)
tud =T_alloc(tfdT_UNITDATA T_ADDR)
for () {
tud >udata maxlen ^ MAXLINE
tud >udata buf = buff
if (t_rcvudata(tfd tud &flags) == 0) {
printfC datagram from ^s\en Xti_ntop(&tud >addr))
ticks = time(NULL)一
23snprintf(buffsizeof(buff)常 24s\er\en ctime(Sticks)):
tud >uddta len= strlen(buff)
T_sndudata(tfdtud)
} else if (t_errno == TLOOK)
T_rcvuderr?tfd NULL) /* просто устанавливаем в исходное состояние */
else
err_xti( trcvudata error )
}——
}
Создание точки доступа XTI
10 16 Наша функция udp_server создает точку доступа и связывает с ней локальный IP-адрес и порт Мы вызываем функцию t_alloc с аргументом T_ADDR и размешаем в памяти структуру t_umdata, так что фактически в памяти размещается буфер только для адреса протокола
Считывание запроса, отправка ответа
17 30 Затем программа входит в цикл считывая запрос клиента с помощью функции t_rcvudata и отправляя ответ с помощью функции t_sndudata Если один из наших ответов генерирует асинхронную ошибку, функция t_rcvudata возвратит ошибку, причем переменная t_errno примет значение TLOOK и для обработки ошибки мы вызовем функцию t_rcvuderr Обратите внимание, что последний аргумент функции t_rcvuderr является пустым указателем — тем самым осуществляется сброс статуса ошибки без возвращения какой-либо информации (поскольку от нас ничего не гребуется в связи с возникновением подобной ошибки) Если бы мы не обрабатывали эти ошибки так, как показано в листинге, а вместо этого при возвращении подобных ошибок функцией t_rcvudata сервер аварийным обрзом заканчивал бы работу, то тогда любой клиент moi бы вызвать сбои на нашем сервере, послав иа него дейтаграмму, а затем внезапно разорвав соединение Когда на узпе клиента бып бы получен ответ, клиент отреагировал бы отправкой сообщения ICMP о недоступности пор га, что привело бы к возвращению ошибки функцией сервера t_rcvudata Следовательно, обработка этих асинхронных ошибок является обазате^ьноп для UDP сервера, использующего XTI
Чтение дейтаграммы по частям
Вспомним наше обсуждение в разделе 20 3 относительно усечения дейтаграмм и различных си гуации, возникающих когда дейтаграмма считывается на сокете UDP но ее длина превосходит количество байтов, запрошенных приложением В XTI подобные сценарии обрабатываются иначе
Вспомним, что последним аргументом функции t_rcvudata является fl agsp Если буфер приложения недостаточно велик для того, чтобы принять в очередь следующую прибывшую дейтаграмму количест во возвращенных байтов будет равно udata maxi еп и бит T_MORE в целочисленном значении на которое указывает fl agsp будет включен Этот флаг указывает приложению, чю необходимо снова вызвать функцию t revudata для считывания оставшейся части дейтаграммы Адрес отправителя и параметры возвращаются функцией только при первом вызове функции t_rcvudata и счш ыванил первой части этой дейтаграммы В результате последующих вызовов функции t revudata для считывания оставшейся части этой дейтаграммы элементы addr 1 еп и opt 1 еп по завершении функции станут нулевыми
Мы можем продемонстрировать использование этой возможности, модифицируя иаш клиент из листинга 313. как показано в листинге 31 6
Листинг 31.6. Использующий XTI UDP-клиент, который считывает возвращенную дейтаграмму по частям
//xt1udp/daytimeudpc1i4 c
#includeunpxti h
2
3
#undef MAXLINE
#define MAXLINE 2
4
5
6
7
8
9
10
11
int
mam(int argc char **argv)
{
inttfd flags
charrecvline[MAXLINE + 1]
socklen_t addrlen
struct t_umtdata *sndptr *rcvptг struct t_uderr *uderr
12
13
if (argc 『3)
err_qu】t( usage a out <hostname or IPaddress> 〈service or port#> )
14
tfd = Udp_cl】ent(argv[l] argv[2] (void **) &sndptr &addrlen)
15
16
rcvptr = T_alloc(tfd T—UNITDATA T_ADDR) uderr = T_alloc(tfd T_UDERROR T_ABDR)
17
p门iuf( sending to %s\en Xti_ntop_host(&sndptr >addr))
18
19
20
21
22
sndptr >udata maxlen = MAXLINE
sndptr >udata len = 1
sndptr >udata buf - recvline
recvlineEO] - 0/* однобайтовая дейтаграмма содержащая нулевой байт *Z
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) {
recvlme[rcvptr->udata len] = 0/★ завершающий нуль */
if (rcvptr->addr len > 0)
prwtf( from Xs Xti_ntop_host(&rcvptr->addr)) printf( Xs\en recvline)""
} else {
if (t_errno “ TLODK) {
T_rcvuderr(tfd uderr)
printfC error 灯d from %s\en
uderr->error Xti ntop host(&uderr->addr))
} else--
err_xti( t_rcvudata error )
flags - 0 …
}
} while (flags & T MORE)
ex1t(0)~
Переопределение константы MAXLJNE
2-3 Мы переопределяем константу MAXLINE, заданную в заголовочном файле unp.h, так, чтобы она была равна 2 байтам — это размер нашего приемного буфера
Создание точки доступа, отправка дейтаграммы серверу
12-22 Эта часть кода не изменилась относительно листинга 31 3
Считывание ответа по 2 байта
J-41 Мы вызываем функцию t_rcvudata в цикле, пока включен бит T_MORE в переменной flags. Мы выводим IP-адрес сервера только для первой части дейтаграммы, пока функция возвращает ненулевое значение поля addr 1 en.
Теперь мы запустим этот клиент и отправим запрос серверу времени и даты: umxware Z 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 для recvli ne (строка 31 в листинге 316, которую мы использовали только для того, чтобы показать, сколько данных возвращается функцией t_rcvudata), мы получим более знакомый для нас форма г вывода результата:
umxware % daytimeudpcli4 bsdi daytime
sending to 206 62 226 35
from 206 62 226 35 Sun Mar 2 12 04 48 1997
Резюме
Две функции XTI, t_rcvudata и t_sndudata, аналогичны функциям reevfrom и sendto. Одна из новых возможностей, предоставляемых XTI и отсутствующая при использовании сокетов, — это чтение дейтаграмм по частям. При этом используется флаг T_MORE, указывающий, что дейтаграмма прочитана не полностью и необходимо снова вызывать функцию t_rcvudata для считывания оставшейся части.
В XTI возвращение асинхронных ошибок организовано с помощью функций t_rcvudata и t_sndudatd, возвращающих ошибку TLOOK. В этом случае вызывается функция t_rcvuderr для получения более подробной информации (в зависимости от используемого протокола) об этой ошибке. Этот вариант имеет определенные преимущества перед обработкой асинхронных ошибок для сокетов (асинхронные ошибки возвращаются, только если сокет является присоединенным), но даже в случае XTI асинхронные ошибки могут быть потеряны, и наше приложение по-прежнему зависит от стека протоколов, определяя, какие именно ошибки ICMP надо возвратить. Более удачным решением является использование демона, подобного icmpd (раздел 25.7), и возвращение ошибок по отдельному каналу.
ГЛАВА 32
Параметры XTI
Введение
Еще одним сложным для восприятия аспектом XTI всегда была обработка параметров. В спецификациях и руководствах многие страницы отводятся описанию сложностей, связанных с обработкой и согласованием параметров, при зтом не приводится никаких примеров, а в конце стоит фраза наподобие следующей: «подробности зависят от реализации».
Термин согласование (negotiation) часто используется при обсуждении параметров XTI. Параметр не «устанавливается», он является предметом «согласования», то есть поставщик может не установить тот параметр, который мы запросили. Когда параметр XTI согласовывается, возвращается фактическое значение параметра, используемое поставщиком, так что мы можем узнать это значение.
В табл. 32.1 перечислены все стандартные параметры XTI, как собственные {generic), которые начинаются с ХТ1_, так и параметры для IPv4.
ПРИМЕЧАНИЕ
В Unix 98 перед всеми именами, начинающимися с INET_, ТР_ и UDP_, добавляется Т_, но в Posix.lg этого не делается. Например, в Posix.lgecTb параметр 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. При установлении значения какого-либо параметра с таким свойством возвращается сообщение об ошибке, если запрошенное значение не может быть присвоено данному параметру. Если же у параметра это свойство отсутствует и мы пытаемся присвоить ему некое значе-
ние, лежащее вне области поддерживаемых для него значений, то поставщик изменит запрошенное значение на некоторое другое из разрешенных пределов Примером такого параметра может служить размер буфера приема XTI_RCVBUF, так как в большинстве систем задан как верхний, так и нижнии предел для этого размера Если мы запросим некоторый размер, меньший нижнего предела или больший верхнего, этот размер будет изменен и доведен до ближайшего приемлемого значения
Таблица 32.1. Параметры XTI
Уровень
Название
Тип данных Сквозной
Обязательный Описание
ХТ1_
XTI_DEBUG
t_uscalar_t[]
Разрешает отслежи
GENERIC
вание пр” отладке
XTI_LINGER
tjinger{}
Задержка при за
крытии если оста
лись данные для
oTjjpaBKM
XTI_RCVBUF
t_uscalar_t
Получение размера
буфера
ХТ1_
t_uscalar_t
Получение мини-
RCVLOWAT
мяльного объема
буфера
XTLSNDBUF
t_uscalar_t
Отпранка размера
буфера
ХТ1_
t—iiscalajt
Отправка мипи
SNDLOWAT
мальиою объема
буфера
Т INET Р
Т INET
u_int
Разрешается от-
BROADCAST
правка Широкове-
тательпых
сообщении
Т INET
u int
Обход поиска в тдб
DONTROUTE
лице маршрути-
зации
1 INET
u_chM]•
Параметры 3ai о-
OPTIONS
ловка IP
T INET
u mt
Позволяет исиоль-
REUSEADDR
зовать локальный
адрес повторно
TJNFTJTOS
u_char•
Тип службы и по-
рядок сгаришпстпа
T」NET_TTL
u_char
Время *м ш “
TJNET一
T TCP
t_kpalive{}
Периодические
KFEPALIVE
проверки действует
ли соединение
T TCP
t uscalar t
TCP MSS (только
MAXSEG
для чтения)
T TCP
t uscalar t
Oi ключает ал го-
NODELAY
ри гм Нагла
Г INET
T UDP
t_uscalar t •
Разрешает исполь-
UDP
CHESKSUM
лопание контроль-
ных сумм
Чтобы задать или получить значение параметра XTI, можно использовать следующие способы
Вызвав функцию t_optmgmt, можно задать любые желаемые параметры (сквозные или локальные) Мы также можем вызвать згу функцию для получения текущего значения параметра или его значения по умолчанию
Для точки доступа UDP мы можем задавать требуемые параметры (сквозные или локальные) при каждом вызове функции t_sndudatd, используя поле opt структуры t_unitdata
Для точки доступа UDP любые сквозные параметры прибывающие с дейтаграммой, возвращаю гея функцией t_rcvudata через поле opt сгруктуры t_um tdata
Для клиента TCP мы можем задать требуемые параметры (сквозные и локальные) при вызове функции t_connect через поле opt структуры t_call
Для сервера TCP любые сквозные параметры, прибывающие с соединением, возвращаются функцией tji sten через поле opt структуры t eal 1
ПРИМЕЧАНИЕ
Функция t_cptmgmt являсгся комбинацией функции getsockopt н setsockopt В API сокетов, тем не менее, отсутствует возможноегьзадавал» парамстры нрн ошравке или получении денГаграмм UDPh при инпциа^шзации или приеме соединении TCP Функции sendmsg и reevmsg позволяют задавать и получать вспомогат ел ьпыс данные и эга возможность используется в протоколе IPv6
В табл 32 2 приводится краткая информация об отправке и получении параметров функциями XTI
Таблица 32.2. Функции XTI, с помощью которых можно задавать или получать значения параметров
Точка доступа Функция
Возвращает только сквозные параметры
Возвращает сквозные и локальные параметры
Задает сквозные и локальные параметры
Любаяc_opcmgint
.
•
TCPt_accept
.
t_conncct
.
.
t_listen
.
t_rcvconnect
•
UDPt_rcvudata
•
t_icvvudatd
.
t_rcvuderr
.
Jsndudata
.
t sndvudata
.
В табл 32 2 указано, что мы можем задавать параметры с помощью функции t-dccept В случае TCP эго невозможно, поскольку при завершении функции t_l isten соединение уже установлено Следовательно, любые сквозные параметры, которыми мы хотим воздеиствовагь на процесс трехэтаппого рукопожатия, должны быть заданы для прослушиваемой точки доступа
Структура t_opthdr
Для того чтобы задать или получить значения параметров XTI, всегда используется структура netbuf, называемая opt, которая является элементом структур t_cal 1, t^optmgmt, t_uderr и t_umdata (см, табл. 28.5). Буфер opt содержит одну или несколько структур t opthdr, после каждой из которых следует значение параметра, struct t_opthdr {
t_uscaTar_t len, /* полная длина параметра
sizeof(struct t_opthd「) + length of value */
t_uscalar_t 1evel. /* протокол, на который воздействует параметр */
t_uscalar_t name /* название параметра */
t_uscalar_t status /*статус */
/* далее следует значение параметра и. возможно, заполнение */ }
ПРИМЕЧАНИЕ
Одно из различий между TLI и XTI заключасгся в тем, что в TLI ничего не говорится о формате буфера параметров, кроме того, чю он зависит ог реализации. Многие реализации ТЫ использовали структуру, называемую opthdr, в которой имеется только три элемента: level, name и 1сп.
На рис. 32.1 мы показываем две из этих трех структур XTI, на которые указывает структура netbuf, являющаяся частью структуры t_umdata.
netbuf{}netbuf{}netbuf{}netbuf{}netbuf{}netbuf{}t^unitdatat}t^unitdatat}0x000x0013717 INET_IP IP TOS13717 INET_IP IP TOS17 INET—IP IP_TTL17 INET—IP IP_TTL- t^opthdxf}- t^opthdxf}-tjopthdr {}-tjopthdr {}
netbuf{}
netbuf{}
netbuf{}
netbuf{}
netbuf{}
netbuf{}
t^unitdatat}
t^unitdatat}
0x00
0x00
137
17 INET_IP IP TOS
137
17 INET_IP IP TOS
17 INET—IP IP_TTL
17 INET—IP IP_TTL
- t^opthdxf}
- t^opthdxf}
-tjopthdr {}
-tjopthdr {}
Рис. 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.2. Запрос значения по умолчанию двух параметров из t_optmgmtРис. 32.2. Запрос значения по умолчанию двух параметров из t_optmgmt
Входные данныеРезулыгат
Входные данныеРезулыгат
Рис. 32.2. Запрос значения по умолчанию двух параметров из t_optmgmt
Рис. 32.2. Запрос значения по умолчанию двух параметров из t_optmgmt
В этом примере мы запрашиваем значения параметров IP TTL и TOS, задаваемые по умолчанию (поле flags имеет в данном случае значение T_DEFAULT), так что для каждого параметра мы задаем только структуру t_opthdr, не указывая никаких значений. Результатом является копия входных данных, при этом значения, заданные по умолчанию, возвращаются после каждой из структур t_opthdr. В структуре t_optnigmt, содержащей результат, поле status также заполняется (его значением является T_SUCCESS).
ПРИМЕЧАНИЕ
В Unix 98 (но не в Pcsix.lg) определены три макроса, которые могут быть использованы при обработке структуры t_opthdr и данных, следующих за пей: T_OPT_FIRSTHDR, T_OPT_NEXTHDR и T_OPT_DATA. Они аналогичны трем макросам: CMSG_FIRST- HDR, CMSG_NXTHDR и CMSG_DATA, предназначенным для обработки вспомогательных данных для сокетов, как показано в разделе 13.6.
Параметры XTI
Для большинства параметров XTI существует прямая аналогия с параметрами сокетов, описанными в главе 7. Поэтому здесь мы приводим лишь краткое описание параметров XTL Также следует отметить, что для определения констант, используемых во всех параметрах IP, TCP и UDP, необходимо включить заголовочный файл <xti_met.h>
ПРИМЕЧАНИЕ
Заметим, что в XTI не определены никакие возможности многоадресной передачи.
Параметр XTI_DEBUG
Этот параметр аналогичен параметру сокетов SO_DEBUG и поддерживается обычно только для TCP. Для того чтобы отключить этот параметр, достаточно не указы-
вать никакого значения в его заголовке. Иными словами, поле 1еп структуры t opthdr должно быть установлено равным размеру этой структуры (как показано, например, на рис. 32.2,16 байтам).
Параметр XTI_LINGER
Этот параметр аналогичен параметру сокетов SO_LINGER и поддерживается TCP. Он определяет, что происходит, когда точка доступа закрывается. Структура t_l mger выглядит следующим образом: struct t_linger {
t_scal3「_t l_onoff. /* T_NO. T_YES */
t_scalar_t l_l】nger. /* T_UNSPEC (используем значение по умолчанию). T_INFINITE или время задержки в секундах */
}.
В табл. 32.1 мы указали, что этот параметр является обязательным, но на самом деле обязательно только значение l_onoff, а значение 1_1 inger не является таковым. Это означает, что верхний и нижний пределы времени задержки устанавливаются для каждой реализации.
В отличие от параметра сокетов SO_LINGER, параметр XTI_LINGER не используется для отправки сегмента RST. Этот сегмент отправляет функция t_snddi s.
Параметры XTI_RCVBUF и XTI_RCVLOWAT
Эти два параметры аналогичны параметрам сокетов SO RCVBUF и SO_RCVLOWAT. Первый из них задает размер приемного буфера точки доступа, а второй — минимальный объем приемного буфера, используемый функциями poll и select,
В табл. 32.1 параметр XTI_RCVBUF не рассматривается как сквозной, но при поддержке каналов TCP с повышенной вместимостью (RFC 1323 [45]) этот параметр на самом деле может считаться сквозным, так как он воздействует на параметр масштабирования окна TCP, который согласовывается в процессе трехэтапного рукопожатия.
Параметры XTI_SNDBUF и XTI_SNDLOWAT
Эти два параметра аналогичны параметрам сокетов SO SNDBUF и SO_SNDLOWAT. Первый из них задает размер буфера отправки точки доступа, а второй — миннмал, ный объем буфера отправки, используемый функциями poll и select.
Параметр T_l P_BROADCAST
Этот параметр аналогичен параметру сокетов SO_BROADCAST. Он может принимать значение T_YES либо T_NO.
Параметр T_IP_DONTROUTE
Этот параметр аналогичен параметру сокетов SO_DONTROUTE. Значением этого параметра может быть либо T_YES, либо T_NO.
Параметр T_IP_OPTIONS
Этот параметр аналогичен параметру сокетов IPJ3PTIONS. Значение этого параметра используется при формировании параметров заголовка IPv4, примеры ко-
торых приведены в главе 24. Для отключения этого параметра достаточно задать его без указания значения (то есть задать только структуру t_opthdr).
Вызывая функцию t_optmgmt с запросом T_CURRENT, мы получаем текущие значения параметров IP, которые будут использоваться в исходящих дейтаграммах.
Параметр T_IP_REUSEADDR
Этот параметр аналогичен параметру сокетов SO_REUSEADDR. Значение этого параметра может быть либо T_YES, либо LN0.
Параметр T_IP_TOS
Этот параметр аналогичен параметру сокетов IP TOS. Значение параметра является комбинацией значений поля приоритета IPv4 (возможные значения приведены в табл. 32.3) и поля типа сервиса (возможные значения приведены в табл. 32.4).
Таблица 32.3. Значения поля приоритета IPv4, используемые с параметром TJP_TOS
MbWWM«««<«M»<»>i««O»®»l><»a4M»MSje«W«MbCW<48J«»<WX45®«4«WC%«4«b«CM«MM««0«iM*K«*«4<>*^^
КонстантаЗначение
T—ROUTINEО
T_PRIORITY1
TJMMEDIATE2
T_FLASH3
Т OVERRIDEFLASH4
T_CRITIC_ECP5
T_INETCONTROL6
T_NETCONTROL7
Таблица 32.4. Значения поля типа сервиса IPv4, используемые с параметром TJP TOS
x^M<lKiKtfxfMt9Kfxaum<imeiuKt^Vii^^AKKKtm№m-^Km-^v>^mwm«>»v>№VK^iiiW^MiM^mw^^
КонстантаОписание
T—NOTOSНормальный
T—LDELAYМинимальная задержка
T_HITHRPTМаксимальная производительность
T—HIRELМаксимальная надежность
T_LOCOSTМинимальная стоимость
Макрос SET_TOS (определенный в подключаемом файле <xti .h>) объединяет свой первый аргумент (значение поля приоритета из табл. 32.3) со вторым аргументом (значением поля типа сервиса из табл. 32.4), и этот результат используется параметром XTI.
Вызывая функцию t_optmgmt с запросом T CURRENT, мы получаем текущее значе* ние параметров IP, которые будут использоваться в исходящих дейтаграммах.
Параметр T_IP_TTL
Этот параметр аналогичен параметру сокета IP_TTL, Значением этого параметра является значение поля TTL протокола IPv4. Этот параметр устанавливается для того, чтобы задать значение, используемое в исходящих дейтаграммах. Тем не
менее не существует способа получить значение поля TTL из полученной дейтаграммы.
Параметр T_TCP_KEEPALIVE
Этот параметр аналогичен параметру сокетов SO_KEEPALIVE — он контролирует отправку пакетов для осуществления проверки наличия связи с собеседником через соединение TCP. В этом параметре XTI используется следующая структура: struct t_kpalive {
t_scalar_t kp_onoff /* T_NO (отключаем). T_YES
(включаем) или T_YES|T_GARBAGE
(включаем и отправляем "мусорный" байт) */
t_scalar_t kp_timeout. /* время ожидания в минутах:
T—UNSPEC - значение по умолчанию */
}.-
Этот параметр аналогичен параметру XTIJINGER в том, что значение kp onoff является обязательным, но значение kp_timeout не является таковым.
ПРИМЕЧАНИЕ
Отправка «мусорного» байта не должна быть обязательным требованием, и на самом деле константа T_GARBAGE была удалена из Unix 98. Использование «мусорного» байта обсуждается на с. 335 книги [94].
Параметр T_TCP_MAXSEG
Этот параметр аналогичен параметру сокетов TCP_MAXSEG. Он предназначен только для чтения и возвращает максимальный размер сегмента (MSS) для точки доступа TCP. Поскольку этот параметр доступен только для чтения, его значение не может являться обязательным.
Параметр T_TCP_NODELAY
Этот параметр аналогичен параметру сокетов TCP_NODELAY. Значением этого параметра может быть либо T_YES (отключение алгоритма Нагла), либо T_NO (значение по умолчанию, алгоритм Нагла включен). Об алгоритме Нагла более подробно говорится в разделе 7.9.
Параметр T_UDP_CHECKSUM
Этот параметр XTI является одним из сквозных параметров, следовательно, функция t_rcvudata всегда возвращает его значение, если запрошены полученные параметры (то есть если значение opt maxlen ненулевое). Значением этого параметра может быть либо T YES, либо T_NO.
ПРИМЕЧАНИЕ
Этот параметр никогда не должен быть включен. Вообще предоставление приложению возможности отключать для какой-либо точки доступа отправку контрольных сумм UDP является ошибкой. Можно привести немало таких примеров, когда при отключении контрольных сумм UDP происходило нарушение целостности данных. Более того, не существует причины, по которой вообще следовало бы отключать контрольные суммы UDP. Единственным разумным применением этого параметра является проверка, включены ли контрольные суммы UDP у собеседника.
Функция 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_opthd「 */
t~scaldr_t flags. /* действие при вводе, результат при выводе */
Поле flags структуры request указывает, какое именно действие требуется предпринять:
Т_СНЕСК — проверить, поддерживаются ли указанные параметры;
T_DEFAULT — получить значения параметров по умолчанию;
' T_CURRENT — получить текущие значения параметров;
T NEGOTIATE — согласовать значения параметров.
В последующих разделах мы исследуем каждое из этих четырех действий.
У нас есть возможность задать сразу несколько параметров в одном и том же вызове функции t_optmgmt, как показано на рис. 32.2. Но в таком случае для всех параметров должно быть задано одно и то же значение поля level. Для рис. 32.2 это не вызывает затруднений, поскольку поле level для обоих параметров имеет значение T_INE、IP. При использовании функции t_optmgmt для согласования сразу нескольких значений параметров возникают определенные затруднения: возвращенное поле flags содержит только один (наихудший) результат, хотя для каадого параметра возвращается его статус (status). Чтобы избежать указанных проблем, проще всего будет оперировать только одним параметром при каждом вызове функции t_optmgmt.
ПРИМЕЧАНИЕ
Эта функция XTI соответствует функциям сокетов getsockopt и setsockopt.
Проверка наличия параметра и получение значения по умолчанию
Нашим первым примером работы с параметрами XTI будет проверка того, какие из параметров, перечисленных в табл. 32.1, поддерживаются в нашей системе.
Для каждого поддерживаемого параметра мы получим и выведем его значение по умолчанию. Программа представлена в листинге 32.11.
Листинг 32.1. Проверка наличия параметров XTI
//xtiopt/checkopts с
1 #1 nd udeMunpxti h"
3 4 5 6 7 8 93 4 5 6 7 8 9struct xti opts {}char*opt_str.t_uscd1 ar_topt_level.t_usca1 ar_topt_name.char - *(*opt_val_str)(struct t_opthdr *). xti_opts[] = {“XTi—DEBUG”.XTI_GENERIC,XTI_DEBUG,xti_str_uscala「d.“XTl[LINGER”.XT 匸GENERIC.XT 匸 LINGER.х{:1[$1т[11пдег."XT匸RCVBUF".XT匸 GENERIC.XT匸RCVBUF.xti_str2uscalard."XT 匸RCVLOWAT".XT 匸GENERIC.XTl[RCVLUWAT.xti^stFuscalard“XT 匸SNDBUF”.XT匸GENERIC.XT匸SNDBUF.xti_str:uscdld「d."XT 匸SNDLOWAT”.XT 匸GENERIC,XTl[SNDLOWAT.xti_stFuscalard."T_lP_BROADCAST".TJNETJP,T^IP_BROADCAST,xti_St匸wyn.“匸IP二DONTROUTE".TJNETJP,匸IP二DONTROUTE.xti_stFuiyn."匸IP二OPTIONS".匸INET]lP.匸IP二OPTIONS.xtCstr二uchard."T-1P~REUSEADDR".T~INET二IP,TJP~REUSEADDR.xti_Str~uiyn,,,TJPJ0S•,.TJNETJP.TJPJOS.xt 匸 stEucharx.“T[IP二TTL”.TJNETJP.TJPZTTL.xti_str2uchard."VCP KEEPALIVE”. 匸INET二TCP.T二TCP_KEEPALIVE.xt匸St匸kpalive."匸 TCP 二 MAXSEG”.匸INE匸 TCP.T:TCP 二 MAXSEG.xti_str[uscalard."匸TCP-NODELAY”.匸 INET^TCP ・Т[ТСР二 NODELAY,xti[str]jsyn."T^UDP_CHECKSUM,•,T-INET~UDP.T~UDP~CHECKSUMxti stFusyn.NULL,0.0.NULLstruct xti opts {}char*opt_str.t_uscd1 ar_topt_level.t_usca1 ar_topt_name.char - *(*opt_val_str)(struct t_opthdr *). xti_opts[] = {“XTi—DEBUG”.XTI_GENERIC,XTI_DEBUG,xti_str_uscala「d.“XTl[LINGER”.XT 匸GENERIC.XT 匸 LINGER.х{:1[$1т[11пдег."XT匸RCVBUF".XT匸 GENERIC.XT匸RCVBUF.xti_str2uscalard."XT 匸RCVLOWAT".XT 匸GENERIC.XTl[RCVLUWAT.xti^stFuscalard“XT 匸SNDBUF”.XT匸GENERIC.XT匸SNDBUF.xti_str:uscdld「d."XT 匸SNDLOWAT”.XT 匸GENERIC,XTl[SNDLOWAT.xti_stFuscalard."T_lP_BROADCAST".TJNETJP,T^IP_BROADCAST,xti_St匸wyn.“匸IP二DONTROUTE".TJNETJP,匸IP二DONTROUTE.xti_stFuiyn."匸IP二OPTIONS".匸INET]lP.匸IP二OPTIONS.xtCstr二uchard."T-1P~REUSEADDR".T~INET二IP,TJP~REUSEADDR.xti_Str~uiyn,,,TJPJ0S•,.TJNETJP.TJPJOS.xt 匸 stEucharx.“T[IP二TTL”.TJNETJP.TJPZTTL.xti_str2uchard."VCP KEEPALIVE”. 匸INET二TCP.T二TCP_KEEPALIVE.xt匸St匸kpalive."匸 TCP 二 MAXSEG”.匸INE匸 TCP.T:TCP 二 MAXSEG.xti_str[uscalard."匸TCP-NODELAY”.匸 INET^TCP ・Т[ТСР二 NODELAY,xti[str]jsyn."T^UDP_CHECKSUM,•,T-INET~UDP.T~UDP~CHECKSUMxti stFusyn.NULL,0.0.NULL
3 4 5 6 7 8 9
3 4 5 6 7 8 9
struct xti opts {
}
char*opt_str.
t_uscd1 ar_topt_level.
t_usca1 ar_topt_name.
char - *(*opt_val_str)(struct t_opthdr *). xti_opts[] = {
“XTi—DEBUG”.XTI_GENERIC,XTI_DEBUG,xti_str_uscala「d.
“XTl[LINGER”.XT 匸GENERIC.XT 匸 LINGER.х{:1[$1т[11пдег.
"XT匸RCVBUF".XT匸 GENERIC.XT匸RCVBUF.xti_str2uscalard.
"XT 匸RCVLOWAT".XT 匸GENERIC.XTl[RCVLUWAT.xti^stFuscalard
“XT 匸SNDBUF”.XT匸GENERIC.XT匸SNDBUF.xti_str:uscdld「d.
"XT 匸SNDLOWAT”.XT 匸GENERIC,XTl[SNDLOWAT.xti_stFuscalard.
"T_lP_BROADCAST".TJNETJP,T^IP_BROADCAST,xti_St匸wyn.
“匸IP二DONTROUTE".TJNETJP,匸IP二DONTROUTE.xti_stFuiyn.
"匸IP二OPTIONS".匸INET]lP.匸IP二OPTIONS.xtCstr二uchard.
"T-1P~REUSEADDR".T~INET二IP,TJP~REUSEADDR.xti_Str~uiyn,
,,TJPJ0S•,.TJNETJP.TJPJOS.xt 匸 stEucharx.
“T[IP二TTL”.TJNETJP.TJPZTTL.xti_str2uchard.
"VCP KEEPALIVE”. 匸INET二TCP.T二TCP_KEEPALIVE.xt匸St匸kpalive.
"匸 TCP 二 MAXSEG”.匸INE匸 TCP.T:TCP 二 MAXSEG.xti_str[uscalard.
"匸TCP-NODELAY”.匸 INET^TCP ・Т[ТСР二 NODELAY,xti[str]jsyn.
"T^UDP_CHECKSUM,•,T-INET~UDP.T~UDP~CHECKSUMxti stFusyn.
NULL,
0.
0.
NULL
struct xti opts {
}
char*opt_str.
t_uscd1 ar_topt_level.
t_usca1 ar_topt_name.
char - *(*opt_val_str)(struct t_opthdr *). xti_opts[] = {
“XTi—DEBUG”.XTI_GENERIC,XTI_DEBUG,xti_str_uscala「d.
“XTl[LINGER”.XT 匸GENERIC.XT 匸 LINGER.х{:1[$1т[11пдег.
"XT匸RCVBUF".XT匸 GENERIC.XT匸RCVBUF.xti_str2uscalard.
"XT 匸RCVLOWAT".XT 匸GENERIC.XTl[RCVLUWAT.xti^stFuscalard
“XT 匸SNDBUF”.XT匸GENERIC.XT匸SNDBUF.xti_str:uscdld「d.
"XT 匸SNDLOWAT”.XT 匸GENERIC,XTl[SNDLOWAT.xti_stFuscalard.
"T_lP_BROADCAST".TJNETJP,T^IP_BROADCAST,xti_St匸wyn.
“匸IP二DONTROUTE".TJNETJP,匸IP二DONTROUTE.xti_stFuiyn.
"匸IP二OPTIONS".匸INET]lP.匸IP二OPTIONS.xtCstr二uchard.
"T-1P~REUSEADDR".T~INET二IP,TJP~REUSEADDR.xti_Str~uiyn,
,,TJPJ0S•,.TJNETJP.TJPJOS.xt 匸 stEucharx.
“T[IP二TTL”.TJNETJP.TJPZTTL.xti_str2uchard.
"VCP KEEPALIVE”. 匸INET二TCP.T二TCP_KEEPALIVE.xt匸St匸kpalive.
"匸 TCP 二 MAXSEG”.匸INE匸 TCP.T:TCP 二 MAXSEG.xti_str[uscalard.
"匸TCP-NODELAY”.匸 INET^TCP ・Т[ТСР二 NODELAY,xti[str]jsyn.
"T^UDP_CHECKSUM,•,T-INET~UDP.T~UDP~CHECKSUMxti stFusyn.
NULL,
0.
0.
NULL
26 mt
27 mamdnt argc. char **argv)
28 {
29 mt fd.
30
struct t_opthdr *topt.
31
struct t_optmgmt *req. *ret:
32
struct xti_opts *ptr.
33 if (argc != 2)
34 err_quit(" usage checkopts <device>f,};
35 fd = T_open(argv[l]. 0_RDWR. NULL).
36 T_bmd(fd, NULL, NULL):
37 req = T_alloc(fd. T_0PTMGMT. T_ALL);
38 ret =匸 dlloc(fd,匸 OPTMGMT.匸 ALL):
39 for (ptr = xti_opts. ptr->opt_str i= NULL, ptr++) {
40 topt = (struct t_opthdr *) req->opt.buf,
41topt->level = ptr->opt_level.
42topt->name = ptr->opt_name;
43topt->len = sizeof(struct t_opthdr).
44req->opt len = topt->len.
Все исходные коды программ, опубликованные в этой кнйге, вы мо5кет^ найти по адресу http:〃 www.piter.com/download.
45req->flags = Т_СНЕСК.
46printfC'^s ". ptr->opt_str).
47 if (t_optmgmt(fd. req. ret) < 0) {
48 err_xti_ret(Ht_optmgmt error")
49} else {
50 topt = (struct t_opthdr *) ret->opt buf.
51 pnntf(«s”. xti_str_f 1 ags (topt->status)).
52535455565758596061626364655253545556575859606162636465if (topt->s tat us == T_SUCCESS 11 topt->status == TREADONLY) { req->flags = T_DEFAULTr
52
53
54
55
56
57
58
59
60
61
62
63
64
65
52
53
54
55
56
57
58
59
60
61
62
63
64
65
if (t_optmgmt(fd. req. ret) < 0) {
err xti ret("t optmgmt error for T DEFAULT").
} else {
topt = (struct t_opthdr *) ret->opt buf.
pnntf(". default = Zs“. (*ptr->opt_va 1 _str) (topt)):
}-
}
prmtf("\en").
}
exit(O).
2-25 Мы определяем и инициализируем структуру, определяющую все параметры XTI, приведенные в табл. 32.1. Последнее поле каждого элемента массива — это указатель на функцию, выводящую значение параметра. Для каждого типа параметра нам нужна своя функция. Здесь мы не приводим исходный код для всех этих функций.
Открытие устройства
35-38 Используя имя устройства в качестве аргумента командной строки, мы открываем это устройство. Это позволят нам дважды выполнить программу — один раз для устройства /dev/tcp, а другой — для устройства /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 или
T_READONLY, то мы снова вызываем функцию 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
XTI—LINGER T_SUCCESS. default = T_NO. 0 sec
XT匸RCVBUF 匸 SUCCESS, default = 16384 XTI~RCVLOWAT ~T SUCCESS default = 1
XTI—SNDBUF T SUCCESS. default = 16384
XT匸 SNDLOWAT -T_SUCCESS. default = 4096 T_IP_BROADCAST ~T_SUCCESS. default = T_NO 匸IP二DONTROUTE SUCCESS, default =匸NO TJPZOPTIONS T_SUCCESS. default = 0 (length of value) 匸IP 二 REUSEADDR ~T_SUCCESS. default = T_NO TJPJOS ^SUCCESS, default = 0x00 匸IP二TTL TSUCCESS. default = 0
TJCP_KEEPALIVE T_SUCCESS. default * T_NO. T_UNSPEC 匸 TCP 二 MAXSEG T_READONLY. default = 512~—
T;CP二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
XTQINGER T_SUCCESS. default = T_NU. 0 sec XTl[RCVBUF 匸SUCCESS, default = 41600 XT匸RCVLOWAT -T_SUCCESS. default = 1 XT匸SNDBUF TJUCCESS, default = 9216
XT匸 SNDLOWAT ~T_SUCCESS. default = 4096 T_IP_BROADCAST ~T_SUCCESS default = T_NO 匸IP 二DONTROUTE 匚SUCCESS, default =匸NO T_IP_OPTIONS T_SUCCESS. default = 0 (Tength of value) 匸IP二REUSEADDR ~T_SUCCESS. default = T_NO 匸IP二TOS T SUCCESS, default = 0x00 — 匸IPjTL T二SUCCESS, default = 0
T_TCP_KEEPALIVE toptmgmt 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 f_NOTSUPPORT
Полученные значения всех параметров (кроме T_IP JTL) таковы, как мы и ожидали. Для параметра T_UDP_CHECKSUM, который не поддерживается TCP, и трех параметров TCP, не поддерживаемых UDP, функция t_optmgmt возвращает ошибку TBADOPT. Поскольку параметр T_UDP_CHECKSUM не поддерживается поставщиком UDP, функция возвращает ошибку T_NOTSUPPORT. Строка length of value (размер значения), соответствующая параметру T_IP_OPTIONS, указывает, что значение поля 1еп в ответе было равно нулю, то есть не было получено никакого значения для вывода.
Получение и установка значений параметров XTI
В этом разделе мы приводим примеры получения и установки значений параметров XTL Мы определяем две собственные функции, xti_getopt и xt?_setopt, последовательность вызовов в которых такая же, как и в функциях getsockopt и setsockopt.
#mcl ude "unpxti h"
int xti_getopt(int fd. int level mt пзте void *optval socklen_t ★optlen)
int xti_setopt(int fd. int level. int name, 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
//1ibxti/xti_getopt с
#include- “unpxti h"
2 3 4 5 6 72 3 4 5 6 7mt
2 3 4 5 6 7
2 3 4 5 6 7
xti_getopt(mt fd mt level, int name, void *optval. socklen t *optlenp)
{一
int rc. len
struct t_optmgmt *req *ret
struct t^opthdr *topt.
req = T_alloc(fd. T/PTMGMT T_ALL).
ret = T_alloc(fd. T[0PTMGMT T_ALL)
if (req->opt maxlerf」0)
err_quit("xtigetopt opt maxlen == 0м);
topt = (struct t_opthdr *) req->opt buf.
topt->level = level.
topt->name = name.
topt->len = sizeof(struct t_opthdr), /* просто t_opthdr{} */
req->opt len = topt->len. ~
req->flags = T_CURRENT.
if (t_optmgmt(fd. req. ret) < 0) {
T_free(req T_0PTMGMT)
T_free(ret. T 二 OPTMGMT),
return (-1),
}
rc = ret->fldgs.
if (rc == T_SUCCESS || rc == T_READONLY) {
/* копируем значение и длину */
topt = (Struct t_opthdr *) ret->opt bufпродолжение 伞
1 78Ю^О1234567 J 222333333331 78Ю^О1234567 J 22233333333
1 78Ю^О1234567 J 22233333333
1 78Ю^О1234567 J 22233333333
len = topt->len - sizeof(struct topthdr); len = minden. *optlenp).
memcpy(optval, topt + 1. len).
*optlenp = len,
}
T_free(req, T_OPTMGMT),
匸free(ret.匸 OPTMGMT).
if (rc == T_SUCCESS И rc == T—READONLY) return (ОУ.
return (-1),/* T_NOTSUPPORT */
Размещение структур для запроса и ответа в памяти
8-11 Мы вызываем функцию t_all ос для выделения в памяти места под структуры, в которые будут записаны запрос и ответ (req и ret). Также мы проверяем, что размер буфера параметров ненулевой.
ПРИМЕЧАНИЕ
В более старых реализациях TLI часто использовалось нулевое значение для размера параметров ТЫ. При этом подразумевалось, что приложение должно выделить собственный буфер для записи этих значений.
Заполнение структуры t_opthdr
12-16 Мы заполняем структуру t__opthdr, записывая в нее значения полей 1 evel и name для данного параметра. В структуру req (запрос) мы не записываем никакого значения, так как для получения текущего значения параметра это не требуется.
Вызов функции t_optmgmt и получение значения параметра
17-31 Мы вызываем функцию t_optmgmt и сохраняем значение поля fl ags структуры 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.3. функция xti setopt: установление значения параметра XTI
//libxti/xti_setopt с
#1 nd ude*'unpxti h"
2 3 4 5 6 72 3 4 5 6 7mt
2 3 4 5 6 7
2 3 4 5 6 7
xti_setopt(mt fd, int 1 evel. mt name, void *optval. socklen_t optlen)
int rc,
struct t_optmgmt *req, *ret.
struct t opthdr *topt,
req = T_alloc(fd. T_OPTMGMT, T_ALL);
ret =匸dlloc(fd,匸 OPTMGMT.匸 ALL).
if (req->opt maxlen == 0)
err_quit("xti_setopt req opt maxlen == 0"):
topt = (struct t_opthdr *) req->opt buf
topt->1evel = level.
topt->name = name,
topt->len = sizeof(struct t_opthdr) + optlen:
if (topt->len > req->opt maxlen)
err_qint("optlen too big”)
req->opt len = topt->len;,
memcpy(topt + 1. optval. optlen); /* копируем значение параметра */
req->flags = T_NEGOTIATE.
if (t_optTngmt(fd. req. ret) < 0) {
Vree(req. T_UPTMGMT).
OreeCret.匸OPTMGMT):
return (-1),-
}
rc = ret->flags:
T_free(req T_0PTMGMT).
匸Tree(ret T^OPTMGMT);
if (rc == T^SUCCESS || rc == T_PARTSUCCESS)
return (05.
return (-1)./* TFAILURE. T_NOTSUPPORT. T_READONLY */
}
Копирование значения, заданного
вызывающим процессом
12-19 Мы копируем значение параметра, заданное вызывающим процессом, в построенный нами буфер, размещая его непосредственно после структуры t_opthdr.
Вызов функции t_optmgmt
20-26Теперь значение поля flags структуры req для функции t_optmgmt равно T_NEGOTIATE.~~
Освобождение занятой памяти и завершение функции
27-31 Если значение параметра не является обязательным, то возвращаемое значение будет равно T PARTSUCCESS, что можно считать признаком успешного выполнения функции.
В XTI мы можем задавать и получать значения параметров с помощью единственного вызова функции t_optmgmt. Для тех параметров, значения которых не являются обязательными (например, размер буферов отправки и приема), это
свойство удобно. При использовании наших собственных функций, представленных в этом разделе, для получения того же результата придется сначала вызвать функцию xti_setopt, а затем xti_getopt. Мы могли бы определить функцию, выполняющую обе эти задачи, но дополнительный вызов функции xti_getopt вряд ли может явиться узким местом для какого-либо приложения.
Пример
Теперь мы покажем пример использования приведенных выше функций. Программа, показанная в листинге 32.4, получает текущее значение максимального размера сегмента TCP (параметр MSS), устанавливает размер буфера отправки равным 65 536, а затем получает и выводит значение размера буфера отправки. Если мы откомпилируем и запустим эту программу, то получим следующий результат: aix % getsetopt
TCP mss = 512
send buffer size = 65536
Листинг 32.4. Пример использования наших функций xti^getppt и xfi^setopt
//xtiopt/getsetopt c
#ind ude "unpxti h"
int
mam(int argc char **argv)
{
mtfd.
socklen t optlen.
t_uscdldr_t mss. sendbuff,
fd = T_open(XTI_TCP. O_RDWR NULL),
T_bind?fd. NULL: NULL):
optlen = sizeof(mss).
Xti_getopt(fd. T_INET_TCP T_TCP_MAXSEG. &mss &optlen).
12printf("TCP mss =絶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. &s@ndbuff. &optlen)
17p门ntf("send buffer size = ^d\entt. sendbuff).
18 exit(O).
19 }
32.7. Резюме
Параметры XTI подлежат согласованию. При этом поставщик может вернуть не такое значение параметра, которое было указано в запросе. Хотя в XTI имеется много возможностей обработки параметров, самым простым способом для получения и установки значений этих параметров является определение двух базовых функций наподобие getsockopt и setsockopt и вызов этих функций из приложений.
ГЛАВА33
Потоки
Введение
Прежде чем рассматривать некоторые из дополнительных свойств 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, pnntf и т. п ).
Обзор
Потоки обеспечивают двустороннее соединение между процессом и драйвером, как показано на рис. 33.1. Хотя нижний блок мы называем драйвером, его не следует ассоциировать с каким-либо аппаратным устройством, поскольку это может быть и драйвер псевдоустройства (например, программный драйвер).
ЯдроЯдроРис. 33.1. Поток между процессом и драйверомРис. 33.1. Поток между процессом и драйвером
Ядро
Ядро
Рис. 33.1. Поток между процессом и драйвером
Рис. 33.1. Поток между процессом и драйвером
Головной модуль потока (stream head) состоит из программ ядра, которые запускаются при обращении приложения к дескриптору потока (например, при вызове функций read, putmsg, ioctl и т. п.).
Процесс может динамически добавлять и удалять промежуточные модули обработки (processing modules) между головным модулем и драйвером. Такой модуль осуществляет некий тип фильтрации сообщений, проходящих в одну или другую сторону по потоку. Этот процесс показан на рис. 32.2.
ЯдроЯдроРис. 33.2. Поток с модулем обработкиРис. 33.2. Поток с модулем обработки
Ядро
Ядро
Рис. 33.2. Поток с модулем обработки
Рис. 33.2. Поток с модулем обработки
В поток может быть помещено любое количество модулей. Под словом «поместить» (push) в данном случае понимается, что каждый новый модуль вставляется сразу после (на рисунке — ниже) головного модуля.
Определенный тип псевдодрайвера называется мультиплексором (multiplexor), Он принимает данные из различных источников Основанная на потоках реализация набора протоколов TCP/IP, используемая, например, в SVR4, может иметь вид, показанный на рнс. 33.3.
Рис. 33.3. Упрощенный вид реализации набора протоколов TCP/IR основанной на потоках
При создании сокета библиотекой сокетов в поток помещается модуль sockmod. Именно комбинация библиотеки сокетов и потокового модуля обеспечивает API сокетов для процесса.
При создании точки доступа XTI библиотекой XTI в поток помещается модуль timod. Именно комбинация библиотеки XTI и потокового модуля обеспечивает API XTI для процесса.
В разделе 28.12 мы упомянули, что обычно для использования функции read или wri te в точке доступа XTI требуется поместить в поток потоковый модуль ti rdwr. Это осуществляется процессом, использующим TCP, который на рис. 33.3 изображен четвертым слева. Вероятно, этот процесс тем самым отказался от использования XTI, поэтому мы убрали надпись «библиотека ХТ1» из соответствующего блока.
Три служебных интерфейса определяют формат сетевых сообщений, проходящих вверх н вниз по потоку. TPI ( Transport Provider Interface — интерфейс поставщика транспортных служб) [103] определяет интерфейс, предоставляемый поставщиком услуг транспортного уровня (например, TCP или UDP).
NPI {Network Provider Interface — интерфейс поставщика сетевого уровня) [102] определяет интерфейс, предоставляемый поставщиком услуг сетевого уровня (например, IP). DLPI {Data Link Provider Interface) — это интерфейс поставщика канального уровня [101]. Еще один источник информации по TPI и DLPI, в котором имеются также исходные коды на языке С, — это [85].
ПРИМЕЧАНИЕ
В Usenet часто можно встретить утверждение типа «в потоковой среде сокеты реализованы через ТЫ (XTI)». Это неверно. Как показано на рис. 33.3, н сокеты, и XTI реализуются поверх TPI. Указанное утверждение часто сопровождается таким: «и поэтому TLI (XTI) быстрее, чем сокеты». Это также неверно. Уровни TCP, UDP и IP остаются теми же независимо от того, что используется — сокеты или XTL Меняется только пользовательская библиотека н модуль, помещаемый в поток, — timed или sokmod. Но автор не имеет в своем распоряжении никаких численных данных для сравнения этих библиотек и модулей, так как узкое место для всех приложений — это передача данных, а структура кода, вероятно, одинакова для XTI н сокетов, если только какая-либо техника оптимизации не была применена в одном случае и отсутствовала в другом.
Каждый компонент потока — головной модуль, все модули обработки и драйвер —содержит по меньшей мере одну пару очередей: очередь на запись и очередь на чтение. Это показано на рис. 33.4.
Рис. 33.4. Каждый компонент потока содержит по меньшей мере одну пару очередей
Типы сообщений
Потоковые сообщения могут быть классифицированы как имеющие высокий приоритет (high priority), входящие в полосу приоритета (priority band) и обычные (normal). Существует 256 полос приоритета со значениями между 0 и 255, причем обычные сообщения соответствуют полосе 0. Приоритет потокового сообщения используется как при постановке сообщения в очередь, так и для управления
потоком (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.
Таблица 33.1. Типы потоковых сообщений, генерируемые функциями write и putmsg
Функция
Управляющая Данные? ФлагиГенерируемый тип сообщения
информация?
write putmsg putmsg putmsg
ДаM_DATA
НетДа0M_DATA
ДаВсе равно0M_PROTO
ДаВсе равноMSG_HIPRIМ_РСPROTO
Что подразумевается под управляющей информацией, данными и флагами, станет ясно из нашего описания функций getmsg и putmsg.
Функции getmsg и putmsg
Данные, передаваемые в обоих направлениях по потоку, состоят из сообщений, а каждое сообщение содержит данные, управляющею информацию или и то и другое. Если мы используем функцию read или write, то мы можем передавать только данные. Для того чтобы процесс мог записывать и считывать как данные, так и управляющую информацию, необходимо добавить две новые функции.
#include <stropts h>
int getmsg(int fd. struct strbuf *ctlptr, struct strbuf *dataptr int *f7agsp)
int putmsg(int fd, const struct strbuf *ctlptr const struct strbuf *dataptr int flags).
Обе функции возвращают неотрицательное значение в случае успешного выполнения (см пояснения в тексте), -1 в случае ошибки
Обе составляющие сообщения — и сами данные, и управляющая информация —описываются структурой strbuf:
struct strbuf {
intmaxien./* максимальный размер буфера 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), в противном случае генерируется сообще-
ние типа M_PROTO либо M_PCPROTO в зависимости от значения аргумента fl ags. Этот аргумент функции putmsg имеет нулевое значение для обычных сообщений, а для сообщений с высоким приоритетом его значение равно RS_HIPRI.
Последний аргумент функции getmsg имеет тип «значение-результат». Если при вызове функции целочисленное значение, на которое указывает аргумент fl agsp, — это 0, то возвращается первое сообщение из потока (которое может быть как обычным, так и имеющим высокий приоритет). Если при вызове функции целочислен ное значение соответствует RSHIPRI, то функция будет ждать появления в головном модуле потока сообщения с высоким приоритетом. В обоих случаях в зависимости от типа возвращенного сообщения значение, на которое указывает аргумент flagsp, будет либо 0, либо RS_HIPRL
Предположим, что мы передаем функции getmsg непустые указатели ctlptr и datdptr. Тогда указанием на отсутствие управляющей информации (возвращается сообщение типа M_DATA) является значение ct】prt->】en, установленное в -1. Аналогично, если отсутствуют данные, указанием на это является значение -1 элемента dataptr->len.
Если функция putmsg выполнилась успешно, то она возвращает нулевое значение, а в случае ошибки возвращается значение —1. Но функция getmsg возвращает нулевое значение только в том случае, если вызывающему процессу было доставлено все сообщение целиком. Если буфер, предназначенный для приема управляющей информации, слишком мал, то возвращается значение MORECTL (о котором заранее известно, что оно является неотрицательным). Аналогично, если буфер для приема данных оказывается слишком мал, возвращается значение MOREDATA. Если же оба эти буфера оказываются слишком малы, то возвращается дизъюнкция (логическое ИЛИ) этих двух флагов.
Функции getpmsg и putpmsg
Когда с выпуском SVR4 к потоковым системам была добавлена поддержка различных полос приоритета, появились новые варианты функций getmsg и putmsg. #mclude <stropts h>
mt getpmsg(int 乃.struct strbuf *ctlptr, struct strbuf *datdptr. mt *b3ndp. mt ^flagsp).
mt putpmsg(int fd. const struct strbuf *ctlptr, const struct strbuf *ddtdptr, mt bdnd, int flags).
Обе функции возвращают неотрицательное значение в случае успешного выполнения, -1 в случае ошибки
Аргумент band функции putpmsg должен иметь значение в пределах от 0 до 255 включительно. Если аргумент fl ags имеет значение MSG.BAND, то генерируется сообщение в соответствующей полосе приоритета. Присваивание аргументу flags значения MSG_BAND и задание полосы 0 эквивалентно вызову функции putmsg. Если значение аргумента flags равно MSG_HIPRI, то аргумент band должен быть равен нулю, и тогда генерируется сообщение с высоким приоритетом. (Обратите внимание на то, что этот флаг имеет название, отличающееся от названия RS_HIPRI, используемого в случае функции putmsg.)
Два целочисленных значения, на которые указывают аргументы bandp и fl agsp функции getpmsg, являются аргументами типа «значение-результат». Целочислен-
ное значение, на которое указывает аргумент fl agsp функции getpmsg, может соответствовать MSG_HIPRI (для чтения сообщений с высоким приоритетом), MSG_BAND (для чтения сообщений из полосы приоритета, по меньшей мере равной целочисленному значению, на которое указывает аргумент bandp) или MSG_ANY (для чтения любых сообщений) По завершении функции целочисленное значение, на которое указывает аргумент bandp, указывает на полосу приоритета прочитанного сообщения, а целое число, на которое указывает аргумент fl agsp, соответствует MSG_HIPRI (если было прочитано сообщение с высоким приоритетом) или MSG_BAND (если было прочитано иное сообщение)
Функция ioctl
Говоря о потоках, мы снова возвращаемся к функции ioctl» которая уже была описана в главе 16
#include <stropts h>
int ioctl(int fd int request /* void *arg */ )
Возвращает 0 в случае успешного выполнения 1 в случае ошибки
Единственным изменением относительно прототипа функции, приведенного в разделе 16 2, является включение заголовочных файлов, необходимых для работы с потоками
Существует примерно 30 запросов (request) так или иначе влияющих на головной модуль потока Каждый из запросов начинается с 1_, и обычно документация на них приводится в руководстве strearm о Мы показали запрос I PUSH в листинге 28 3, когда помещали модуль ti rdwr в поток
При обсуждении управляемого сигналом ввода-вывода с использованием XTI в главе 34 мы обсудим запрос I_SETSIG
TPI: интерфейс поставщика транспортных служб
На рис 33 3 мы показали, что TPI — это интерфейс, предоставляющий доступ к транспортному уровню для рас положенных выше уровней Этот интерфейс используется в потоковой среде как сокетами, так и XTI Из рис 33 3 видно, что комбинация библиотеки сокетов и sokmod, а также комбинация библиотеки XTI и imod обмениваются сообщениями TPI с TCP и UDP
TPI является интерфейсом, основанным на сообщениях (message-based) Он определяет сообщения, которыми обменивается приложение (например, XTI или библиотека сокетов), и транспортный уровень Точнее, TPI задает формат этих сообщений и то, какое действие производит каждое из сообщений Во многих случаях приложение посылает запрос поставщику (например, «Связать данный локальный адрес»), а поставщик посылает обратно ответ («Выполнено» или «Ошибка») Некоторые события, происходящие асинхронно на стороне поставщика (например, прибытие запроса на соединение с сервером), инициируют отправку сигнала или сообщения вверх по потоку
Мы можем обойти как XTI, так и сокеты, и использовать непосредственно TPI В этом разделе мы заново перепишем код нашего простого клиента времени и даты
с использованием 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
#include unpxti h
#】nc】ude〈sys/stream h>
#i nd ude<sys/tihdr h>
void tpi_bind(int const void * size_t)
void tpi_connect(int const void * s7ze__t)
ssize_t tpi_read(int void * size t)
void tpi_close(int)一
Нам нужно включить еще один дополнительный заголовочный файл помимо vsys/tihdr h>, содержащего определения структур для всех сообщений TPI
Листинг 33.2. Функция main для нашего клиента времени и даты с использованием TPI
//streams/tpi_daytime с
1 #шс1 ude tp】daytime h
2 3 4 5 6 72 3 4 5 6 7int
2 3 4 5 6 7
2 3 4 5 6 7
maindnt argc char **argv)
{
8
if (argc 2)
9
err_quit( usage
tpi_daytime <IPaddrfes§>"),
10
fd = Open (XTIJCP
O_RDWR 0)
intfd n
charrecvline[MAXLINE + 1]»
struct sockaddr in myaddr servaddr
11/* связываем 叩опзвольннм локальный адрес */
12 bze「o(&myaddr sizeof(myaddr))
13 myaddr sin_family = AF」NET
14 myaddr sin_addr s_addr = htonl(INADDR_ANY)
15 myaddr sin_port = htons(O)
16 tpi_bind(fd &myaddr sizeofCstruct sockaddr_】n))
17/* заполняем адрес сервера */
18bzero(&servaddr sizeof(servaddr))
19servaddr sin_family = AF_INET
продолжение 少продолжение 少20 servaddr sin_port ^ htons(13) /* сервер времени и даты */
продолжение 少
продолжение 少
Все исходные коды программ опубликованные в этой книге вы можете найти по адресу http // www piter сот/do* nload
Листинг 33.2 (продолжение)
21 Inet_pton(AF_INET. argv[l], &servaddr sin_addr).
22 tpi_connect(fd. &servaddr. sizeof(struct sockaddr_in)).
23 for (. ) {
24 if ( (n = tpi_「ead(fd, recvline, MAXLINE)) <= 0) {
25if (n == 0)
26break,
27else
28err_sys(”tpi_read error")
} 一 ~
recvline[n] = 0./* завершающий нуль */
fputs(recvline. stdout).
}
tpi_close(fd).
exit(0),
}
Открытие транспортного устройства, связывание локального адреса
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_b】nd показана в листинге 33.3.
Листинг 33.3. Функция tpi_bjnd: связывание локального адреса с точкой доступа //streams/tpi^bind с
include ~ "tpi_daytime h"
void
33.6. TPI: интерфейс поставщика транспортных служб 917
3
4
5
6
7
8
9
10
11
12
13
14
15
tpi bmd(int fd const void *add「size t addrlen)
{ 一
struct {
struct T_bind_req msgjdr.
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
b】nd_req msg_hdr PRIM_type = T_BIND_REQ,
bind[「eq msg^hdr ADDR 二length 二 addrTen,
bmd_req msg_hdr ADDR_offset = sizeof(struct T_bind_req)
bind_req msg_hdr CONlND_number = 0
memcpy(bind_req addr addr addrlen). /* sockaddr_in{} */ ;;,
21
22
23
ctlbuf len = sizeof(struct 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_dck).
ctlbuf len = 0
ctlbuf buf ^ (char *) &tnnd_dck flags = RS_HIPRI.—
Getmsg(fd, &ct】buf NULL &flags). if (ctlbuf len < (int) sizeof(long)) err_quit("bdd 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) sizeof(struct T error ack)) err_quitCbad length for T_ERROR一ACK”)
error_ack = (struct T error_ack *) &bind_ack msg_hdr.
err_quit("TJRROR^ACK^from bind (况 紀尸
error ack ->TL I_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/tihdr п> определяет структуру T_bind_req: struct T_bmd_req {
long ~ PRIM_type /* T_BIND_REQ */
longADDR[length/* длина адреса */
longADDR^offset./* смещение адреса */
}.}.unsigned long CONIND__number /* сообщения о соединении */
}.
}.
/* далее следует адрес протокола для связывания */
Все запросы TPI определяются как структуры, начинающиеся с поля типа 1 ong. Мы определяем свою собственную структуру bind_req, начинающуюся со структуры T_bi nd_req, после которой располагается буфер, содержащий локальный адрес для связывания. TPI ничего не говорит о содержимом буфера — оно определяется поставщиком. Поставщик TCP предполагает, что этот буфер содержит структуру sockaddr_i п.
Мы заполняем структуру、bi nd req, устанавливая элемент ADDR_1 ength равным размеру адреса (16 байт для структуры адреса сокета Интернета), а элемент ADDR_ offset — равным байтовому сдвигу адреса (он следует непосредственно за структурой T_bi nd_「eq). У нас нет гарантии, что это местоположение соответствующим образом выровнено для записи структуры sockaddr_i п, поэтому мы вызываем функцию memcpy, чтобы скопировать структуру вызывающего процесса в нашу структуру bi nd_req. Мы присваиваем элементу CONIND number нулевое значение, потому что находимся на стороне клиента, а не на стороне сервера.
Вызов функции putmsg
21-23 TPI требует, чтобы только что созданная нами структура была передана поставщику как одно сообщение M_PROTO. Следовательно, мы вызываем функцию putmsg, задавая структуру bmd_req в качестве управляющей информации, без каких-либо данных и с флагом 0.*
Вызов функции getmsg для чтения сообщений с высоким приоритетом
24-30 Ответом на наш запрос T_BIND_REQ будет либо сообщение T_BIND_ACK, либо сообщение T_ERROR_ACK. Сообщения, содержащие подтверждение, отправляются как сообщения с высоким приоритетом (M PCPROTO), так что мы считываем их при помощи функции getmsg с флагом RS_HIPRI. Поскольку ответ является сообщением с высоким приоритетом, он получает преимущество перед всеми обычными сообщениями в потоке.
Эти два сообщения выглядят следующим образом:
struct T bind—dck { longpRlMJype,longADDR_length.struct T bind—dck { longpRlMJype,longADDR_length.longunsigned longlongunsigned longADDR_offset CONIND_numt)er.ADDR_offset CONIND_numt)er./* T_BIND_ACK *//* длина адреса *//* смещение адреса *//* индекс подключения для помещения в очередь/* T_BIND_ACK *//* длина адреса *//* смещение адреса *//* индекс подключения для помещения в очередь*/*/}•}•}.}.longPRIM type/* T_ERROR_ACK */longERROR_prim/* примитивная ошибкаlongTLI_error,/* код ошибки TLI */longUNIX_error,/* код ошибки UNIX */longPRIM type/* T_ERROR_ACK */longERROR_prim/* примитивная ошибкаlongTLI_error,/* код ошибки TLI */longUNIX_error,/* код ошибки UNIX *//* затем следует связанный адрес *//* затем следует связанный адрес */struct T_error_ack {struct T_error_ack {
struct T bind—dck { longpRlMJype,
longADDR_length.
struct T bind—dck { longpRlMJype,
longADDR_length.
long
unsigned long
long
unsigned long
ADDR_offset CONIND_numt)er.
ADDR_offset CONIND_numt)er.
/* T_BIND_ACK */
/* длина адреса */
/* смещение адреса */
/* индекс подключения для помещения в очередь
/* T_BIND_ACK */
/* длина адреса */
/* смещение адреса */
/* индекс подключения для помещения в очередь
*/
*/
}•
}•
}.
}.
long
PRIM type
/* T_ERROR_ACK */
long
ERROR_prim
/* примитивная ошибка
long
TLI_error,
/* код ошибки TLI */
long
UNIX_error,
/* код ошибки UNIX */
long
PRIM type
/* T_ERROR_ACK */
long
ERROR_prim
/* примитивная ошибка
long
TLI_error,
/* код ошибки TLI */
long
UNIX_error,
/* код ошибки UNIX */
/* затем следует связанный адрес */
/* затем следует связанный адрес */
struct T_error_ack {
struct T_error_ack {
В начале каждого сообщения указан его тип, так что мы можем начать считывать ответ, предполагая, что это сообщение LBIND_ACK, а затем, прочитав его тип, обрабатывать его тем или иным способом. Мы не ждем никаких данных от поставщика, поэтому третий аргумент функции getmsg мы задаем как пустой указа-
ПРИМЕЧАНИЕ
Когда мы проверяем, соответствует ли количество возвращенной управляющей информации по меньшей мере размеру длинного целого, нужно проявить осторожность, преобразуя значение sizeof в целое число. Оператор sizeof возвращает целое число без знака, но существует вероятность того, что значение возвращенного поля 1еп будет -1. Поскольку при выполнении операции сравнения слева располагается значение со знаком, а справа — без знака, компилятор преобразует значение со знаком в значение без знака. Если рассматривать -1 как целое без знака в архитектуре с дополнением до 2, это число получается очень большим, то есть -1 оказывается больше 4 (если предположить, что длинное целое число занимает 4 байта).
Обработка ответа
31-33 Если ответ — это сообщение T_BIND一АСК, то связывание прошло успешно, и мы возвращаемся. Фактический адрес, связанный с точкой доступа, возвращается в элементе addr нашей структуры bind_dck, которую мы игнорируем.
34-39 Если ответ — это сообщение T_ERROR_ACK, мы проверяем, было ли сообщение получено целиком, и выводим три значения, содержащиеся в возвращенной структуре. В этой простой программе при возникновении ошибки мы просто прекращаем выполнение и ничего не возвращаем вызывающему процессу.
Чтобы увидеть ошибки, которые могут возникнуть в результате запроса на связывание, мы слегка изменим нашу функцию mam и попробуем связать какой- либо порт, отличный от 0. Например, если мы попробуем связать порт 1 (что требует прав привилегированного пользователя, так как это порт с номером меньше 1024), мы получим следующий результат:
а1х % tpi_daytime 206.62.226.33
T_ERROR_ACK from bind (3 0)
В этой системе значение константы EACCESS равно 3. Если мы поменяем номер порта, задав значение, большее 1023, но используемое в настоящий момент другой точкой доступа TCP, мы получим:
d】x % tpi daytime 206.62.226.33
T_ERROR_ACK from bind (23. 0)
В данной системе значение константы EADDRBUSY равно 23.
ПРИМЕЧАНИЕ
Эта ошибка была введена в TPI для поддержки XTL Более старые версии TPI с поддержкой TLI в такой ситуации связали бы другой, неиспользуемый в настоящий момент порт, если запрошенный порт был бы занят. Это означало бы, что сервер, связывающий заранее известный порт, должен был бы сравнивать возвращенный адрес (из сообщения T_bind_ack, которое возвращается функцией t_bmd, если ее третий аргумент является непустым указателем) с запрошенным адресом, и если они не равны, прекращать работу.
Следующая функция показана в листинге 33.4. Это функция tpi_connect, устанавливающая соединение с сервером.
Листинг 33.4. Функция tpi_connect: установление соединения с сервером
//streams/tpi_connect с
Листинг 33.4 (продолжение)
tpi_connect(int fd const void *addr size_t addrlen)
4「
struct {
structr_conn_reqmsg_hdr
charaddr[128]
} conn_req
struct {
structT_conn一conmsg_hdr
charaddr[128]
} conn_con
struct~strbuf ctlbuf
union T_primitives rcvbuf
struct T_error_ack *error_ack
struct T_discon_ind *discon_ind
int flags
conn_req msgjidr PRIM type = T_CONN_REQ
conn_req msg_hdr DESTJength = addrTen
conn二req msgjidr DEST_offset = sizeof(struct T_conn_req>,
21conn_req msgjidr DPT_length = 0
22conn_req msg_hdr 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 ОУ
27ctlbuf maxlen = sizeof(union f^primitives)
28ctlbuf len = 0
29ctlbuf buf = (char*) &rcvbuf
30flags = RS—HIPRI
31Getmsg(fd &ctlbufNULL &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 )
40error_ack ^ (struct T_error_acR *) &rcvbuf
41err_quit( tpi.connect T_ERROR_ACK from conn (先d 定)
42error_ack >TLI_error error_ack >UNIX_error)
43 default
44 err_quit( tpi_connect unexpected message type W rcvbuf type).
45}—
46ctlbufmaxlen =sizeof(conn_con)
47ctlbuflen=0
48ctlbufbuf=(char*)&conn_con
49flags = 0
50Getmsg(fd &ctlbuf NULL &flags)
51 if (ctlbuf len < (int) sizeof(long))
52 err_quit( tpi_connect2 bad length from getmsg )
53 switch (сопп_соп msg_hdr PRIM_type) {
54 case T_CONN_CON
55 break
56 case T_DISCON_IND
57 if (ctlbuf Ten < (int) sizeof(struct T_discon_ind))
58 err_quit( tpi_connect2 bad length for T_DISCON_IND )
59discon_md = (struct T_discon_ind *) &conn_con msg_hdr
60err_quit( tpi_connect2 T_DISCON_IND from conn (%d了
61discon_ind >DISCON_reason)
62636465666263646566default
62
63
64
65
66
62
63
64
65
66
err_quit( tp】_connect2 unexpected message type Zd conn_con msg_hdr PRIM_type)
}--
Заполнение структуры запроса и отправка поставщику
18 26 В TPI определена структура T_conn_req, содержащая адрес протокола и параметры для соединения
struct T_conn_「eq { long 一 PRIM_type long DESTJength long DESToffset long OPT_Tength longOPT 二 offset/* затем следуютstruct T_conn_「eq { long 一 PRIM_type long DESTJength long DESToffset long OPT_Tength longOPT 二 offset/* затем следуют/* T_CONN一REQ */
struct T_conn_「eq { long 一 PRIM_type long DESTJength long DESToffset long OPT_Tength longOPT 二 offset
/* затем следуют
struct T_conn_「eq { long 一 PRIM_type long DESTJength long DESToffset long OPT_Tength longOPT 二 offset
/* затем следуют
/* длина адреса получателя */
/* смещение адреса получателя */
/* длина параметров */
/* смещение параметров */
адреса протокола и параметры соединения */
Как и в случае функции tpi_bi nd, мы определяем свою собственную структуру с именем conn req, которая включает в себя структуру T_conn_req, а также содержит место для адреса протокола Мы заполняем структуру conn.req, обнуляя поля ОРТ一1 ength и OPT_offset Мы вызываем функцию putmsg только с управляющей информацией и флагом 0 для отправки сообщения типа M_PROTO вниз по потоку
Чтение ответа
"45 Мы вызываем функцию getmsg, ожидая получить б ответ либо сообщение Т_ОК_АСК, если было начато установление соединения, либо сообщение T_ERROR一АСК (которые мы уже показывали выше) В случае ошибки мы завершаем выполнение программы Поскольку мы не знаем, сообщение какого типа мы получим, мы определяем объединение с именем T^pnnntives для приема всех возможных запросов и ответов и размещаем это объединение в памяти как входной буфер для управляющей информации при вызове функции getmsg
struct T_ok_ack {
longPRIM_type/* T—OK—ACK */
longCORRECT_prim/* корректный примитив */
Ожидание завершения установления соединения
6 65 Сообщение Т_ОК_АСК, полученное нами на предыдущем этапе, указывает лишь на то, что соединение успешно начало устанавливаться Теперь нам нужно дож» даться сообщения T_CONN_CON, указывающего на то, что другой конец соединения подтверждает получение запроса на соединение
struct Т conn con {/* T_CONN_CON */longPRIM_type.longRESJength./* длина адреса собеседника */longRES]offset./* смещение адреса собеседника */longOPT_length/* длина параметра */longOPT_offset,/* смещение параметра *//*далее следуютадрес протокола и параметры собеседникаstruct Т conn con {/* T_CONN_CON */longPRIM_type.longRESJength./* длина адреса собеседника */longRES]offset./* смещение адреса собеседника */longOPT_length/* длина параметра */longOPT_offset,/* смещение параметра *//*далее следуютадрес протокола и параметры собеседника
struct Т conn con {
/* T_CONN_CON */
long
PRIM_type.
long
RESJength.
/* длина адреса собеседника */
long
RES]offset.
/* смещение адреса собеседника */
long
OPT_length
/* длина параметра */
long
OPT_offset,
/* смещение параметра */
/*
далее следуют
адрес протокола и параметры собеседника
struct Т conn con {
/* T_CONN_CON */
long
PRIM_type.
long
RESJength.
/* длина адреса собеседника */
long
RES]offset.
/* смещение адреса собеседника */
long
OPT_length
/* длина параметра */
long
OPT_offset,
/* смещение параметра */
/*
далее следуют
адрес протокола и параметры собеседника
Мы снова вызываем функцию getmsg, но ожидаемое нами сообщение посылается как сообщение типа M_PROTO, а не как сообщение M_PCPROTO, поэтому мы обнуляем флаги. Если мы получаем сообщение T_CONN_CON, значит, соединение установлено, и мы возвращаемся, но если соединение не было установлено (по причине того, что процесс собеседника не запущен, истекло время ожидания или еще по какой-либо причине), то вместо этого вверх по потоку отправляется сообщение T_DISCON_IND: struct T_discon_ind {
long - PRIM_type. /* T_DISCON_IND */
long DISCON_reason. /* причина разрыва соединения */
long SEQ_number. /* порядковый номер */
}.—
Мы можем посмотреть, какие ошибки могут быть возвращены поставщиком. Сначала мы задаем IP-адрес узла, на котором не запущен сервер времени и даты: solaris26 % tpi_daytime 140.252.1.4 tpi_connect2 Т 二 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 % tpi daytime 192.3.4.5 tpi_connect2 T_DISCON_IND from conn (148)
На этот раз мы получаем ошибку EHOSTUNREACH. Различие в том, что в первый раз не было возвращено сообщение ICMP о недоступности узла, а во второй раз мы получили это сообщение.
Следующая функция, которую мы рассмотрим, — это tpi_redd, показанная в листинге 33.5. Она считывает данные из потока.
Листинг 33.5. Функция tpi_read: считывание данных из потока //streams/tpi_read с
2 3 4 5 6 7 82 3 4 5 6 7 8include - "tpi_daytime h" ss】ze_t
2 3 4 5 6 7 8
2 3 4 5 6 7 8
tpi_read(int fd. void *buf size_t len)
{ ~-
struct strbuf ctlbuf, struct strbuf datbuf. union T_primitives rcvbuf: intflags,
ctlbuf maxlen = sizeof(union Tjjrimltlves);
ctlbuf buf = (char *) &rcvbuf:
datbuf maxlen = len:
datbuf buf = buf.
datbuf.len = 0.
flags = 0.
GetmsgCfd. &ctlbuf. &datbuf, &flags):
if (ctlbuf len >= (int) sizeof(long)) {
if (rcvbuf type== T_DATA_IND)
return (datbuf len)/一
else if (rcvbuftype ==T_ORDREL_IND)
return (0).一 一
else
err_quit("tpi_read unexpected type W, rcvbuf.type);
} else if (ctlbuf len == -1)
return (datbuf len);
else
err_quit("tpi_read bad 1ength from getmsg"):
}——
Считывание управляющей информации и данных, обработка ответа
9-26 На этот раз мы вызываем функцию getmsg для считывания как данных, так и управляющей информации. Структура strbuf, предназначенная для данных, указывает на буфер вызывающего процесса. В потоке события могут развиваться по четырем различным сценариям.
Данные могут прибыть в виде сообщения M_DATA, и указанием на это является возвращенное значение длины управляющей информации, равное -1. Данные скопированы в буфер вызывающего процесса функцией getmsg, и функция просто возвращает длину этих данных.
Данные могут прибыть как сообщение T_0ATA_IN0, в этом случае управляющая информация будет содержаться в структуре T_data_i nd:
struct T_ddta_ind {
longPRIM_type. /* T_DATA_IND */
longMORE^flag. /* еще данные */
}.'
Если возвращено такое сообщение, мы игнорируем поле MORE_fIag (оно вообще не задается для таких протоколов, как TCP) и просто возвращаем длину данных, скопированных в буфер вызывающего процесса функцией getmsg. Сообщение T_ORDEL_IND возвращается, если все данные получены и следующим элементом является сегмент FIN:
struct T_ordrel_md {
long - PRIM type, /* T ORDREL IND */
}.~— -
Это нормальное завершение, о котором рассказывается в разделе 28.9. Мы просто возвращаем нулевое значение, указывая вызывающему процессу, что по соединению получен признак конца файла.
Сообщение 7^DISCON_IND возвращается, если произошел разрыв соединения. Этот сценарий обсуждался в разделе 28.10, где было сказано, что это происхо-
дит в случае 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 с
#indude "tpi daytime h"
2 3 4 5 62 3 4 5 6void
2 3 4 5 6
2 3 4 5 6
tpi_close(int fd)
{"
struct T_ordrel_req ordrel_req, struct strbuf ctlbuf.
ordrel_req PRIM_type = T_ORDREL_REQ
ctlbuf len = sizeof(struct T_ordrel_req).
ctlbuf buf = (char *) &ordreT_req
Putmsg(fd. &ctlbuf. NULL. 0).
Close(fd)
}
Отправка запроса о завершении собеседнику
7-10 Мы формируем структуру T_ordrel_req:
struct I_ordrel_req {
long PRIM_type. /* T_ORDREL_REQ */
}——
и посылаем ее как сообщение M PROTO с помощью функции putmsg. Это соответствует функции XTI Jsndrel.
Этот пример позволил нам почувствовать специфику TPL Приложение посылает сообщения вниз по потоку (запросы), а поставщик посылает сообщения вверх по потоку (ответы). Некоторые обмены сообщений организованы согласно простому сценарию «запрос-ответ» (связывание локального адреса), в то время как остальные могут занять некоторое время (установление соединения), позволяя нам заняться чем-то другим в процессе ожидания ответа. Для знакомства с TPI мы выбрали этот пример (написание клиента TCP) из-за его относительной про
стоты. Если бы мы решили написать с использованием TPI TCP-сервер, обрабатывающий одновременно несколько соединений (как показано в разделе 30.7), это было бы гораздо сложнее.
Из вышесказанного должно быть понятно, что между TPI и функциями XTI довольно тесная связь. С другой стороны, связь между сокетами и TPI уже не столь очевидна. Тем не менее и библиотека сокетов, и XTI упрощают наши приложения, так как с их помощью решается множество вопросов, связанных с TPI.
ПРИМЕЧАНИЕ
Можно сравнить количество системных вызовов, необходимых для осуществления определенных сетевых операций, показанных в этой главе, в случае применения TPI и в случае, когда используется ядро, реализующее сокеты. Связывание с локальным адресом в случае TPI требует двух системных вызовов, но в случае сокетного ядра требуется только один вызов [ 105, с. 454]. Для установления соединения на блокируемом дескрипторе в случае использования TPI требуется три системных вызова, а в случае сокетного ядра — только один [105, с. 466].
.7. Резюме
XTI часто реализуется с использованием потоков. Для обеспечения доступа к потоковой подсистеме вводятся четыре новые функции: getmsg, putmsg, getpmsg и putpmsg. Также в потоковой подсистеме широко используется уже описанная ранее функция wctl. TPI представляет собой потоковый интерфейс системы SVR4, предоставляющий доступ из верхних уровней на транспортный уровень. Он используется как сокетами, так и XTI, как показано на рис. 33.3. В этой главе в качестве примера использования основанного на сообщениях интерфейса мы разработали версию клиента времени и даты, в котором непосредственно применяется интерфейс TPL
Упражнение
В листинге 33.6 мы вызываем функцию putmsg, чтобы отправить вниз по потоку запрос на нормальное завершение соединения, а затем немедленно вызываем функцию close для закрытия потока. Что произойдет, если наш запрос будет потерян потоковой подсистемой, а мы закроем поток?
ГЛАВА 34
XTI: дополнительные функции
Введение
В предыдущих главах мы рассмотрели функции XTI для
клиентов TCP;
поиска имен узлов и служб;
серверов TCP;
клиентов и серверов UDP;
параметров;
типичных реализаций потоков.
В этой главе рассматриваются остальные функции XTI.
Неблокируемый ввод-вывод
Точка доступа может быть переведена в неблокируемый режим. Для этого требуется задать флаг 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, если соединение устанавливается, но процесс установления еще не завершен.
Функция t_】isten немедленно возвращает значение -1. При этом значение t_errno становится равным T_N0DATA. если на данный момент нет соединений, готовых к тому, чтобы пр肘刀оясение их приняло (с помощью функции t_accept). Если данные для приема отсутствуют, то четыре функции — t_rcv, t_rcvudata, t_tcvv и t_rcvvudata — возвращают -1. При этом переменная t_errno принимает значение TNODATA. Если некоторое количество данных доступно, то эти данные возвращаются приложению, хотя их может быть меньше, чем было запрошено приложением. (Последние две из упомянутых выше функций встречаются в этой книге впервые и будут описаны в разделе 34.8.)
” Если поставщик не в состоянии принимать какие-либо данные, то четыре функции —t_snd, t_sndudata, t_sndv и t_sndvudata — возвращают значение -1. При этом переменная t_errao принимает значение TFLOW. Если часть данных может быть принята, то возвращаемое значение может быть меньше количества, запрошенного функциями t_snd и t_sndv. Две функции для работы с дейтаграммами записывают сразу целую дейтаграмму, в противном случае они возвращают ошибку. (Последние две из упомянутых выше функций встречаются в этой книге впервые и будут описаны в разделе 34.9.)
Функция t_rcvconnect
В предыдущем разделе мы упоминали об инициировании соединения в неблокр^ руемом режиме, когда для того, чтобы дождаться завершения установлений fco- единения, вызывается функция t_rcvconnect.
#include <xti h> int t_getstate(mt 2).
Обычно эта функция выполняется в несколько этапов следующим образом:
С помощью функции t_open создается точка доступа и устанавливается в неблокируемый режим.
Функция t_connect инициирует установление соединения. Поскольку точка доступа находится в неблокируемом режиме, эта функция немедленно возвращает значение -1, a t_errno принимает значение TN0DATA.
В некоторый последующий момент времени процесс вызывает функцию t_rcv - connect, чтобы определить, завершилось ли установление соединения. Если точка доступа вышла из неблокируемого режима (после вызова на этапе 2 функции t connect процесс отключил флаг отсутствия блокировки), то функция t_rcvconnect блокируется, пока соединение не будет установлено. Если же точка доступа все еще находится в неблокируемом режиме, то этот вызов функции t_rcvconnect либо немедленно возвращается со значением 0, если соединение установлено, либо возвращает значение -1 со значением t_errno, равным TN0DATA, если соединение еще не установлено.
Обратите внимание, что в случае блокируемой функции ^.connect (режим работы по умолчанию) поставщик возвращает информацию в структуре t ea】】,на которую указывает третий аргумент функции t_connect. Но в случае неблокируемой функции t_connect эта информация возвращается в структуре t_cal 1, на которую указывает второй аргумент функции t_rcvconnect.
Если приложение не переводит точку доступа из неблокируемого режима в блокируемый в промежутке между вызовами функций t connect и t_rcvconnect (второй и третий этапы, упомянутые выше), то вызов функции t_「cvconnect для определения того, когда завершится неблокируемое установление соединения,— просто потеря времени, так как приложение должно вызывать функцию t_rcvcon - nect в теле некоторого цикла, ожидая, когда соединение установится (или будет возвращена ошибка). Этот процесс называется опросом (po/Z?唸).Для ожидания завершения неблокируемого установления соединения более удачным решением было бы вызывать либо функцию sei ect, либо функцию poll (см. главу 6) или использовать управляемый сигналом ввод-вывод (см. раздел 34Д1).
Функция t_getinfo
Вернемся к структуре t_i nfo, которую возвращает функция t open (см. раздел 28.2). Следующая функция возвращает вызывающему процессу ту же информацию.
#include <xti h>
int t_getinfo(int fd struct t_info ^wfo)
Возвращает 0 в случае успешного выполнения 1 в случае ошибки
Эта функция вызывается, например, функцией t_alloc для получения такой информации об уже открытой точке доступа, как требуемые значения размера буфера
Функция t_getstate
С каждой транспортной точкой доступа связано целое число, характеризующее ее текущее состояние (current state) Следующая функция возвращает вызываю* щему процессу это целое число
nclude <xti h>
Mt t_getstate(int fd)
Возвращает текущее состояние в случае успешного выполнения -1 в случае ошибки
Текущее состояние характеризуется одной из констант, приведенных в табл. 34.1.
Последние три колонки указывают состояния, допустимые для различных типов служб (см. табл. 28.3).
Таблица 34.1. Возможные состояния точки доступа XTI
%3f*S*:UM»<»»r«AM»KMM«W»MMC«MM»MCMC«C4»MWMC««M(4MH(4»:0«400MKMCMM4(«C«W««U«f0№№« 4«««««№4«««»!4<««8:tw;{«w^»»««»54m«(«*«»mw»!«c»b»«"»х««««m4»»5«m««««5*«mM»w*m4(«»0»>!*
Состояние
Описание
T_COTS
T_COTS_ORD
T_CLTS
T_DATAXFER
Передача данных
•
•
TJDLE
Связанное, но не активное
•
•
•
T—INCON
Входящее соединение ждет обработки
•
•
на неактивной точке доступа
T—INREL
Входящий запрос на нормальное
•
завершение
T_OUTCON '
Исходящее соединение ждет обработки
•
•
на активной точке доступа
T_OUTREL
Исходящий запрос на нормальное
•
завершение
T—UNBND
Несвязанное
•
•
•
T—UNINIT
Не инициализированное начальное
•
•
•
и конечное состояние
Для того чтобы показать, как именно меняется состояние конечной точки в результате вызовов различных функций XTI и других событий, происходящих в этой точке, можно построить диаграмму переходов состояний. Эта диаграмма также позволит показать, какие функции XTI допустимы для тех или иных состояний Например, в состоянии TJJNINT допустим вызов только одной функции 一 t_open, в результате чего точка доступа переходит в состояние T UNBANO. В состоянии T_UNBND возможны четыре события:
В случае успешного выполнения функции t close состояние точки доступа меняется на TJJNINT.
Вызов функции t_optmgmt допустим, но он не меняет состояния точки доступа. (Одним из аспектов, которые не могут быть отражены в диаграмме переходов состояний точки доступа, является изменение способа обработки параметров при переходе из одного состояния в другое. Например, параметр T_UDP_CHESKSUM обрабатывается в состоянии TJJNBND не так, как в других состояниях.)
Успешное выполнение функции t_bind изменяет состояние точки доступа на T_IDLE.—
Допускается передача соединения на точку доступа (с помощью функции t_accept), в результате чего состояние точки доступа меняется на T_DATAXFER. Если переходить к дальнейшим состояниям точки доступа, диаграмма переходов становится слишком громоздкой, поэтому мы не будем продолжать здесь ее рассмотрение.
Функция t_sync
Исторически интерфейс TLI был реализован как библиотека функций в системе SVR3. Рассмотрим npoi рамму, использующую TLI, которая вызывает функцию ехес, как показано на рис. 34.1. Программа А — это, возможно, прослушивающий сервер, ожидающий прибытия соединения, которое он должен принять, после чего он вызовет функцию ехес для выполнения программа В (обработка клиентского запроса). Напомним, что идентификатор процесса не меняется при выполнении ехес, но в память вызывающего процесса записывается новая программа, которая начинает выполняться как функция mam.
С этим сценарием в системе SVR3 связана следующая проблема: информация о состоянии сохраняется и в библиотеке TLI в пределах данного процесса, и в коде поставщика в ядре После выполнения функции ехес вся информация о состоянии в библиотеке стирается, и библиотека возвращается в исходное состояние, готовая к выполнению новой программы Целью функции t_sync является синхронизация состояния библиотеки (справа на рис. 34.1) при выполнении новой программы поставщиком в ядре.
В системе SVR4, однако, отпадает необходимость в вызове функции t_sync в сценарии, изображенном на рис. 34.1. Библиотечные функции TLI могут определить необходимость синхронизации самостоятельно. Например, если в библиотеке имеется переменная, объявленная следующим образом: static mt synced /* инициализируется нулем при запуске программы */
то каждая функция может начинаться с поеледовательности, аналогичной приведенной ниже:
t_connect(fd, )
if (synced -= 0)
t_sync(fd) /* также задает synced = 1 */
}}\&\&
}
}
\&
\&
Рис. 34.1. Реализация вызова функции ехес в TLI
В то время как описанным выше способом обрабатывается ситуация с вызовом функции ехес, имеется еще один сценарий, который, хотя и нечасто встречается, требует вызова функции t_sync: это происходит, когда несколько процессов совместно используют точку доступа XTI. В этом сценарии предполагается, что процессы взаимодействуют между собой и каждый из них вызывает функцию t_sync, когда это становится необходимо (что зависит, безусловно, от специфики приложения). Один из примеров ситуации, когда необходима функция t_sync,— это взаимодействие между родительским “ дочерним процессами, когда родительский процесс вызывает функцию t_li sten, а дочерний затем вызывает функцию t_accept. Родительскому процессу потребуется вызвать функцию t_sync для обновления библиотечной копии значения, указывающего на состояние точки доступа, которое может измениться после вызова дочерним процессом функции t_accept.
include <xti h>
int t_sync(int fd):
Возвращает текущее состояние в случае успешного выполнения, -1 в случае ошибки
При успешном выполнении эта функция возвращает одно из значений, приведенных в табл. 34.1.
ПРИМЕЧАНИЕ
В API сокетов не существует функции, которая выполняла бы аналогичное действие.
Функция t_unbind
Действие функции t_unfcn nd противоположно действию функции t_bi nd. include <xti h>
mt t_unbind(int 力).
Возвращает 0 в случае успешного выполнения, -1 в случае ошибки
Эта функция отключает точку доступа, определяемую дескриптором fd. Для этой точки доступа после выполнения данной функции не будут приниматься никакие данные. Тем не менее можно вызвать функцию t bi nd и связать с данной точкой доступа другой локальный адрес.
ПРИМЕЧАНИЕ
В случае сокетов подобное действие может быть выполнено только для присоединенных сокетов UDP путем вызова функции bind, если задать некорректный адрес.
Функции t_rcw и t_rcwudata
Эти две функции расширяют возможности, предоставляемые функциями t_rcv и _rcvudata, для работы с вектором буферов Rector of buffers), а не просто с отдельным буфером. Они обеспечивают возможности распределяющего чтения (scatter read).
ПРИМЕЧАНИЕ
Эти две функции вместе с двумя функциями, описанными в следующем разделе, были введены в Posix.lg.
Концепция работы с вектором буферов берет свое начало от функций readv и writev, а также функций recvmsg и sendmsg.
#include <xti h>
int t_rcvv(int fd struct tiovec *7ov, unsigned int lovcnt. int * flags},
int t_rcvvudata(int fd, struct t_umtdata *unitddtd,
struct t_iovec *7ov. unsigned int wvcnt, wt * flags).
Обе функции возвращают количество считанных или записанных байтов в случае успешного выполнения. -1 в случае ошибки
Аргумент iov обеих функций является указателем на массив структур t_iovec: struct t iovec {
void *iov_base /* начальный адрес буфера */
size_t iov_len, /* размер буфера в байтах */
Количество элементов в массиве задается аргументом movent. Максимальное количество элементов в массиве задается константой T_I0V^MAX, определенной в подключаемом заголовочном файле <xti h>. Значение этой константы не менее 16.
Сравнивая эти новые функции с их приведенными ранее аналогами, мы получим следующие выводы:
• Указатель на буфер и его длина являются соответственно вторым и третьим аргументами функции t_rcv.
Указатели на буферы и их длины содержатся в массиве структур t_iovec для функции t_rcvv, и ее второй аргумент указывает па этот массив структур, а третий задает количество элементов массива.
Для функции t_rcvudata указатель на буфер и его размер задаются полем udata структуры t_umtdata. Эта функция возвращает нулевое значение в случае успешного выполнения, а фактическая длина полученной дейтаграммы содержится в поле udata len структуры t_umtdata.
Для функции t_rcvvudata указатели на буфер и их размеры содержатся в поле udata структуры t_um tdata. Третьим аргументом функции является указатель на этот массив структур, а четвертым — количество элементов в этом массиве. Указатель на структуру t_umtdata — это второй аргумент функции. Поля addr и opt этой структуры по-прежнему используются (для адреса протокола отправителя и любых полученных параметров), но поле udata игнорируется. Эта функция возвращает количество байтов в дейтаграмме, а не нулевое значение.
Функции t_sndv и t.sndvudata
Эти две функции расширяют возможности, предоставляемые функциями t_snd и t_sndudata, для работы с вектором буферов вместо одного буфера. Это аналоги двух функций, описанных в предыдущем разделе, которые предоставляют возможность объединяющей записи, или записи со слиянием (gather wrzze).
#include <xti h>
int t_sndv(int fd struct t_iovec *iov unsigned mt lovcnt mt flags')
Возвращает количество считанных или записанных байтов в случае успешного выполнения
-1 в случае ошибки
int t_sndvudata(int fd struct t_umtdata ^unitdata
struct t_iovec *70/ unsigned int lovcnt}
Возвращает 0 в случае успешного выполнения -1 в случае ошибки
Аргумент iov в обеих функциях — это указатель на массив структур t iovec, которые были описаны в предыдущем разделе. Количество элементов в массиве задается аргументом i ovent.
Буферы вывода задаются вторым и третьим аргументами функции t_sndv, которые несколько отличаются от соответствующих аргументов функции t_snd. Для функций, работающих с дейтаграммами, буфер вывода задается элементом udata структуры t unidata в случае функции t__sndudata и вектором iov в случае функции t_sndvudata. Поле udata структуры t__umdata функция t_sndvudata игнорирует.
Функции t_rcvreldata и t_sndreldata
Если мы посылаем сообщение о нормальном завершении с помощью функции t_sndrel (см. раздел 28.9), мы не можем вместе с этим уведомлением посылать какие-либо данные (единственным аргументом функции является дескриптор). Но если это сообщение посылается функцией t.sndchs (см. раздел 28 10), мы можем заодно отправить и данные (в поле udata структуры t_call) Для функции t_rcvrel, в отличие от t_rcvdis, имеется такое же ограничение. Для тою чтобы
преодолеть его, в XTI введены две новые функции, способные посылать и получать данные вместе с уведомлением о нормальном завершении.
include <xti h>
int t_sndreldata(int fd. const struct t_discon *discon),
int t_rcvreldata(int fd struct t_chscon ^discon)
Обе функции возвращают 0 в случае успешного выполнения -1 в случае ошибки
Различие между этими двумя функциями и функциями t_sndrel и t_rcvrel заключается в появлении второго аргумента (указателя на структуру t_di scon).
Эти функции могут быть полезны только в том случае, когда поставщик поддерживает отправку данных вместе с сообщением о нормальном завершении, на что указывает флаг T_ORDRELDATA в поле fl ад структуры t_? nfo (см. табл. 28.4). Если эта функциональность поддерживается, то количество данных, отправляемых вместе с уведомлением о нормальном завершении, ограничено значением поля discon структуры t_info.
ПРИМЕЧАНИЕ
Эта функциональная возможность не является обязательной и не поддерживается протоколом TCP.
Управляемый сигналом ввод-вы вод
Управляемый сигналом ввод-вывод обеспечивается потоковой системой, а не XTL Имя сигнала в данном случае — SIGPOLL, и для доставки сигнала процессу недостаточно просто задать функцию-обработчик сигнала. Процесс также должен сообщить ядру о своей готовности принять сигнал с помощью запроса I_SETSIG потоковой функции ioctl, указав при этом, какие условия могут сгенерировать данный сигнал. Здесь можно заметить аналогию с действиями, необходимыми для получения сигналов SIGIO и SIGURG, о которых мы говорили, обсуждая API сокетов.
Третий аргумент функции ioctl — это целое число, определяющее условия, при которых должен быть сгенерирован сигнал SIGPOLL. Если это нулевое значение, процесс не будет больше получать сигнал SIGPOLL для потока. Значение этого аргумента также может быть сформировано из следующих констант путем применения логического сложения:
S_BANDURG. Если этот флаг задан вместе с флагом S_RDBAND, вместо сигнала SIGPOLL будет сгенерирован сигнал SIGURG, когда можно считывать сообщение из более высокой, чем нулевая, полосы приоритета.
S_ERROR. Ошибка в потоке.
S_HANGUP. До головного модуля потока дошло сообщение о зависании.
S—HIPRL Может быть прочитано сообщение с высоким приоритетом.
SJNPUT. Эквивалентно S_RDNORM | S RDBAND и означает, что может быть прочитано сообщение из любой полосы приоритета (включая нулевую).
S_OUTPUT. На очередь записываемых сообщений, расположенную непосредственно под головным модулем потока, не распространяется управление передачей (для обычных сообщений из полосы приоритета 0).
S_MSG. Сигнальное потоковое сообщение расположено в начале очереди считываемых сообщений в потоке.
S_RDNORM. Можно считывать обычное сообщение (из полосы приоритета 0).
S_RDBAND. Может быть прочитано сообщение из полосы приоритета выше нулевой.
S_WRNORM. Эквивалент S_OUTPUT.
SJWRBAND. На очередь записываемых сообщений, расположенную непосредственно под головным модулем потока, не распространяется управление передачей (для сообщений из полосы приоритета выше нулевой).
ПРИМЕЧАНИЕ
Флаг S BANDURG используется в API сокегов, когда он реализуется с использованием потоков.
Для записи сообщений не существует аналога флагу S HIPRI, используемому при чтении сообщений. Это объясняется тем, что функции putmsg и putpmsg не блокируются при отправке сообщений с высоким приоритетом: па эти сообщения не распространяется управление передачей.
Потоковый сигнал SIGPOLL используется как для управляемого сигналом ввода-вывода, так и для уведомлении о прибытии внеполосных данных. Это соответствует двум сигналам — SIGIO и SIGURG, которые упоминались при описании API сокетов.
Действием по умолчанию для сигнала SIGPOLL является завершение процесса, поэтому, используя этот сигнал, мы должны установить обработчик сигнала, а затем вызвать функцию ioctl для включения сигнала.
ПРИМЕЧАНИЕ
Имеется некоторое противоречие между действиями по умолчанию для сигнала SIGPOLL (завершение процесса, как только что было сказано) и для сигнала SIGIO. В Posix. 1g указано, что по умолчанию сигнал SIGIO игнорируется. Поскольку в системах SVR4 указано, что эти два сигнала одинаковы, эти системы должны будут изменить действие по умолчанию для сигнала SIGPOLL (он должен также игнорироваться), чтобы обеспечить совместимость с Posix. 1g.
Хотя сигнал SIGPOLL генерируется в различных ситуациях, обычные приложения, не взаимодействующие с внеполосными данными, заинтересованы только в условиях, характеризуемых константами S RDNORM и S_WRNORM.
Внеполосные данные
Внеполосные данные называются в XTI срочными данными. Поддержку этой функциональности обеспечивает поставщик транспортных услуг и потоковая система. В главе 33 мы упоминали, что внеполосные данные часто реализуются как обычные данные из полосы приоритета 1. Обычные данные, не являющиеся внеполосными, относятся к полосе приоритета 0.
ПРИМЕЧАНИЕ
В главе 33 мы также говорили, что поскольку внеполосные данные TCP не являются истинными срочными данными (в том смысле, который вкладывается в это понятие в случае XTI), факт ически они реализуются как данные из полосы приоритета 0, а не 1.
Все, что мы говорили в главе 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 при появлении внеполосных данных требуется, чтобы при вызове функции wctl с запросом I_SETSIG ее третьим аргументом была константа S RDNORM (см. раздел 34.11). При этом сигнал будет генерироваться и в случае появления обычных данных, поэтому мы должны вызвать функцию t_l ook из нашего обработчика сигнала и проверить, какое событие имело место — T DATA или T_EXDATA (см. табл. 28.8). Как только получен сегмент TCP со срочным указателем, в XTI устанавливается событие T—EXDATA, и оно остается установленным, пока все данные вплоть до срочного указателя не будут получены.
ПРИМЕЧАНИЕ
Сигнал SIGPOLL соответствует сигналу SIGURG в случае сокеюв.
При использовании функции poll для ожидания прибытия внеполосных данных (см. раздел 6.10) поле events структуры poll fd должно быть равно POLLRDNORM, поскольку эти данные рассматриваются как обычные данные из полосы приоритета 0. В листинге 34.2 и в листинге В.2 мы проверяем, что внеполосные данные TCP представляются для функции poll как обычные данные. Также отметим, что подобная трактовка внеполосных данных в XTI отличается от их трактовки в случае интерфейса сокетов, где внеполосные данные рассматриваются как принадлежащие к полосе приоритета.
ПРИМЕЧАНИЕ
Использование функции poll соответствует вызову функции select и ожиданию возникновения исключительной ситуации, по функция poll не сообщает, данные какого типа прибыли — для этого мы должны вызвать функции t look и t_rcv.
Напомним, что по умолчанию API сокетов удаляет внеполосный байт из потока обычных данных, помещая его в специальный одиобайтовый буфер, из которого приложение считывает данные с помощью функции reev с флагом MSG_OOB. В XTI нет аналога этим действиям: внеполосные данные TCP всегда принимаются вместе с обычными данными, что в случае сокетов реализуется с помощью параметра SO OOBINLINE.
Теперь мы рассмотрим несколько примеров, демонстрирующих, как функция poll и управляемый сигналом ввод-вывод работают с внеполосными данными XTI.
Пример использования сигнала SIGPOLL
В листинге 34.1 показана программа, использующая сигнал SIGPDLL для уведомления о наличии данных в точке доступа XTL
Листинг 34.11.Получение обычных и внеполосных данных с использованием сигнала SIGPOLL в точке доступа XTI
//xtioob/tcprecvOl с
#include unpxti h
2345678923456789#define NREAD 100 int listenfd connfd void sig_poll(mt) int
23456789
23456789
main(int argc char **argv) { int n flags
char buff[NREAD + 1]/* +1 для нуля на конце */
if (argc == 2)
11stenfd = Tcp listen(NULL argv[l] NULL)
else if (argc == 3)
11 stenfd = Tcp_hsten(argv[l] argv[2] NULL)
else一
erjqtnt( usage tcprecvOl [ <host> ] <port#> )
16 connfd = Xti^accept(1istenfd NULL NULL)
17Signal(SIGPOLL sig_poll)
18Ioctl(connfd I_SETSIG S^RDNORM)
19202122232425262728293031323334351920212223242526272829303132333435for ( ) {
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
flags = 0
if ( (n = t_rcv(connfd buff NREAD &flags)) < 0) {
if (t_errno == TLOOK) {
if ( (n = T_look(connfd)) == T_ORDREL) {
printfC received T_ORDREL\en )
exit(0) —
} else err_quit( unexpected event after t_rcv Xd". H). }--
err_xti( t_rcv error )
} - -
bufffn] = 0/* завершающим нуль */
pnntf( read Zd bytes Zs flags = Zs\en
n buff Xti_flags_str(flags))
36 void
37 sig_pcll(int sign。)
38 {-
39 pnntf( SIGPOLL received event - %s\en". Xti tlook str(connfd))
40 }—-
Создание прослушиваемой точки доступа и ожидание соединения
10 16 Мы вызываем нашу функцию tcp_listen для создания прослушиваемой точки доступа, а затем нашу функцию xti accept для приема соединения
Все исходные коды программ, опубликованные в этой книге вы можете найти по адресу http // www piter com/download
Установка обработчика сигнала
17 18 Мы вызываем функцию signal для установки обработчика сигнала SIGPOLL, а затем вызываем функцию loctl, чтобы генерировать сигнал в случае прибытия внеполосных данных на точку доступа
Считывание данных (цикл)
19 34 Мы вызываем функцию t_rcv для получения данных В случае, если собеседник закрывает соединение, мы осуществляем нормальное закрытие Мы выводим количество байтов полученных данных и сами эти данные, а также значение аргумента fl ags, возвращенное функцией t_rcv Наша функция xti_fl ags_str возвращает указатель на сообщение, описывающее флаги, которые были переданы в качестве аргументов
Обработчик сигнала
36 40 Наш обработчик сигнала просто выводит сообщение, указывающее на текущее событие в данной точке доступа Наша функция xti_tlook_str вызывает функцию t_l ook и возвращает указатель на сообщение, описывающее текущее событие для данной точки доступа
Мы запустили эту программу, а также запустили в качестве клиента программу из листинга 21 1 Ниже приводятся результаты работы нашего сервера
umxware % tcprecvOl 9999 read 3 bytes 123 flags = 0 SIGPOLL received event = T_EXDATA read 1 bytes 4 flags = T_£xPEDITED SIGPOLL received event =〒一DATA read 2 bytes 56 flags = 0 SIGPOLL received event = T_EXDATA read 1 bytes 7 flags = ^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 rev возвращает флаг T_EXPEDITED Из последующих строк вывода мы видим, что каждый раз, когда прибывает сегмент TCP, генерируется сигнал SIGPOLL и чтобы узнать, какое событие произошло, следует вызвать функцию t_l ook
Когда мы заканчиваем считывание данных и получаем уведомление о том, что клиент закрыл свой конец соединения, генерируется соответствующий сигнал 冥 и происходит событие T_ORDREL, как мы и ожидали
Пример использования функции poll
В нашем следующем примере, приведенном в листинге 34 2, используется функция poll
Листинг 34.2. Получение обычных и внеполосных данных с использованием функции poll в точке доступа XTI
//xt1oob/tcp recv03 с
#include unpxti h
fdefine NREAD 100
int listenfd connfd
int
mdin(int argc char **a「gv)
{
int n flags
charbuff[NREAD + 1]/* +1 для нуля на конце */
struct pol1fd pollfd[l]
if (argc == 2)
listenfd = Tcp_listen(NULL argv[l] NULL)
else if (argc == 3)
listenfd = Tcp_listen(argv[l] argv[2] NULL)
else
15 err_quit( usage tcprecv03 [ <host> ] <port#> )
16 connfd = Xti^accept(1istenfd NULL NULL)
17pollfd[0] fd = connfd
18pollfd[0] events = POLLIN
19 for ( ) {
20 Poll(pol Ifd 1 INFTIM)
21p门ntf( revents = ^x\en pollfd[0] revents)
if (pollfdEO] revents & POLLIN) {
flags = 0
if ( (n = t_rcv(connfd buff NREAD &flags)) < 0) {
if (t_errno = TLOOK) {
if f (n = TJook(connfd)) == T_ORDREL) {
p门ntf( received T_ORDREL\en )
exit(0)“
} else
err_quit( unexpected event after t rcv 象Г. n)
}"—
err_xti (t_rcv error )
}— 一
buff[n] = 0/* завершающим нуль */
printf( read %d bytes %s flags = Zs\en
n buff Xti_flags_str(flags))
}一 —
}
}
Ожидание запроса на соединения от клиента
10 16 Создание прослушиваемой точки доступа и прием запроса на соединение с клиентом остались такими же, как в листинге 34 1
Подготовка к вызову функции poll
17 18 Мы размещаем в памяти массив pol 1 fd, состоящий из одного элемента, и инициализируем его таким образом, чтобы получать уведомление в случае прибытия в точку доступа обычных или приоритетных данных
Вызов функции poll
19 38 Мы вызываем функцию poll с аргументом timeout (время ожидания), равным INFTIM (то есть время ожидания не ограничено) Когда функция завершается, мы выводим значение поля revents структуры pollfd, чтобы узнать, данные какого типа прибыли в точку доступа Если событие — POLL IN, то мы вызываем функцию t__rcv для считывания данных и выводим данные и возвращенные значения флагов
Мы запускаем эту программу, а в качестве клиента возьмем программу, приведенную в листинге 21 1 (ту же программу, что использовалась как клиент в предыдущем примере)
umxware % tcprecv03 7777 гevents = 1umxware % tcprecv03 7777 гevents = 1read 3 bytes revents = 1 read 1 bytes revents = 1 read 2 bytes revents 二 1 read 1 bytes revents = 1 read 2 bytes revents = 1read 3 bytes revents = 1 read 1 bytes revents = 1 read 2 bytes revents 二 1 read 1 bytes revents = 1 read 2 bytes revents = 1123 flags = 04 flags = T_EXPEDITED56 flags = 07 flags = T—EXPEDITED89 flags = 0123 flags = 04 flags = T_EXPEDITED56 flags = 07 flags = T—EXPEDITED89 flags = 0received T_ORDRELreceived T_ORDREL
umxware % tcprecv03 7777 гevents = 1
umxware % tcprecv03 7777 гevents = 1
read 3 bytes revents = 1 read 1 bytes revents = 1 read 2 bytes revents 二 1 read 1 bytes revents = 1 read 2 bytes revents = 1
read 3 bytes revents = 1 read 1 bytes revents = 1 read 2 bytes revents 二 1 read 1 bytes revents = 1 read 2 bytes revents = 1
123 flags = 0
4 flags = T_EXPEDITED
56 flags = 0
7 flags = T—EXPEDITED
89 flags = 0
123 flags = 0
4 flags = T_EXPEDITED
56 flags = 0
7 flags = T—EXPEDITED
89 flags = 0
received T_ORDREL
received T_ORDREL
Каждый раз при завершении функции poll происходит событие, обозначенное как 1, что в данной системе соответствует событию POLLIN По значениям флагов, возвращенных функцией t_rcv, мы узнаем, данные какого типа прибыли в точку доступа
Поставщики транспортных служб закольцовки
Во многих реализациях 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 в зависимости от того, какой из них первым расположен в файле netconfig
В XTI при непосредственном использовании этих поставщиков следует учитывать одну особенность — используемые адреса называются гибкими адресами (flex addresses), которые представляют собой произвольные строки, занимающие
один или более байтов Эти адреса не заканчиваются символами конца строки (нулями) их длина задается элементом 1 еп структуры netbuf, содержащей адрес Но поле addr структуры t_i nfo может быть возвращено со значением-1 (T_INFINITE), в результате чего функция t_all ос не сможет разместить в памяти буфер для этого адреса
ПРИМЕЧАНИЕ
Одно из различии между XTI и TLI заключается в гом, как функция t alloc интерпретирует константу T INFINITE В ТЫ эта функция по умолчанию размещает в памяти буфер размером 1024 байта, в то время как в XTI функция t_alloc в таком случае не размещает буфер в памяти
Резюме
Неблокируемый режим ввода-вывода для точки доступа XTI устанавливается при вызове функции t_open с флагом O_NONBLOCK или (для уже существующей точки доступа) с помощью функции fcntl Происходящие при этом изменения в точке доступа аналогичны тем, что имеют место для сокетов, исключение составляет неблокируемая функция t_connect Для ожидания завершения установления соединения мы пользуемся функцией t_rcvconnect
В XTI определены четыре новые функции ввода-вывода, предназначенные для работы с векторами буферов t_rcvv, t_rcvvudatd, t_sndv и t_sndvudata
Для точки доступа XTI допускается управляемый сигналом ввод-вывод Чтобы войти в этот режим, необходимо вызвать функцию ioctl и задать все условия, при которых должен генерироваться сигнал, с помощью флагов S_xxx Внеполосные данные отсылаются функцией t_snd, если установлен флаг T_EXPEDJTED Для получения внеполосных данных не требуется никаких специальных усилий функция t_rcv возвращает флаг T_EXPED ITED Кроме того, сигнал может быть сгенерирован, когда прибывают внеполосные данные
ПРИЛОЖЕНИЯ
ПРИЛОЖЕНИЕ A
Протоколы IPv4, IPv6, ICMPv4 и ICMPv6
А. 1. Введение
В этом приложении приведен обзор протоколов IPv4, IPv6, ICMPv4 и ICMPv6. Данный материал позволяет глубже понять рассмотренные в главе 2 протоколы TCP и UDP. Некоторые возможности IP и ICMP рассматриваются также более подробно и в других главах, например параметры IP (см. главу 24) наряду с программами Ping и Traceroute (см. главу 25).
А.2. Заголовок IPv4
Уровень IP обеспечивает не ориентированную на установление соединения (connectionless) и ненадежную службу доставки дейтаграмм (RFC 791 [81]). Уровень IP делает все возможное для доставки IP-дейтаграммы определенному адресату, но не гарантирует, что дейтаграмма будет доставлена. Если требуется надежная доставка дейтаграммы, она должна быть обеспечена на более высоком уровне. В случае приложений TCP надежность обеспечивается уровнем TCP. В случае приложений UDP надежность должно обеспечивать само приложение, поскольку уровень UDP также не предоставляет гарантии падежной доставки дейтаграмм, что было показано на примере в разделе 20.5.
Одной из наиболее важных функций уровня IP является маршрутизаъщя (уо〃貪〃g). Каждая IP-дейтаграмма содержит адрес отправителя и адрес получателя. На рис. А.1 показан формат заголовка IPv4.
Значение 4-разрядного поля версия (version) равно 4. Это версия протокола IP, используемая с начала 80-х.
В поле длина заголовка (header length) указывается полная длина IP-заголовка, включающая любые параметры, описанные 32-разрядными словами. Максимальное значение этого 4-разрядпого поля равно 15, и это значение задает максимальную длину IP-заголовка 60 байт. Таким образом, если заголовок занимает фиксированные 20 байт, то 40 байт остается на различные параметры. 8-разрядное поле тип службы (сервиса) gype-of-service, TOS) включает 3-раз- рядное поле приоритета (которое игнорируется), 4 бита, определяющие тип сервиса, и неиспользуемый бит, который должен быть нулевым. Данное поле можно установить с помощью параметра сокета IP_TOS (см. табл. 7.4).
ОО3434787815 1615 163131Версия(4)Версия(4)ДлиназаголовкаДлиназаголовкаТипсервисаТипсервисаОбщая длина (в байтах)Общая длина (в байтах)ИдентификацияИдентификация。台接。台接Смещение фрагментаСмещение фрагментаВремя жизни(TTL)Время жизни(TTL)ПротоколПротоколКонтрольная сумма заголовкаКонтрольная сумма заголовка32-битовый IPv4-anpec отправителя32-битовый IPv4-anpec отправителя32-битовый IPv4-anpec получателя32-битовый IPv4-anpec получателя77Параметры (если есть)Параметры (если есть)ДанныеДанные20 байт20 байт
О
О
34
34
78
78
15 16
15 16
31
31
Версия(4)
Версия(4)
Длиназаголовка
Длиназаголовка
Типсервиса
Типсервиса
Общая длина (в байтах)
Общая длина (в байтах)
Идентификация
Идентификация
。台接
。台接
Смещение фрагмента
Смещение фрагмента
Время жизни(TTL)
Время жизни(TTL)
Протокол
Протокол
Контрольная сумма заголовка
Контрольная сумма заголовка
32-битовый IPv4-anpec отправителя
32-битовый IPv4-anpec отправителя
32-битовый IPv4-anpec получателя
32-битовый IPv4-anpec получателя
7
7
Параметры (если есть)
Параметры (если есть)
Данные
Данные
20 байт
20 байт
Рис. А.1. Формат заголовка IPv4
Поле общая длина (total length) имеет размер 16 бит и задает полную длину IP- дейтаграммы в байтах, включая заголовок IPv4. Количество данных в дейтаграмме равно значению этого поля минус длина заголовка, умноженная на 4. Данное поле необходимо, поскольку некоторые каналы передачи данных заполняют кадр до некоторой минимальной длины (например, Ethernet) и возможна ситуация, когда размер действительной IP-дейтаграммы окажется меньше требуемого минимума.
16-разрядное поле идентификации (identification) является уникальным для каждой IP-дейтаграммы и используется при фрагментации и последующей сборке в единое целое (см. раздел 2.9).
Бит DF (флаг запрета фрагментации), бит MF (указывающий, что есть еще фрагменты для обработки) и 13-разрядное поле смещения фрагмента (fragment offset) также используются при фрагментации и последующей сборке в единое целое. 8-разрядное поле времени жизни (time-to-livef 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-заголовка (включая параметры). В качестве алгоритма вычисления исполь-
зуется стандартный алгоритм контрольных сумм для Интернета — простое суммирование 16-разрядных обратных кодов, как показано в листинге 25.10.
Два поля — IPv4 ^адрес отправителя (source IPv4 address) и IPv4-адрес получав теля (destination IPv4 address) — занимают по 32 бита.
Поле параметров {options} описывается в разделе 24.2, а пример IPv4-napa- метра маршрута от отправителя приведен в разделе 24.3.
А.З. Заголовок IPv6
На рис. А.2 показан формат заголовка IPv6 (RFC 1883(25]).
了 Значение 4-разрядного поля номера версии (version) равно 6. Данное поле занимает первые 4 бита первого байта заголовка (так же как и в версии IPv4, см. рис. АД), поэтому если получающий стек IP поддерживает обе версии, он имеет возможность определить, какая из версий используется. Когда в начале 90-х развивался протокол IPv6 и еще не был принят номер версии 6, протокол назывался IPng (IP next generation — IP нового поколения). До сих пор можно встретить ссылки на IPng.
” Поле приоритета занимает 4 бита и устанавливается отправителем.
ПРИМЕЧАНИЕ
Полезность этого поля до сих пор является предметом исследований. Документ RFC 1883 [25] определяет для этого поля два набора значений: значения от 0 до 7 обозначают трафик, когорый может откладываться при перегруженной линии (лапример, ТСР- даппые), а значения от 8 до 15 — трафик, который откладывать нельзя (например, пакеты реального времени, требующие передачи с постоянной скоростью). С середины 1997 года предполагалось, что эти четыре бита не имеют значения для получателя, в то время как отправитель может установить бит младшего разряда, указав тем самым, что трафик является «интерактивным» (то есть задержка важнее пропускной способности). Также зти 4 бита могут быть переписаны маршрутизатором для частных целей.
Поле метки потока (flow label) занимает 24 бита и может заполняться приложением для данного сокета случайным образом. (Использование данного поля все еще остается экспериментальным.) Поток представляет собой последовательность пакетов от конкретного отправителя определенному получателю, для которых отправитель потребовал специальную обработку промежуточными маршрутизаторами. Если для данного потока отправитель назначил метку, она уже не изменяется. Метка потока, равная нулю (по умолчанию), обозначает пакеты, не принадлежащие потоку.
Комбинация полей приоритета и метки потока называется информацией о потоке (flow infomation). Оба поля содержатся в элементе sin6_flowinfo структуры адреса сокета sockaddr_i пб (см. листинг 3.3).
Поле длины данных (payload length) занимает 16 бит и содержит длину данных в байтах, которые следуют за 40 байтами 1Ру6-заголовка. Нулевое значение этого поля указывает, что длина требует больше 16 бит и содержится в параметре размера увеличенного поля данных (jumbo payload length option) (см. рис. 24.5). Данные с увеличенной таким образом длиной называются джумбограммой (jumbogram).
Следующее поле содержит 8 бит и называется полем следующего заголовка (next headed. Оно аналогично полю протокола (protocol) IPv4. Действительно, когда верхний уровень в основном не меняется, используются те же значения,— например, 6 для TCP и 17 для UDP. Но при переходе от IPv4 к IPv6 возникло так много изменений, что для последнего было принято новое значение 58.
Поле ограничения пересылок, или предельного количества транзитных узлов, (hop limit) аналогично полю TTL IPv4. Значение этого поля уменьшается на единицу каждым маршрутизатором, через который проходит дейтаграмма, и дейтаграмма отбрасывается тем маршрутизатором, который уменьшает данное поле до нуля. Значение этого поля можно установить и получить с помощью параметров сокета IРV6_UNICAST_HOPS и IPV6_MULTICAST_H0PS (см. раздел 7.8 и 19.5). Параметр сокета IPV6_H0PLIMIT также позволяет установить это поле и узнать его значение для полученной дейтаграммы.
Два следующих поля — IPv6-адрес отправителя (source IPv6 address) и IPv6- адрес получателя {destination IPv6 address) — занимают по 128 бит.
ПРИМЕЧАНИЕ
Дейтаграмма IPv6 может иметь несколько дополнительных заголовков, следующих за 40-байтовым заголовком IPv6 Поэтому поле называется «следующий заголовок», а не «протокол». В дейтаграмме IPv4 присутствует только один заголовок протокола после заголовка IPv4.
В ранних спецификациях IPv4 маршрутизаторы должны были уменьшать значение поля TTL на единицу или на значение, равное количеству секунд, в течение которых маршрутизатор обрабатывал пакет (это значение могло быть больше единицы) Отсюда и название «время жизни». Но на самом деле значение этого поля всегда уменьшалось на единицу. В версии IPv6 значение поля ограничения пересылок уменьшается всегда на единицу, отсюда и смена названия.
Наиболее значительным изменением, произошедшим при переходе от IPv4 к IPv6, несомненно, является увеличение поля адресов в IPv6. Другое изменение
относится к упрощению заголовка, поскольку чем проще заголовок, тем быстрее он будет обработан маршрутизатором. Кроме того, можно отметить еще несколько различий между заголовками:
В IPv6 нет поля длины заголовка, поскольку в заголовке отсутствуют параметры. Существует возможность использовать после фиксированного 40-байтового заголовка дополнительные заголовки, но каждый из них имеет свое поле длины.
Два адреса IPv6 выровнены по 64-разрядной границе, если заголовок также является 64-разрядным. Такой подход может увеличить скорость обработки на 64-разрядных архитектурах.
В заголовке IPv6 нет поля фрагментации, поскольку для этой цели существует специальный заголовок фрагментации. Такое решение было принято, поскольку фрагментация является исключением, а исключения не должны замедлять нормальную обработку.
Заголовок IPv6 не включает в себя свою контрольную сумму. Такое изменение было сделано, поскольку все верхние уровни — TCP, UDP и ICMPv6 — имеют свои контрольные суммы, включающие в себя заголовок верхнего уровня, данные верхнего уровня и такие поля из 1Ру6-заголовка, как IPv6-anpec отправителя, IPv6-anpec получателя, длина данных и следующий заголовок. Исключив контрольную сумму из заголовка, мы приходим к тому, что маршрутизатор, перенаправляющий пакет, не должен будет пересчитывать контрольную сумму заголовка после того, как изменит поле ограничения пересылок. Ютючевым моментом здесь также является скорость маршрутизации.
Если это ваше первое знакомство с 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) адресов, о которых будет вскоре рассказано.
7 бит
24 бита
КлассА
0
Идентификатор сети
Идентификатор узла
14 бит
16 бит
Класс В
1
0
Идентификатор сети
Идентификатор узла
21 бит
1
1
1
0
Адрес широковещательной группы
Класс D
28 бит
1
1
1
1
0
Зарезервировано для будущего использования
Класс Е
27 бит
Рис. А.З.Формат адресов IPv4
110Идентификатор сетиИдентификатор узла110Идентификатор сетиИдентификатор узлаКлассСКлассС8 бит8 битАдреса IPv4 обычно записываются как четыре десятичных числа, разделенных точкой, причем каждое из десятичных чисел представляет один из четырех байтов 32-разрядного адреса. Такая запись называется точечно-десятичной. Как показано в табл. А.1, первое из четырех десятичных чисел обозначает класс адреса.
1
1
0
Идентификатор сети
Идентификатор узла
1
1
0
Идентификатор сети
Идентификатор узла
КлассС
КлассС
8 бит
8 бит
Таблица А.1. Диапазоны значений пяти различных классов адресов IPv4
Класс
Диапазон
А
В С
D Е
ОтООООдо 127 255 255 255
От 128000 до 191255 255 255
От 192000 до 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, указы-
вают адрес сети, а нулевые биты — адрес узла. Поскольку биты со значением 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].
Адреса подсетей
Обычно 1Ру4-адреса разделяются на подсети (RFC 950 [72]). Такой подход добавляет еще один уровень иерархии адресов:
идентификатор сети (присваивается предприятию);
идентификатор подсети (выбирается предприятием);
идентификатор узла (выбирается предприятием).
Граница между идентификатором сети и идентификатором подсети фиксирована префиксной длиной присвоенного адреса сети. Эта префиксная длина присваивается организациям их интернет-провайдером. Граница же между идентификатором подсети и идентификатором узла выбирается предприятием. Все узлы данной подсети имеют одинаковую маску подсети, которая и определяет границу между идентификатором подсети и идентификатором узла. Биты, заполненные единицами в маске подсети, соответствуют идентификатору подсети, а биты, заполненные нулями, — идентификатору узла.
В качестве примера рассмотрим подсеть, показанную на рис. 1.7. Адрес сети, присвоенный интернет-провайдером, выглядит как 206.62.226.0/24 и является сетью класса С. Далее, оставшиеся 8 бит разделены на 3-битовый идентификатор
подсети и 5-битовый идентификатор узла. Рисунок А.4 иллюстрирует такое деление. Маска подсети, соответствующая таким адресам, имеет вид OxffffffeO или 225.225.225.224.
присвоенного адреса сет^
24 бита、
маской подсеть
г 3 бита 、
г5 бит
Адрес сети=206.62.226.0/24
Идентификатор подсети
Идентификатор узла
1111 1111 1111 1111 1111 1111
1111
4
00000
Единичные биты соответствуют адресу сети и идентификатору подсети
Нулевые биты соответствуют
Адрес:
Маска подсети:
Граница определенаГраница
префиксной длинойопределена
идентификатору
узла
Рис. А.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 нижнюю подсеть. Сети nodo.edu присвоен адрес 140.252.0.0/16, что в точности соответствует сети класса В. Затем NO АО делит оставшиеся 16 бит на 8-битовый идентификатор подсети и 8-битовый идентификатор узла, что характерно для организаций с адресами класса В. Такое деление показано на рис. А.5.
Маска этой подсети имеет вид OxffffffOO или 255.255.255.0. Приведенная подсеть имеет идентификатор подсети, состоящий только из единиц, и такая подсеть обозначается как 140.252.1.0/24.
Граница определена префиксной длиной присвоенного адреса сетк
16 бит、
Границе on ределе не маской подсетк
f 8 бит、
f 8 бит
Адрес:
Адрес сети=140.252.0.0/16
Идентификатор подсети
Идентификатор узла
Маска подсети:
1111 1111 1111 1111 1111 1111
1111
0000000000
Единичные биты соответствуют адресу сети и идентификатору подсети
Нулевые биты соответствуют идентификатору узла
Рис. A.5. 16-битовый адрес сети с 8-битовым идентификатором подсети и 8-битовым идентификатором узла
Адрес закольцовки
По соглашению адрес 127.0.0.1 присвоен интерфейсу закольцовки на себя (loopback interface). Все, что посылается на этот IP-адрес, получается самим узлом. Обычно этот адрес используется при тестировании клиента и сервера на одном узле. Этот адрес известен под именем INADDR_LOOPBACK.
ПРИМЕЧАНИЕ
Любой адрес из подсети 127/8 можно присвоить интерфейсу закольцовки, но обычно используется именно 127.0.0.1.
Неопределенный адрес
Адрес, состоящий из 32 нулевых битов, является в IPv4 неопределенным (unspecified) адресом. В пакете IPv4 он может появиться только как адрес получателя в тех пакетах, которые посланы узлом, находящимся в состоянии загрузки, когда узел еще не знает своего IP-адреса. В API сокетов этот адрес называется универсальным адресом (wildcard address) и обычно обозначается INADDR_ANY.
Многоинтерфейсность и псевдонимы адресов
Традиционно много интерфейсный узел определяется как узел с несколькими интерфейсами, например узел, имеющий два интерфейса Ethernet или интерфейсы Ethernet и РРР. Каждый из интерфейсов должен иметь свой уникальный IPv4- адрес. При подсчете интерфейсов (для определения, является ли узел многоинтерфейсным) интерфейс закольцовки не учитывается.
Маршрутизатор по определению является многоинтерфейсным, поскольку он пересылает пакеты, поступившие на один интерфейс, через другой интерфейс. Но обратное неверно, то есть многоинтерфейсный узел не является маршрутизатором, если он не передает пакеты. Действительно, многоинтерфейсный узел еще не может рассматриваться как маршрутизатор. Он будет функционировать как маршрутизатор, только если он сконфигурирован для такой работы (обычно администратор должен включить соответствующие параметры конфигурации).
Термин «многоинтерфейсность» является более общим и охватывает два различных сценария (раздел 3.3.4 RFC 1122 [9]).
Узел с несколькими интерфейсами является миогоинтерфейсным, при этом каждый интерфейс должен иметь свой IP-адрес. Это традиционное определение.
Современные узлы имеют возможность присваивать одному физическому интерфейсу несколько IP-адресов. Каждый IP-адрес, созданный в дополнение к первичному, или основному (primary), называется альтернативным именем, псевдонимом (alias) или логическим интерфейсом. Часто альтернативные IP-адреса используют ту же маску подсети, что и основной адрес, но имеют другие идентификаторы узла. Но допустима также ситуация, когда псевдонимы имеют адрес сети или подсети, совершенно отличный от первичного адреса. В разделе 16.6 приведен пример альтернативных адресов.
Таким образом, многоинтерфейсные узлы — это узлы, имеющие несколько интерфейсов, независимо от того, являются ли эти интерфейсы физическими или логическими.
ПРИМЕЧАНИЕ
Многой11терфейсность также используется в другом контексте. Сеть, имеющая несколько соединений с сетью Интернет, также называется многойнтерфсйсной. Например, некоторые сайты имеют два соединения с Интернетом вместо одного, что обеспечивает дублирование на случай неполадок.
А.5. Адресация IPv6
Адреса IPv6 содержат 128 бит и обычно записываются как восемь 16-разрядных шестнадцатеричных чисел. По сути, здесь не существует классов адресов, как было в IPv4. Вместо этого старшие биты 128-разрядного адреса обозначают тип адреса [36]. В табл. А.2 приведены различные значения старших битов и соответствующий им тип адреса.
Таблица А.2. Значение старших битов адреса IPv6
<»*®*Я»«>«*:1Мио®бг«>«е?^яв>5я>йв«»вй»{3«»яада54с0Юи»в9даявди«г?«^^
Назначение
Форматный префикс
Зарезервирован
Не присвоен
Зарезервирован для NSAP
Зарезервирован для IPX
Не присвоен
Не присвоен
Не присвоен
Объединяемые глобальные индивидуальные адреса
Не присвоен
Не присвоен
Не присвоен
Не присвоен
Не присвоен
Не присвоен
0000 0000
0000 0001
0000 001
0000010
0000 011
00001
0001
001
010
011
109
101
110
1110
■продолжение 夕
Таблица A.2 (продолжение}
Назначение
Форматный префикс
Не присвоен
Нс присвоен
Не присвоен
Не присвоен
Индивидуальный в пределах физической подсети адрес
Индивидуальный в пределах сайта адрес
Адрес многоадресной передачи
НПО
1111 10
1111110
1111 11100
1111 111010
1111 1110 11
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 Engineering Task Force) в марю 1997 года было принято решение продолжать использование других форм индивидуалы 1ых адресов.
Формат объединяемых индивидуальных адресов определяется в [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 — уровень управления доступом к среде передачи), которые присвоены большинству карт сетевых интерфейсов локальной сети. Этот идентификатор должен автоматически
313 бита 16 бит64 бита
001
TLA】
ID
I11
NLA ID 111
1 SLA ID (
1111I1
Идентификатор интерфейса
i1Ii111
Открытая топология ТопологияИдентификатор
сайтаинтерфейса
Рис. А.6. Объединяемый глобальный индивидуальный адрес IPv6
присваиваться интерфейсу и по возможности основываться на МАС-адресе карты. Более подробное описание построения идентификаторов интерфейса, основанных на EUI-64, описывается в приложении А [36].
Тестовые адреса бЬопе
бЬопе — это виртуальная сеть, используемая для тестирования протоколов IPv6 (см. раздел Б.З). На момент написания этой книги объединяемые глобальные индивидуальные адреса еще не назначались, хотя уже планировалось использование специального формата этих адресов в сети бЬопе [37]. Вместо этого для всех узлов в сети бЬопе используется формат адреса, документированный в RFC 1897 [39] и приведенный на рис. А.7.
5f
AS
Первые 24 бита 1Ру4-адреса сети
Идентификатор подсети
Идентификатор интерфейса
1 байт 21
3
1
2
6
Рис. А.7. Тестовые адреса IPv6 для сети бЬопе
Эти адреса рассматриваются как временные, и узлы, использующие такие адреса, необходимо будет перенумеровать, когда будут назначены объединяемые глобальные индивидуальные адреса.
Старший байт имеет значение 0x5f. 16~битовое поле AS (autonomous system number) — это автономный системный номер, присвоенный организации или ее интернет-провайдеру. Они используются в версии IPv4 для идентификации доменов маршрутизаторов. Следующее поле содержит 24 старших бита текущего 1Ру4-адреса узла. Идентификатор подсети — это то, что задает организация, а идентификатор интерфейса обычно является 48-битовым адресом IEEE 802 МАС.
В разделе 9.2 был показан 1Ру6-адрес 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 запрашивается 1Ру6-ад- рес узла, который имеет только адреса IPv4.
Рисунок 10.3 показывает, что использование данного типа адресов с сокетом IPv6 приводит к отправке IPv4-дейтаграмма узлу. Такие адреса не хранятся ни в каких файлах данных DNS — при необходимости они создаются сервером.
На рис. А 8 приведен формат таких адресов. Младшие 32 бита содержат адрес IPv4.
0000 0000
FFFF
Адрес IPv4
80 бит1632
Рис. А.8. Адреса IPv4, преобразованные к виду IPv6
При записи 1Ри6~адреса последовательная строка из нулей может быть сокращена до двух двоеточий. Вложенный 1Ру4-адрес представлен в точечно-десятичной записи. Например, преобразованный к виду IPv6 IPv4-адрес 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-мари1рутизатора, должен создать DNS-запись типа АААА, содержащую адрес IPv4, совместимый с IPv6. Любой другой 1Ру6-узел, посылающий 1Ру6-дейтаграмму на адрес IPv4, совместимый с IPv6, должен упаковать {encapsulate) IPv6-дейтаграмму в заголовок IPv4 — такой способ называется автоматическим туннелированием {automatic tunnel). Более подробно вопросы туннелирования будут рассмотрены в разделе Б 3, а на рис. Б 2 будет приведен пример I Руб-дейтаграмм такого типа, упакованных в заголовок IPv4. Однако в сети бЬопе каждый туннель должен быть сконфигурирован (configured) (то есть записан администратором в файл запуска), тогда как при использовании адреса IPv4, совместимого с IPv6, вручную необходимо конфигурировать только адрес (то есть поместить в файл данных DNS запись типа АААА), после чего будет осуществляться автоматическое туннелирование.
На рис. А 9 показан формат адреса IPv4, совместимого с IPv6.
В качестве примера такого адреса можно привести 206 62 226 33.
0000 0000
0000
Адрес IPv4
80 бит1632
Рис. А.9. Адрес IPv4, совместимый с IPv6
Адрес закольцовки
Адрес IPv6 1, состоящий из 127 нулевых битов и единственного единичного бита, является адресом закольцовки IPv6. В API сокетов он называется i n6addr_ loopback или IN6ADDR_L00PBACK」NIT.
Неопределенный адрес
Адрес IPv6, состоящий из 128 нулевых битов, записываемый как 0 0 или просто ,является неопределенным адресом IPv6 (unspecified address). В пакете IPv6
он может появиться только как адрес получателя в пакетах, посланных узлом, который находится в состоянии загрузки и еще не знает своего IРуб-адреса
В API сокетов этот адрес называется универсальным адресом, и его использование, например, в функции bind для связывания прослушиваемого сокета TCP означает, что сокет будет принимать клиентские соединения, предназначенные любому из адресов узла. Этот адрес имеет имя in6addr_any или IN6ADDR_ANY_INIT.
Адрес локальной связи
Адрес локальной связи (link-local, локальный в пределах физической подсети) используется для соединения в пределах одной физической подсети, когда известно, что дейтаграмма не будет перенаправляться. Примерами использования таких адресов являются автоматическая конфигурация адреса во время загрузки и поиска собеседника (neighbor discovery) (подобно ARP для IPv4). На рис. А.10 приведен формат такого адреса
1111111010
0000 0000
Идентификатор интерфейса
10 бит5464
Рис. А.10.1Р7б-адрес локальной связи
Такие адреса всегда начинаются с fe80. Маршрутизатор IPv6 не должен перенаправлять дейтаграммы, у которых в поле отправителя или получателя указан адрес локальной связи, по другому соединению. В разделе 9.2 приведен адрес локальной связи, связанный с именем aix-611.
Адрес, локальный на уровне сайта
Адрес, локальный в пределах сайта, используется для адресации внутри предприятия, когда не требуется глобальный префикс. На рис. А.11 показан формат таких адресов.
1111111011
0000 . . . 0000
Идентификатор подсети
Идентификатор интерфейса
10 бит38
16
64
Рис. А.11. IPve-адрес, локальный в пределах сайта
Такие адреса всегда начинаются с fecO. Маршрутизатор IPv6 не должен перенаправлять дейтаграммы, для которых в поле отправителя или получателя указан такой адрес, за пределы предприятия.
А.6. ICMPv4 и ICMPv6: протокол управляющих сообщений в сети Интернет
Протокол ICMP (Internet Control Message Protocol) является необходимой и неотъемлемой частью любой реализации IPv4 или IPv6. Протокол ICMP обычно используется для обмена сообщениями об ошибках между узлами, как маршрутизирующими, так и обычными, но иногда этот протокол используется и прило-
жениями Например, приложения Ping и Traceroute (см. главу 25) используют протокол ICMP.
Первые 32 бита сообщений совпадают для ICMPv4 и ICMPv6 и приведены на рис А. 12 ICMPv4 документируется в RFC 792 [82], a ICMPv6 -- в RFC 1885 [21].
Тип
Код
Контрольная сумма
781516
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 (Эхо-ответ)
Пользовательский процесс (Ping)
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
КМйММ*№
Тип
Код
Описание
Обработчик или еггпо
10
Destination host administratively prohibited (Узел получагеля запреше» администратором)
EHOSTUNREACH
И
Network unreachable for TOS (Ceib недоступна для TOS)
EHOSTUNREACH
12
Host unreachable for TOS (Узел недос1упеп для TOS)
EHOSTUNREACH
13
Communication administratively prohibited (Связь запрещена администратором)
(Игнорируется )
14
Host precedence violation (Нарушение порядка старшинства узлов)
(Игнорируется)
15
Pi eccdence cutoff in effect (Дсиствует сгаршипсию узлов)
(Игнорируется)
4
0
Source quench (Отключение отправителя)
Обрабатывается ядром в случае TCP, игнорируется в cjiyqaeUDP
5
Redirect (Перенаправление)
0
Redirect for network (Перенаправление для сети)
Ядро обновляет таблицу маршрутизации
1
Redirect for host (Перенаправление дня узла)
Ядро обновляет таблицу маршрутизации
2
Redirect for type-of^serviceand network
Ядро обновляет таблицу
(Перенаправление для тина сервиса и cein)
маршрутизации
3
Redirect for type of service and host
Ядро обновляет таблицу
(Перенаправление для гипа сервиса и узла)
маршрутизации
8
0
Echo reejuest (Эхо-запрос)
Ядро генерирует от вст
9
0
Router advertisement (Извещение маршрут изатора)
Пользова гольскии процесс
10
0
Router solicitation (Запрос маршрутизагору)
Пол ьзовательс ки и процесс
И
Tune exceeded (Превышено время передачи)
0
TTL equals 0 during transit
(Время жизни равно 0 во время передачи)
Пользова гсльскии процесс
1
TTL equals 0 during reassembly (Время жизни равно 0 во время сборки)
Пользова! ельскии процесс
12
Paranieter 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 (Получатель недоступен)
ОNo route to destination (Her маршрута до получателя)EHOSTUNREACH
~"■ продолжение 夕
Таблица А.4 (продолжение)
оШММЖМ
Тип
Код
Описание
Обработчик или еггпо
1
Administratively prohibited, firewall filter (Запрейleno администраэором, фильтр брандмауэра)
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 (Запрос о членстве в I руппе)
Пользовательский процесс
131
0
Group membership report (Отчегочленстве в труппе)
Пользовательский процесс
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 на узлах. Когда устанавливается новое ТCP-соединение, каждая сторона может определить, поддерживает ли другая сторона новую возможность, и если для обоих узлов это так, ею можно воспользоваться.
Иная ситуация с изменениями IP-уровня, такими как многоадресная передача, появившаяся в конце 80-х, или новая версия протокола IPv6, возникшая в середине 90-х, поскольку они требуют изменений на всех узлах и на всех маршрутизаторах. Но люди хотят начать использовать новые возможности, не дожидаясь, когда все системы будут модернизированы. Для этого существующий протокол IPv4 был дополнен так называемыми виртуальными сетями (virtual network), использующими туннели (tunnels).
Б.2. МВопе
Наш первый пример виртуальной сети, построенной с использованием туннелей, —это сеть МВопе, которая начала использоваться примерно с 1992 года [27]. Если два или более узлов в локальной сети поддерживают многоадресную передачу, то на всех этих узлах могут быть запущены приложения многоадресной передачи, которые могут общаться друг с другом. Для соединения одной локальной сети с другой локальной сетью, также содержащей узлы с возможностью многоадресной передачи, между двумя узлами из каждой локальной сети конфигурируется туннель, как показано на рис. Б.1. На этом рисунке отмечены следующие шаги:
L Приложение на узле отправителя МН 1 посылает групповую дейтаграмму адресам класса D.
На рисунке эта дейтаграмма показана как UDР-дейтаграмма, поскольку большинство приложений многоадресной передачи используют протокол UDP. Более подробно о многоадресной передаче и о том, как посылать и получать многоадресные дейтаграммы, рассказано в главе 19.
Дейтаграмма принимается всеми узлами в локальной сети, поддерживающими многоадресную передачу, в том числе и MR2. Отметим, что MR2 также
MR5MR5Заголовок IPv4, добавленный начальной точкой туннеля и удаленный конечной точкой туннеля 1Ру4-адрес получателя = индивидуальный адрес конечной точки туннеля, поле протокола IPv4 = 4 (IPv4 в IPv4)Заголовок IPv4, добавленный начальной точкой туннеля и удаленный конечной точкой туннеля 1Ру4-адрес получателя = индивидуальный адрес конечной точки туннеля, поле протокола IPv4 = 4 (IPv4 в IPv4)
MR5
MR5
Заголовок IPv4, добавленный начальной точкой туннеля и удаленный конечной точкой туннеля 1Ру4-адрес получателя = индивидуальный адрес конечной точки туннеля, поле протокола IPv4 = 4 (IPv4 в IPv4)
Заголовок IPv4, добавленный начальной точкой туннеля и удаленный конечной точкой туннеля 1Ру4-адрес получателя = индивидуальный адрес конечной точки туннеля, поле протокола IPv4 = 4 (IPv4 в IPv4)
Рис. Б.1. Упаковка IPv4 в IPv4, применяемая в МВопе
работает как многоадресный маршрутизатор, на котором запущена программа mrouted, осуществляющая маршрутизацию многоадресной передачи.
MR2 добавляет перед дейтаграммой другой IPv4-заголовок, в котором в поле адреса получателя записан индивидуальный адрес конечного узла туннеля (tunnel endpoint) MR5. Этот индивидуальный адрес конфигурируется администратором узла MR2 и считывается программой mrouted при ее запуске. Аналогичным образом индивидуальный адрес узла MR2 сконфигурирован на узле MR5 — на другом конце туннеля. В поле протокола нового IPv4-заголовка установлено значение 4, соответствующее упаковке IPv4 в IPv4. Дейтаграмма посылается следующему маршрутизатору, UR3, который явно указан как маршрутизатор направленной передачи, то есть не поддерживает многоадресную передачу, и поэтому приходится использовать туннель. Выделенная на рисунке серым цветом часть 1Ру4-дейтаграммы не изменяется по сравнению с шагом 1, только значение поля TTL в выделенном цветом 1Ру4-заголовке уменьшается на 1.
UR3 узнает адрес получателя из самого внешнего 1Ру4-заголовка и перенаправляет дейтаграмму следующему маршрутизатору направленной передачи — UR4.
UR4 доставляет дейтаграмму по назначению — узлу MR5, который является конечным узлом туннеля.
MR5 получает дейтаграмму, и поскольку в поле протокола указана упаковка IPv4 в IPv4, удаляет внешний 1Ру4-заголовок и передает оставшуюся часть дейтаграммы (копию той, которая была групповой дейтаграммой в локальной сети, изображенной на рисунке вверху) в качестве многоадресной дейтаграммы в своей локальной сети.
Все узлы сети, изображенной на рисунке внизу, поддерживающие многоадресную передачу, получают многоадресную дейтаграмму.
В результате многоадресная дейтаграмма, отправленная в локальной сети, изображенной вверху, передается как многоадресная дейтаграмма в локальной сети, изображенной внизу. Это происходит несмотря на то, что два маршрутизатора, присоединенные к этим двум локальным сетям, а также все маршрутизаторы между ними не поддерживают многоадресную передачу.
В данном примере показана функция маршрутизации многоадресной передачи, осуществляемая программой mrouted, запущенной на одном из узлов в каждой из локальных сетей. Таким образом запускается сеть МВопе. Около 1996 года большинство основных поставщиков маршрутизаторов начали включать функцию групповой маршрутизации в свои маршрутизаторы. Если бы два маршрутизатора направленной передачи UR3 и UR4 на рис. Б.1 имели возможность маршрутизации многоадресной передачи, то нам не пришлось бы запускать ^routed, а маршрутизаторы 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,
хотели соединить их вместе с помощью виртуальной сети, не дожидаясь поддержки 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, используемая в сети бЬопе
На узле HR2 имеется сконфигурированный туннель до узла HR3 Этот туннель позволяет посылать IPv6-дейтаграмма между двумя конечными узлами туннеля через сеть IPv4 путем упаковки 1Ру6-дейтаграмм в 1Ру4-дейтаграм- мы (упаковка IPv6 в IPv4) В поле протокола указано значение 4 Отметим, что оба узла IPv4/IPv6 на концах туннеля — HR2 и HR3 — работают как мар-
шрутизаторы IPv6, поскольку они перенаправляют IРуб-дейтаграммы, получаемые на один интерфейс, через другой интерфейс Сконфигурированный туннель считается интерфейсом, хотя он является виртуальным, а не физическим интерфейсом
Конечный узел туннеля (HR3) получает упакованную дейтаграмму, отбрасывает 1₽¥4-заголовок и посылает 1₽¥б-дейтаграмму в свою локальную сеть
Дейтаграмма приходит по назначению на узел Н4
Описанные виртуальные сети нужны лишь на время переходного периода, так как когда промежуточные маршрутизаторы получат требуемую функциональность (многоадресная передача в случае МВопе и маршрутизация IPv6 в случае бЬопе), необходимость в виртуальных сетях исчезнет Мы описали обе эти виртуальные сети, поскольку некоторые примеры в тексте книги используют понятия МВопе и бЬопе
ПРИЛОЖЕНИЕ В
Техника отладки
Это приложение содержит некоторые рекомендации и описание методов отладки сетевых приложений. Ни один из приведенных методов не является панацеей от всех возможных проблем, однако существует множество инструментальных средств, с которыми следует ознакомиться, чтобы в дальнейшем использовать подходящие для конкретной среды.
Трассировка системного вызова
Многие версии Unix предоставляют возможность 1расснровкн (отслеживания) системных вызовов. Зачастую это может оказаться полезным методом отладки.
Работая на этом уровне, необходимо различать системный вызов и функгщю. Системный вызов является точкой входа в ядро, и именно это можно отследить с помощью инструментальных средств, описанных в данном разделе. Стандарт Posix и большинство других стандартов используют термин функция, вкладывая в это понятие тот же смысл, что и пользователи, хотя на самом деле это может быть системный вызов. Например, в Беркли-ядрах socket 一 это системный вызов, хотя программист приложений может считать, что зто обычная функция языка С. В системе SVR4, как будет показано далее, это функция из библиотеки сокетов, которая содержит вызовы putmsg и getmsg, в действительности являющиеся системными вызовами.
В этом разделе мы рассмотрим системные вызовы, задействованные в работе клиента времени и даты. В листинге 1.1 была показана версия с использованием сокетов, а XTI-версия приведена в листинге 28.2.
Библиотека потоковых сокетов SVR4
Начнем с потоковых реализаций сокетов, таких, как поддерживаются в системе SVR4. Программа truss SVR4 предназначена для запуска программы и трассировки выполняемых системных вызовов. Запустим эту программу следующим образом:
unixware % truss -о truss.out -v getmsg, putmsg, loctl \ daytimetcpcli 140 252 1 54
(Длинную команду пришлось записать в две строки.) Параметр -о направляет вывод в файл (данный метод отладки всегда связан с большим количеством выводимых данных), а параметр -v включает подробную отладку для трех указанных системных вызовов. Он выдает дополнительную информацию об аргументах для этих системных вызовов.
Начало вывода (примерно 40 строк) относится к связыванию программы с динамическими библиотеками, использующими ввод-вывод с распределением памяти. Это осуществляется с помощью системных вызовов open и шшар. (Последний системный вызов описан в разделе 12.9 [93] и не имеет отношения к рассмотрению сетевых API.) Эту часть вывода мы рассматривать не будем.
Далее следует открытие файла /etc/netconfi g и считывание (read) всего файла (806 байт):
open (*' /etc/netconf 1 g'. O_RDONLY. 0666)= 3
read(3. Mt c p\t t p i „ с о t s" . 8192) = 806
read(3 0x0804A6B8 8192)= 0
close(3)= 0
Некоторые не относящиеся к рассматриваемому вопросу системные вызовы ioctl и 1 seek опущены. Вместо пропущенных вызовов вставлено многоточие. Значение 3 справа от знака равенства — это возвращаемый дескриптор для системного вызова open. Системный вызов read запрашивает 8192 байта, а возвращает значение 806 (размер файла). Следующий вызов read возвращает нулевое значение (конец файла). Программа truss также показывает первые 12 байт, возвращаемые функцией read (начинающиеся с tcp), являющиеся началом первой строки файла. Затем файл закрывается. Можно предположить, что считывание файла netconfig заканчивается, когда при первом вызове функции soket запускается библиотека сокетов.
Следующий системный вызов представляет собой открытие (вызов open) устройства /dev/tcp, и возвращаемый дескриптор опять равен 3.
open('7dev/tcp".O_RDWR. 027776333624)=3
ioctl(3.I_FIND."sockmod")=0
ioctl(3.I_PUSH.^,sockпlod^,)=0
ioctl(3.I_STR 0x080467E4)=0
cmd=Tl_BIND timeout=-l 1en=32 dp=0x0804ABA8
Вызов ioctl после вызова open проверяет, включен ли в поток модуль sockmod. Возвращаемое нулевое значение говорит о том, что этого модуля в потоке нет, и вызов 1 octi с аргументом I_PUSH помещает его в поток. Далее следует несколько вызовов ioctl для потоков и обработка множества сигналов (которая здесь опускается). Вызов ioctl с аргументом I_STR посылает внутреннему потоку сообщение ioctl, при этом используется команда TI_BIND. Длина 32, вероятно, означает запрос T_BIND_REQ, состоящий из четырех 4-байтовых полей в структуре T_bi nd_req (она описана при обсуждении листинга 33.3), за которой следует 16-байтовая структура sockaddr_in. Вероятно, это связывание некоторого локального адреса, выполняемое библиотекой сокетов при вызове connect на неприсоединенном сокете.
ПРИМЕЧАНИЕ
Мы ожидали увидеть здесь putmsg, а затем getmsg, как в функции tpi bind.
Далее рассмотрим первый вызов putmsg, осуществляемый только для получения управляющей информации, со значением флага 0.
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
1en=56 buf=0x0804ABA8
dat
maxlen=128
1en=-1buf =0x08046808
Длина 36. вероятно, относится к запросу T CONN REQ (см. листинг 33.4): пять 4-байтовых значений, за которыми следует 16-байтовая структура sockaddr_i п. Следующий вызов getmsg возвращает 8 байт управляющей информации без каких- либо данных, что, вероятно, является сообщением Т_ОК_АСК (см. листинг 33.4). Следующий вызов getmsg возвращает 56 байт управляющей информации без каких-либо данных, что, вероятно, является сообщением T_CONN_CON (см. листинг 33.4): пять 4-байтовых полей, 16-байтовая структура socksddr_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)= О
ctl maxlen=428 len=8 buf^0x0804ABA8
dat maxlen=4096 len=26 buf=0x08046BB0
writed. 'F r 1 Apr 4 0"26)- 26
getmsg(3 0x08046AEC 0x08046AE0. 0x08046B14)= 0
ctl maxlen=428 len=-lbuf=0x0804ABA8
dat maxlen=4096 len=0buf=0x08046BB0
_exit(0)
Далее наш клиент вызывает wri te, чтобы записать в стандартный поток вывода (дескриптор 1) 26 байт данных. Следующий вызов getmsg возвращает управляющую информацию и 0 байт данных 一 вероятно, указание от поставщика о том, что встретился конец файла.
ПРИМЕЧАНИЕ
В этом месте ожидается получить сообщение T_ORDREL_IND.
Потоковая XTI-библиотека SVR4
В следующем примере рассмотрим потоковые XTI-библиотеки операционной системы Solaris 2.6. Предполагается, что системные вызовы будут аналогичны вызовам, описанным в разделе 33.6
После отображения библиотеки в память с помощью системного вызова mmap обнаруживаем вызов open для поставщика транспортных служб TCP, после чего следует проверка модуля timod и включение данного модуля в поток.
300300ореп(м/dev/tcp' O_RDWR) 1oct1(3 I_FIND 'timed') ioctl(3 匸PUSH ' timod'•)
3
0
0
3
0
0
Затем мы обнаруживаем несколько вызовов ioctl вместе с обработкой сигналов (которую мы опускаем). Один из этих вызовов ioctl появляется в ответ на наш вызов t_bi nd, и оказывается, что библиотека XTI осуществляет связывание путем данного вызова ioctl в модуле timod. (Аналогично тому, что мы видели в предыдущем примере сокетов и потоков в UnixWare.)
Первый вызов putmsg посылает 36 байт управляющей информации и не посылает никаких данных Вероятно, это запрос T_CONN_REQ из нашего вызова t_connect.
putmsg(3 ctlgetmsgO ctl dat flagsgetmsgO ctl dat flagsputmsg(3 ctlgetmsgO ctl dat flagsgetmsgO ctl dat flags0xEFFFE7F4 maxlen=912 0xEFFFE710 ггах1еп=9120xEFFFE7F4 maxlen=912 0xEFFFE710 ггах1еп=9120x00000000 0)len*36 buf=0x0002BAB80xEFFFE700. 0xEFFFE71C)le产8buf=0x0002ClE80x00000000 0)len*36 buf=0x0002BAB80xEFFFE700. 0xEFFFE71C)le产8buf=0x0002ClE8maxlen=0 len—1buf=0x00000000maxlen=0 len—1buf=0x000000000x00010xEFFFE7F4. 0xEFFFE770 0xEFFFE77C) maxlen=912 len=36 buf=0x0002BAB8 maxi en=0 len=-l buMxOOOOOOOO0x00010xEFFFE7F4. 0xEFFFE770 0xEFFFE77C) maxlen=912 len=36 buf=0x0002BAB8 maxi en=0 len=-l buMxOOOOOOOO0x00000x0000=0ЛОКОХОХОХОХОХОЮ" =0'0\0\0\013\0\0\0\0"=0ЛОКОХОХОХОХОХОЮ" =0'0\0\0\013\0\0\0\0"=0"\0\0\0\f\0\0\010”=0"\0\0\0\f\0\0\010”
putmsg(3 ctl
getmsgO ctl dat flags
getmsgO ctl dat flags
putmsg(3 ctl
getmsgO ctl dat flags
getmsgO ctl dat flags
0xEFFFE7F4 maxlen=912 0xEFFFE710 ггах1еп=912
0xEFFFE7F4 maxlen=912 0xEFFFE710 ггах1еп=912
0x00000000 0)
len*36 buf=0x0002BAB8
0xEFFFE700. 0xEFFFE71C)
le产8buf=0x0002ClE8
0x00000000 0)
len*36 buf=0x0002BAB8
0xEFFFE700. 0xEFFFE71C)
le产8buf=0x0002ClE8
maxlen=0 len—1buf=0x00000000
maxlen=0 len—1buf=0x00000000
0x0001
0xEFFFE7F4. 0xEFFFE770 0xEFFFE77C) maxlen=912 len=36 buf=0x0002BAB8 maxi en=0 len=-l buMxOOOOOOOO
0x0001
0xEFFFE7F4. 0xEFFFE770 0xEFFFE77C) maxlen=912 len=36 buf=0x0002BAB8 maxi en=0 len=-l buMxOOOOOOOO
0x0000
0x0000
=0
ЛОКОХОХОХОХОХОЮ" =0
'0\0\0\013\0\0\0\0"
=0
ЛОКОХОХОХОХОХОЮ" =0
'0\0\0\013\0\0\0\0"
=0
"\0\0\0\f\0\0\010”
=0
"\0\0\0\f\0\0\010”
Также видно, что Solaris выводит первые 8 байт буфера в шестнадцатеричной системе с управляющими последовательностями языка С. При вызове putmsg имеем 7 нулевых байтов, за которыми следует байт 0x10 (16). Но на самом деле это два 4-байтовых поля: первые 4 байта — это запрос T_CONN_REQ (нулевое значение), далее следует длина адреса получателя (16).
Первый вызов getmsg в предыдущем выводе возвращает сообщение Т_ОК__АСК, а следующий — сообщение 7_CONN_CON. Можно определить тип первого возврата- емого сообщения, поскольку Т_ОК_АСК соответствует значение 19 (0x13), а следующие 4 байта указывают на уже известный примитив (T_CONN_REQ, значение которого, как уже говорилось, равно 0). Можно также определить тип второго возвращаемого сообщения, поскольку T_CONN_CON имеет значение 12 (которое выводится на экран как управляющий символ языка С для перехода на новую страницу — \f), за которым следует длина адреса собеседника (16, отображается как 0x10).
Solaris выводит флаговую переменную, на которую указывает последний аргумент в системном вызове getmsg, и мы видим, что первый вызов возвращает значение 1 (это MSG_HIPR1, как н ожидалось, поскольку сообщение Т_ОК_АСК — это сообщение типа M PCPROTO), а второй вызов возвращает нулевое значение (что также совпадает с ожидаемым результатом, так как это обычное сообщение). Также следует отметить, что сообщение Solans 匸CONN_CON ие возвращает никаких параметров (длина 36), в то время как в примере с UnixWare оно возвращало еще 20 байт параметров.
Следующий интересующий нас системный вызов — это другой вызов getmsg, который, вероятно, осуществляется в ответ на вызов t_rcv. Этот вызов возвращает 26 байт данных без управляющей информации, вероятно, сообщение M_DATA с ответом сервера.
getmsgO 0xEFFFE7EC. OxEFFFEZDC ctlmaxien=912 len=-ldat maxlen=4096 1en=26 flags 0x0000writed ' F r 1 Apr 4 getmsg(3 0xEFFFE7EC 0xEFFFE7DC ctlmaxlen=912 len=4dat max〕en=4096 len=-lgetmsgO 0xEFFFE7EC. OxEFFFEZDC ctlmaxien=912 len=-ldat maxlen=4096 1en=26 flags 0x0000writed ' F r 1 Apr 4 getmsg(3 0xEFFFE7EC 0xEFFFE7DC ctlmaxlen=912 len=4dat max〕en=4096 len=-l0xEFFFE81C) buf=0x0002BAB8 buf«0xEFFFE8D0V 26)0xEFFFE81C) buMx0002BAB8 buf=0xEFFFE8D00xEFFFE81C) buf=0x0002BAB8 buf«0xEFFFE8D0V 26)0xEFFFE81C) buMx0002BAB8 buf=0xEFFFE8D0=0'F r 1 Apr=26=0"\0\0\017,=0'F r 1 Apr=26=0"\0\0\017,
getmsgO 0xEFFFE7EC. OxEFFFEZDC ctlmaxien=912 len=-l
dat maxlen=4096 1en=26 flags 0x0000
writed ' F r 1 Apr 4 getmsg(3 0xEFFFE7EC 0xEFFFE7DC ctlmaxlen=912 len=4
dat max〕en=4096 len=-l
getmsgO 0xEFFFE7EC. OxEFFFEZDC ctlmaxien=912 len=-l
dat maxlen=4096 1en=26 flags 0x0000
writed ' F r 1 Apr 4 getmsg(3 0xEFFFE7EC 0xEFFFE7DC ctlmaxlen=912 len=4
dat max〕en=4096 len=-l
0xEFFFE81C) buf=0x0002BAB8 buf«0xEFFFE8D0
V 26)
0xEFFFE81C) buMx0002BAB8 buf=0xEFFFE8D0
0xEFFFE81C) buf=0x0002BAB8 buf«0xEFFFE8D0
V 26)
0xEFFFE81C) buMx0002BAB8 buf=0xEFFFE8D0
=0
'F r 1 Apr
=26
=0
"\0\0\017,
=0
'F r 1 Apr
=26
=0
"\0\0\017,
flags 0x0000
_exit(0)
Наш клиент вызывает write, и следующий вызов getmsg возвращает сообщение LORDERLJND (4 байта управляющей информации без данных). Сообщению T—ORDERL INO соответствует значение 23, которое выводится на экран как 0x17.
Сокеты ядра BSD
В следующем примере мы рассмотрим операционную систему BSD/OS — Беркли-ядро, в котором все функции сокетов являются системными вызовами. Программа трассировки системных вызовов имеет название ktrace. Она выводит информацию о трассировке в файл (по умолчанию имя этого файла ktrace out), который можно вывести на экран с помощью kdump. Клиент сокета запускается следующим образом:
bsdi % ktrace daytimetcpcli 206.62.226.43
Fri Apr 4 17 24 30 1997
Затем запускаем kdump, чтобы направить трассировочную информацию в стандартный поток вывода.
13187 daytimetcpcli CALL
13187 daytimetcpcli RET
13187 daytimetcpcli CALL
13187 daytimetcpcli RET
socket(0x2 0x1.0)
socket 3
connect(0x3 0xefbfc9a0 0x10)
connect 0
13187 daytimetcpcli CALL
read(0x3.Oxefbfc9b0 0x1000)
13187 daytimetcpcli GIO
fd 3 read 26 bytes
•'Fri Apr 4 17 24 30
1997\r\nM
13187 daytimetcpcli RET
read 26/0xla
13187 daytimetcpcli CALL
write(0x1,0x9000,0xla)
13187 daytimetcpcli GIO
fd 1 wrote 26 bytes
"Fri Apr 4 17 24 30
1997\r\n*'
13187 daytimetcpcli RET
write 26/0xla
13187 daytimetcpcli CALL
read(0x3.0xefbfc9b0,0x1000)
13187 daytimetcpcli GIO
fd 3 read 0 bytes
13187 daytimetcpcli RET
read 0
13187 daytimetcpcli CALL
exit(O)
Число 13 187 является идентификатором процесса. CALL идентифицирует системный вызов, RET обозначает возвращение управления, GI0 подразумевает общий процесс ввода-вывода. Мы видим системные вызовы socket и connect, за которыми следуют вызовы read, возвращающие 26 байт. Наш клиент записывает эти байты в стандартный поток вывода, и при следующем вызове read возвращает нулевое значение (конец файла).
Сокеты ядра Solaris 2.6
Операционная система Solaris 2.x основывается на SVR4, и во всех версиях ранее 2.6 сокеты реализуются так, как показано на рис. 33.3. Однако во всех версиях SVR4 с подобными реализациями сокетов существует одна проблема: они редко
обеспечивают полную совместимость с сокетами Беркли-ядер. Для обеспечения дополнительной совместимости в Solaris 2.6 способ реализации изменен за счет использования файловой системы sockfs. Такой подход обеспечивает поддержку сокетов ядра, что можно проверить с помощью truss на нашем клиенте (использующем сокеты).
Solaris26 % 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. OxEFFFE8c8. 16)= 0
name = 198 69 10 4/13
read(3. и S a t Apr 5 1"4096) = 26
Sat Apr 5 11 32 07 1997
writed. "Sat Apr 51”26) = 26
read(3, 0xEFFFE8D8. 4096)= 0
_exit(0)
Первые три аргумента системного вызова so_socket являются нашими аргументами socket.
Далее мы видим, что connect является системным вызовом, a truss при вызове с флагом -V connect выводит на экран содержимое структуры адреса сокета, на которую указывает второй аргумент (IP-адрес и номер порта). Мы не показываем системные вызовы, относящиеся к стандартным потокам ввода и вывода.
ПРИМЕЧАНИЕ
Одним из побочных аффектов новой реализации являегся добавление в ядро 18 системных вызовов.
Стандартные службы Интернета
Рекомендуем ознакомиться со стандартными службами Интернета, приведенными в табл. 2.1. Для тестирования наших клиентов мы много раз использовали службу, позволяющую определить дату и время. Служба, игнорирующая присылаемые данные, является удобным портом, на который можно отправлять данные. Эхо-служба аналогична эхо-серверу, неоднократно упоминаемому в этой книге.
ПРИМЕЧАНИЕ
В настоящее время многие сайты перекрывают доступ к этим службам с помощью брандмауэров, так как некоторые атаки типа «отказ в обслуживании» (DoS), имевшие место в 1996-году, были направлены именно па эти службы (см. упражнение 12.3). Тем не менее можно успешно использовать эти службы внутри локальной сети.
Программа sock
Программа sock, написанная автором книги, впервые появилась в книге [94], где широко использовалась для генерации специальных условий, большинство которых затем проверялось с помощью программы tepdump. Удобство этой програм-
мы заключается в том, что она генерирует такое множество различных сценариев, что нет необходимости писать специальные тестовые программы.
В этой книге исходный код программы не приведен (более 2000 строк на языке С), но он находится в свободном доступе (см. предисловие).
Программа работает в одном из четырех режимов, и в каждом из них можно использовать либо протокол TCP, либо протокол UDP.
Клиент стандартного ввода и стандартного вывода (рис. В.1).
(клиент) Соединение TCP(клиент) Соединение TCPРис. В.1. Клиент sock: стандартный ввод и стандартный выводРис. В.1. Клиент sock: стандартный ввод и стандартный вывод
(клиент) Соединение TCP
(клиент) Соединение TCP
Рис. В.1. Клиент sock: стандартный ввод и стандартный вывод
Рис. В.1. Клиент sock: стандартный ввод и стандартный вывод
В клиентском режиме все, что считывается из стандартного потока ввода, передается в сеть, а все, что получается из сети, записывается в стандартный поток вывода. Должны быть указаны IP-адрес сервера и номер порта, и в случае TCP выполняется активное открытие.
Сервер стандартного ввода и стандартного вывода. Этот режим аналогичен предыдущему, за исключением того, что программа связываег заранее известный порт со своим сокетом и в случае TCP осуществляе гея пассивное открытие.
Клиент-отправитель (рис, В.2).
sock
(клиент-отправитель)
Соединение TCPили дейтаграммы UDP
Рис. В.2. Программа sock в качестве клиента-отправителя
Программа осуществляет фиксированное количество передни рахитов некоторого определенного размера в сеть.
Сервер-получатель (рис. В.З).
sock(сервер-получатель)sock(сервер-получатель)Рис. В.З. Программа sock в качестве сервера - получателяРис. В.З. Программа sock в качестве сервера - получателя
sock
(сервер-получатель)
sock
(сервер-получатель)
Рис. В.З. Программа sock в качестве сервера - получателя
Рис. В.З. Программа sock в качестве сервера - получателя
Программа осуществляет фиксированное количество считываний из сети. Эти четыре рабочих режима соответствуют следующим четырем командам:
sock [лардметры] узел служба sock [пардмегры] -s [узел] служба sock [параметры] -т узел службе sock [пдрдметры] 7s [узел] служба
где узел— это имя или IP-адрес узла, а служба — это имя или помер порта. В двух серверных режимах выполняется связывание с универсальным адресом, если не задан необязательный параметр узел.
Можно также определить около 40 параметров командной строки, запускающих дополнительные возможности программы. Здесь мы не будем подробно останавливаться на этих параметрах, отметим только, что можно использовать почти все параметры сокетов, упомянутые в главе 7. Запуск программы без аргументов выводит на экран краткое описание всех параметров:
Ь п связывает л в качестве клиентского локального номера порта
с конвертирует символ новой строки в CR/LF и наоборот
f а b с d р удаленный IP-адрес = a b с d удаленный номер порта = р
g а b с d свободная маршрутизация
h половинное закрытие TCP при получении EOF из стандартного потока ввода
тот叩авма данных на сокет прием данных с сокета (w/-s)
j а b с d присоединение к группе многоадресной передачи
к осуществляет write или wntev порциями
1 а b с d р клиентский локальный IP-адрес = a b с d локальный номер порта = р
п п размер буфера для записи клиентом рассылки1'(по умолчанию 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
EпараметрIP二 RECVDSTADDR
F порождение дочерних процессов (fork) после установления соединения (параллельный ТСР-сервер)
G а b с d жесткая маршрутизация
Н п параметр IP_TOS (16=min del 8=max thru 4=max rel 2初1门 cost)
I сигнал SIGID
T n параметр IP_TTL
К параметр SO 二 KEEPALIVE
L n параметр SO 二 LINGER n = linger time
N параметр TCP_NODELAY
0 n время (в мс) для ожидания после вызова listen но перед первым приемом (accept)
Р п время (в мс) перед первым считыванием или записью (рассылка/叩нем)
Q п время (в мс) ожидания после получения FIN. но перед закрытием
R п параметр SO_RCVBUF
S п параметр S0 二 SNDBUF
Т параметр SO_REUSEPORT
U п войти в срочный режим прежде чем записать число п (только для отправителя)
V использовать writevO вместо wnteO включает -к
W игнорировать ошибки записи для клиента прмема
~Х п параметр TCP_MAXSEG (устанавливает MSS)
Y параметр SO_DONTROUTE
Z MSG-PEEK -
2 параметр IP_DNESBCAST (255 255 255 255) для широковещательной передачи
В.4. Небольшие тестовые программы
Другим полезным методом отладки, которым автор пользовался при написании книги, является создание небольших тестовых программ, позволяющих увидеть, как работает одно конкретное свойство в тщательно выстроенной тестовой ситуации. При написании небольших тестовых программ полезно иметь набор библиотечных функций-оберток и некоторых простых функций вывода сообщений об ошибках, наподобие тех, что использовались на протяжении всей книги. Такой подход уменьшает размер создаваемого кода и в то же время обеспечивает требуемую проверку ошибок.
Пример: определение полосы приоритета внеполосных данных XTI
Чтобы проиллюстрировать этот метод в комплексе с трассировкой системных вызовов, ответим на вопрос: как XTI посылает внеполосные данные в TCP? Установим ТСР-соедмнение с сервером, вызовем t_snd и отправим 1 байт с установленным флагом T_EXPEDITED. В листинге ВЛ1 приведена наша простая тестовая программа.
Листинг В.1. Простая тестовая программа, показывающая, как XTI отсылает внеполосные данные TCP
//debug/test01 с
#i nd ude'* unpxti h"
int
main。nt argc char **argv)
{
int tfd.
if (argc i= 3)
er^quitC usage testOl <hostname/1 Paddress> <service/pQft#>"):
tfd = Tcp_connect(argv[l]. argv⑵).
t_snd(tfd, M,'. 1 T—EXPEDITED),
exit(0).
}
Затем запускаем данную программу под управлением Solaris 2.6, используя программу truss для трассировки системных вызовов.
solans26 % truss -v putmsg.putpmsg testOl 198.69.10.4 discard
Последние строки вывода отвечают на наш вопрос:
putpmsg(3 0xEFFFF7D4 0xEFFFF7C0. 0. 0x0004) = 0
ctl maxlen»8len=8buf=0xEFFFF7CC '\0\0\004\0\0\0\0M
dat maxlen=l]en=lbuf=0x00015318 AO"
Все исходные коды программ, опубликованные в этой книге, вы можеЛ' найти по адресу http:// www piter com/download.
Третий аргумент 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 с
include "unpxti h"
int
main(int argc. char **argv)
{
int11stenfd connfd n. fl祈:
charbuff[MAXLINE],
struct pollfd fds[l],
if (argc == 2)
11 stenfd = Tcpji Sten (NULL argv[l] NULL).
else if (argc == 3)
11stenfd = Tcpjisten(argv[l], argv[2] NULL).
else
err_quit("usage daytinietcpsrvOl [ <host> ] <service or port〉").
connfd = Xti_accept(1istenfd NULL. 0).
fds[O] fd = connfd
fds[0] events = POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI,
17 for (. ) {
18 n = polKfds 1, INFTIM).
19 pnntfCpoll returned Zd revents = OxXxXen". n. fds[O] revents).
20 n = 7_rcv(connfd. buff, sizeof(buff) &flags).
21pnntf("received Zd bytes. flags = Zd\en", n, flags)
22}
23 }
Здесь мы задаем все четыре возможных события и выводим на экран возвращаемое событие. Далее мы вызываем t_rcv и выводим количество байтов, возвращаемых вместе с флагами.
Чтобы послать этой программе и обычные, и внеполосные данные, на другом узле запустим в качестве клиента программу sock:
solans % sock -v -1 -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
wrote 1 bytes
wrote 1 byte of urgent data
wrote 1 bytes
wrote 1 bytes
Флаг -v включает режим подробного вывода, ~i заставляет программу записывать (отправлять) данные в сеть (режим клиента рассылки), -w 1 приводит к записи порциями по 1 байту, -п 3 производит три записи, -U 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
fl ags =
0
poll returned 1.
revents
=0x41
t_rcv error An event requires attention
Каждый раз возвращается событие POLL IN или POLLRDNORM, что говорит о том, что функция pol 1 специально не обрабатывает прибывающие внеполосные данные. (Ищем значения двух битов в возвращаемом событии 0x41 в заголовочном файле <sys/pol 1 h>. При написании подобных тестовых программ гораздо проще и быстрее выводить эти значения в численном представлении, а затем искать их в соответствующих заголовочных файлах.) Но t_rcv возвращает флаг 2 (T_EXPEDITED),
когда возвращает внеполосные данные.
Программа tcpdump
Бесценным средством отладки в сетевом программировании является такая программа, как tcpdump. Она считывает пакеты из сети и выводит на экран большое количество информации об этих пакетах. Эта программа также позволяет нам задать некоторые критерии отбора пакетов, в результате чего будут выводиться только пакеты, удовлетворяющие этим критериям. Например,
% tcpdump '(udp and port daytime) or 】cmp'
выводит только UDР-дейтаграммы с номером порта отправителя или получателя, равным 13 (сервер времени и даты), или ICMP-пакеты. Следующая команда: % tcpdump 'tcp and port 80 and tcp[13:l] & 2 != O'
выводит только TCP-сегменты с номером порта отправителя или получателя, равным 80 (сервер HTTP), у которых установлен флаг SYN. Флаг SYN имеет значение 2 в 13-м байте от начала TCP-заголовка. Следующая команда:
% tcpdump *tcp and tcp[0:2] > 7000 and tcp[0:2] <= 7005'
выводит только те ТСР-сегмепты, у которых номер порта отправителя лежит в интервале от 7001 до 7005. Номер порта отправителя занимает 2 байта в самом начале ТСР-заголовка (нулевое смещение).
В приложении А книги [94] более подробно описано действие данной программы.
ПРИМЕЧАНИЕ
Эта программа доступна по адресу ftp.ee.lbl.gov н работает под множеством реализаций Unix. Она написана Ван Якобсоном (Van Jacobson), Крэгом Лсресом (Craig Leres) и Стивеном МакКаном (Steven McCanne).
Некоторые поставщики предлагают свои программы, обладающие теми же возможностями. Например, в Solans 2.x есть программа snoop. Но программа tepdump функционирует под множеством версий Unix, а возможность использования одного и того же средства в неоднородном окружении является большим преимуществом.
Программа netstat
В тексте книги много раз испол1>зовалась программа netstat. Эта программа служит для следующих целей.
Она выводит статус точек доступа сети. Это было показано в разделе 5.6, когда мы прослеживали статус нашей точки доступа при запуске клиента и сервера.
Она показывает, к какой группе принадлежит каждый из интерфейсов узла. Обычно для этой цели используется флаг та, а в Solaris 2.x используется флаг -д.
С параметром т эта программа сообщает статистику по каждому протоколу. Подобный пример был приведен в разделе 8.13, когда мы говорили о недостаточном управлении потоками в UDP.
При использовании параметра -г программа выводит таблицу маршрутизации, а с параметром -i — информацию об интерфейсе. Эта возможность была использована в разделе 1.9, когда с помощью программы netstat мы выясняли топологию сети.
Программа netstat обладает и другими возможностям и, а многие поставшики добавляют свои собственные. Обратитесь к руководству по вашей системе.
Программа Isof
Название Isof происходит от «list open file» (перечислить открытые файлы). Как и tepdump, эта программа является общедоступной и представляет собой удобное средство для отладки, которое было перенесено на множество версий Unix.
Одним из общих способов применения программы Isof при работе в сети является выявление процесса, имеющего открытый сокет, по указанному IP-адресу или порту. Программа netstat позволяет выяснить, какой IP-адрес или порт используется, а также узнать состояние TCP-соединения, но она не позволяет идентифицировать процесс. Например, чтобы определить, какой процесс запустил сервер времени и даты, выполним следующую команду:
solans % Isof -1 TCP:由ytime
COMMAND PID USER FD TYPE DEVICE SIZE/OFF INODE NAME
inetd 222 root 15u wet 0xf5a801f8 OtO TCP * daytime
В выводе приводятся следующие данные: команда (данный сервис обеспечивается сервером inetd), идентификатор процесса, владелец процесса, дескриптор
(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, показанного в листинге Г.11. Этот файл подключает все стандартные системные заголовочные файлы, необходимые для работы большинства программ, а также некоторые общие системные заголовочные файлы. В нем также определены такие константы, как MAXLINE, прототипы функций ANSI С для тех функций, которые мы определяем в тексте (например, read line), и все используемые нами функции-обертки. Сами прототипы в приведенном ниже листинге мы не показываем.
Листинг Г.1. Заголовочный файл unp.h
//lib/unp.h
/* Наш собственный заголовочный файл */
#ifndef —unp_h
#define —unp_h
#1nclude и../config.h" /* параметры конфигурации для данной ОС */
/* "../config.h" генерируется сценарием configure */
/* изменив список директив #1nclude.
нужно также изменить файл acsite.m4 */
8 #1nclude
<sys/types.h>
/* основные системные гипы данных */
9 #1nclude
<sys/socket.h>
/* основные определения сокетов */
10 #1 nclude
<sys/tinie.h>
/* структура timeval{} для функции selectО */
11 #1nclude
<time.h>
/* структура timespec{} для функции pselectO */
12 #1nclude
<netinet/in.h>
/* структура sockaddr_in{} и другие сетевые определения */
13 #1 nclude
<arpa/inet.h>
/* inet(3) функции */
14 #1nclude
<errno.h>
15 #1nclude
<fcntl.h>
/* для неблокируемых сокетов */
16 #1nclude
<netdb.h>
17 #1nclude
〈signal.h>
18 #include
<stdio.h>
19 #1nclude
<stdlib.h>
20 #1nclude
<string.h>
21 #1nclude
<sys/stat.h>
/* для констант S_xxx */
22 #1nclude
<sys/uio.h>
/* для структуры iovec{} и readv/writev */ продолжение ^
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http:〃 www.piter.com/download.
Листинг Г.1 (продолжение)
23 #1nclude<unistd h>
#include<sys/wait h>
#include <sys/un h> /* для доменных сокетов Unix */
#ifdef HAVE_SYS_SELECT_H
include <sys7select h> /* для удобства */
#endif
#ifdef HAVE_PDLL_H
#】nclude〈poll h> /* для удобства */
#endif
#ifdef HAVE_STRINGS_H
#include<stnngs h> /* для удобства */
#endif
35 /* Три заголовочных файла обычно нужны для вызова ioctl
36 * для сокета/файла <sys/ioctl h> <sys/filio h> <sys/sockto h>
37 */
38 #ifdef HAVE^SYS_IOCTL_H
39 include <sys7ioctl h>
40 #endif
41 #ifdef HAVE_SYS_FILIO_H
42 #indude <sys7fiho h>
43 #endif
44 #ifdef HAVE_SYS_SOCKIO_H
45 #include <sys7sockio h>
46 #endif
47 #】fdef HAVE_PTHREAD_H
48 #ind ude <pthread h>
49 #endif
50 /* OSF/1 фактически запрещает recv()卄 sendO в <sys/socket h> */
51 #】fdef_osf_
52 #undefrecv
53 #undefsend
54 #defmerecv (abed)recvfrom(a.b.c.d.O.O)
55 #defmesend (abcd)sendto(a.b.c.d.O.O)
56 #endif
57 #ifndef INADDR_NONE
/* $$ Ic inaddr_n6ne$$ */
58 #def1 ne INADDR_NONE Oxffffffff /* должно было быть в <netinet/in h> */
59 #endif
60 #ifndef SHU1_RD
/* три новые константы Posix lg */
61 #define SHUT_RD
0/* отключение чтения */
62 #define SHUT^WR
1/* отключение записи */
63 #define SHUT_RDWR 2/* отключение чтения и записи */
64 #endif
65 #ifndef INET ADDRSTRLEN
/* $$ Ic INET一A$DRSTRLEN$$ */
66 #def?ne INEt_ADDRSTRLEN 16 /* ddd ddd ddd ddd\e0
67-1234567890123456 */
68 #endif
69 /* Нужно даже если нет поддержки IPv6 чтобы мы всегда могли разместить в памяти
70 буфер требуемого размера без директив #ifdef в коде */
71 #ifndef INET6_ADDRSTRLEN
/* $$ Ic INET6_ADDRSTRLEN$$ */
72 #define INET6_ADDRSTRLEN 46 /* максимальная длина строки адреса IPv6
73хххх хххх хххх хххх хххх хххх хххх хххх или
74хххх хххх хххх хххх хххх хххх ddd ddd ddd ddd\e0
751234567890123456789012345678901234567890123456 */
76 #endif
77 /* Определяем bzeroO как макрос если эта функция отсутствует в стан口叩тно百 Оиблиотеке С */
78 #ifndef HAVE_BZERO
79 #define bzero(ptr n) memset(ptr 0 n)
80 #endif
81 /* В более старых распознавателях отсутствует gethostbyname2() */
82 #ifndef HAVE_GETH0STBYNAME2
#define gethostbyname2(host family) gethostbyname((host))
#endif
/* Структура возвращаемая функцией recvfrom_flags() */
struct in_pktinfо {
struct in_addr 】pi_addr /* IPv4 адрес получателя */
int ipi_ifindex /* полученный индекс интерфейса */
}~
/* Нам нужны более новые макросы CMSG_LEN() и CMSG_SPACE() но в настоящее время их
поддерживают далеко не все реализации Им требуется
макрос ALIGNO но зто зависит от реализации */
#ifndef CMSG_LEN
#define CMSG_LEN(size)(sizeof(struct cmsghdr) + (size))
#endif
#ifndef CMSG_SPACE
^define CMSG_SPACE(size)(sizeof(struct cmsghdr) + (size))
#endif
/* Posix 1g требует макрос SUN_LEN() но он。叩едехен
не во всех реализациях Этот макрос 4 4BSD работает
независимо от того имеется ли поле длины */
#ifndef SUN_LEN
define SUN二LEN(su) \e
(sizeof7*(su)) - sizeof((su) >sun_path) + strlen((su) >sun_path))
#endif
/* В Posix 1g домен Unix называется локальным IPC
Но пока не во всех системах 。叩еделенн AF.LOCAL и PF_LOCAL */
#i fndef AF_LOCAL
#define AF_LDCAL AF_UNIX
#end】f
#ifndef PF_LOCAL
112 ^define PF^LOCAL PF_UNIX
113 #endif
114 /* Posix 1g требует 。叩еде冊нмя константы INFTIM в <poll h> но во многих системах
115 она по 叩ежнему определяется в <sys/stropts h>
116 Чтобы не подключать все функции работы с потоками определяем ее здесь
117 Это стандартное значение но нет гарантии что оно равно 1 */
продолжение 少
Листинг ГЛ (продолжение)
118 #ifnctef INFTIM
119 #define INFTIM(-1) /* бесконечный тайм-аут */
120 #ifdef HAVE_POLL_H
121 ^define INFTTm_UNPH /* надо указать в unpxti h. что эта константа определена здесь */
122 #endif 一
123 #endif
124 /* Это значение можно было бы извлечь из SOMAXCONN в <sys/socket h>,
125 но многие ядра по-叩ежнему определяют его как 5 хотя на самом деле поддерживается гораздо больше */
127 /* Различные константы */128 #define MAXLINE 4096129 ^define MAXSOCKADDR 128130 #define BUFFSIZE 8192127 /* Различные константы */128 #define MAXLINE 4096129 ^define MAXSOCKADDR 128130 #define BUFFSIZE 8192126 ^define LISTENQ 1024/* Второй аргумент функции listenO */
127 /* Различные константы */
128 #define MAXLINE 4096
129 ^define MAXSOCKADDR 128
130 #define BUFFSIZE 8192
127 /* Различные константы */
128 #define MAXLINE 4096
129 ^define MAXSOCKADDR 128
130 #define BUFFSIZE 8192
/* максимальная длина текстовой строки */
/* максимальный размер структуры адреса сокета */
/* размер буфера для чтения и записи */
131 /* Определение номера клиент-с印вер */131 /* Определение номера клиент-с印вер */порта который может быть использован для взаимодействияпорта который может быть использован для взаимодействия132 #define133 #defme134 #define135 #define132 #define133 #defme134 #define135 #defineSERV_PORT SERV二 PORT—STR UNIXSTR_PATH UNIXDG_PATHSERV_PORT SERV二 PORT—STR UNIXSTR_PATH UNIXDG_PATH9877/*клиентыисерверыTCP и U0P*/'9877'/*клиентыисерверыTCP и UDP*/w/tmp/umx strM/*потоковыеклиентыи серверыдоменаUnix */”/tmp/unix dg"/*клиентыисерверыпротоколадейтаграммдомена Unix */9877/*клиентыисерверыTCP и U0P*/'9877'/*клиентыисерверыTCP и UDP*/w/tmp/umx strM/*потоковыеклиентыи серверыдоменаUnix */”/tmp/unix dg"/*клиентыисерверыпротоколадейтаграммдомена Unix */
131 /* Определение номера клиент-с印вер */
131 /* Определение номера клиент-с印вер */
порта который может быть использован для взаимодействия
порта который может быть использован для взаимодействия
132 #define
133 #defme
134 #define
135 #define
132 #define
133 #defme
134 #define
135 #define
SERV_PORT SERV二 PORT—STR UNIXSTR_PATH UNIXDG_PATH
SERV_PORT SERV二 PORT—STR UNIXSTR_PATH UNIXDG_PATH
9877/*клиентыисерверыTCP и U0P*/
'9877'/*клиентыисерверыTCP и UDP*/
w/tmp/umx strM/*потоковыеклиентыи серверыдоменаUnix */
”/tmp/unix dg"/*клиентыисерверыпротоколадейтаграмм
домена Unix */
9877/*клиентыисерверыTCP и U0P*/
'9877'/*клиентыисерверыTCP и UDP*/
w/tmp/umx strM/*потоковыеклиентыи серверыдоменаUnix */
”/tmp/unix dg"/*клиентыисерверыпротоколадейтаграмм
домена Unix */
136 /* Дальнейшие определения сокращают преобразования типов аргументов-указателей */
137 #define SA struct sockaddr
138 #defme FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IR0H)
139 /* заданные по умолчанию разрешения на доступ для новых файлов */
140 #defme DIR_MODE (FILE_MODE | S_IXUSR | S_IXGRP | S」XOTH)
141 /* заданные по умолчанию разрешения на доступ к фа/лам для новых каталогов */
142 typedef void Sigfunc (int) /* для обработчиков сигналов */
143 #define ппп(а b)((а) < (Ь) ? (а)(Ь),
144 #def1 ne max(a b)((а) > (b) 7 (a)(b))
145 #ifndef HAVE_ADDRINFO_STRUCT
146 #mclude ' /lib/addrmfo h”
147 #end】f
148 #ifndef HAVE」F_NAMEINDEX_STRUCT
149 struct if_nameindex {
150unsigned mt if_mdex/*12 .. */
151char *if_name/* имя. заканчивающееся нулем: *1e0".…青
152 }”
153 #endif
154 #ifndef HAVE_TIMESPEC_STRUCT
155 struct timespec {
156time_t tv_sec/* секунды */
157longtv nsec/* и наносекунды 刁
158 }亠
159 #endif
Г.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.
Строки, начинающиеся с #define, относятся к тем свойствам, которые обеспечены данной системой. Закомментированные строки и строки, начинающиеся с #undef, относятся к свойствам, данной системой не поддерживаемым
Листинг Г.2. Заголовочный файл config.h для BSD/OS
】386-pc-bsdi3 0/config h
2 /* Определяем константы если имеется соответствующий заголовочный файл */2 /* Определяем константы если имеется соответствующий заголовочный файл */1 /* config h Автоматически генерируется сценарием configure */
2 /* Определяем константы если имеется соответствующий заголовочный файл */
2 /* Определяем константы если имеется соответствующий заголовочный файл */
/* <netconfig h> *//* <netdiг h> *//* <pthread h> *//* <netconfig h> *//* <netdiг h> *//* <pthread h> */3 #define CPU_VENDOR_OS "i386-pc-bsdi3 O'
/* <netconfig h> */
/* <netdiг h> */
/* <pthread h> */
/* <netconfig h> */
/* <netdiг h> */
/* <pthread h> */
/* #undefHAVE二NETDIR—H */#define HAVE_PTHREAD_H T/* #undefHAVE二NETDIR—H */#define HAVE_PTHREAD_H T/* #undef HAVE_NETCONFIG_H */
/* #undefHAVE二NETDIR—H */
#define HAVE_PTHREAD_H T
/* #undefHAVE二NETDIR—H */
#define HAVE_PTHREAD_H T
/* <strings h> */ /* <xti_inet h> */ /* <sys/filio h> */ /* <sys/ioctl h> */ /* <sys/select h> */ /* <sys/sockio h> */ /* <sys/sysctl h> *//* <sys/time h> *//* <strings h> */ /* <xti_inet h> */ /* <sys/filio h> */ /* <sys/ioctl h> */ /* <sys/select h> */ /* <sys/sockio h> */ /* <sys/sysctl h> *//* <sys/time h> */#define HAVE_STRINGS_H 1
/* <strings h> */ /* <xti_inet h> */ /* <sys/filio h> */ /* <sys/ioctl h> */ /* <sys/select h> */ /* <sys/sockio h> */ /* <sys/sysctl h> */
/* <sys/time h> */
/* <strings h> */ /* <xti_inet h> */ /* <sys/filio h> */ /* <sys/ioctl h> */ /* <sys/select h> */ /* <sys/sockio h> */ /* <sys/sysctl h> */
/* <sys/time h> */
/* #undef HAVE_XTI_INET_H */
#define HAVE_SYS[FILiO_H T
#defme HAVENSYSJDCTL_H 1
^define HAVE_SYS~SELECT_H 1
#define HAVE_SYS]SOCKIO_H 1
^define HAVE^SYS 二 SYSCTLJd 1
define HAVE—SYS二TIME』1
15 /* Определена если можно подключить <time h> и <sys/time h> */
16 #defme TIME_WITH_SYS_TIME 1
17 /* Определены если имеются18 #define HAVE_BZERO 119 Refine HAVE_GETH0STBYNAME220 /* #undef HAVE_PSELECT */21 #define HAVE VSNPRINTF 117 /* Определены если имеются18 #define HAVE_BZERO 119 Refine HAVE_GETH0STBYNAME220 /* #undef HAVE_PSELECT */21 #define HAVE VSNPRINTF 1соответствующие функции */1соответствующие функции */1
17 /* Определены если имеются
18 #define HAVE_BZERO 1
19 Refine HAVE_GETH0STBYNAME2
20 /* #undef HAVE_PSELECT */
21 #define HAVE VSNPRINTF 1
17 /* Определены если имеются
18 #define HAVE_BZERO 1
19 Refine HAVE_GETH0STBYNAME2
20 /* #undef HAVE_PSELECT */
21 #define HAVE VSNPRINTF 1
соответствующие функции */
1
соответствующие функции */
1
/* Определенн. если прототипы функций есть
/* #undef HAVEjGETADDRINFO_PROTO */
/* #undefHAVE二 GETNAMEINFO[PROTO */
define HAVE_GETHDSTNAME_PROTO 1
#define HAVE_GETRUSAGE_PROTO 1
#define HAVE_HSTRERROR_PROTD 1
/* 牝indefHAVE^IF_NAMETOINDEX_PROTO */
#define HAVE_INET_ATON_PROTO 1
^define HAVE_INET PTON_PRDTO 1
/* #undefHAVEJSFDTYPE_PROTO */
/* #undefHAVE_PSELECT_PROTO */
#define HAVE_SNPRINTF_PROTO 1
в заголовочном файле *//* <netdb h> *//* <netdb h> *//* <unistd h> *//* <sys/resource h> *//* <netdb h> *//* <net/if h> *//* <arpa/inet h> *//* <drpa/met h> *//* <sys/stat h> *//* <sys/select h> *//* <stdio h> *//* <sys/socket h> */в заголовочном файле *//* <netdb h> *//* <netdb h> *//* <unistd h> *//* <sys/resource h> *//* <netdb h> *//* <net/if h> *//* <arpa/inet h> *//* <drpa/met h> *//* <sys/stat h> *//* <sys/select h> *//* <stdio h> *//* <sys/socket h> */叩ос)(ш/сен〃е 按叩ос)(ш/сен〃е 按34 /* #undef HAVE_SOCKA1MARK_PROTO */
в заголовочном файле */
/* <netdb h> */
/* <netdb h> */
/* <unistd h> */
/* <sys/resource h> */
/* <netdb h> */
/* <net/if h> */
/* <arpa/inet h> */
/* <drpa/met h> */
/* <sys/stat h> */
/* <sys/select h> */
/* <stdio h> */
/* <sys/socket h> */
в заголовочном файле */
/* <netdb h> */
/* <netdb h> */
/* <unistd h> */
/* <sys/resource h> */
/* <netdb h> */
/* <net/if h> */
/* <arpa/inet h> */
/* <drpa/met h> */
/* <sys/stat h> */
/* <sys/select h> */
/* <stdio h> */
/* <sys/socket h> */
叩ос)(ш/сен〃е 按
叩ос)(ш/сен〃е 按
Листинг Г 2 (продолжение)
35 /* Определены. если определены соответствующие структуры */
/* <netdb h> *//* <net/if h> *//* <net/if_dl h> *//* <time h> *//* <netdb h> *//* <net/if h> *//* <net/if_dl h> *//* <time h> */36 /* #undef HAVE_ADDRINFO_STRUCT */
/* <netdb h> */
/* <net/if h> */
/* <net/if_dl h> */
/* <time h> */
/* <netdb h> */
/* <net/if h> */
/* <net/if_dl h> */
/* <time h> */
37 /* #undefHAVE J F^NAME I NDEX_STRUCT */
38 #define HAVE_SOCKADDR_DL_STRUCT 1
39 #define HAVE[TIMESPEC[STRuCT 1
40 /* Определены если имеется указанное свойство */
41 #defme HAVE_SOCKADDR_SA_LEN 1/* структура sockaddr{} содержит поле sa_len */
42 #def1 ne HAVE_MSGHDR_MSG_CONTROL 1 /* структура msghdr{} содержит поле msg_control */
/* большинство /* ДЛЯ AIX */ /* для OSF 3 2/* большинство /* ДЛЯ AIX */ /* для OSF 3 2здесь */*/здесь */*/43 /* Имена устройств XTI для TCP и UDP */
/* большинство /* ДЛЯ AIX */ /* для OSF 3 2
/* большинство /* ДЛЯ AIX */ /* для OSF 3 2
здесь */
*/
здесь */
*/
44 /* #undef HAVE_DEV_TCP */
45 /* #undefHAVE二DEV二XTI—TCP */
46 /* #undefHAVE[DE匸STREaMS_XTISO_TCP */
47 /* При необходимости определяем типы данных */47 /* При необходимости определяем типы данных */48 /* #undef48 /* #undef4950515249505152/* #undef /* #undef/* #undef /* #undefint8_t */ intl6_t */ int32~t */int8_t */ intl6_t */ int32~t */#defme uint8_t unsigned char#defme uint8_t unsigned char^define uintl6_t unsigned^define uintl6_t unsigned53 #defme uint32_t unsigned53 #defme uint32_t unsignedshort mtshort mt545556545556/* #undef size__t *//* #undef ssize_t *//* socklen_t должен иметь/* #undef size__t *//* #undef ssize_t *//* socklen_t должен иметь/* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types/* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/typesh> */ h> */ h> */ h> */ h> */ h> */ h> */ h> */h> */ h> */ h> */ h> */ h> */ h> */ h> */ h> */575B596061575B596061тип uint32_t. но configure определяет еготип uint32_t. но configure определяет егокак unsigned int, так как это значение используется в начале компиляции, иногда до того, как в данной реализации определяется тип uint32_t */как unsigned int, так как это значение используется в начале компиляции, иногда до того, как в данной реализации определяется тип uint32_t */#defme socklen_t unsigned mt#define sa_famiTy_t SA_FAMILY_T#define SA_FAMILY_T uint8_t#defme socklen_t unsigned mt#define sa_famiTy_t SA_FAMILY_T#define SA_FAMILY_T uint8_t/* <sys/socket h> */ /* <sys/socket h> *//* <sys/socket h> */ /* <sys/socket h> */6262#defme t_scalar t int32 t#defme t_scalar t int32 t63 #define t_uscalar_t umt32_t63 #define t_uscalar_t umt32_t/* <xti h> */ /* <xti h> *//* <xti h> */ /* <xti h> */
47 /* При необходимости определяем типы данных */
47 /* При необходимости определяем типы данных */
48 /* #undef
48 /* #undef
49
50
51
52
49
50
51
52
/* #undef /* #undef
/* #undef /* #undef
int8_t */ intl6_t */ int32~t */
int8_t */ intl6_t */ int32~t */
#defme uint8_t unsigned char
#defme uint8_t unsigned char
^define uintl6_t unsigned
^define uintl6_t unsigned
53 #defme uint32_t unsigned
53 #defme uint32_t unsigned
short mt
short mt
54
55
56
54
55
56
/* #undef size__t */
/* #undef ssize_t */
/* socklen_t должен иметь
/* #undef size__t */
/* #undef ssize_t */
/* socklen_t должен иметь
/* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types
/* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types /* <sys/types
h> */ h> */ h> */ h> */ h> */ h> */ h> */ h> */
h> */ h> */ h> */ h> */ h> */ h> */ h> */ h> */
57
5B
59
60
61
57
5B
59
60
61
тип uint32_t. но configure определяет его
тип uint32_t. но configure определяет его
как unsigned int, так как это значение используется в начале компиляции, иногда до того, как в данной реализации определяется тип uint32_t */
как unsigned int, так как это значение используется в начале компиляции, иногда до того, как в данной реализации определяется тип uint32_t */
#defme socklen_t unsigned mt
#define sa_famiTy_t SA_FAMILY_T
#define SA_FAMILY_T uint8_t
#defme socklen_t unsigned mt
#define sa_famiTy_t SA_FAMILY_T
#define SA_FAMILY_T uint8_t
/* <sys/socket h> */ /* <sys/socket h> */
/* <sys/socket h> */ /* <sys/socket h> */
62
62
#defme t_scalar t int32 t
#defme t_scalar t int32 t
63 #define t_uscalar_t umt32_t
63 #define t_uscalar_t umt32_t
/* <xti h> */ /* <xti h> */
/* <xti h> */ /* <xti h> */
64 /* Определены если система поддерживает указанное свойство */
65 #define IPV4 1
/* IPv4 V в верхнем регистре */
66 #define IPv4 1
/* IPv4, v в нижнем регистре, на всякий случай */
67 /* #undef IPV6 */
/* IPv6. V в верхнем регистре */
68 /* #undef IPv6 */
/* IPv6 V в нижнем регистре на всякий случай */
69 #define UNIXDOMAIN 1
/* доменные сокеты Unix */
70 #defme UNIXdomain 1
/* доменные сокеты Unix */
71 #define MCAST 1
/* поддержка многоадресной передачи */
Г.З. Заголовочный файл unpxti.h
Все программы XTI подключают заголовочный файл unpxti h, показанный в листинге Г.З. Как и в случае заголовочного файла upn h в разделе Г.1, мы пропускаем все прототипы функций. Мы также пропускаем все определения、ххх между T」NET_TCP и T TP BR0ADCAST, так как они практически идентичны.
Листинг Г.З. Заголовочный файл unpxti.h для программ
//XTIlibxti/unpxti h
#i f ndef —unp_xti_h
#define —unp_xti_h
#mclude "unp h"
4 5 6 7 8 94 5 6 7 8 9include <xt】h>
4 5 6 7 8 9
4 5 6 7 8 9
#ifdef HAVE_XTI_INET_H
#1nclude <xti_inet h>
#endif
#ifdef HAVE_NETCONFIG_H
#include〈netconf1g h>
#endif
#ifdef HAVE_NETDIR_H include <netdir h>
#endif
#ifdef INFTIM一UNPH
羸ndef INFTIM- /* отсутствовала в <pol1.h>. не определена для <str0pts.t> */
#endif
#include<stropts h>
/* Обеспечение переносимости с новыми именами начинающимися с Т_
в выпуске XNS 5 отсутствует в Posix 1g */
#ifndef T_INET_TCP
#define TJNET_TCP INET_TCP
#endif
56 #ifndef T_IP_BROADCAST
57 #define T_IP_BROADCAST IPJROADCAST
58 #endif
59 /* Определение соответствующих устройств для t open() */
60 #ifdef HAVE_DEV_TCP-
61 #define XTI_TCP"/dev/tcp"
62 #define XT匸 UDP“/dev/udp”
63 #endif
64 #ifdef HAVE_DEV_XTI_TCP
65 #define XTI_TCP"/dev/xti/tcp"
66 #define XTI_UDP"/dev/xti/udp"
67 #endif
68 #ifdef HAVE_DEV_STREAMS_XTISO_TCP
69 #defi ne XTI_TCP,7dev/streanis/xti so/tcp+"/* + для XPG4 */
70 #define XTI_UDP7dev/streams/xtiso/udp+,'/* + для XPG4 */
71 #endif
72 /* устройство для t_open() в t_accept(). задается с помощью tcp_listen() */
73 extern char xti_serv_dev[]
Г.4. Стандартные функции обработки ошибок
В этой книге мы определяем набор своих собственных функций для обработки ошибок. Причина, по который мы создаем эти функции, заключается в том, что они позволяют нам обрабатывать ошибки с помощью одной строки кода, как, например, показано ниже: if {условие ошибки)
err_sys(0opMar prmtf с любым количеством аргументов),
.Р2-
вместо
if (условие ошибки) {
char buff[200]
snpnntf(buff sizeof(buff), формзт printf с любым когичеством аргументов), perror(buff)
exit(l)
Наши функции обработки ошибок используют следующую возможность ANSI С* список аргументов может иметь переменную длину Более подробную информацию об этом вы найдете в разделе 7 3 книги [57]
В табл Г 1 показано, в чем заключаются разллчия между функциями обработки ошибок Если глобальная целочисленная переменная daemon_proc отлична от нуля, то сообщение об ошибке передается функции syslog с указанным уровнем, в противном случае оно отправляется в с.андар^ый поток вывода сообщений об ошибках
Таблица Г.1. Стандартные функции обрабо~ки ошибок
Функция
strerror (errno?)
Завершение?
Уровень syslog
errdump
Да
abort。,
LOG_ERR
err_msg
Нет
relurn
LOG_INFO
err_quit
Нет
ex底⑴,
LOG_ERR
err_ret
Да
% turn.
LOG_INFO
err sys
Да
exit(l)
LOG_ERR
err_xtj
Да
ехЛ(1)
LOG_ERR
crr_xti_ret
Да
return
LOG_INFO
В листинге Г 4 показаны первые пять функции 厶二 ^абл Г 1
Листинг Г.4. Стандартные функции обработки охибок
//lib/error с
1 #т nd udeunp h
2 #include3 #include2 #include3 #include<stdarg h><syslog h><stdarg h><syslog h>/* заголовочный файл ANS C */ /* для sysUg ) */
2 #include
3 #include
2 #include
3 #include
<stdarg h>
<syslog h>
<stdarg h>
<syslog h>
4 int daemonjDroc /* устанавливаепя з ченулезое зн чеке с помощью daemon^imtO */
5 static void err_doit(int int cons ciir * va_* st)
/* Нефатальная ошибка связанная с системным вызовом
Выводим сообщение и возвращаем у「равлсние */
void
err ret(const char *fmt )
{"
vaji st ap
va_start(ap fmt)
err_doit(l LOG_INFO fmt ap).
va_end(ap)
return
}
/* Фатальная ошибка связа卜卜asсисте^н^ч зьвовом
Выводим сообщение / завершаем рабо у */
void
err_sys(const char *frt
{ 一
vaji st ap
va_start(apfmt)
err_doit(lLOG—ERR fnt ap)
va_end(ap)一
exTt(l)
}
/* Фатальная ошибка связа-лая г системным вызовом
Выводим сообщение сохраняем дамп памяти процесса и заканчиваем работу ★/
void
err_dump(const char *fmt
{ ~
vajist ap
va_start(ap fmt)
err_doit(l LOG_ERR f/ ap)
va_end(ap) ~
abortО/* сохрангег гагп памяти и заканчиваем работу */
exit(l)
}
/* Нефатальная ошибка не относяпа^ся к системному вызову
Выводим сообщение и возвращаем угравление */
void
err msg(const char *fmt )
{-
va_list ap
va_start(ap fmt)
err_don(0 LOG_IN「O fnt ep),
va_end(ap)
return
}
/* Фатальная ошибка не относяшаяся к системному вызову
Выводим сообщение и заканчиваем работу */
void
err_quit(const char *fmt )
{ ~
va_l】st ap
va_start(ap fmt)
err_doit(0 LOG.ERR fmt аз、
va_end(ap)
exTt(l)
}
/* Выводим сообщение и во^вращзек узравлени
Вызывающи процесс задает e^rnoflag и eve */продолжаш(少
Листинг Г.4 (продолжение)
64 static void
65 err__doit(int errnoflag mt level const char *fmt va_hst ap)
66 {一一
67interrno_save n
68charbuf[MAXLINE + 1]
69 errno_save * errno /* значение может понадобиться вызвавшему процессу */
70 #ifdef HAVE_VSNPRINTF
71vsnpmntflbuf MAXLINE fmt ар) /* защищенный вариант */
72 #else
73vsprintf(buf fmt ар)/* незащищенный вариант */
74 #endif
75 n = strlen(buf)
76 if (errnoflag)
77 snprintf(buf + n MAXLINE - n. “ Zs' strerror(errno_save))
78strcat(buf\en)
797f (daemon_proc){
80syslog(level buf)
81} else {
82fflushcstdout) /* если stdout и stderr совпадают */
83fputs(buf stderr)
84fflush(stderr)
85}
86return
87 }
ПРИЛОЖЕНИЕ Д
Решения некоторых упражнений
Глава 1
3 В операционной системе AIX получаем.
aix % daytimetcpcli 206 62 226 33
socket error Addr family not supported by protocol
Для получения дополнительной информации об этой ошибке сначала используем программу grep, чтобы найти строку Addr в заголовочном файле <sys /errno h>
aix Z grep Addr Zusr/include/sys/errno h
#defme EAFNOSUPPORT 66 /* Семейство адресов не поддерживается семейством протоколов */
#define EADDRINUSE 67 /* Адрес уже используется */
Первая строка содержит значение переменной errno и возвращается функцией socket Далее смотрим в руководство пользователя.
а1х % man socket
В большинстве руководств пользователя в конце под заголовком «Errors» приводится дополнительная, хотя и лаконичная информация об ошибках.
Заменяем первое описание на следующее.
mt sockfd n counter = О
Добавляем оператор
counter++
в качестве первого оператора цикла while. Наконец, прежде чем прервать программу, выполняем
printf( counter = %d\en counter)
На экран всегда выводится значение
. Объявим переменную т типа int и заменим вызод^функции write на (радующий
for (1=0 1 V strlen(buff) i++)
Write(connfd &buff[i] 1)
Результат зависит от расположения клиентского узла и узла сервера Если клиент и сервер находятся на одном узле, счетчик обычно равен 1 Это значит, что даже если сервер выполнит функцию write 26 раз, данные будут возвращены за одну операцию считывания (read) Но если клиент запущен в Solans 2 5.1,
а сервер в BSD/OS 3.0, счетчик обычно равен 2. Просмотрев пакеты Ethernet, мы увидим, что первый символ отправляется в первом пакете сам по себе, а следующий пакет содержит остальные 25 символов. (Обсуждение алгоритма Нагла в разделе 7.9 объясняет причину такого поведения.) Если клиент запущен в BSD/OS 3.0, а сервер в Solaris 2.5.1, то счетчик будет равен 26. При просмотре пакетов мы обнаружим, что каждый символ передается в отдельном пакете.
Цель этого примера — продемонстрировать, что разные реализации TCP по- разному поступают с данными, поэтому наше приложение должно быть готово считывать данные как поток байтов, пока не будет достигнут конец потока.
Глава 2
Все 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).
Если с помощью какого-либо редактора осуществить поиск термина «stream» (поток) в указателе RFC (см. решение предыдущего упражнения), мы выясним, что RFC 1819 определяет версию 2 потокового протокола Интернета. Какую бы информацию, которая может содержаться в RFC, мы ни искали, для поиска следует использовать указатель (каталог) RFC.
В версии IPv4 при таком значении MSS генерируется 576-байтовая дейтаграмма (20 байт для заголовка IPv4 и 20 байт для заголовка TCP). Это минимальный размер буфера для сборки фрагментов в IPv4.
В данном примере сервер (а не клиент) осуществляет активное закрытие.
Узел в сети Token Ring не может посылать пакет, содержащий больше чем 1460 байт данных, поскольку полученное им значение MSS равно 1460. Узел в сети Ethernet может посылать пакет размером до 4096 байт данных, но не превышающий величину MTU исходящего интерфейса (Ethernet) во избежание фрагментации. Протокол TCP не может превысить величину MSS, объявленную другой стороной, но он всегда может посылать пакеты меньшего размера.
В разделе «Protocol Numbers» (номера протоколов) RFC «Assigned Numbers》 («Присвоенные номера») указано значение 89 для протокола OSPF.
Глава 3
В языке С функция не может изуенить значение аргумента, передаваемого по значению. Чтобы вызван! ая функция изменила значение, передаваемое вызывающим процессом, требуется, чтобы вызывающий процесс передал указатель на значение, подлежащее изменению.
Указатель должен увеличиваться на количество считанных или записанных байтов, но в языке С нет возможности увеличивать указатели типа void (поскольку компилятору не известно, на какой тип данных указывает указатель).
Глава 4
Посмотрите на определение1 констант, начинающихся с INADDR_, кроме INADDR_ANY (состоит из нулевых битв) и TNADDR_NONE (состоит из единичных битов). Например, адрес многоадресной передачи класса D INADDR_MAX_LOCAL_GROUP определяется как OxeOOOOOff с комментарием «224.0.0.255», что явно указывает на порядок байтов узла.
Приведем новые строки, добавленные после вызова connet: len = sizeof(cliaddr).
Getsockname(sockfd, (SA *) Scliadd". &len) pnntf("local addr 総\en”
Sock_ntop((SA *) &cliaddr len))
Это требует описания переменной len как socklen_t, a cliaddr как структуры stuct sockaddr_in. Обратите внимание, что аргумент типа «значение-результат» для функции get sock name (len) должен быть до вызова функции инициализирован размером переменной, на которую указывает второй аргумент. Наиболее частая ошибка программирования при использовании аргументов типа «значение-результат» заключается в том, что про эту инициализацию забывают,
Когда дочерний процесс вызывает функцию close, счетчик ссылок уменьшается с 2 до 1, так что клиенту не посылается сегмент FIN. Позже, когда родительский процесс вызывает функцию close, счетчик ссылок уменьшается до нуля, и тогда сегмент FIN посылается.
Функция accept возвращает значение EINVAL, так как первый аргумент не является прослушиваемым сокетом.
Вызов функции 11 sten без вызова функции bi nd присваивает прослушиваемому сокету динамически назначаемый порт.
Глава 5
Длительность состояния TIME一WAIT должна находиться в интервале между 1 и4 минутами, что дает величину MSL от 30 секунд до 2 минут.
Наши клиент-серверные программы не работают с двоичными файлами. Допустим, что первые 3 байта в файле являются двоичной единицей (l)t двоичным нулем (0) и символом новой строки. При вызове функции fgets в листин*
ге 5.4 либо считывается MAXLINE-1 символов, либо считываются символы до символа новой строки или до конца файла. В данном примере функция считает три символа, а затем прервет строку нулевым байтом. Но вызов функции strlen в листинге 5.4 возвращает значение 1, так как она остановится на первом нулевом байте. Один байт посылается серверу, но сервер блокируется в своем вызове функции readl i ne, ожидая символа новой строки. Клиент блокируется, ожидая ответа от сервера. Такое состояние называется зависанием, или взаимной блокировкой: оба процесса блокированы и при этом каждый ждет от другого некоторого действия, которое никогда не произойдет. Проблема заключается в том, что функция fgets обозначает нулевым байтом конец возвращаемых ею данных, поэтому данные, которые она считывает, не должны содержать нулевой байт.
Программа Telnet преобразует входные строки в NVT ASCII (см. раздел 26.4 книги [94]), что означает прерывание каждой строки 2-символьной последовательностью CR (carriage return — возврат каретки) и LF (linefeed — новая строка). Наш клиент добавляет только разделитель строк (newline), который в действительности является символом новой строки (linefeed, LF). Тем не менее можно использовать клиент Telnet для связи с нашим сервером, поскольку наш сервер отражает каждый символ, включая CR, предшествующий каждому разделителю строк.
Нет, последние два сегмента из последовательности завершения соединения не посылаются. Когда клиент посылает серверу данные после уничтожения дочернего процесса сервера (ввод строки another li ne, см. раздел 5.12), сервер TCP отвечает сегментом RST. Сегмент RST прекращает соединение, а также предотвращает переход в состояние TIME_WAIT на стороне сервера (конец соединения, осуществивший активное закрытие).
$. Ничего не меняется, потому что процесс, запущенный на узле сервера, создает прослушиваемый сокет и ждет прибытия запросов на соединение. На третьем шаге мы посылаем сегмент данных, предназначенный для установленного соединения TCP (состояние ESTABLISHED). Наш сервер с прослушиваемым сокетом не увидит этот сегмент данных, и TCP сервера по-прежнему будет посылать клиенту сегмент RST.
В листинге Д. 11 приведена программа. Запуск этой программы в AIX генерирует следующий вывод:
aix % tsigpipe 206.62.226.34
SIGPIPE received
write error Broken pipe
Начальный вызов функции sleep и переход в режим ожидания на 2 секунды нужны, чтобы сервер времени и даты отправил ответ и закрыл свой конец соединения. Первая функция write отправляет сегмент данных серверу, который отвечает сегментом RST (поскольку сервер времени и даты полностью закрыл свой сокет). Обратите внимание, что наш TCP позволяет писать в сокет, получивший сегмент FIN. Второй вызов функции sleep позволяет полу-
Все исходные коды программ, опубликованные в этой книге, вы можете найти по гщресу http// www.piter.com/download.
чить от сервера сегмент RST, а во втором вызове функции write генерируется сигнал SIGPIPE. Поскольку наш обработчик сигналов возвращает управление, функция write возвращает ошибку EPIPE.
Листинг Д.1. Генерация SIGPIPE
//tcpcliserv/tsigpipe с
#include "unp h"
void
sig_pipe(int sign。)
{
printf("SIGPIPE received\enH);
return:
}
int
mdinUnt argc. char **argv)
{
mt sockfd,
struct sockaddr_in servaddr:
if (argc '= 2)
err_quit("usage tcpcli <IPaddress>M),
sockfd == Socket(AF_INET. SOCK_STREAM. 0):
bzero(&servaddr sizeof(servaddr)):
servaddr sin_family = AF_INET,
servaddr sw_port = htons(13). /* сервер времени и даты */
Inet_pton(AF_INET. argv[l], &servaddr sin_addr).
20Signal(SIGPIPE, sig_pipe).
21Connect(sockfd. (SA *) &servaddr, sizeof(servaddr)).
22 sleep(2).
23Write(sockfd "hello". 5);
24 sleep(2).
25 Wmte(sockfd. ''world". 5).
26 exit(0).
27 }
В предположении, что узел сервера поддерживает модель системы с гибкой привязкой (см. раздел 8.8), все будет работать, то есть узел сервера примет IP- дейтаграмму (которая в данном случае содержит TCP-сегмент), прибывшую на самый левый канал, даже если IP-адрес получателя является адресом самого правого канала. Это можно проверить, если запустить наш сервер на узле bsdi (см. рис. 1.7), а затем запустить клиент на узле Solaris, но на стороне клиента задать другой IP-адрес сервера (206.62.226.66). После установления соединения, запустив на стороне сервера программу netstat, мы увидим, что локальный IP-адрес является IP-адресом получателя из клиентского сегмента SYN, а не IP-адресом канала, на который прибыл сегмент SYN (как отмечалось в разделе 4.4).
Наш клиент был запущен в системе Intel с прямым порядком байтов, где 32-раз- рядное целое со значением 1 хранится так, как показано на рис. Д.1.
32-разрядное целое
У
00
00
00
01
Адрес: А+3 А+2 А+1 А
Рис. Д.1. Представление 32-разрядного целого числа 1 в формате прямого порядка байтов
4 байта посылаются на сокет в следующем порядке: А, А+7, А+2 и А+3, и там хранятся в формате обратного порядка байтов, как показано на рис. Д.2.
I 01 I 00 I 00 I 00 I
А А+1А+2 А+3
Рис. Д.2. Представление 32-разрядного целого числа с рис. Д.1 в формате обратного порядка байтов
Значение 0x01000000 интерпретируется как 16 777 216. Аналогично, целое число 2, отправленное клиентом, интерпретируется сервером как 0x02000000, или 33 554 432. Сумма этих двух целых чисел равна 50 331 648, или 0x03000000. Когда это значение, записанное в обратном порядке байтов, отправляется клиенту, оно интерпретируется клиентом как целое число 3.
Но 32-разрядное целое число -22 представляется в системе с прямым порядком байтов так, как показано на рис. Д.З (мы предполагаем, что используется поразрядное дополнение до двух для отрицательных чисел).
I ff I J еа J
А+3 А+2 А+1 А
Рис. Д.З. Представление 32-разрядного целого числа -22 в формате Гфямого порядка байтов
В системе с обратным порядком байтов это значение интерпретируется как Oxeaffffff, или -352 521 537. Аналогично, представление числа -77 в прямом порядке байтов выглядит как 0xffffffb3, но в системах с обратным порядком оно представляется как 0xb3ffffff, или -1 275 068 417. Сложение, выполняемое сервером, приводит к результату 0x9efffffe, или -1 627 389 954. Полученное значение в обратном порядке байтов посылается через сокет клиенту, где в прямом порядке байтов оно интерпретируется как 0xfeffff9e, или -16 777 314 — это то значение, которое выводится в нашем примере.
Метод правильный (преобразование двоичных значений в сетевой порядок байтов), но нельзя использовать функции htonl и ntohl. Хотя символ 1 в названиях данных функций обозначает «long», эти функции работают с 32-разряд- ными целыми (раздел 3.4). В 64-разрядных системах long занимает 64 бита, и эти две функции работают некорректно. Для решения этой проблемы еле-
дует определить две новые функции hton64 и ntoh64, но они ие будут работать в системах, представляющих значения типа long 32 битами.
В первом сценарии сервер будет навсегда блокирован при вызове функции readn в листинге 5.14, поскольку клиент посылает два 32-разрядпых значения, а сервер ждет два 64-разрядных значения. В случае, если клиент и сервер поменяются узлами, клиент будет посылать два 64-разрядных значения, а сервер считает только первые 64 бита, интерпретируя их как два 32-разрядных значения. Biopoe 64-разрядное значение останется в приемном буфере сокета сервера. Сервер отправит обратно 32-разрядное значение, и клиент навсегда заблокируется в вызове функции readn в листинге 5.13, поскольку будет ждать для считывания 64-разрядное значение.
И. Функция IP-маршрутизации просматривает IP-адрес получателя (IP-адрес сервера) и пытается по таблице маршрутизации определить исходящий интерфейс II следующий маршрутизатор (см. главу 9 [94]). В качестве адреса отправителя используется первичный IP-адрес исходящего интерфейса, если сокет еще не связан с локальным IP-адресом.
Глава 6
Массив целых чисел содержится внутри структуры, а язык С позволяет использовать со структурами оператор присваивания.
Если функция select сообщает, что сокет готов к записи, причем буфер отправки сокета вмещает 8192 байта, а мы вызываем для этого блокируемого сокета функцию write с буфером размером 8193 байта, то функция write можег заблокироваться, ожидая места для последнего байта. Операции считывания на блокируемом сокете будут возвращать сообщение о неполном считывании, если доступны какие-либо данные, но операции записи на блокируемом сокете заблокированы до принятия всех данных ядром. Поэтому, чтобы избежать блокирования при использовании функции sei ect для проверки на возможность записи, следует переводить сокет в неблокируемый режим.
Если оба дескриптора готовы для чтения, выполняется только первый тест — тест сокета. Но это не прерывает работу клиента, а только лишь уменьшает ее эффективность. Поэтому если при завершении функции sei ect оба дескриптора готовы для чтения, первое условие i f оказывается истинным, в результате чего сначала вызывается функция read] ine для считывания из сокета, а затем функция fputs для записи в стандарт пый поток вывода. Следующее условие if пропускаете父(поскольку мы добавили else), но функция seiect вызывается снова, сразу находит стандартное устройство ввода, готовое к чтению, и завершается. Суть в том, что условие готовности стандартного потока ввода для чтения сбрасывается считыванием из сокета, а не возвратом функции select.
Воспользуйтесь функцией getrl i mi t для получения значений константы RLIMIT_ NOFILE, а затем вызовите функцию setrlimit для установки текущего гибкого предела (rl im_cu「)равным жесткому пределу (rl im_max). Например, в Solaris 2.5 гибкий предел равен 64, но любой процесс может увеличить эго значение до используемого по умолчанию значения жесткого предела (1024).
Функции get「limit и setrlimit не входят в стандарт Posix.l, но требуются в Unix 98.
Серверное приложение непрерывно посылает данные клиенту, клиент TCP подтверждает их прием и отбрасывает.
Функция shutdown с аргументами SHUT_WR н SHUT_RDWR всегда посылает сегмент FIN, в то время как функция close посылает сегмент FIN, только если в момент вызова функции close счетчик ссылок дескриптора равен 1.
Функция read] i ne возвращает ошибку, и наша функция-обертка Read] i ne завершает работу сервера. Но серверы должны справляться с такими ситуациями. Обратите внимание на то, как мы обрабатываем эти условия в листинге 6.6, хотя даже этот код не является удовлетворительным. Рассмотрим, что произойдет, если соединение между клиентом и сервером прервется и время ожидания одного из ответов сервера будет превышено. Возвращаемой ошибкой может быть ошибка ETIMEDOUT.
Обычно сервер не должен прекращать свою работу из-за подобных ошибок. Он должен записать ее в файл журнала, закрыть сокет и продолжать обслуживание других клиентов. Следует понимать, что обработка таких ошибок путем прекращения работы сервера недопустима для серверов, у которых один процесс выполняет обработку всех клиентов. Но если сервер был дочерним процессом, обрабатывающим только один клиент, то прекращение работы одного дочернего процесса не отразится ни на родительском процессе (который, по нашему предположению, обрабатывает все новые соединения и порождает новые дочерние процессы), ни на одном из других дочерних процессов, обрабатывающих другие клиенты.
Глава 7
Решение упражнения приведено в листинге Д.2. Вывод строки данных, возвращаемых сервером, был удален, поскольку это значение нам не нужно.
Листинг Д.2. Вывод размера приемного буфера сокета и MSS до и после установления соединения
//sockopt/rcvbuf с
#inc1ude "unp h"
include <netinet/tcp h> /* для TCP_MAXSEG */
int
main(int argc. char **argv)
{
mt sockfd.rcvbuf.mss,
socklen.t len.
8struct sockaddr_inservaddr,
if (argc != 2)
err_quit( "usage rcvbuf <IPaddress>^^).
sockfd = Socket(AF_INET. SOCK_STREAM. 0):
len = sizeof(rcvbuf);
Getsockopt(sockfd. SOL_SOCKET. SO_RCVBUF. &rcvbuf. &len).
len = sizeof(mss).
Getsockopt(sockfd. IPPROTO_TCP. TCP_MAXSEG. &mss, &len).
printf("defaults. SO_RCVBUF =紀.MSS =紀\en”. rcvbuf, mss);
bzero(&servaddr. sizeof(servaddr)).
servaddr sm_famly = AF_INET.
servaddr sin_port = htons(13). /* сервер времени и даты */
Inet_pton(AF_INET. argv[l]. &servaddr sin_addr).
Connect(sockfd. (SA *) &servaddr. sizeof(servaddr))
len = sizeof(rcvbuf).
GetsockoptCsockfd. S0L_S0CKET. S0_RCVBUF. &rcvbuf. &len).
len = sizeof(niss).
Getsockopt(sockfd. IPPROTO_TCP. TCP_MAXSEG &mss. &len).
pnntf("after connect S0_RCVBUF = id MSS = %d\en”. rcvbuf. mss);
exit(O).
}
He существует какого-то одного «правильного» вывода для данной программы. Результаты зависят от системы. Некоторые системы (в особенности Solaris 2.5.1 и более ранние версии) всегда возвращают нулевой размер буфера сокета, не давая нам возможности увидеть, что происходит с этим значением в процессе соединения.
До вызова функции connect выводится значение MSS по умолчанию (часто 536 или 512), а значение, выводимое после вызова функции connect, зависит от возможных параметров MSS, полученных от собеседника. Например, в локальной сети Ethernet после выполнения функции connect MSS может иметь значение 1460. Однако после соединения (connect) с сервером в удаленной сети значение MSS может быть равно значению по умолчанию, если только ваша система не поддерживает обнаружение транспортной MTU. Если это возможно, запустите во время работы вашей программы программу tcpdump или подобную ей (см. раздел В.5), чтобы увидеть фактическое значение параметра MSS в сегменте SYN, полученном от собеседника.
Многие реализации после установления соединения округляют размер приемного буфера сокета в большую сторону, чтобы он был кратным MSS. Чтобы узнать размер приемного буфера сокета после установления соединения, можно исследовать пакеты с помощью программы типа tepdump и посмотреть, каков размер объявленного окна TCP.
Разместите в памяти структуру ] i nger по имени 1 mg и проинициализируйте ее следующим образом:
str_cli(stdin, sockfd).
ling l_onoff = 1.
ling l_hnger = 0,
SetsockopKsockfd, SOL_SOCKET. SO_LINGER, &11ng. sizeof(ling)):
exit(0).
Это заставит TCP на стороне клиента прекратить работу путем отправки сегмента RST вместо нормального обмена четырьмя сегментами. Дочерний процесс сервера вызывает функцию read] i ne, возвращает ошибку ECONNRESET и выводит следующее сообщение:
readline error Connection reset by peer
Клиентский сокет не должен проходить через состояние ожидания TIME_WAIT} даже если клиент выполняет активное закрытие.
Первый клиент вызывает функции setsockopt, bind и connect. Но если второй клиент вызовет функцию bi nd между вызовами функций bi nd и connect первого клиента, возвращается ошибка EADDRINUSE. Но как только первый клиент установит соединение с собеседником, вызов функции bi nd второго клиента будет работать, поскольку сокет первого клиента уже присоединен. В случае возвращения ошибки EADDRINUSE второму клиенту следует вызывать bind несколько раз, а не останавливаться при появлении первоп ошибки — это единственный способ справиться с данной ситуацией. (Подобная ситуация гонок описана в стандарте Posix.lg )
Запускаем программу на узле без поддержки многоадресной передачи (UnixWare 2.12).“
umxware 弘 sock -s 9999 & запускаем первый сервер с универсзльным адресом
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
00 127 0 0 1 9999
* *
LISTEN
tcp
00 206 62 226 37 9999
* *
LISTEN
tcp
00 * 9999
★ *
LISTEN
6 Сначала пробуем на узле без поддержки многоадресной передачи (UnixWare 2.1.2).
umxware Z 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 al ready in use не мояем запустить второй
В обоих случаях мы задаем параметр SO REUSEADDR, но это не действует
Теперь попробуем проделать то же на узле с поддержкой многоадресной передачи, но без поддержки параметра SO_REUSEADDR (Solaris 2.6).
solans26 Z sock -s -u 8888 & запускаем первый
1135
solans26 % sock -s -u 8888
can't bind local address Address already in use
solans26 % sock -s -u -A 8888 & снова пробуем запустить второй с -А; работает
sol а门$26 % netstat -па | grep 8888 дубпмрованное связывание
* 8В88Idle
* 8888Idle
В этой системе задавать параметр SO REUSEADDR было необходимо только для второго связывания.
Наконец, запускаем сценарий в BSD/OS 3 0, где поддерживается как многоадресная передача, так и параметр SO_REUSEPORT Сначала пробуем использовать SO_REUSEADDR для обоих серверов, но это не работает
bsdi % sock -u -s -A 7777 &
17610
bsdi X sock -u -s -A 7777
can't bind local address Address already in use
Тогда пробуем использовать параметр SO_REUSEPORT только для второго сервера. Это также не работает, так как полностью дублированное связывание требует включения данного параметра для всех сокетов, совместно использующих соединение.
bsdi 弘 sock -u -s 8888 &
17612
bsdi % sock -u -s -T 8888
can t bind local address Address already in use
Наконец, задаем параметр SO_REUSEPORT для обоих серверов, и этот вариант работает.
bsdi % sock -u -s -T 9999 &
17614
bsdi % sock -u -s T 9999 &
⑵17615
bsdi % netstat -na | grep 9999
udp00 * 9999
udp00 * 9999
Этот параметр ( d) не делает ничего, поскольку программа Ping использует ICMP-сокет, а параметр сокета SO_DEBUG влияет только на TCP-сокеты. Описание параметра сокета SO_DEBUG всегда было довольно расплывчатым, наподобие «этот параметр допускает отладку на соответствующсм уровне протокола», и единственный уровень протокола, где реализуется данный параметр,— это TCP.
Временная диаграмма приведена на рис Д.4.
1/2rttОтложенный сегмент АСКRTTОбработка на сервере1/2rttОтложенный сегмент АСКRTTОбработка на сервере
1/2rtt
Отложенный сегмент АСК
RTT
Обработка на сервере
1/2rtt
Отложенный сегмент АСК
RTT
Обработка на сервере
Рис. Д.4. Взаимодействие алгоритма Нагла с задержанными сегментами АСК
Установка параметра сокета TCP_NODELAY приводит к немедленной отправке данных из второй функции wri te, даже если имеется еще один небольшой пакет,
ожидающий отправки. Это показано на рис. Д.5. Полное время в данном примере превышает 150 миллисекунд.
Обработка на сервереОбработка на сервере
Обработка на сервере
Обработка на сервере
Рис. Д.5. Предотвращение алгоритма Нагла путем установки параметра TCP—NODELAY
Как показано на рис. Д.6, преимущество данного решения состоит в уменьшении числа пакетов.
Рис. Д.6. Использование функции writev вместо параметра сокета TCP_NODELAY
В разделе 4.2.3.2 говорится, что «задержка ДОЛЖНА быть меньше 0,5 секунды, а в потоке полноразмерных сегментов СЛЕДУЕТ использовать сегмент АСК по крайней мере для каждого второго сегмента». Беркли-реализации задерживают сегмент АСК более чем на 200 миллисекунд [105, с. 821].
Родительский процесс сервера в листинге 5.1 большую часть времени блокирован в вызове функции accept, а дочерний процесс в листинге 5.2 большую часть времени блокирован в вызове функции read, который содержится в функции read] 1 ne. Проверка работоспособности с помощью параметра SO_KEEPALIVE не влияет на прослушиваемый сокет, поэтому в случае, если клиентский узел выйдет из строя, родительский процесс не пострадает. Функция read дочернего процесса возвратит ошибку ETIMEDOUT примерно через 2 часа после последнего обмена данными через соединение.
Клиент, приведенный в листинге 5.4, большую часть времени блокирован вызовом функции fgets, который, в свою очередь, блокирован операцией чтения из стандартной библиотеки ввода-вывода на стандартном устройстве ввода. Когда примерно через 2 часа после последнего обмена данными через соеди-
нение истечет время таймера проверки работоспособности и проверочные сообщения не выявят работоспособности сервера, ошибка сокета, ожидающая обработки, примет значение ETIMEDOUT. Но клиент блокирован вызовом функции fgets, поэтому он не увидит этой ошибки, пока не осуществит чтение или запись на сокете. Это одна из причин, по которой в главе 6 листинг 5.4 был изменен таким образом, чтобы использовать функцию select.
Этот клиент большую часть времени блокирован вызовом функции sei ect, которая сообщит, что сокет гогов для чтения, как только ожидающая обработки ошибка будет установлена в ETIMEDOUT (как показано в предыдущем решении).
Только двумя сегментами, а не четырьмя. Вероятность того, что таймеры двух систем будут строго синхронизированы, очень мала, следовательно, на одном конце соединения таймер проверки работоспособности сработает немного раньше, чем на другом. Первый из сработавших таймеров посылает проверочное сообщение, заставляя другой конец послать в ответ сегмент АСК. Но получение проверочного сообщения приводит к тому, что таймеру проверки работоспособности с более медленными часами будет присвоено новое значение —он сдвинется на 2 часа вперед.
Изначально в API сокетов не было функции 11 sten Вместо этого четвертый аргумент функции socket содержал параметр сокета, а параметр SD_ACCEPTCON использовался для задания прослушиваемого сокета. Когда добавилась функция 11 sten, флаг остался, по теперь его может устанавливать только ядро [105, с. 456].
Глава 8
Да Функция read возвращает 4096 байт данных, а функция recvfrom возвращает 2048 байт (первую из двух дейтаграмм) Функция recvfrom на сокете дейтаграмм никогда не возвращает больше одной дейтаграммы, независимо от того, сколько приложение запрашивает.
Если протокол использует структуры адреса сокета переменной длины, clilen может быть слишком длинным В главе 14 будет показано, что это не вызывает проблем со структурами адреса доменного сокета Unix, но корректным решением будет использовать для функции sendto фактическую длину, возвращаемую функцией recvfrom.
Запуск программы pi ng с такими параметрами позволяет просмотреть ICMP- сообщения, получаемые узлом, на котором она запущена. Мы используем уменьшенное количество отправляемых пакетов вместо обычного значения 1 пакет в секунду, только чтобы уменьшить объем выводимой на экран информации. Если запустить наш UDP-клиент на узле solans, указав IP-адрес сервера 206.62.226.42, а затем запустить программу ping, получим следующий вывод:
sola门s % 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)
for udp from solans kohala com (206 62 226 33) to alpha kohala com (206 62 226 42) port 9877
Прослушиваемый сокет может иметь приемный буфер определенного размера, но прослушиваемым ТCP-сокетом данные никогда не принимаются. Большинство реализаций не выделяют заранее память под буферы отправки и приема. Размеры буферов сокета, определяемые параметрами SO_SNDBUF и SD_RCVBUF, являются предельными значениями для cooi ветствующего сокета.
Запустим программу 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-ошибку недоступности порта. Наблюдая за сетью с помощью программы tcpdurnp, мы увидим, что 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
Использование функции pri ntf на стороне клиента приведет к возникновению задержки между отправками дейтаграмм, что позволит серверу получать большее количество дейтаграмм. Использование функции р门 ntf на стороне сервера приведет к тому, что сервер будет терять большее количество дейтаграмм.
Наибольший размер I Pv4-дейтаграммы составляет 65 535 байт и ограничивается 16-разрядным полем полной длины, показанным на рис. А.1.1Р-заголо- вок*требует 20 байт, UDP-заголовок 一 8 байт, и для пользовательских данных остается не более 65 507 байт. В IPv6 (без поддержки джумбограмм) размер IP-заголовка составляет 40 байт, и под пользовательские данные отводится 65 487 байт.
В листинге Д.З приведена новая версия dg_cl i. Если забыть установить размер буфера отправки, Беркли-ядра возвратят из функции sendto ошибку EMSGSIZE, поскольку размер буфера отправки сокета обычно меньше, чем максимально возможный размер UDР-дейтаграммы (чтобы убедиться в этом, выполните упражнение 7.1).
Листинг Д.З. Запись дейтаграммы UDP/IPv4 максимального размера //udpcliserv/dgclibig с
#include ' ипр h‘
#undef MAXLINE
#define MAXLINE 65507
void
dg_cli(FILE *fp int sockfd const SA *pservaddr socklen_t servlen)
{
int size
char send!ine[MAXLINE], recvline[MAXLINE + 1].
ssizjt n
size = 70000
SetsockoptCsockfd S0L_S0CKET S0_SNDBUF &size sizeof(size));
SetsockoptCsockfd S0L_S0CKET SD_RCVBUF &size sizeof(size)):
Sendto(sockfd sendline MAXLINE 0 pservaddr servlen)
n = Recvfrom(sockfd recvline MAXLINE 0 NULL NULL).
pnntf(Mreceived Zd bytes\en" n)
}
Но если установить размеры буферов сокета клиента, как показано в листинге Д 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
В листинге Д.4 приведена программа, вызывающая функцию gethostbyaddr.
Листинг Д.4. Изменение листинга 9.1 для вызова функции gethostbyaddr //names/hostent2 с
#include 'unp h
int
main(int argc char **argv)
{
char *ptr **pptr
char str[INET6_ADDRSTRLEN]
struct hostent *hptr
продолжение 少продолжение 少8 while (--argc > 0) {
продолжение 少
продолжение 少
Листинг Д.4 (продолжение)
ptr = *++argv
if ( (hptr ^ gethostbyname(ptr)) == NULL) {
err_msg(' gethostbyname error for host Zs Zs'
ptr hstrerror(h_errno))
continue
14}
15printf("official hostname Zs\erThptr->h_name)
16for (pptr = hptr->h_aliases *pptr«= NULL pptr++)
17p门ntf(' alias %s\en' *pptr)
18switch (hptr >h_addrtype){
19case AF_INET -
20 #ifdef AFJNET6
21case AF_INET6
22 #endif ~
23 pptr = hptr->h_addr_1ist
24 for ( *pptr '= NULL pptr++) {
25pnntf( \etaddress ^s\en'
26Inet_ntop(hptr->h_addrtype *pptr str, sizeof(str)));
27if ( (hptr = gethostbyaddr(*pptr hptr->h_length
28ptr->h_a dd rtype)) == NULL)
29pnntfC \et(gethostbyaddr failed)\en')
30else if (hptr->h_name •= NULL)
31p门ntf('\etname = Zs\en" hptr->h_name)
32e!se
33pnntf("\et(no hostname returned by get host by add r 八 en”)
34}
35break
36default
37err_ret( 'unknown address type").
38break
39}
40}
41 exit(O)
42 }
Эта программа корректно работает на узле с единственным IP-адресом. Если запустить программу из листинга 9.1 на узле с четырьмя IP-адресами, то получим*
solans 芯 hostent gemin】 tuc.noao.edu
official hostname gemim tuc noao edu
address140252854
address140252454
address140252354
address140252111
Но если запустить программу из листинга Д.4 на том же узле, в выводе будет только первый IP-адрес:
solans % hostent2 gemim.tuc noao edu
official hostname gemim tuc noao edu
address 140 252 8 54
name = gemim tuc noao edu
Проблема заключается в том, что две функции, gethostbyname и gethostbyaddr, совместно используют одну и ту же структуру hostent, как было показано в раз-
деле 11.14 Когда наша новая программа вызывает функцию gethostbyaddr, она повторно использует данную структуру вместе с областью памяти, на которую структура указывает (массив указателей h_addr_l 1st), стирая три оставшиеся 1Р~адреса, возвращаемые функцией gethostbyname.
Если ваша система не поддерживает повторно входимую версию функции gethostbyaddr (см. раздел И.15), то прежде чем вызывать функцию gethostbyaddr, вам следует создать копию массива указателей, возвращаемых функцией gethostbyname, и данных, на которые указывает этот массив.
Функция my_addr приведена в листинге Д.5, а функция mai п — в листинге Д.6.
Листинг Д.5. Версия листинга 9.3, вызывающая функцию gethostname
//names/туaddг1 с
#wclude'unp h'
#i nclude<sys/param h>
char **
my_9ddrs(int *addrtype)
5「
struct hostent *hptr,
char myname[MAXHDSTNAMELEN]
if (gethostname(myname sizeof(myname)) < 0)
return (NULL)
if ( (hptr = gethostbyname(myname)) == NULL)
return (NULL)
*addrtype = hptr->h_addrtype
return (hptr->h addr_list)
}——
Если функция gethostbyname возвращает структуру hostent, определяющую один или более адресов IPv6, значение h_length будет равно 16. Поэтому струкгура sock add r_in будет переполнена, и часть данных запишется поверх того, что было расположено в памяти далее за эюп структурой Данный параметр распознавателя следует устанавливать только в том случае, если программа готова работать с адресами IPv6, а наша программа для этого не предназначена. Этот пример также объясняет, почему длина аргумента 印ешсру должна соответствовать размеру получателя (в данном примере sizeof(struct in_addr)), а не размеру отправителя (hp т h_length), даже если оба эти размера одинаковы.
. Сервер chargen отправляет клиенту данные до тех пор, пока клиент не закрывает соединение (то есть пока вы не завершите выполнение клиента).
Листинг Д.6. Тестовая программа для листингов 9 3 и Д.5 //names/prmyaddrsl с
#incl ude'unp h'
char **my_add「s(int *)
int
main(int argc. char **argv)
{продолжение 少
Листинг Д.6 (продолжение)
mt addrtype.
char **pptr. buf[INET6_ADDRSTRLEN],
if ( (pptr = my_addrs(&addrtype)) == NULL)
err_quit("rny_addrs error"),
for ( *pptr ,= NULL. pptr++)
IIprintf("\etaddress ^s\en",
Inet_ntop(addrtype. *pptr. buf. sizeof(buf))>:
exit(O).
}
Как упоминалось в связи с рис. 9.3, это особенность новейших версий BIND. В листинге Д.7 приведена измененная версия. Порядок тестирования строки с именем узла имеет значение. Сначала мы вызываем функцию inet_pton, поскольку она обеспечивает быстрый тест «внутри памяти» (in-memory) для проверки, является ли строка допустимым IP-адресом в точечно-десятичной записи. Только если тест заканчивается неудачно, мы запускаем функцию gethostbyname, которая обычно требует некоторых сетевых ресурсов и времени. Если строка является допустимым IP-адресом в точечно-дссятнчной записи, мы создаем свой массив указателей (addrs) на один IP-адрес, оставив без изменений цикл, использующий pptr.
Поскольку адрес уже был переведен в двоичное представление в структуре адреса сокета, мы заменяем вызов функции memepy в листинге 9.4 на вызов функции Tnemmove, так как при вводе IP-адреса в точечно-десятичной записи исходное и конечное поля в данном вызове одинаковые. Случай перекрывания полей при работе с функциями memepy и memmove рассматривается в упражнении 30.3.
Листинг Д.7. Допускаем как использование IP-адреса в точечно-десятичной записи, так и задание имени узла, номера порта или имени службы
//names/daytimetcpcl12 с
include "unp h"
int
main(int argc. char **argv)
{
intsockfd. n.
charrecvline[MAXLINE + 1].
structsockaddr_in servaddr.
structin_addr**pptr.*addrs[2];
structhostent*hp.
structservent*sp.
if (argc i= 3)
err_quit("usage daytimetcpcli2 <hostname> 〈service〉").
bzero(&servaddr. sizeof(servaddr)).
14servaddr sin_family = AF_INET.
15 if (inet_pton(AF_INET. argv[lj. &servaddr sin_eddr) == 1) {
16 addrs[O] = &servaddr sin_dddr.
17addrs[l] = NULL.
18pptr = &addrs[0],
19} else if ( (hp = gethostbyname(argv[l])) != NULL) {
20pptr = (struct ?n_addr★*) hp->h_addrjist,
21} else--
22err_quit("hostname error for Zs Xs", argv[l], hstrerror(h_errno));
23 if ( (n = atoi(argv[2])) > 0)
24servaddr sin_port = htons(n).
25 else if ( (sp = getservbyname(argv[2], "tcp")) l= NULL)
26servaddr sin_po「t = sp->s_port.
27 e!se
28err_quit("getservbyname error for%s". argv[2]);
29 for ( *pptr i= NULL. pptr++) {
30sockfd = Socket(AF_INET. SOCK_STREAM. 0).
31memmove(&servaddr sin_addr. *pptr. sizeof(struct in_addr));
32prmtf("trying 狂\en":
33Sock_ntop((SA *) &servaddr, sizeof(servaddr))).
34 ' if (connect(sockfd. (SA *) &servaddr. sizeof(servaddr)) == 0)
35 . break./* успех */
36 ' err_ret("connect error").
37 c1ose(sockfd),
38}
39 if (*pptr == NULL)
40 err_quit("unable to connect").
41 while ( (n = Read(sockfd. recvline MAXLIME)) > 0) {
42 recvline[n] = 0./* завершающий нуль */
43 Fputs(recvline. stdout),
44}
45ex?t(0).
46 }
Программа приведена в листинге Д.8.
Листинг Д.8. Изменение листинга 9.4 для работы с IPv4 и IPv6
//names/daytimetcpd12 с
1 #mclude "unp h"
23456 7 8910112323456 7 89101123int
23456 7 89101123
23456 7 89101123
main(int argc. char **argv)
{
intsockfd, n,
charrecv1ine[MAXLINE + 1].
struct sockaddr_m servaddr.
struct sockaddr_in6 servaddr6; struct sockaddr~*sa.
sock1en_t salen, struct 7n_addr **pptr. struct hostent *hp. struct servent *sp.
if (argc 3)
15 err_qint("usage daytimetcpcli3 <hostname> <service>");
16 if ( (hp = gethostbyname(argv[l])) == NULL)
Листинг Д.8 (продолжение)
err^quitC'hostname error for Zs Zs”. argv[l], hstrerror(h_errno)):
if ( Tsp = getservbyname(argv[2]. wtcp,f)) == NULL)
err_quit("getservbyname error for Zs". argv[2]).
pptr = (struct in_addr **) hp->h_addr_1ist
for (. *pptr i= NULL, pptr++) { 一 ~
sockfd = Socket(hp->h_addrtype, SOCK STREAM. 0).
if (hp->h_addrtype == AF」NET) {
sa = (SA *) &servaddr.
salen = sizeof(servaddr),
} else if (hp->h_addrtype == AF_INET6) {
sa = (SA *) &servaddr6.
salen = sizeof(servaddr6).
} else
err_quit("unknown addrtype ^d".hp->h_addrtype):
bzero(sa. salen).
sa->sa_family = hp->h_addrtype.
sock_set_port(sa. salen. sp->s_port).
sock_set_addr(sa. salen *pptr7.
pnntf("trying ^s\en" Sock_ntop(sa. salen)),
if (connect(sockfd. sa salen) == 0)
break,/* успех */
err_ret("connect error").
closelsockfd).
}
if (*pptr == NULL)
err_quit("unable toconnect").
while ( (n = Read(sockfd recvline, MAXLINE)) > 0) {
recvline[n] = 0./*завершающийнуль*/
Fputs(recvline stdout).
}
exit(0)
}
Используем значение h_addrtype, возвращаемое функцией gethostbyname, для определения типа адреса. Также используем функции sock_set_port и sock_ set_add「(см. раздел 3.8), чтобы установить два соответствующих поля в структуре адреса сокета.
Эта программа работает, однако имеется два ограничения. Во-первых, мы должны обрабатывать все различия, следя за h addrtype и задавая соответствующим образом sa или salen. Более удачным решением было бы иметь библиотечную функцию, которая не только просматривает имя узла и имя службы, но и заполняет всю структуру адреса сокета (например, getaddrinfo, см. раздел 11.2). Во-вторых, эта программа компилируется только на узлах с поддержкой IPv6. Чтобы ее можно было откомпилировать на узле, поддерживающем только IPv4, следует добавить в код огромное количество директив #i fdef, что, несомненно, усложнит программу.
К вопросу независимости от протоколов мы вернемся в главе 11 и рассмотрим более совершенные способы обеспечения этой независимости.
Глава 10
Ниже приведен вывод, в котором некоторые фрагменты опущены (например, процедура входа и содержимое каталогов).
solans Z 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 /bfhHs.
solans Z ftp sunos
Connected to sunos5 kohala com
220 sunosb 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 e3e3.3172) (0 bytes)
Глава 11
Разместите в памяти большой буфер (превышающий по размеру любую структуру адреса сокета) и вызовите функцию getsockname. Третий аргумент является аргументом типа «значение-результат», возвращающим фактический размер адресов протоколов. К сожалению, это допускают только структуры адреса сокета с фиксированной длиной (IPv4 и IPv6). Нет гарантии, что это будет работать с протоколами, которые могут вернуть структуру адреса сокета переменной длины (доменные сокеты Unix, см. главу 14).
Сначала размещаем в памяти массивы, содержащие имя узла и имя службы: char host[NI_MAXHOST]. serv[NI_MAXSERV].
После того как функция accept возвращает управление, вызываем вместо функции sock_ntop функцию getnamei nfo:
if (getnameinfo(cliaddr. len. host. NI_MAXHOST serv. NI_MAXSERV.
NI_NUMERICHOST | NI_NUMERICSERV) == 0)—
printf("connection from Ms 反s\en" nost. serv).
Поскольку это сервер, определяем флаги NI_NUMERICHDST и NI_NUMERICSERV, чтобы избежать поиска в DNS и /etc/services.
Первая проблема состоит в том, что второй сервер пе может связаться (bi nd) с тем же портом, что и первый сервер, поскольку не установлен параметр сокета SO REUSEADDR. Простейший способ справиться с такой ситуацией состоит в том, чтобы создать копию функции udp_server, переименовать ее в udp_server_ reuseaddr, сделать так, чтобы она установила параметр сокета, и вызывать ее в сервере.
Если бы переменная aipnext была глобальной, функция getaddnnfo не была бы безопасной в многопоточной среде.
Да,содержит. Обратившисьпоссылкеftp://ftp.isi.edu/in-notes/iana/assignments/ port-numbers, можно увидеть, что имена служб cl/1 и 914с/д содержат косую черту.
Когда клиент выводит Trying 206 62 226 35 , функция gethostname возвращает IP-адрес. Пауза перед этим выводом означает, что распознаватель ищет имя узла. Вывод Connected to bsdi kohala com означает, что функция connect возвратила управление. Пауза между этими двумя выводами говорит о том, что функция connect пытается установить соединение.
Глава 12
Закрытие дескрипторов в daemon_imt функцией close приводит к закрытию прослушиваемого TCP-сокета, созданного с помощью функции tcp_listen. Поскольку программа, написанная как демон, может быть запущена из некоторого системного сценария, запускаемого при загрузке, не следует ожидать, что будет выведено какое-либо сообщение об ошибке. Все сообщения об ошибках, даже ошибка загрузки, такая как неправильный аргумент командной строки, должны сохраняться в файлах журнала с помощью функции syslog.
TCP-версии серверов echo, discard и chargen запускаются как дочерние процессы, после того как демон inetd вызовет функцию fork, поскольку эти три сервера работают, пока клиент не прервет соединение. Два других ТСР-серве- ра, time и daytime, не требуют использования функции fork, поскольку эти службы легко реализовать (получить текущую дату, преобразовать ее, записать и закрыть соединение). Эти два сервера обрабатываются непосредственно демоном 1 netd. Все пять UDP-служб обрабатываются без использования функции fork, поскольку каждая из них генерирует единственную дейтаграмму в ответ на клиентскую дейтаграмму, которая запускает эту службу. Эти пять служб обрабатываются напрямую демоном i netd.
Это известная атака типа «отказ в обслуживании» [17]. Первая дейтаграмма с порта 7 заставляет сервер chargen отправить дейтаграмму обратно на порт 7. На эту дейтаграмму приходит эхо-ответ, и серверу chargen посылается другая дейтаграмма. Происходит зацикливание. Одним из решений, реализованным в системе BSD/OS, является игнорирование дейтаграмм, направленных любому внутреннему серверу, если номер порта отправителя пришедшей дейтаграммы принадлежит одному из внутренних серверов. Другим решением может быть запрещение этих внутренних служб — либо с помощью демона 1 netd на каждом узле, либо на маршрутизаторе, связывающем внутреннюю сеть
IP-адрес и номер порта клиента могут быть получены из структуры адреса сокета, заполняемой функцией accept.
Причина, по которой демон inetd не делает этого для UDP-сокета, сосюит в том, что чтение дейтаграмм (recvfrorn) осуществляется с помощью функции ехес сервером, а не самим демоном inetd.
Демон inetd может считать дейтаграмму с флагом MSG.PEEK (см. раздел 13.7), только чтобы получить IP-адрес и номер порта клиента, но оставляет саму дейтаграмму для чтения серверу.
Глава 13
Если не установлен обработчик, первый вызов функции signal будет возвращать значение SIG DFL, а вызов функции signal для восстановления обработчика просто вернет его в исходное состояние.
Приведем цикл for: for ( .) {
if ( (п к Recv(sockfd. recvline MAXLINE, MSG_PEEK)) •- 0) break /* сервер закрыл соединение */
Ioctl(sockfd FIONREAD &npend)
p门ntf(”%d bytes from PEEK, Zd bytes pending'en". n. лpend):
n = Read(sockfd, recvlme, MAXLINE)
recvline[n] = 0/* завершающий нуль */
Fputs(recvline stdout), }
Данные продолжают выводиться, поскольку выход из функции main ~ это то же самое, что и возврат из этой функции. Функция 印a in вызывается программой запуска на языке С следующим образом: exit(main(argc argv))
Следовательно, вызывается функция exit, а затем и программа очистки стандартного ввода-вывода.
Глава 14
Функция unlink удаляет имя файла из файловой системы, и когда клиент позже вызывает функцию connect, она не выполнится. Это не влияет на прослушиваемый сокет сервера, но клиенты не смогут выполнить функции connect после вызова функции unlink.
Клиент не сможет соединиться с сервером с помощью функции connect, даже если полное имя существует, поскольку для успешного соединения с помощью функции connect доменный сокет Unix должен быть открыт и связан с этим полным именем (см. раздел 14.4).
При выводе адреса протокола клиента путем вызова функции sock_ntop мы получим сообщение datagram from (no pathname bound) (дейтаграмма от (имя не задано)), поскольку по умолчанию с сокетом клиента не связывается никакое имя.
Одним из решений является проверить доменный сокет Unix в функциях udp_cl 1 ent и udp_connect и связать с сокетом при помощи функции bi nd временное полное имя. Это приведет к зависимоеги от протокола в библиотечной функции, но не в нашем приложении.
Даже если мы заставим сервер вернуть в функции write 1 байт па его 26-байтовый ответ, использование функции sleep на стороне клиента гарантирует, что все 26 сегментов будут получены до вызова функции read, в результате чего функция read вернет полный ответ. Это еще одно подтверждение тому, что TCP является потоком байтов с отсутствием границ записи.
Чтобы использовать доменные протоколы Unix, запускаем клиент и сервер с двумя аргументами командной строки /local (или /ит х) и /trnp/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 мы использовали reevmsg, флаг MSG_EOR возвращался бы в поле msg_f 1 ags каждый раз, когда reevmsg возвращала бы 1 байт. Такой подход в TCP не срабатывает, поскольку отправляющий ТСР не анализирует флаг MSG EOR в отсылаемом mbuf и в любом случае у нас нет возможности передать этот флаг принимающему TCP в TCP-заголовке. (Выражаем благодарность Мату Томасу (Matt Thomas) за то, что он указал нам это недокументированное «средство».)
В листинге Д.9 приведена реализация данной программы. Версия XTI приведена в листинге Д.14.
Листинг Д.9. Определение фактического количества собранных в очередь соединений для различных значений аргумента backlog
//debug//backlog с 1 #include"unp h'
#define PORT 9999
#define ADDR ”127 0 0 1”
#define MAXBACKLOG 100
/* глобальные переменные */
struct sockaddr_in serv.
p】d_t pid./* ди^рпмй* проце^сй */
intpipefd[2],
#definepfd pipefd[l]/*сокетродительскогоnpouecca */
#definecfd pipefd[0]/*сокетдочернегопроцесса*/
/* прототипы функций */
voiddo_parent(void).
voiddo_child(vo7d),
14int
15 main(int argc, char **argv)
16{
17 if (argc ,= 1)
18 err_quit("usage backlog").
19Socketpair(AF_UNIX. SOCK_STREAM, 0, pipefd):
20 bzero(&serv. sizeof(serv)).
21serv sin_fannly = AF_INET.
22serv sin_port = htons(PORT).
23 Inetj}ton(AF_INET, ADDR. &serv s】n_dddr).
24 if ( (pid = ForkO) == 0)
25do_child()
26 else'
27do_parent()
28 exit(O).
29 }
30 void
31 parent alrm(int signo)
32 {~
33 return /* прерывание блокированной функции connect() */
34 }
35 void
36 do_parent(void)
37 { ~
38 int backlog, j, k. junk. fd[MAXBACKLOG + 1].
39 Close(cfd).
40 Signal (SIGALRM parental rm).
41 for (backlog = 0. backlog <= 14, backlog++) {
42 pnntf("backlog = %d “ backlog)
43Write(pfd, ^backlog, sizeof(wt)). /* сообщение значения дочернему процессу*/
44Read(pfd &junk, sizeof(int))./* ожидание дочернего процесса*/
45 for (j = 1 j <= MAXBACKLOG J++) {
46fd[j] = Socket(AF_INET SOCK.STREAM. 0)
47alarm(2).
48 if (connect(fd[j] (SA *) &serv sizeof(serv)) < 0) {
49if (errno *= EINTR)
50err_sys("connect error, j ^ %d”. j).
51print?("timeout. %d connections completedVen*', j - 1);
52for (k = 1 k <= j. k++)
53Close(fd[k])
продолжение 我продолжение 我54break,/* следующее значение backlog *1
продолжение 我
продолжение 我
Листинг Д.9 (продолжение)
55}
56alarm(O).
57}
58if (j > MAXBACKLDG)
59printfC^dconnections?\enn. 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 ReadCcfd, &backlog. sizeof(mt)). /* ожидание родите/тьского процесса */
71 while (backlog >= 0) {
72 listenfd = Socket(AF_INET, SOCK_STREAM. 0).
73 Setsockopt(1istenfd, S0L_S0CKET SO_REUSEADDR. &on sizeof(on)).
74 Bind(listenfd. (SA ★) &serv. sizeofTserv)).
75 Listen(listenfd. backlog) /* начало прослушивания */
76Write(cfd &junk sizeof(int))./* сообщение родительскому прцессу*/
77Read(cfd, &backlog. sizeof(int)),/* ожидание родительского процесса*/
78 Closed!stenfd)./* также закрывает все соединения в очереди */
79 }
80 }
Глава 15
Дескриптор используется совместно родительским и дочерним процессами, поэтому его счетчик ссылок равен 2. Если родительский процесс вызывает функцию close, счетчик ссылок уменьшается с 2 до 1, и пока он больше нуля, сегмент FIN не посылается. Еще одна цель вызова функции shutdown — послать сегмент FIN, даже если дескриптор больше нуля.
Родительский процесс продолжит запись в сокет, получивший сегмент FIN, а первый сегмент, посланный серверу, вызовет получение сегмента RST в ответ. После этого функция wri te пошлет родительскому процессу сигнал SIGPIPE, как показано в разделе 5.12.
Когда дочерний процесс вызывает функцию getppid для отправки сигнала SIGTERM, возвращаемый идентификатор процесса будет равен 1. Это указывает на процесс init, наследующий все продолжающие работать дочерние процессы, родительские процессы которых завершились. Дочерний процесс будет пытаться послать сигнал процессу mit, не имея необходимых прав доступа. Но если не исключается, что данный клиент будет запущен с правами привилегированного пользователя, позволяющими посылать сигналы процессу imt, то возвращенное функцией getppid значение должно быть проверено перед ЛТППЯРК-ПМ гмгия пя
Если удалить эти две строки, вызывается функция select. Но функция select немедленно завершится, поскольку соединение установлено к сокет открыт для записи. Эта проверка и оператор goto предотвращают ненужный вызов функции select.
Это может случиться, если сервер отправляет данные сразу, как только завершается его функция accept, н если узел клиента занят, когда приходит второй пакет трехэтапного рукопожатия для завершения соединения со стороны клиента (см. рис. 2.5). SMTP-серверы, например, немедленно отсылают клиенту сообщение по новому соединению, прежде чем произвести из него счшьгвание.
Глава 16
Нет, это не имеет значения, поскольку первые три элемента объединения в листинге 16.1 являются структурами адреса сокета.
Глава 17
Элемент sd】_n】en будет равен 5, а элемент sdl_alen будет равен 8. Для этого требуется 21 байт, поэтому размер округляется до 24 байт [105, с. 89] в предположении, что используется 32-разрядная архитектура.
На этот сокет никогда не посылается ответ от ядра. Данный параметр сокета (SO_USELff)PBACK) определяет, посылает ли ядро ответ отправляющему процессу, как показано на с. 649-650 [105]. По умолчанию этот параметр включен, поскольку большинство процессов ожидают ответа. Но отключение данного параметра препятствует отправке ответов отправителю.
Глава 18
Если вы получаете большое количество ответов, они могут следовать каждый раз в разном порядке. Правда, отправляющий узел обычно выводится первым, поскольку дейтаграммы, направленные к нему или от него, не появляются в реальной сети.
Величина 1472 — это размер MTU Ethernet (1500) минус 20 байт 1Р-заголов~ ка и минус 8 байт UDP-заголовка.
Когда в BSD/OS обрабо гчик сигналов записывает байт в канал, а затем завершается, функция se】ect возвращает ошибку EINTR. Она вызывается заново и при завершении сообщает о возможности чтения из канала.
Глава 19
. Если запустить программу, то вывод будет следующий: solans % udpcli05 224.0.0.1
from 206 62 226 34from 206 62 226 43from 206 62 226 42from 206 62 226 34from 206 62 226 43from 206 62 226 42Thu Jun 19 17 28 32 1997
from 206 62 226 34
from 206 62 226 43
from 206 62 226 42
from 206 62 226 34
from 206 62 226 43
from 206 62 226 42
Thu Jun 19 17 28 32 1997
Thu Jun 19 17 28 32 1997
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 ii Linux Единственные не ответившие узлы с поддержкой многоадресной передачи —это узел в Solans и маршрутизатор Cisco
Происходит следующее В адресе получателя ИВР-дейтафаммы стоит 224 0 0 1 — это группа всех узлов, в которой должны состоять узлы, поддерживающие многоадресную передачу U DP-дейтаграмма посылается как многоадресный кадр Ethernet, и все узлы с поддержкой многоадресной передачи должны получить ее поскольку все они входят в указанную i руппу Все отвечающие узлы передают полученную UDР-дейтаграмму серверу времени и даты (обычно он является частью демона inetd), даже если этот сокет не находится в группе Однако реализация Solans требует, чтобы сокет получателя вступил в группу для получения дейтаграммы
Этот пример показывает, что UDP-программа, не предназначенная для отве тов на многоадресные дейтаграммы все же может эти дейта! раммы получать Подобное мы видели в примере с сервером времени и даты из главы 18 UDP- программа, которая не была разработана для ответа на широковещательные дейтаграммы, могла их получать
В листинге Д 10 показаны простые изменения функции main для связывания (bind) с адресом многоадресной передачи и портом О
Листинг Д. 10. Функция main UDP-клнента, осуществляющая связывание с адресом многоадресной передачи
//mcast/udpcli06 с
#т nd udeunp h
int
maindnt а где char **argv)
{
intsockfd
socklenj salen
struct sockaddr *cli *serv
if (aгде『2)
err_quit( usage udpcli06 <IPaddress> )
sockfd = Udp_client(argv[l] daytime (void **) &serv &salen)
cli = Malloc(salen)
memcpyCcli serv salen) /* копируем структуру адреса сокете */
sock_set_port(clт salen 0)/* и устанавливаем порт в 0 */
Bindlsockfd cli salen)
dg_cli(stdin sockfd serv salen)
exit(O)
}
К сожалению, все три системы, на которых проводилась проверка — BSD/OS, Digital Unix и Solans 2 5, — позволяют использовать функцию bind, а затем посылают UDР-дейтаграммы с IP-адресом многоадресной передачи отравителя Все пять отвечающих систем (те же, что и в предыдущем упражнении)
в ответе переставляют IP-адреса отправителя и получателя, так что все пять ответов являются многоадресными * На узлах с поддержкой многоадресной передачи с получаемыми ответами ничего не происходит, поскольку порт получателя в ответах является динамически назначаемым и при связывании с адресом многоадресной передачи выбирается ядром клиента, а с этим портом не связан никакой сокет ICMP-сообщения о недоступности порта в ответ на многоадресные UDР-дейтаграммы не генерируются
Если мы запустим программу ping для группы узлов 224 0 0 1 на нашем узле sol ап s, получим следующий вывод
solans Z ping 224 0 0 1
PING 224 0 0 1 56 data bytes
64 bytes from solans kohala com (206 62 226 33)】cmp_seq=0 timeM 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=ll 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)】cmp_seq=0 time=15 ms
64 bytes from sunos5 kohala com (206 62 226 36) icmp_seq=0 time=17 ms
64 bytes from bsch2 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, который не поддерживает многоадресную передачу
Поскольку ядро не поддерживает многоадресную передачу, оно воспринимает получателя как обычный IP-адрес Ядро ищет 224 0 0 1 в обычной таблице маршрутизации и выбирает маршрут по умолчанию, указывающий на маршрутизатор gw (см рис 1 7) Этому маршрутизатору посылается направленный эхо-запрос ICMP с IP-адресом 224 0 0 1 и аппаратным адресом интерфейса Ethernet выбранного маршрутизатора (аппаратный адрес не является адресом многоадресной передачи) Маршрутизатор принимает полученный пакет, поскольку он адресован его интерфейсу, а IP-адрес отправителя является адресом группы многоадресной передачи, к которой он принадлежит
Если запустить программу 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) 1cmp一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) ппп/avg/max = 3/13/24
Мы предполагали получить ответ от узла bsdi, поскольку он является маршрутизатором многоадресной передачи в подсети с туннелем в Mbone (см раздел Б 2) и на нем запущен демон mrouted Маршрутизатор gw также отвечает, но он не работает как маршрутизатор многоадресной передачи
В листинге 19 10 мы видим, что отметки времени NTP представляют собой количество секунд, прошедших с 1 января 1990 года В году содержится 31 356 000 секунд (356x24x60x60), так что два значения составляют около 96,7 лет (не считая високосные годы) и имеют смысл Кроме того, номер версии данного объявления больше, чем идентификатор сеанса (который, как мы полагаем, был задан, когда сеанс был впервые анонсирован), что также имеет смысл
. Величина 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 — наибольшее значение количества микросекунд
. При обсуждении этих двух параметров сокета в разделе 7 5 отмечалось, что параметр сокета SO_REUSEADDR эквивалентен параметру SO_REUSEPORT, если связываемый IP-адрес является адресом многоадресной передачи Это упрощает переносимость нашего кода, поскольку в противном случае нам следовало бы написать
#ifdef SO_REUSEPORT
SetsockoptCfd SOL_SOCKET SO_REUSEPORT &on sizeof(on))
#e】se
SetsockoptCfd SOL_SOCKET SO_REUSEADDR &on sizeof(on))
#endif
Некоторые системы не будут доставлять сокету многоадресные дейтаграммы, если сокет не включен в группу Одного только связывания с портом в таких системах недостаточно Это относится к Solans 2 5 Беркли-реализации, наоборот, доставляют дейтаграммы всем подходящим сокетам, независимо от того, включен ли сокет в группу Напомним, что функция bi nd_ubcast вызывается из листинга 19 14 для связывания (bind) сокета с универсальным адресом, так что данный сокет к группе не присоединен
И Будут получены двенадцать дополнительных дейтаграмм в листинге 19 22 посылаются три многоадресных дейтаграммы, и каждая из них доставляется четырем подходящим сокетам (три сокета многоадресной передачи плюс универсальный сокет)
Глава 20
Повторный вызов функции sock_ntop использует свой собственный статический буфер для хранения результата Если мы вызовем ее дважды в качестве аргумента в вызове pnntf, второй вызов приведет к перезаписи результата первого вызова
Да, если ответ содержи г 0 байт пользовательских данных (например, структура hdr)
Поскольку функция sei ect не изменяет структуру timeval, которая определяет ее ограничение по времени, нам следует заметить время отправки первого
пакета (оно возвращается в миллисекундах функцией rtt ts) Если функция 、el ect сообщает, что сокет готов к чтению, заметьте текущее время, а если функция recvmsg вызывается повторно, вычислите новый тайм-аут для функции select
Обычным решением будет создать по одному сокету на каждый адрес интерфейса, как было сделано в разделах 19 11 и 20 6, и отправлять ответ с того же сокета, на который пришел запрос
Вызов функции getaddnnfo без аргумента имени узла и без флага AI_PASSIVE заставляет эту функцию считать, что используется локальный адрес 0 1 (для IPv6) или 127 0 01 (для IPv4) Напомним, что структура адреса сокета IPv6 возвращается функцией getaddri nfo перед структурой адреса сокета IPv4 при условии, что поддерживается протокол IPv6 Если узел поддерживает оба протокола, вызов функции socket в udp_cl т ent закончится успешно при укзании семейства протоколов AF1NET6
В листинге ДИ приведена не зависящая от протокола версия программы
Листинг Д.11. Не зависящая от протокола версия программы из раздела 20 6
//advio/udpserv04 с
#】nclude unpifi h
voidmydg echo(int SA * socklen_t)
3 4 5 6 7 8 93 4 5 6 7 8 9int
3 4 5 6 7 8 9
3 4 5 6 7 8 9
main(int argc char **argv)
{
int sockfd family port
const int on = 1
pid_t pid
socklen t salen
struct sockaddr *sa *wild
struct ifi info *1fi *1fihead
if (argc == 2)
sockfd = Udp_cllent(NULL argv[l] (void **) &sa &salen)
else if (argc 二 3)
sockfd = Udp_client(argv[l] argv[2] (void **) &sa &salen)
else 一
err_quit( usage udpservO4 [ <host> ] <service or port>*)
farm ly = sa->sa_family
port = sock_get_port(sa salen)
Close(sockfd)/* хотим узнать семейство порт salen */
21 for (ifihead = ifi = Get_ifi_wfo(family 1)
22 ifi '= NULL ifi ^ ifi^>ifi next) {
/* связывание с многоадресными адресами */
sockfd - SockeKfamily SOCK_DGRAM 0)
Setsockopt(sockfd SOL_SOCKET SO_REUSEADDR Son. sizeof(ort^):
sock_set_port(ifi- >ifijaddr salen port)
Bindfsockfd ifi->ifi_addr salen)
p门ntf( bound %s\en Sock^ntopdfi >ifi_addr salen)),
продолжение 我продолжение 我if ( (pid = ForkO) == 0) { /* дочернин процесс */
продолжение 我
продолжение 我
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
Листинг Д.11 (продолжение)
mydg_echo(sockfd ifi >ifi_addr salen) exit,。) /* никогда не выполняется */
}
if (7f7 >ifi_flags & IFF—BROADCAST) {
/* попытка связывания с широковещательным адресом */
sockfd = Socket(family SOCK_DGRAM 0)
SetsockoptCsockfd 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) {
pnntf( EADDRINUSE %s\en
Sock_ntop(ifi->ifi_brdaddr salen))
Close(sockfd)
continue
} else
err_sys( bind error for 紀
Sock_ntop(ifi >ifi_brdaddr salen))
}--
printfC bound %s\enSock_ntop(ifi >ifi_brdaddr salen)):
if ( (pid = Fork()) == 0) { /* дочерним процесс */ mydg_echo(sockfd ifi >ifi_brdaddr salen) exiUO) /* никогда не выполняется */
}
}
}
/* связывание с универсальным адресом */
sockfd = Socket(family SOCK_DGRAM 0)
Setsockopt(sockfd SOL_SOCKET SO_REUSEADDR &on sizeof(on))
wild = Malloc(salen)
memcpyCwild sa salen) /* копирует семейство и порт */ $ock_set_wild(wild salen)
Bi nd(sockfd wild salen)
printf( bound 常s\enSock_ntop(wild salen))
if ( (pid = ForkO) == 0) { /* дочерним процесс */
mydg_echo(sockfd wild salen)
exit(O) /* никогда не выполняется */
}
exit(O)
void、
mydg^echo(int sockf d SA ★myaddr socklen t salen)
{ --
mtn
charmesg[MAXLINE]
socklen_t len
struct sockaddr *cli
cli = Malloc(salen)
78 len = salen
79 n = RecvfromCsockfd mesg MAXLINE 0 cli &len)
80 prmtf( child Zd datagram from Zs
81 getpidO Sock_ntop(cli len))
82 prwtf( to 弘s\en Sock_ntop(myaddr salen))
83 SendtoCsockfd mesg n 0 cli len)
84 }
85 }
Глава 21
Да, разница есть В первом примере 2 байта отсылаются с единственным срочным указателем, который указывает на байт, следующий за b Во втором же примере (вызываются две функции) сначала отсылается символ а с указателем срочности, который указывает на следующий за ним байт, а за этим сегментом следует еще один TCP-сегмент, содержащий символ b с другим указателем срочности указывающим на следующий за ним байт
В листинге Д 12 приведена версия программы с использованием функции poll
Листинг Д. 12. Версия программы из листинга 21 4, использующая функцию poll вместо функции select
//oob/tcprecv03p с
2 3 4 5 6 72 3 4 5 6 7include unp h int
2 3 4 5 6 7
2 3 4 5 6 7
main(int argc char **argv)
{
int listenfd connfd n justreadoob - 0,f
charbuff[100]
struct pol1fd pollfdtl]
if (argc == 2)
listenfd = Tcp_listen(NULL argv[l] NULL)
else if (argc == 3)
1istenfd = Tcp_listen(argv[l] drgv[2] NULL)
else
err_quit( usage tcprecv03p [ <host> ] <port#> )
connfd = Accept(listenfd NULL NULL)
pollfd[0] fd = connfd
pollfd[0] events = POLLRDNORM
for ( ) {
if (justreadoob == 0)
pollfd[0] events |= POLLRDBAND
Poll(polIfd 1 INFTIM)
if (pollfd[0] revents & POLLRDBAND) {
n = Recv(connfd buff sizeof(buff) 1 MSG_OOB)
buff[n] = 0/* завершающим нуль */ -
printf( read Zd 00B byte %s\en n buff)
justreadoob = 1
pollfd[0] events &= ^POLLRDBAND /* отключение бита */
}
if (pollfdEO] revents & POLLRDNORM) {
Листинг Д.12 (продолжение)
if ( (n = Read(connfd buff, sizeof(buff) - 1)) == 0) {
printf(“received E0F\en"),
exit(O).
}
buff[n] = 0/* завершающий нуль */
printfC'readXdbytes Zs\erT n buff)
35justreadoob=0
36}
37}
38 }
Глава 22
Нет, такая модификация приведет к ошибке. Проблема состоит в том, что nqueu уменьшается до того, как завершается обработка элемента массива dg[iget], что позволяет обработчику сигналов считывать новую дейтаграмму в данный элемент массива.
Глава 23
В примере с функцией fork будет использоваться 101 дескриптор, один прослушиваемый сокет и 100 присоединенных сокетов. Но каждый из 101 процесса (один родительский и 100 дочерних) имеет только один открытый дескриптор (игнорируем все остальные, такие как стандартный поток ввода, если сервер не является демоном). В случае сервера с потоками используется 101 дескриптор для одного процесса. Каждым потоком (включая основной) обрабатывается один дескриптор.
Обмена двумя последними сегментами завершения ТС P-соединения (сегмент FIN сервера и сегмент АСК клиента в ответ на сегмент FIN сервера) не произойдет. Это переведет клиентский конец соединения в состояние FIN_ WAIT_2 (см. рис. 2.4). Беркли-реализации прервут работу клиентского конца, если он остался в этом состоянии, по тайм-ауту через 11 минут [105, с. 825827]. У сервера же в конце концов закончатся дескрипторы.
Это сообщение будет выводиться основным программным потоком в том случае, когда он считывает из сокета признак конца файла 〃 при этом другой поток продолжает работать. Простейший способ выполнить это — объявить другую внешнюю переменную по имени done, инициализируемую нулем. Прежде чем функция copyto программного потока вернет управление, она установит эту переменную в 1. Основной программный поток проверит эту переменную, и если она равна нулю, выведет сообщение об ошибке. Поскольку значение переменной устанавливает только один программный поток, пет необходимости в синхронизации.
Глава 24
На стороне сервера ничего не изменится. Во-первых, маршрутизатор использует обычную модель системы с гибкой привязкой, поэтому он принимает вхо-
дящие дейтаграммы из Ethernet, даже если адрес получателя является адресом другого его интерфейса. Во-вторых, при использовании маршрутизации IPv4 от отправителя перенаправляющий узел заменяет его адрес в списке на адрес исходящего интерфейса. Исходящим интерфейсом является интерфейс Ethernet (206.62.226.62), независимо от того, на какой адрес отправлен пакет.
Ничего не изменится. Все системы являются соседями, поэтому гибкая маршрутизация идентична жесткой.
Мы бы поместили EOL (нулевой байт) в конец буфера.
Поскольку программа ping создает символьный (неструкгурированный) сокет (см. главу 25), она получает полный IP-заголовок, включая все IP-napa- метры, для каждой дейтаграммы, которую она считывает с помощью функции recvfrom.
Потому что сервер rl ogind запускается демоном inetd (см. раздел 12.5).
Проблема заключается в том, что пятый аргумент функции setsockopt является указателем на длину, а не самой длиной. Эта ошибка, вероятно, была выявлена, когда впервые использовались прототипы ANSI С.
Ошибка оказалась безвредной, поскольку, как отмечалось, для отключения параметра сокета IP_OPTIONS можно либо задать пустой указатель в качестве четвертого аргумента, либо установить нулевое значение в пятом аргументе (длине) [105, с. 269].
Глава 25
Недоступными являются поле номера версии и поле следующего заголовка в IPv6. Поле полезной длины доступно либо как аргумент одной из функций вывода, либо как возвращаемое значение одной из функций ввода, но если требуется параметр увеличенного поля данных (jumbo payload option), сам параметр приложению недоступен. Заголовок фрагментации также недоступен приложению.
В конце концов приемный буфер клиентского сокета заполнится, и при этом функция демона write будет заблокирована. Мы не хотим, чтобы это произошло, поскольку демон тогда перестанет обрабатывать данные на всех своих со* кетах. Простейшим решением является следующее: демон должен сделать свой конец соединения домена Unix с клиентом неблокируемым. Для этого демон должен вызывать функцию write вместо функции-обертки Write и игнорировать ошибку EWOULDBLDCK.
По умолчанию Беркли-ядра допускают широковещательную передачу через символьный сокет [105, с. 1057]. Поэтому параметр сокета SO_BROADCAST необходимо определять только для UDP-сокетов.
Наша программа не проверяет адреса многоадресной передачи и не устанавливает параметр сокета IP_MULTICAST_IF. Следовательно, ядро выбирает исходящий интерфейс, вероятно, просматривая таблицу маршрутизации для
224.0.0.1. Мы также не устанавливаем значение поля I P_MULTICAST_TTL, поэтому по умолчанию оно равно 1, и это правильное значение.
Глава 26
Этот флаг означает, что буфер перехода устанавливается функцией sigsetjmp (см. листинг 26.6). Хотя этот флаг может казаться лишним, существует вероятность, что сигнал может быть доставлен после того, как устанавливается обработчик ошибок, но перед тем, как вызывается функция sigsetjmp. Даже если программа не вызывает генерацию сигнала, сигнал все равно может быть сгенерирован другим путем (например, как в случае с команд。访 kill).
Глава 27
Родительский процесс оставляет прослушиваемый сокет открытым в том случае, если ему позже будет необходимо создать дополнительный дочерний процесс с помощью функции fork (это будет расширением нашего кода).
Для передачи дескриптора действительно можно вместо потокового сокета использовать сокет дейтаграмм. В случае сокета дейтаграмм родительский процесс не получает признака конца файла на своем конце канала, когда дочерний процесс прерывается преждевременно, но для этих целей родительский процесс может использовать сигнал SIGCHLD. Следует иметь в виду, что эта ситуация отличается от случая с применением нашего демона i cmpd (см. раздел 25.7): тогда между клиентом и сервером не было иерархических отношений (родительский процесс — дочерний процесс), поэтому использование признака конца файла было единственным способом для сервера обнаружить исчезновение клиента.
Глава 28
Как правило, не используется, поскольку единственным типом приложений, которые присваивают полю qlen отличное от нуля значение, является ориентированный на соединение сервер (например, TCP). Но серверы обычно связываются с заранее известным портом, не позволяя системам выбирать динамически назначаемый порт. Исключением является сервер RPC (Remote Procedure Call — удаленный вызов процедур), который связывается с динамически назначаемым портом, азатем регистрирует его с помощью программы отображения портов RPC.
Получение сообщений ICMP о недоступности получателя в ответ на сегмент SYN не является фатальной ошибкой. TCP может передавать сегмент SYN определенное количество раз либо пока не истечет время, отведенное для повторных передач.
Функция wri te, предназначенная для записи в сокет, получивший сегмент RST, генерирует сигнал SIGPIPE. Если процесс ничего не делает с этим сигналом, по умолчанию процесс прерывается. Если процесс игнорирует сигнал, функция write возвращает ошибку EPIPE.
Глава 29
Да, функция является безопасной в многопоточной среде Функция setnetconfig динамически выделяет память, используемую для храпения структуры net- config и массивов, на которые она указывает. Эта память освобождается с помощью функции endnetconfi g. Следует быть внимагельным и не ссылаться на возвращаемую функцией getnetconfi g структуру netconfi g после вызова функции endnetconfi д, поскольку отведенная ей память уже освобождена.
Программа приведена в листинге Д 13
Листинг Д.13. Сравнение Т ALL вместо определения каждой структуры netbuf //debug/test06 с
#include unpxti h
int
main(mt argc char **argv)
{
int fd
struct t_cal 1 *tcal 1
fd = T_open(XTI_TCP O_RDWR NULL)
tcall 二 T_alloc(fd T_CALL T_ALL)
printfC first t_alloc OK\en")
tcall = T_d11oc(fd. T_CALL T_ADDR | T_OPT | T_UDATA)
printf( second t_a1loc 0K\en'?
exit(O)
}
После запуска этой программы получим следующий вывод:
alpha Z test06 fi rst t/lloc OK t_a11oc error system error Invalid argument
Точка доступа TCP не поддерживает пользовательские данные с запросом на соединение (значение ~2 для строки, содержащей функцию connect в табл. 28.1). Когда определена функция T_ALL, функция t_alloc пропускает эти неподдерживаемые структуры, и на рис. 29.2 мы видим, что значение udata 】еп равно нулю, a udata buf — пустой указатель Но при указании любой из грех структур netbuf в качестве третьего аргумента функции t_al 1 ос (а не функции T_ALL) будет возвращена ошибка, если хоть одно из заданных полей не поддерживается. Это еще одна причина, по которой всегда следуег использовать функцию T_ALL.
Функция не может определить тип структуры только по ее указателю. Если функция t_free освобождает только память, выделенную под структуру, этот аргумент не потребуется. Но поскольку она проходит через структуру и освобождает все буферы, на которые указывают структуры netbuf внутри нее, этой функции необходимо знать тип структуры
Порядок следования элементов этой структуры не гарантирован.
Глава 30
Этот способ ие работает, поскольку для записи целочисленного значения обычно используется 32 бита, в то время как указатели требуют 64 бита (см. табл. 1.5).
Константа РАТН_МАХ стандарта Posix.l не содержи г завершающего нулевого байта.
На рис. Д.7 мы показали 4-байтовый массив и два 2-байговых поля с перекрыванием.
x[0】
x[l]
X⑵
x[3]
,Источник ,
иН
иh
Приемник
Рис. Д・7・ Перекрывание областей памяти при копировании
Если копирование выполняется от начала исходной области Д6 начала конечной, как в следующем фрагменте программы на языке С:
while (nbytes--)
*dst++ = *src++.
то тогда выполняются два оператора присваивания
х[1] = х[2].
х[2]工 х[3].
что корректно. Но если копирование делается в другом направлении, как в следующем фрагменте кода:
sre += nbytes dst += nbytes. while (nbytes--) *--dst = *--src.
то два оператора присваивания будут выглядеть следующим образом:
х[2] и х[3].
х[1]二 х[2]
Такое присваивание некорректно. Заранее не известао, в каком направлении функция memepy осуществляет копирование, следовательно, при перекрывании должна вызываться функция memmove. Функция memmove обрабатывает перекрывание путем копирования в «правильном» направлении, в зависимости от взаимоотношений между исходной и конечной областью.
В листинге Д.14 приведены две функции: do_parent и do chi Id. Предварительно следует подключить в листинге Д.9 заголовочный файл unpxti h вместо unp h.
Листинг Д.14. Определение действительного номера поставленных в очередь соединений для различных значений qlen
void
do__pa rent (void)
3/
38
39
int qlen. j, к. junk, fd[MAXBACKLOG + 1]. struct Jcall teal 1,
40
41
Close(cfd)
Signal(SIGALRM, parent_alrm).
42
43
44
45
for (qlen = 0. qlen <= 14. q1en++) {
pn ntffqlen Td ".qlen), WriteCpfd, &qlen. sizeof(int)). /* сообщает дочернему процессу значение */ Read(pfd. &junk. sizeof(int)), /* ждет дочерний процесс*/
46
47
48
for (j = 0, j <= MAXBACKLOG. J++) {
fd[j] = T_open(XTl_TCP, 0_RDWR NULL): T_bind(fd[j]. NULL: NULL):
49 ♦
50
51
52
53
tea11 addr maxlen = sizeof(serv). tcall addr len = sizeof(serv), tcali addr buf = &serv.
tcall opt len = 0.
tcall udata len = 0.
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
alarm(2).
if (t_connect(fd[j]. &tcal1. NULL) < 0) { if ferrn。 != EINTR)
err_xti("t_connect error. j = Md", j).
printf("timeout. %d connections completed\en". j - 1):
for (k = 1. к v j, k++)
T_close(fd[k]), break./* следующее значение qlen */
}
alarm(0).
}
if (j > MAXBACKLOG) prmtfCXd connect ions?\en". MAX BACKLOG): }
qlen = -1./* сообщаем дочернему процессу, что все сделано */
HriteCpfd. &qlen. sizeof(mt)).
}
71
72
73
74
75
void
do_child(void)
{一
int listenfd qlen junk.
• struct t_bind tbind, tbindret.
76
f
? Close(pipefd[l])
77
78
79
/ Read(cfd &qlen sizeof(int))./* ждет родительский процесс */
while (qlen >= 0) {
1istenfd = T_open(XTI_TCP. 0_RDWR NULL).
80
81
82
83
tbind addr maxlen = sizeof(serv).
tbind addr len = sizeof(serv); tbind addr.buf = &serv.
tbind qlen = qlen.
84
85
tbindret addr maxien = 0.
tbindret addr len = 0:、c
проаолжение 夕
Листинг Д-14 (продолжение)
86 T_bind(1istenfd. &tbind. &tbindret).
87printfCreturned qlen = %d. ". tbindret qlen).
88fflush(stdout).
WriteCcfd &junk, sizeof(int)),/* сообщает родительскому 叩oueccy*/
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, содержащая имя файла, приведена рядом с названием каждого проекта, так как в ней содержится номер версии.
Для книг, статей и других источников, имеющих электронные версии, указаны адреса сайтов. Они мо1ут меняться, поэтому следите за списком обновлений на сайте автора книги http://www.kohala.com/~rstevens.
Albi£z, Р., Liu, С. DNS and BIND, Second Edition 〃。‘Reilly & Associates Sebastopol, Calif., 1997.
Almquist, R Type of Service in the Internet Protocol Suite //RFC 1349 (July), 1992.'
Обсуждается использование поля тип сервиса в заголовке IPv4.
Atkinson, R.J. IP Authentication Header //RFC 1826 (Aug.), 1995.*
Atkinson, RJ. IP Encapsulating Security Payload (ESP) //RFC 1827 (Aug.), 1995.
Baker, E Requirements for IP Version 4 Routers //RFC 1812 (June), ed. 1995.
Borman, D. A. Frequency of RST Terminated Connections //January 30, 1997, end2end-interest mailing list.
http://www.kohala.com/-rstevens/borman.97jan30.txt
Borman, D. A. TCP and UDP over IPv6 J umbograms //RFC 2147 (May), 1997.
Borman, D. A ”Re: SYN/RST cookies", June 6,1997, tcp-impl mailing list.
http://www.kohala.corn/~rstevens/borman.97jun06.txt
Braden, R.工 Requirements for Internet Hosts — Communication Layers //RFC 1122 (Oct.), ed. 1989.
Первая часть Host Requirements RFC: канальный уровень, IPv4, ICMPv4, IGMPv4, ARP, TCP и UDP."
Braden, R. T. TIME-WAIT Assassination Hazards in TCP //RFC 1337 (May), 1992.
Braden, R.工 Extending TCP for Transactions — Concepts //RFC 1379 (Nov.), 1992.
Braden, R Г TCP Extensions for High Performance: An Update, 1993.
Интернет-проект http://www.kohala.com/-rstevens/tcplw-extensions.txt Это обновленная редакция документа RFC 1323 [45], которая никогда не печаталась как RFC, но обновление этого документа должно когда-то появиться.
Braden, R £ Т Т/ТСР — TCP Extensions for Transactions. Functional Specification//RFC 1644 (July), 1994.
Braden, R. T., Borman, D. Л., Partridge, C. Computing the Internet Checksum //RFC 1071 (Sent.), 1988.
Bradner, S O. The Internet Standards Process — Revision 3 //RFC 2026 (Oct.), 1996.'
Butenhof, D. R. Programming with POSIX Threads //Addison-Wesley, Reading, Mass, 1997.
CERT 1996a. UDP Port Denial-of-Service Attack //Advisory CA-96.01, Computer Emergency Response Team, Pittsburgh, Pa. (Feb.).
ftp://info.cert.org/pub/cert_advisories/CA-96.01.UDP_service_deniaL
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_flooding
Cheswick, W. R, Bellovin, £ M. Firewalls and Internet Security: Repelling the Wily Hacker //Addison-Wesley, Reading, Mass, 1994.
Comer, D £., Lm,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/lin/TCP.atm.ps.Z
Conta. A, Deering, S. £. Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification //RFC 1885 (Dec.), 1995.
Crawford, M. A Method for the Transmission of IPv6 Packets over Ethernet Networks, //RFC 1972 (Aug.), 1996.
Crawfordy M. A Method for the Transmission of IPv6 Packets over FDDI Networks //RFC 2019 (Oct.), 1996.
Deering, S. £. Host Extensions for IP Multicasting //RFC 1112 (Aug.), 1989.
Deering, S. £., Hinden, R. Internet Protocol, Version 6 (IPv6) Specification //RFC 1883 (Dec.), 1995.
Dewar, R. В. K, Smcsna, M. Microprocessors: A Programmer's View //McGraw- Hill, New York. 1990.
Enksson^ H. MBONE: The Multicast Backbone //Communications of the ACM, vol. 37, no. 8, pp. 54-60 (Aug.), 1994.
Fenner, W. С. Личное общение, 1997.
Fuller,匕 Li,T., Yu,J.上,Varadhan, K. Classless Inter-Domain Routing (CIDR)' An Address Assignment and Aggregation Strategy //RFC 1519 (Sent.), 1993.
Garfinkel, 5.L., and Spafford, E. H. Practical UNIX and Internet Security //Second Edition, OfReilly & Associates, Sebastopol, Calif., 1996.
Gierth, Л. Личное общение, 1996.
Gilligan, R. Е, Thomson, S, Bound J, Stevens, W. R. Basic Socket Interface Extensions for IPv6 //RFC 2133 (Apr.), 1997.
Handley, M SAP: Session Announcement Protocol //Nov., 1996.
Интернет-проект draft-ietf-mmusic-sap-OO.txt.
Handley, M.^Jacobson, V. SDP: Session Description Protocol //Mar., 1997.
Интернет-проект draft-ietf-mmusic-sdp-03.txt.
Hinden, R, Deering, S. E. IP Version 6 Addressing Architecture //RFC 1884 (Dec.), 1995.
Hinden, R, Deering, 5. £. IP Version 6 Addressing Architecture //July, 1997.
Интернет-проект draft-ietf-ipngwg-addr-arch-v2-02.txt. Получив статус RFC, он должен заменить RFC 1884 [35].
Hinden, R, Fink, R, PostelJ. B. IPv6 Testing Address Allocation //July, 1997. Интернет-проект draft-ietf-ipngwg-testv2-addralloc-01.txt. Получив статус RFC, он должен заменить RFC 1897 [39].
Hinden. R, О De", M., Deering, S. E An IPv6 Aggregatable Global Unicast Address Format //July, 1997.
Интернет-проект draft-ietMpngwg-unicast-aggr-02.txt. Получив статус RFC, он должен заменить RFC 2073 [86].
Hinden, R.t PostelJ. В. IPv6 Testing Address Allocation //RFC 1897 (Jan.), 1996.
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 не распространяются свободно через Интернет.
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 не распространяются свободно через Интернет.
IEEE 1997b. Guidelines for64-bit Global Identifier (EUL64) Registration Authority //Institute of Electrical and Electronics Engineers, Piscataway, N.J.
http://standards.ieee.org/db/oui/tutoriaLs/EUI64.html
Jacobson, V., Congestion Avoidance and Control //Computer Communication Review, vol. 18, no. 4, pp. 314-329 (Aug.), 1988.
ftp://ftp.ee.lbLgov/papers/congavoid.ps.Z. Классическая статья, описывающая алгоритмы медленного старта и предотвращения перегрузки сети для TCP.
Jacobson, И “Re: half baked anycastoff idea…二 27 June, 1994, end2end-interest mailing list.
http://www.kohala.com/~rstevens/vanj.94jun27.txt
Jacobson,匕 Braden, R. £, Borman, D. A. TCP Extensions for High Performance // RFC 1323 (May.), 1992.
Описываются параметр масштабирования окна, параметр отметки времени, алгоритм PAWS, а также приводятся причины необходимости этих модификаций. [12] представляет собой обновленную версию этого документа RFC.
Jacobson,匕 Braden, /?. Г, Zhang. L. TCP Extensions for High-Speed Paths, //RFC 1185 (Oct), 1990.
Josey, A. Go Solo 2: The Authorized Guide to Version 2 of the Single UNIX Specification //Prentice Hall, Upper Saddle River, NJ., ed. 1997.
Joy, W. N. Личное общение, 1994.
Kamf 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.
Katz, D. Transmission of IP and ARP over FDDI Network //RFC 1390 (Jan.), 1993.
Katz, D. IP Router Alert Option //RFC 2113 (Feb.), 1997.
Katz, D., Atkinson. RJ., Partridge, C.Jackson, A. IPv6 Router Alert Option //June, 1997.
Интернет-проект draft-ietf-ipngwg-ipv6router-alert-02.txt.
Kent, S. T. U.S. Department of Defense Security Options for the Internet Protocol //RFC 1108 (Nov.), 1991.
Kent, ST., Atkinson, R.J. IP Authentication Header,//July, 1997.
Интернет-проект draft-ietf-ipsec-auth-header-01.txt
Kent, S. Г, Atkinson, R.J. IP Encapsulating Security Payload (ESP) //July, 1997.
Интернет-проект draft-ietf-ipsec-esp-v2-00.txt.
Kernighan, B. WC, Pike, R. The UNIX Programming Environment //Prentice Hall, Englewood Cliffs, NJ., 1984.
Kernighan, B. W„ Ritchie, D. MThe C Programming Language //Second Edition, Prentice Hall, Englewood Cliffs, NJ., 1988.
Korn, D. G., Vb, К. P. HSIFO:Safe/Fast String/File IOM //Proceedings of the 1991 Summer USENIX Conference, pp. 235-255, Nashville, Tenn.,1991.
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.96aprlO.txt
Maslen, Г M. MRe:gethostbyXXXX() and Threads'*, Message-ID <maslen.862463530 @shellx>, Usenet, comp.programming.threads Newsgroup //May, 1997.
http:〃 www.kohaLa.com/~rstevens/masLen.97mayOLtxt.
Maufer. Г, Semeria, G Introduction to IP Multicast Routing //May, 1997.
Интернет-проект draft-ietf-mboned-intro-multicast-02.txt.
McCann, J., Deering, 5. E, Mogul, J. C. Path MTU Discovery for IP version 6, // RFC 1981 (Aug.), 1996.
McCanne, S., Jacobson, К The BSD Packet Filter*: A New Architecture for UserLevel 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
McDonald, D.厶 A Simple IP Security API Extension to BSD Sockets //Mar., 1997.
Интернет-проект draft-mcdonald-simple-ipsec-api-01.txt.
McDonald, D. £., 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.hut.fi/ssh/crypto/pf-key.ps
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.
McKusick, M. K., Bostic, K, Karels, MJ, QuaitermanJ. 5. Design and Implementation of the 4.4BSD Operating System //Addison-Wesley, Reading, Mass, 1996.
Meyer, D. Administratively Scoped IP Multicast //June, 1997.
Интернет-проект draft-ietf-mboned-admin-ip-space-03.txt.
Mills, D. £. Network Time Protocol (Version 3): Specification, Implementation, and Analysis //RFC 1305 (Mar.), 1992.
Mills, D. £. Simple Network Time Protocol (SNTP) Version 4 for IPv4, IPv6 and OSI //RFC 2030 (Oct.), 1996.
MogulyJ. C., Deering, S, E. Path MTU Discovery //RFC 1191 (Aug.), 1990.
Mogul, J. C., Postel, J. B. Internet Standard Subnetting Procedure //RFC 950 (Aug.), 1985.
Nemeth, E. Личное общение, 1997.
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.
Partridge, С., Mendez, Т., Milliken, W. Host Anycasting Service //RFC 1546 (Nov.), 1993.*
Partridge, C, Pink, S A Faster UDP //IEEE/ACM Transactions on Networking, vol. 1, no. 4, pp. 429-440 (Aug.), 1993.
Paxson, V. End-to-End Routing Behavior in the Internet //Computer Communication Review, vol. 26, no. 4, pp. 25-38 (Oct.), 1996.
ftp://ftp.ee.lbl.gOv/papers/routing.SIGC0MM.ps.Z l
Piscitello, D. M. FTP Operation Over Big Address Records (FOOBAR) //RFC 1639 (June), 1994.
Plauger, PJ. The Standard C Library //Prentice Hall, Englewood Cliffe, NJ., 1992.
PostelJ. 3. User Datagram Protocol //RFC 768 (Aug.), 1980.
PostelJ. B. Internet Protocol //RFC 791 (Sent.)» ed. 1981a.
PostelJ. B. Internet Control Message Protocol //RFC 792 (Sent.), ed. 1981b.
PostelJ. B, Transmission Control Protocol //RFC 793 (Sent.), ed. 1981c.
Pusateri, T. IP Multicast Over Token-Ring Local Area Networks //RFC 1469 Qune), 1993.
Rago, S A. UNIX System V Network Programming //Addison- Wesley, Reading, Mass, 1993.
Rekhter, У., Lothberg, R, Hinden, &., Deering, S. £., PostelJ. B, An IPv6 ProviderBased Unicast Address Format //RFC 2073 (Jan.), 1997.
Reynolds,}. X., Postel, J. B. Assigned Numbers //RFC 1700 (Oct.), 1994.
Некоторая информация, содержащаяся в этом документе RFC, может устареть еще до момента выхода обновленной версии RFC. Все данные для этого документа RFC взяты из файлов, находящихся по адресу ftp://ftp.isi.edu/in~ notes/iana/assignments, обновляемых по мере поступления новой информации. Этот документ RFC содержит ссылки на файлы в указанном каталоге, и за самой новой информацией рекомендуется обращаться к этим файлам.
Ritchie, D. М. A Stream Input-Output System //AT&T Bell Laboratories Technical Journal, vol. 63, no. 8, pp. 1897-1910 (Oct.), 1984.
Satus, P. H. A Quarter Century of Unix //Addison-Wesley, Reading, Mass, 1994.
Salus, P. H. Casting the Net: From ARPANET to Internet and Beyond //Addison- Wesley, Reading, Mass, 1995.
Schimmel, C. UNIX Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers //Addison-Wesley, Reading, Mass, 1994.
Srinivasan, R. XDR: External Data Representation Standard //RFC 1832 (Aug.), 1995.
Stevens, W. R. Advanced Programming in the UNIX Environment //Addison- Wesley, Reading, Mass, 1992.
Программирование в Unix — детальное описание.
Stevens, W. R. TCP/IP Illustrated, Volume 1: The Protocols //Addison-Wesley, Reading, Mass, 1994.
Введение в протоколы Интернета.
Stevens, W. R TCP/IP Illustrated, Volume 3: TCP for Transactions, HTTP, NNTP, and the UNIX Domain Protocols //Addison-Wesley, Reading, Mass, 1996.
Stevens, W. R., Thomas, M Advanced Sockets API for IPv6 //June, 1997.
Интернет-проект draft-stevens-advanced-api-04.txt.
Tanenbaum, A. S Operating Systems Design and Implementation //Prentice Hall, Englewood Cliffs, N.J., 1987.
Thomas, S., Transmission ofIPv6 Packets over Token Ring Networks //June, 1997. Интернет-проект draft-ietf-ipngwg-trans-tokenring-OO.txt.
Thomson, 5., Huitema, C. DNS Extensions to Support IP version 6 //RFC 1886 (Dec.), 1995.
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
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.htni.
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
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.
Vixie, Р. Л. Личное общение, 1996.
Wright, G.凡 Stevens, W. 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_ifhame, элемент, 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_mPF_, 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
AF_ISO, константа, 120
AF_KEY, константа, 119
AF_LINK, константа, 98, 501, 507 AFLOCAL, константа, 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_C AN ONNA ME, константа, 316, 325,357,358,364
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
A1_PASSIVE, константа, 316,322, 332,352, 367, 616,1017
ai protocol, элемент, 316,319,368 ai_socktype, элемент, 316,319,362,
366
aio_read 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,47L 659,746,984,1021
API, программный интерфейс приложений, 26,39
ARP (Address Resolution Protocol), 65,123.249,264,471,485,502, 515,518,714,763
операции с кэшем, функция ioctl, 485
агр, программа, 486
arp_flags, элемент, 485
arp_ha, элемент, 485
arpjpa, элемент, 485
<arpa/nameser.h>, заголовочный файл, 773
arpreq, структура, 471,485
AS (автономная система), 953
ASCII, 40,283,990
asctime, функция, 662
asctime_r, функция, 662
at, программа, 375
ATF_COM, константа, 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, E, 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
bindmcast, функция, 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
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
checkdup, функция, 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一
CMSG_F1RSTHDR, макрос, 407, 408,585,895
CMSGLEN, константа, 979 CMSG_LEN, макрос, 407,408 cmsglen, элемент, 404,406,408, 705一
cmsg_level, элемент, 404,406, 614, 703/705
CMSG_NXTHDR, макрос, 407, 408,585,895
CMSG SPACE, константа, 979 CMSGSPACE, макрос, 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
Comer, D. Е., 234,1028
config.h, заголовочный файл, 432, 981
configure, программа, 981
CONIND number, элемент, 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
connect_nonb, функция, 454,458
исходный код, 455
connect_timeo, функция, 393
исходный код, 393
connld, модуль, 435
const, квалификатор, 104,125,188
Conta, А., 1028
copyto, функция, 657,1020
cpio, программа, 57 CPU_VENDOR_OS, константа, 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
DEST_lcngth, элемент, 921 DESToffset, элемент, 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
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
DL_ATTACH_REQ константа, 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 do_parent, функция, 1024 dom_family, элемент, 121 domain, структура, 121
DoupnikJ, 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
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
err_sys, функция, 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 ETH_P_ARP. константа, 761 ЕТН_Р_1Р, константа, 761 ETH_P_IPV6, константа, 761
EtheieE 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
excel, функция, 430 определение, 137
execle, функция определение, 137
execlp, функция, 138 определение, 137
exeev, функция определение, 137
exeeve, функция, 137
exeevp, функция, 138
определение, 137
exit, функция, 41,72,139,153,162, 252,411,416,433,612,658,984, 1009
F
F—CONNECTING, константа, 462, 465
F DONE, константа, 465, 675
Lflags, элемент, 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
fjlogin, элемент, 434 fc_ngroups, элемент, 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
FDCLR, макрос, 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, флаг завершения (TCP- заголовок), 71,205,413,758
FIN_WAIT_1, состояние, 72 FIN[WAIT[2, состояние, 72,153,
16L 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
freeaddrinfb, функция, 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 ga_echeck, функция, 351, 367 ga_nsearch, функция, 351, 354 ga_port, функция, 360,361,367 ga_serv, функция, 358,361,367
gaunix, функция, 351,363
gai_hdr.h, заголовочный файл, 349
gai strerror, функция, 321
определение, 321
Garfinkel, 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
getchar unlocked, функция, 662
getconninfo, функция, 316
getgnd, функция, 662
gctgrid_r, функция, 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
gethostbyaddr_r, функция, 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,301t 322,344,357
определение, 291
gethostent, функция, 301 gethostname, функция, 295,302,
766,1003
определение, 295
getifaddrs, функция, 475
getlogin, функция, 662
getlogin_r, функция, 662
getmsg, функция, 181, 777,907, ^9Г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
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
h_errno, элемент, 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
Hogue, J., 31 home_page, функция, 460, 673 Host Requirements RFC, 1027 HOST_NOT_FOUND, константа, 287
HOSTSELF, константа, 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 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 -
ICMP6_FILTER_SETBLOCK, макрос, 714
ICMP6_FILTER_
SETBLOCKALL, макрос, 714
[CMP6_FILTER_SETPASS, макрос, 714
ICMP6_FILTER_SETPASSALL, макрос, 714
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 if_indextoname, функция, 508,548, 588
исходный код, 510 определение, 508
if_msghdr, структура, 492,507
i匚name, поле, 980
if_name, элемент, 509,512
if_nameindex, структура, 980
определение, 509
iLnameindex, функция, 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
IFF_BROADCAST, константа, 485
iff[pointopoint,
константа, 485
IFF PROMISC, константа, 761
IFF_UP, константа, 485
IFI_ALIAS, константа, 568 ifi_hlen, элемент, 478,483,507 ifi_infb, структура, 474,476,478, 481,483,489,504,507,567, 571, 606
ifi_next, элемент, 476
ifm_addrs, элемент, 492,499
ifmjype, элемент, 506
IFNAMSIZ, константа, 508 ifraddr, элемент, 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
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 add jt, тип данных, 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
IN6JS_ADDR_MC_GLOBAL, макрос, 310
IN6JS_ADDR_MC_ LINKLOCAL, макрос, 310
IN6」S_ADDR_MC_ NODELOCAL, макрос, 310
IN6_IS_ADDR_MC_ORGLOCAL, макрос, 310
IN6JS_ADDR_MC_SITELOCAL, макрос, 310
IN6」S_ADDR_MULTICAST, макрос, 310
IN6」S_ADDR_SITELOCAL, макрос, 310
IN6_IS_ADDR_UNSPECIFIED, макрос, 310
IN6」S_ADDR_V4coMРАТ, макрос, 310
IN6_IS_ADDR_V4MApPED, макрос, 306,311,314, 719, 822 определение, 310
in6_pktinfb, структура, 582,612, 706
определение, 612 m6addr_any, константа, 125,955 IN6ADDR_ANY_INIT, константа, 125, 320,322, 352,418,613,955
in6addr_Ioopback, константа, 954
IN6ADDR_LOOPBACKJNIT, константа, 954
INADDR—ANY, константа. 46,80, 125,148「151,240, 257,320, 322, 352,418,543,828,916,950,989
INADDR_LOOPBACK, константа, 950
INADDR_MAX_LOCAL_ GROUP, константа, 989
INADDR NONE, константа, 96, 978,989"
inet_addr, функция, 40,92,105,117 определение, 105
INET—ADDRSTRLEN, константа, ПО, 978 inet_aton, функция, 105, 117 определение, 105
INETIP, константа, 895
inet ntoa, функция, 92,105, 345, 662
определение, 105 inet_ntop, функция, 92,105,116, 13M 289,343,345,371,373,487, 588
версия только для IPv4, исходный код, 108 определение, 107
inetpton, функция, 40,43,92,105, 116^334,345,355,1004
версия только для IPv4, исходный код, 108 определение, 107
inet_pton_Ioose, функция, 117 inet_srcrt_add, фуш<ция, 691, 695
V Стивенс UNIX взаимодействие процессов — СПб Питер, 2002
Все исходные коды программ, о] 1убликоваипые в этой книге, вы можете найти по адресу http:// www piter com/download.
17 for (1 « 0. 1 < RTAX_MAX.卄+) {
18 if (addrs & (1 « i)) {
19rtl_mfo[l]==SG.
20NEXf_SA(sa)
21} else
22rti info[i]=NULL:
23}"
CA I
7/* Сначала проверяем, не связан ли уже этот адрес */
8 for (1 - 0. 1 < ndddrs, 1++) {
9 if (sock_cmp_addr(addrs[i] addr^sa, sabind salen) = 0)
10 return.
11 }
12 fd = Socket(sabind->sa_fami1у. SOCK_DGRAM. 0).
13 sock_set_port(sab】nd salen port).npodoMue
У. Р. Стивенс
e е戶
勿Я
ЛАОС
10010100
11010100111101001101100100111
100101001110101001m
Л1001101100100111
Г01010110100101100001001100
iooioioom<
31010011
Г101001101100100111101010110100
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-7ISBN 013490012Х (англ.)ISBN 5-318-00535-7© 1998 Prentice Hall PTR
ISBN 013490012Х (англ.)
ISBN 5-318-00535-7
ISBN 013490012Х (англ.)
ISBN 5-318-00535-7
© Перевод на русский язык, ЗАО Издательский дом «Питер», 2003 © Издание на русском языке, оформление, ЗАО Издательский дом «Питер», 2003
Краткое содержание
Предисловие26