Текст
                    Сети TCP/IP
ТомЗ
Разработка приложений типа клиент/сервер
для Linux/POSIX
Дуглас Э. Камер
Факультет компьютерных наук
Университета Пердью
Дэвид Л. Стивене
IBM Corporation
ш
Издательский дом "Вильяме"
Москва ¦ Санкт-Петербург ¦ Киев
2002


ББК 32.973.26.018.2.75 К18 УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией СЛ. Тригуб Перевод с английского и редакция К А. Птпицына По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу: info@wiliiamspublishing.com, http://www.williamspublishing.com Камер, Дуглас Э., Стивене, Дэвид Л. К18 Сети TCP/IP, том 3. Разработка приложений типа клиент/сервер для Linujc/POSIX. : Пер. с англ. — М. : Издательский дом "Вильяме", 2002. — 592 с. : ил. — Парал. тит. англ. ISBN 5-8459-0296-7 (рус.) Операционная система Linux становится все более популярной и все чаще применяется как система для управления многими серверами. Эта книга предназначена для программистов, стремящихся изучить все тонкости создания сетевых приложений для Linux. В ней в основном рассматриваются принципы взаимодействия типа клиент/сервер и приведены алгоритмы работы клиентских и серверных компонентов распределенных программ. Каждый проект проиллюстрирован практическим примером, и наряду с этим описаны все необходимые методы организации сетевого взаимодействия, включая шлюзы прикладного уровня и туннелирование. Кроме того, в книге рассматривается несколько стандартных прикладных протоколов, на примере которых описаны алгоритмы и методы реализации. Книга рассчитана на широкий круг читателей, а также может служить в качестве учебника для студентов старших курсов и аспирантов соответствующих специальностей. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Prentice Hall, Inc. Authorized translation from the English language edition published by Prentice Hall, Ptr., Copyright © 2001 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2002 ISBN 5-8459-0296-7 (рус.) © Издательский дом "Вильяме", 2002 ISBN 0-13-032071-4 (англ.) © Prentice Hall, Inc., 2001
Оглавление Предисловие 24 Глава 1. Введение и краткий обзор 29 Глава 2. Модель взаимодействия типа клиент/сервер и проектирование программного обеспечения 37 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера 51 Глава 4. Прикладной интерфейс к протоколам 65 Глава 5. API-интерфейс сокетов 73 Глава 6. Алгоритмы и задачи проектирования клиентского программного обеспечения 87 Глава 7. Примеры клиентского программного обеспечения 103 Глава 8. Алгоритмы и задачи проектирования серверного программного обеспечения 121 Глава 9. Последовательные серверы без установления логического соединения (UDP) 145 Глава 10. Последовательные серверы с установлением логического соединения (TCP) 153 Глава 11. Параллельные серверы с установлением логического соединения (TCP) 161 Глава 12. Применение потоков для обеспечения параллельной работы (TCP) 169 Глава 13. Однопотоковые параллельные серверы (TCP) 179 Глава 14. Мультипротокольные серверы (TCP, UDP) 187 Глава 15. Мультисервисные серверы (TCP, UDP) 195 Глава 16. Единообразное и эффективное управление параллельной работой сервера 215 Глава 17. Распараллеливание работы клиентских программ 229 Глава 18. Туннелирование на транспортном и прикладном уровнях 241 Глава 19. Шлюзы прикладного уровня 251
Глава 20. Внешнее представление данных (XDR) 271 Глава 21. Принципы дистанционного вызова процедур (RPC) 283 Глава 22. Построение распределенных программ (принципы использования программы rpcgen) 305 Глава 23. Построение распределенных программ (пример использования программы rpcgen) 317 Глава 24. Принципы работы сетевой файловой системы (NFS) 351 Глава 25. Протоколы сетевой файловой системы (протокол NFS и протокол монтирования) Глава 26. TELNET (структура программы) Глава 27. Клиент TELNET (практическая реализация) Глава 28. Потоковая передача аудио- и видеоинформации (принципы организации и проект протокола RTP) Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации (пример реализации RTP) Глава 30. Практические рекомендации и методы улучшения функционирования серверов Linux Глава 31. Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер Приложение 1. Системные вызовы и библиотечные процедуры, применяемые с сокетами Приложение 2. Операции с дескрипторами файлов и сокетов в системе Linux Список литературы Предметный указатель
Содержание Предисловие 24 Глава 1. Введение и краткий обзор 29 1.1. Межсетевые приложения на основе протоколов TCP/IP 29 1.2. Проектирование приложений для распределенной среды 29 1.3. Стандартные и нестандартные прикладные протоколы 30 1.4. Пример использования стандартного прикладного протокола 30 1.5. Пример сеанса TELNET 31 1.6. Применение протокола TELNET для доступа к другой службе 32 1.7. Прикладные протоколы и функциональные возможности программного обеспечения 33 1.8. Службы с точки зрения того, кто их предоставляет 34 1.9. Основное назначение данной книги 34 1.10. Резюме 35 Материал для дальнейшего изучения 35 Упражнения 36 Глава 2. Модель взаимодействия типа клиент/сервер и проектирование программного обеспечения 37 2.1. Введение 37 2.2. Основная модель сетевого взаимодействия 38 2.3. Терминология и основные понятия 38 2.3.1. Клиенты и серверы 39 2.3.2. Привилегии и сложность 39 2.3.3. Стандартное и нестандартное клиентское программное обеспечение 40 2.3.4. Параметризация клиентов 40 2.3.5. Серверы с установлением и без установления логического соединения 41 2.3.6. Серверы, не поддерживающие и поддерживающие состояние 43 2.3.7. Пример файлового сервера, не поддерживающего состояние 43 2.3.8. Пример файлового сервера, поддерживающего состояние 44 2.3.9. Идентификация клиента 45 2.3.10. Отказ от сопровождения информации о состоянии — это проблема протокола 47 2.3.11. Функционирование серверов в качестве клиентов 47 2.4. Резюме 47 Материал для дальнейшего изучения 49 Упражнения 49 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера 51 3.1. Введение 51 3.2. Обеспечение параллельной работы в сетях 51 3.3. Обеспечение параллельной работы в серверах 53 3.4. Терминология и основные понятия 53 3.4.1. Понятие процесса 53 3.4.2. Совместное использование локальных и глобальных переменных 55 3.4.3. Вызовы процедур 56
3.5. Пример создания параллельного процесса 56 3.5.1. Пример последовательной обработки на языке С 56 3.5.2. Параллельная версия 57 3.5.3. Квантование времени 58 3.5.4. Понятие однопотокового процесса 60 3.5.5. Проявление различий между процессами 60 3.6. Выполнение нового кода 61 3.7. Переключение контекста и проектирование программного обеспечения протокола 61 3.8. Параллельная работа и асинхронный ввод/вывод 62 3.9. Резюме 63 Материал для дальнейшего изучения 63 Упражнения 63 Глава 4. Прикладной интерфейс к протоколам 65 4.1. Введение 65 4.2. Неформальная спецификация программного интерфейса протокола 65 4.2.1. Преимущества и недостатки неформальной спецификации 66 4.3. Функциональные средства интерфейса 66 4.4. Концептуальная спецификация интерфейса 67 4.5. Системные вызовы 67 4.6. Два основных подхода к организации сетевой связи 68 4.7. Основные функции ввода/вывода, применяемые в Linux 69 4.8. Применение функций ввода/вывода Linux для работы с протоколами TCP/IP 4.9. Резюме Материал для дальнейшего изучения Упражнения Глава 5. API-интерфейс сокетов 5.1. Введение 5.2. Сокеты Berkeley 5.3. Определение интерфейса протокола 5.4. Понятие сокета 5.4.1. Дескрипторы сокетов и дескрипторы файлов 5.4.2. Системные структуры данных для сокетов 5.4.3. Перевод сокета в активный или пассивный рейсинг 5.5. Определение адреса оконечной точки 5.6. Обобщенная структура адреса 5.7. Основные системные вызовы в API-интерфейсе сокетов 5.7.1. Вызов функции socket 5.7.2. Вызов функции connect 5.7.3. Вызов функции send 5.7.4. Вызов функции recv 5.7.5. Вызов функции close 5.7.6. Вызов функции bind 5.7.7. Вызов функции listen 5.7.8. Вызов функции accept 5.7.9. Применение функций read и write с сокетами 5.7.10. Сводные данные по вызовам функций сокетов 5.8. Вспомогательные процедуры преобразования 5.9. Применение вызовов функций сокетов в программе 5.10. Символические константы для параметров вызова функций сокетов 8 Содержание
5.11. Резюме 85 Материал для дальнейшего изучения 85 Упражнения 85 Глава 6. Алгоритмы и задачи проектирования клиентского программного обеспечения 87 6.1. Введение 87 6.2. Изучение алгоритмов, а не конкретных реализаций 87 6.3. Архитектура клиента 88 6.4. Определение местонахождения сервера 88 6.5. Синтаксический анализ параметра адреса 90 6.6. Поиск доменного имени 90 6.7. Поиск общепринятого порта по имени 91 6.8. Номера портов и сетевой порядок байтов 92 6.9. Поиск протокола по имени 92 6.10. Алгоритмы клиентов TCP 93 6.11. Распределение сокета 93 6.12. Выбор локального номера порта протокола 94 6.13. Фундаментальная проблема выбора локального IP-адреса 94 6.14. Подключение сокета TCP к серверу 95 6.15. Взаимодействие с сервером с использованием протокола TCP 96 6.16. Получение ответа из соединения TCP 96 6.17. Закрытие соединения TCP 97 6.17.1. Потребность в частичном закрытии 9 7 6.17.2. Операция частичного закрытия 9 7 6.18. Программирование клиента UDP 98 6.19. Подключенные и неподключенные сокеты UDP 98 6.20. Применение функции connect для работы с сокетом UDP 99 6.21. Обмен данными с сервером с использованием протокола UDP 99 6.22. Закрытие сокета, в котором используется протокол UDP 100 6.23. Частичное закрытие сокета UDP 100 6.24. Предупреждение о ненадежности протокола UDP 100 6.25. Резюме 100 Материал для дальнейшего изучения 101 Упражнения 101 Глава 7. Примеры клиентского программного обеспечения 103 7.1. Введение 103 7.2. Значимость небольших примеров 103 7.3. Сокрытие тонкостей реализации 104 7.4. Пример библиотеки процедур для клиентских программ 104 7.5. Реализация процедуры connectTCP 105 7.6. Реализация процедуры connectUDP 105 7.7. Процедура, которая формирует соединения 106 7.8. Применение примеров библиотечных процедур 108 7.9. Служба DAYTIME 109 7.10. Реализация клиента TCP для службы DAYTIME 109 7.11. Чтение данных из соединения TCP 111 7.12. Служба TIME 111 7.13. Доступ к службе TIME 112 7.14. Точные показания времени и сетевые задержки 112 7.15. Клиент UDP для службы TIME 112 7.16. Служба ECHO 114 Содержание 9
7.17. Клиент TCP для службы ECHO 114 7.18. Клиент UDP для службы ECHO 116 7.19. Резюме 118 Материал для дальнейшего изучения 118 Упражнения 118 Глава 8. Алгоритмы и задачи проектирования серверного программного обеспечения 121 8.1. Введение 121 8.2. Концептуальный алгоритм сервера 121 8.3. Сравнение параллельных и последовательных серверов 122 8.4. Сравнение методов доступа с установлением и без установления логического соединения 122 8.5. Характеристики транспортных протоколов 123 8.5.1. Характеристики протокола TCP 123 8.5.2. Характеристики протокола UDP 123 8.6. Выбор транспортного протокола 124 8.7. Серверы с установлением логического соединения 124 8.8. Серверы без установления логического соединения 125 8.9. Нарушения в работе, надежность и отказ от поддержки состояния 126 8.10. Оптимизация серверов, не поддерживающих состояние 127 8.11. Четыре основных типа серверов 129 8.12. Время обработки запроса 130 8.13. Алгоритмы последовательного сервера 130 8.14. Алгоритм последовательного сервера с установлением логического соединения 130 8.15. Привязка к общепринятому адресу с использованием шаблона INADDR_ANY 131 8.16. Перевод сокета в пассивный режим 132 8.17. Прием входящих запросов на установление соединения и их использование 132 8.18. Алгоритм последовательного сервера без установления логического соединения 132 8.19. Формирование адреса ответа в сервере без установления логического соединения 132 8.20. Алгоритмы параллельного сервера 133 8.21. Один ведущий и несколько ведомых потоков 134 8.22. Алгоритм параллельного сервера без установления логического соединения 134 8.23. Алгоритм параллельного сервера с установлением логического соединения 135 8.24. Способы обеспечения параллельной работы сервера 136 8.25. Применение в качестве ведомых потоков отдельных программ 137 8.26. Псевдопараллельная организация работы с применением одного потока 137 8.27. Области применения серверов различных типов 138 8.28. Сводные данные по типам серверов 139 8.29. Важная проблема тупиковой ситуации в работе сервера 140 8.30. Альтернативные реализации 141 8.31. Резюме 141 Материал для дальнейшего изучения 142 Упражнения 142 10
Глава 9. Последовательные серверы без установления логического соединения (UDP) 145 9.1. Введение 145 9.2. Создание пассивного сокета 145 9.3. Организация работы 148 9.4. Пример сервера TIME 149 9.5. Резюме 151 Материал для дальнейшего изучения 151 Упражнения 151 Глава 10. Последовательные серверы с установлением логического соединения (TCP) 153 10.1. Введение 153 10.2. Распределение пассивного сокета TCP 153 10.3. Сервер службы DAYTIME 154 10.4. Схема организации процессов 154 10.5. Пример сервера DAYTIME 155 10.6. Закрытие соединений 157 10.7. Закрытие соединения и уязвимость сервера 158 10.8. Резюме 158 Материал для дальнейшего изучения 159 Упражнения 159 Глава 11. Параллельные серверы с установлением логического соединения (TCP) 161 11.1. Введение 161 11.2. Служба ECHO 161 11.3. Сравнение последовательных и параллельных реализаций 162 11.4. Схема организации процессов 162 11.5. Пример параллельного сервера ECHO 163 11.6. Удаление информации о сбойных процессах из системных таблиц 166 11.7. Резюме 167 Материал для дальнейшего изучения 167 Упражнения 167 Глава 12. Применение потоков для обеспечения параллельной работы (TCP) 169 12.1. Введение 169 12.2. Краткий обзор потоков Linux 169 12.3. Преимущества применения потоков 170 12.4. Недостатки потоков 171 12.5. Дескрипторы, задержка и выход 171 12.6. Завершение работы потока 172 12.7. Координация и синхронизация работы потоков 172 12.7.1. Мьютекс 172 12.7.2. Семафор 173 12.7.3. Условные переменные 173 12.8. Пример сервера, реализованного с применением потоков 174 12.9. Текущий контроль и управление 177 12.10. Резюме 178 Материал для дальнейшего изучения 178 Упражнения 178 Содержание 11
Глава 13. Однопотоковые параллельные серверы (TCP) 179 13.1. Введение 179 13.2. Активизация обработки данных сервером 179 13.3. Управляемая данными активизация обработки в одном потоке 180 13.4. Схема организации работы однопотокового сервера 181 13.5. Пример однопотокового сервера службы ECHO 182 13.6. Резюме 184 Материал для дальнейшего изучения 185 Упражнения 185 Глава 14. Мультипротокольные серверы (TCP, UDP) 187 14.1. Введение 187 14.2. Чем обусловлено стремление уменьшить количество серверов 187 14.3. Проект мультипротокольного сервера 188 14.4. Схема организации процессов 188 14.5. Пример мультипротокольного сервера службы DAYTIME 189 14.6. Понятие разделяемого кода 192 14.7. Параллельные мультипротокольные серверы 192 14.8. Резюме 193 Материал для дальнейшего изучения 193 Упражнения 193 Глава 15. Мультисервисные серверы (TCP, UDP) 195 15.1. Введение 195 15.2. Объединение нескольких серверов в одной программе 195 15.3. Проект мультисервисного сервера без установления логического соединения 196 15.4. Проект мультисервисного сервера с установлением логического соединения 197 15.5. Параллельный мультисервисный сервер с установлением логического соединения 198 15.6. Реализация однопотокового мультисервисного сервера 198 15.7. Вызов отдельных программ из мультисервисного сервера 199 15.8. Мультисервисные и мультипротокольные проекты 200 15.9. Пример с мультисервисным сервером 201 15.10. Статическая и динамическая конфигурация сервера 207 15.11. Суперсервер inetd 208 15.12. Пример с суперсервером inetd 210 15.13. Перечень разновидностей серверов 211 15.14. Резюме 212 Материал для дальнейшего изучения 212 Упражнения 213 Глава 16. Единообразное и эффективное управление параллельной работой сервера 215 16.1. Введение 215 16.2. Выбор между последовательным и параллельным проектами 215 16.3. Степень распараллеливания 216 16.4. Распараллеливание с учетом потребностей 217 16.5. Издержки распараллеливания 217 16.6. Издержки и задержки 217 16.7. Замедления, вызванные малыми задержками 218 12 Содержание
16.8. Предварительное создание ведомых потоков или процессов 219 16.8.1. Предварительное создание ведомых процессов и потоков в системе Linux 220 16.8.2. Применение метода предварительного создания в сервере с установлением логического соединения 220 16.8.3. Мьютекс, блокировка файла и параллельные вызовы функции accept 222 16.8.4. Применение метода предварительного создания в сервере без установления логического соединения 223 16.8.5. Метод предварительного создания, неравномерный трафик и система NFS 224 16.8.6. Применение метода предварительного создания в многопроцессорной системе 224 16.9. Отсроченное создание ведомых процессов 224 16.10. Единая основа обоих методов 226 16.11. Объединение методов 226 16.12. Резюме 227 Материал для дальнейшего изучения 228 Упражнения 228 Глава 17. Распараллеливание работы клиентских программ 229 17.1. Введение 229 17.2. Преимущества параллельной работы 229 17.3. Необходимость в использовании средств управления 230 17.4. Одновременное взаимодействие с несколькими серверами 231 17.5. Принципы разработки параллельных клиентов 231 17.6. Однопотоковые реализации 232 17.7. Пример параллельного клиента, в котором используется служба ECHO 233 17.8. Описание параллельной клиентской программы 237 239 17.9. Оценка результатов применения параллельной организации работы в приведенном выше примере кода 239 17.10. Резюме 239 Упражнения 239 Глава 18. Туннелирование на транспортном и прикладном уровнях 241 18.1. Введение 241 18.2. Мультипротокольная среда 241 18.3. Совместное применение различных сетевых технологий 242 18.4. Динамическое создание каналов 244 18.5. Инкапсуляция и туннелирование 245 18.6. Туннелирование через объединенную сеть IP 245 18.7. Туннелирование на прикладном уровне между клиентами и серверами 246 18.8. Туннелирование, инкапсуляция и коммутируемые телефонные линии 247 18.9. Резюме 248 Материал для дальнейшего изучения 248 Упражнения 248 Содержание 13
Глава 19. Шлюзы прикладного уровня 251 19.1. Введение 251 19.2. Клиенты и серверы в среде с ограниченными возможностями 251 19.2.1. Недостатки ограниченного доступа 251 19.2.2. Компьютеры с ограниченными функциональными возможностями 252 19.2.3. Ограничения, налагаемые на взаимодействие между компьютерами по требованиям защиты 252 19.3. Применение прикладных шлюзов 252 19.4. Обеспечение взаимодействия компьютеров с помощью почтового шлюза 254 19.5. Реализация почтового шлюза 254 19.6. Сравнение прикладных шлюзов и туннелей 255 257 19.7. Прикладные шлюзы и ограничения по обмену данными, существующие в сети Internet 257 19.8. Применение прикладных шлюзов для защиты информации 259 19.9. Прикладные шлюзы и проблема дополнительного участка маршрута 259 19.10. Пример с прикладным шлюзом 261 19.11. Реализация прикладного шлюза 262 19.12. Код прикладного шлюза 264 19.13. Пример обмена сообщениями с помощью шлюза 265 19.14. Применение сценария rfcd в сочетании с файлом .forward или программой slocal 266 19.15. Универсальный прикладной шлюз 266 19.16. Принципы работы программы slirp 267 19.17. Поддержка соединений программой slirp 267 19.18. IP-адресация и slirp 268 19.19. Резюме - 268 Материал для дальнейшего изучения 269 Упражнения 270 Глава 20. Внешнее представление данных (XDR) 271 20.1. Введение 271 20.2. Различные способы представления данных 271 20.3. Проблема асимметричного преобразования и быстрого роста числа его вариантов 272 20.4. Сетевой стандартный порядок байтов 273 20.5. Внешнее представление данных, фактически признанное стандартным 274 20.6. Типы данных XDR 275 20.7. Неявные типы 276 20.8. Программная поддержка для стандарта XDR 276 20.9. Библиотечные процедуры XDR 276 20.10. Последовательная сборка сообщения 277 20.11. Процедуры преобразования библиотеки XDR 278 20.12. Потоки XDR, ввод/вывод и протокол TCP 280 20.13. Записи, границы записей и дейтаграммный ввод/вывод 280 20.14. Резюме 281 Материал для дальнейшего изучения 281 Упражнения 282 14
Глава 21. Принципы дистанционного вызова процедур (RPC) 283 21.1. Введение 283 21.2. Модель дистанционного вызова процедур 283 21.3. Два подхода к разработке распределенных программ 284 21.4. Концептуальная модель локального вызова процедур 285 21.5. Расширение процедурной модели 286 21.6. Выполнение локального вызова процедур и возврат управления 286 21.7. Процедурная модель в распределенных системах 287 21.8. Аналогия между взаимодействием типа клиент/сервер и дистанционным вызовом процедур 288 21.9. Организация средств распределенных вычислений в виде программы 288 21.10. Определение дистанционного вызова процедур, предложенное компанией Sun Microsystems 289 21.11. Удаленные программы и процедуры 289 21.12. Сокращение числа параметров 290 21.13. Обозначение удаленных программ и процедур 290 21.14. Возможность применения нескольких версий одной удаленной программы 291 21.15. Применение принципа взаимного исключения к процедурам удаленной программы 292 21.16. Организация связи • 293 21.17. Обмен данными по принципу доставки не менее одного экземпляра дейтаграммы 293 21.18. Повторная передача запроса RPC 294 21.19. Привязка удаленной программы к порту протокола 295 21.20. Динамическая привязка портов 295 21.21. Алгоритм функционирования службы привязки портов RPC 296 21.22. Формат сообщения ONC RPC 297 21.23. Упорядочение параметров удаленной процедуры 299 21.24. Аутентификация 299 21.25. Пример представления сообщения RPC 300 21.26. Пример поля аутентификации UNIX 300 21.27. Резюме 302 Материал для дальнейшего изучения 302 Упражнения 303 Глава 22. Построение распределенных программ (принципы использования программы rpcgen) 305 22.1. Введение 305 22.2. Применение дистанционных вызовов процедур 305 22.3. Вспомогательные средства программирования RPC 306 22.4. Разделение программы на локальные и удаленные процедуры 308 22.5. Добавление кода, необходимого для дистанционного вызова процедур 308 22.6. Процедуры-заглушки 309 22.7. Применение нескольких удаленных процедур и доставка дистанционного вызова по назначению 310 22.8. Имя клиентской процедуры-заглушки 310 22.9. Применение инструментального средства rpcgen для построения распределенных программ 311 22.10. Вывод программы rpcgen и интерфейсные процедуры 312 Содержание 15
22.11. Входные и выходные данные программы rpcgen 313 22.12. Применение программы rpcgen для построения и клиента, и сервера 313 22.13. Резюме 314 Материал для дальнейшего изучения 315 Упражнения 315 Глава 23. Построение распределенных программ (пример использования программы rpcgen) 317 23.1. Введение 317 23.2. Иллюстрация применения программы rpcgen 317 23.3. Операции со словарем 317 23.4. Восемь этапов создания распределенного приложения 318 23.5. Этап 1: построение нераспределенной прикладной программы 319 23.6. Этап 2: деление программы на две части 323 23.7. Этап 3: создание спецификации rpcgen 328 23.8. Этап 4: выполнение программы rpcgen 330 23.9. Файл .h, сформированный программой rpcgen 330 23.10. Файл вызова процедур преобразования XDR, сформированный программой rpcgen 332 23.11. Код клиентской программы, сформированный программой rpcgen 333 23.12. Код серверной программы, сформированный программой rpcgen 335 23.13. Этап 5: подготовка интерфейсных процедур-заглушек 337 23.13.1. Клиентские интерфейсные процедуры 337 23.13.2. Серверные интерфейсные процедуры 339 23.14. Этап 6: трансляция и компоновка клиентской программы 341 23.15. Этап 7: трансляция и компоновка серверной программы 344 23.16. Этап 8: Запуск серверной программы и вызов клиентской программы на выполнение 345 23.17. Применение утилиты make 346 23.18. Резюме 348 Материал для дальнейшего изучения 349 Упражнения 349 Глава 24. Принципы работы сетевой файловой системы (NFS) 351 24.1. Введение 351 24.2. Сравнение средств дистанционного доступа к файлам и передачи файлов 351 24.3. Операции с файлами на удаленном компьютере 352 24.4. Доступ к файлам в разнородной сетевой среде 352 24.5. Серверы, не поддерживающие состояние 353 24.6. Система NFS и организация работы с файлами в операционной системе UNIX 353 24.7. Обзор файловой системы UNIX 354 24.7.1. Основные определения 354 24.7.2. Последовательность байтов без границ между записями 354 24.7.3. Идентификаторы владельца и группы файла 354 24.7.4. Защита и доступ 355 24.7.5. Организация работы с файлом по принципу "открыть-читать-писать-закрыть" 356 24.7.6. Передача данных 356 24.7.7. Право на поиск в каталоге 357 24.7.8. Произвольный доступ 357 16 Содержание
24.7.9. Выполнение поиска за пределами файла 358 24.7.10. Позиция в файле и параллельный доступ 358 24.7.11. Организация записи при параллельном доступе 359 24.7.12. Имена и пути доступа к файлам 359 24.7.13. Индексный узел: информация, хранимая вместе с файлом 360 24.7.14. Назначение функции stat 361 24.7.15. Механизм именования файлов 362 24.7.16. Монтирование файловой системы 363 24.7.17. Преобразование имен файлов 365 24.7.18. Символические ссылки 365 24.8. Файлы в системе NFS 366 24.9. Типы файлов NFS 366 24.10. Режимы работы с файлами в системе NFS 367 24.11. Атрибуты файла NFS 368 24.12. Клиент и сервер NFS 368 24.13. Клиентские операции в системе NFS 369 24.14. Клиент NFS в системе UNIX 370 24.15. Выполнение операций монтирования в системе NFS 371 24.16. Дескриптор файла 372 24.17. Применение дескрипторов вместо составных имен файлов 373 24.18. Определение позиции в файле при работе с сервером, не поддерживающим состояние 374 24.19. Операции с каталогами 375 24.20. Получение листинга каталога с применением операции, не поддерживающей состояние 375 24.21. Поддержка нескольких иерархий в сервере NFS 376 24.22. Протокол монтирования 376 24.23. Транспортные протоколы, применяемые в системе NFS 377 24.24. Резюме 377 Материал для дальнейшего изучения 378 Упражнения 378 Глава 25. Протоколы сетевой файловой системы (протокол NFS и протокол монтирования) 381 25.1. Введение 381 25.2. Применение средств дистанционного вызова процедур для определения протокола 381 25.3. Определение протокола с использованием структур данных и процедур 382 25.4. Объявления констант, типов и данных NFS 383 25.4.1. Константы NFS 383 25.4.2. Объявления typedef спецификации NFS 384 25.4.3. Структуры данных NFS 384 25.5. Процедуры NFS 386 25.6. Назначение операций NFS 387 25.6.1. NFSPROC3_NULL (процедура 0) 387 25.6.2. NFSPROC3_GETATTR (процедура 1) 387 25.6.3. NFSPROC3JSETATTR (процедура 2) 387 25.6.4. NFSPROC3_LOOKUP (процедура 3) 388 25.6.5. NFSPROC3_ACCESS (процедура 4) 388 25.6.6. NFSPROC3JEIEADLINK (процедура 5) 388 25.6.7. NFSPROC3JREAD (процедура 6) 388 25.6.8. NFSPROC3_WRITE (процедура 7) 388 Содержание 17
25.6.9. NFSPR0C3_CREATE (процедура 8) 388 25.6.10. NFSPROC3_MKDIR (процедура 9) 388 25.6.11. NFSPROC3_SYMLINK (процедура 10) 388 25.6.12. NFSPROC3JVIKNOD (процедура 11) 389 25.6.13. NFSPROC3_REMOVE (процедура 12) 389 25.6.14. NFSPROC3_RMDIR (процедура 13) 389 25.6.15. NFSPROC3_RENAME (процедура 14) 389 25.6.16. NFSPROC3JLINK (процедура 15) 389 25.6.17. NFSPROC3_READDIR (процедура 16) 389 25.6.18. NFSPROC3_READDIRPLUS (процедура 17) 390 25.6.19. NFSPROC3_FSSTAT (процедура 18) 390 25.6.20. NFSPROC3_FSINFO (процедура 19) 390 25.6.21. NFSPROC3JPATHCONF (процедура 20) 390 25.6.22. NFSPROC3_COMMIT (процедура 21) 390 25.7. Протокол монтирования 391 25.7.1. Определения констант протокола монтирования 391 25.7.2. Определения типов монтирования 391 25.7.3. Структуры данных монтирования 391 25.8. Процедуры протокола монтирования 392 25.9. Назначение операций монтирования 393 25.9.1. MOUNTPROC3_NULL (процедура 0) 393 25.9.2. MOUNTPROC3JMNT (процедура 1) 393 25.9.3. MOUNTPROC3JDUMP (процедура 2) 393 25.9.4. MOUNTPROC3JJMNT (процедура 3) 393 25.9.5. MOUNTPROC3JUMNTALL (процедура 4) 394 25.9.6. MOUNTPROC3_EXPORT (процедура 5) 394 25.10. Протокол NFS и средства аутентификации протокола монтирования 394 25.11. Блокировка файла 396 25.12. Изменения в протоколе NFS, связанные с переходом от версии 3 к версии 4 396 25.13. Резюме 396 Материал для дальнейшего изучения 397 Упражнения 397 Глава 26. TELNET (структура программы) 399 26.1. Введение 399 26.2. Краткий обзор 399 26.2.1. Терминал пользователя 399 26.2.2. Команды и управляющая информация 400 26.2.3. Терминалы, окна и файлы 400 26.2.4. Необходимость параллельной организации работы 400 26.2.5. Модель процессов для клиента TELNET 401 26.3. Алгоритм клиента TELNET 402 26.4. Терминальный ввод/вывод в системе Linux 402 26.4.1. Управление драйвером устройства 403 26.5. Установление параметров работы терминала 404 26.6. Глобальные переменные, применяемые для хранения информации о состоянии 406 26.7. Восстановление параметров работы терминала перед выходом из программы 407 26.8. Приостановка и возобновление работы клиентской программы 408 26.9. Спецификация конечного автомата 409 18 Содержание
26.10. Внедрение команд в поток данных TELNET 409 26.11. Согласование опций 410 26.12. Симметрия запросов и предложений 410 26.13. Определение символов TELNET 411 26.14. Конечный автомат для обработки данных, поступающих с сервера 412 26.15. Переходы между состояниями 413 26.16. Реализация конечного автомата 413 26.17. Компактное представление конечного автомата 415 26.18. Форма компактного представления, применяемая во время выполнения 416 26.19. Реализация компактного представления 417 26.20. Построение матрицы переходов конечного автомата 418 26.21. Конечный автомат для вывода в сокет 419 26.22. Определения конечного автомата для вывода в сокет 421 26.23. Конечный автомат для уточнения опции 422 26.24. Определения конечного автомата, уточняющего опции 423 26.25. Инициализация конечного автомата 423 26.26. Параметры клиентской программы TELNET 424 26.27. Основа клиента TELNET % 425 26.28. Реализация основного конечного автомата 429 26.29. Резюме 430 Материал для дальнейшего изучения 430 Упражнения 430 Глава 27. Клиент TELNET (практическая реализация) 433 27.1. Введение 433 27.2. Исполнительные процедуры конечного автомата 433 27.3. Регистрация типа запроса опции 433 27.4. Выполнение пустой операции 434 27.5. Ответ на сообщения WILL/WONT, соответствующие запросу опции эхо-повтора 434 27.6. Ответ на сообщения WILL/WONT, соответствующие не поддерживаемым опциям 436 27.7. Ответ на сообщения WILL/WONT, соответствующие опции подавления символов GA 437 27.8. Выработка ответа DO/DONT на запрос опции двоичной передачи 438 27.9. Ответ за запросы DO/DONT, содержащие требования по применению неподдерживаемых опций 439 27.10. Ответ на запрос DO/DONT по применению опции двоичной передачи 439 27.11. Ответ на запрос DO/DONT с требованием передать информацию о типе терминала 440 27.12. Уточнение опции 442 27.13. Передача информации о типе терминала 443 27.14. Завершение операции уточнения 444 27.15. Передача символа на сервер 444 27.16. Отображение входящих данных на терминале пользователя 446 27.17. Применение обозначений опций termcap для управления терминалом пользователя 448 27.18. Вывод блока данных на сервер 449 27.19. Взаимодействие с клиентским процессом 450 27.20. Ответ на недопустимые команды 451 27.21. Вывод протокола в файл 451 Содержание 19
27.22. Реализация средств ведения протокола 452 27.23. Инициализация средств ведения протокола 452 27.24. Сбор символов имени файла протокола 453 27.25. Открытие файла протокола 454 27.26. Прекращение ведения протокола 455 27.27. Вывод информации о состоянии 456 27.28. Резюме 457 Материал для дальнейшего изучения 457 Упражнения 457 Глава 28. Потоковая передача аудио- и видеоинформации (принципы организации и проект протокола RTP) 459 28.1. Введение 459 28.2. Потоковая служба 459 28.3. Доставка данных в реальном времени 460 28.4. Предусмотренные протоколом средства компенсации неравномерной задержки 460 28.5. Повторная передача, потеря и восстановление 461 28.6. Транспортный протокол передачи дацных в реальном времени 462 28.7. Преобразование и смешивание потоков 463 28.8. Отсроченное воспроизведение и флуктуационные буферы 464 28.9. Протокол управления RTP (RTCP) 465 28.10. Синхронизация нескольких потоков < 466 28.11. Транспортный протокол RTP и передача от многих ко многещ 467 28.12. Сеансы, потоки, порты протокола и демультиплексирование 468 28.13. Основные принципы выбора кодировки 469 28.14. Концептуальная организация программного обеспечения RTP 470 28.15. Структура процессов/потоков 472 28.16. Назначение API-интерфейса 474 28.17. Проект флуктуационного буфера и повторная буферизация 475 28.18. Обработка событий 476 28.19. Нарушения воспроизведения и сложности при обработке отметок времени 476 28.20. Размер библиотеки функций обработки данных в реальном времени 477 28.21. Пример программы воспроизведения МРЗ 478 28.22. Резюме 478 Материал для дальнейшего изучения 479 Упражнения 479 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации (пример реализации RTP) 481 29.1. Введение 481 29.2. Интегрированная реализация 481 29.3. Архитектура программы 481 29.4. Определения протокола RTP 482 29.5. Обработка значений времени 485 29.6. Обработка пропусков в последовательности пакетов RTP 486 29.7. Обработка очереди пакетов RTP 488 29.8. Обработка входных данных RTP 489 29.9. Ведение статистики для формирования отчетов RTCP 491 29.10. Инициализация программного обеспечения RTP 492 29.11. Определения RTCP 496 20 Содержание
29.12. Прием отчетов отправителя RTCP 498 29.13. Выработка отчетов получателя RTCP 499 29.14. Создание заголовка RTCP 500 29.15. Вычисление продолжительности паузы перед формированием очередного отчета RTCP 501 29.16. Выработка сообщения о завершении обмена данными по протоколу RTCP 502 29.17. Объем исходного кода программы, созданной в рамках интегрированной версии 502 29.18. Резюме 503 Материал для дальнейшего изучения 503 Упражнения 504 Глава 30. Практические рекомендации и методы улучшения функционирования серверов Linux 505 30.1. Введение 505 30.2. Функционирование в фоновом режиме 505 30.3. Программирование сервера для работы в фоновом режиме 506 30.4. Открытые дескрипторы и наследование дескрипторов 508 30.5. Программирование сервера для закрытия унаследованных дескрипторов 508 30.6. Сигналы с управляющего терминала 508 30.7. Программирование сервера для отключения от управляющего терминала 509 30.8. Переход в безопасный и известный каталог 509 30.9. Программирование сервера для перехода в другой каталог 509 30.10. Маска umask в системе Linux 510 30.11. Программирование сервера для установки маски umask 510 30.12. Группа процессов 510 30.13. Программирование сервера для выхода из группы процессов 511 30.14. Дескрипторы для стандартных устройств ввода/вывода 511 30.15. Программирование сервера для открытия стандартных дескрипторов 511 30.16. Взаимоисключение для сервера 512 30.17. Программирование сервера для предотвращения запуска нескольких копий 512 30.18. Регистрация идентификатора процесса сервера 513 30.19. Программирование сервера для регистрации его идентификатора процесса 513 30.20. Ожидание завершения работы дочернего процесса 514 30.21. Программирование сервера с учетом необходимости ожидания завершения работы каждого дочернего процесса 514 30.22. Посторонние сигналы 514 30.23. Программирование сервера с учетом предотвращения воздействия посторонних сигналов 515 30.24. Применение средств ведения системного журнала 515 30.24.1. Формирование сообщений для записи в журнал 515 30.24.2. Преимущество перенаправления и использование стандартного устройства вывода сообщений об ошибках 515 30.24.3. Недостатки метода перенаправления ввода/вывода 516 30.24.4. Решение на основе взаимодействия типа клиент/сервер 516 30.24.5. Механизм syslog 517 30.24.6. Классы сообщений syslog 517 Содержание 21
30.24.7. Средства syslog 517 30.24.8. Уровни приоритета syslog 518 30.24.9. Применение системы syslog 518 30.24.10. Пример файла конфигурации системы syslog 519 30.25. Резюме 520 Материал для дальнейшего изучения 521 Упражнения 521 Глава 31. Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер 523 31.1. Введение 523 31.2. Определение тупиковой ситуации 523 31.3. Трудность обнаружения тупиковой ситуации 524 31.4. Предотвращение тупиковой ситуации 525 31.5. Тупиковая ситуация, возникающая при взаимодействии между клиентом и сервером 525 31.6. Предотвращение тупиковых ситуаций при взаимодействии между одной парой приложений 526 31.7. Исчерпание ресурсов сервера, обслуживающего ряд клиентов 526 31.8. Активные соединения и исчерпание ресурсов 527 31.9. Отказ от применения блокирующих операций 528 31.10. Процессы, соединения и связанные с ними ограничения 528 31.11. Циклические зависимости между клиентами и серверами 529 31.12. Изучение зависимостей 530 31.13. Резюме 531 Упражнения 531 Приложение 1. Системные вызовы и библиотечные процедуры, применяемые с сокетами 533 Введение 533 Системный вызов accept 534 Системный вызов bind 535 Системный вызов close 536 Системный вызов connect 537 Системный вызов fork 538 Библиотечная функция gethostbyaddr 539 Библиотечная функция gethostbyname 540 Системный вызов gethostid 541 Системный вызов gethostname 542 Системный вызов getpeername 543 Библиотечная функция getprotobyname 544 Библиотечная функция getservbyname 545 Системный вызов getsockname 546 Системный вызов getsockopt 547 Системный вызов gettimeofday 548 Системный вызов listen 549 •Системный вызов read 550 Системный вызов recv 551 Системный вызов recvfrom 552 Системный вызов recvmsg 553 Системный вызов select 554 Системный вызов send 555 Системный вызов sendmsg 556 22 Содержание
Системный вызов sendto 557 Системный вызов sethostid 558 Системный вызов setsockopt 559 Системный вызов shutdown 560 Системный вызов socket 561 Системный вызов write 562 Приложение 2. Операции с дескрипторами файлов и сокетов в системе Linux 563 Введение 563 Передача дескрипторов в виде неявных параметров 563 Преимущества применения постоянных дескрипторов 564 Необходимость в переупорядочении дескрипторов 564 Переупорядочение дескрипторов 564 Флажок close-on-exec 565 Резюме 565 Материал для дальнейшего изучения 566 Упражнения 566 Список литературы 567 Предметный указатель 576 Содержание 23
Бегущему по волнам! Предисловие Мне выпала особая честь представить сторонникам принципов разработки систем с открытым исходным кодом третий том замечательной серии книг доктора Дугласа Э. Камера — "Сети TCP/IP". Сама история разработки с открытым исходным кодом и развития протоколов TCP/IP служит превосходным примером того, насколько взаимосвязаны эти два направления. Во-первых, только после появления средств обмена данными по сети сложились предпосылки совместной работы программистов из разных стран мира, а во-вторых, одни из самых первых разработок программного обеспечения с открытым исходным кодом были посвящены именно реализации протоколов TCP/IP. Многие еще помнят начало 80-х годов. Это было задолго до того, как разработки с открытым исходным кодом стали излюбленной темой компьютерной прессы. В те дни лишь немногие исследователи сумели понять, какие проблемы лежат в основе архитектуры и реализации сетей. Дуглас и тогда был в числе лидеров. Он возглавил работы по обширной программе исследований и внес огромный вклад в решение многочисленных проблем. В начале 90-х годов впервые появились предпосылки широкомасштабного применения сетевых технологий на производстве, но стал ощущаться большой дефицит знаний и опыта. Тем, кому было поручено создать на предприятии вычислительную среду на основе протоколов объединенных сетей, приходилось решать нелегкие задачи. Дуглас взял на себя обязанности по обучению и подготовке специалистов по сетям. Он раскрывал перед ними все тайны профессии и делился своими знаниями, накопленными нелегким трудом. И вот настал XXI век. На помощь ветеранам пришло новое поколение разработчиков, которые создают распределенные приложения для Internet. Сравнительно недавно были созданы такие замечательные приложения для Internet, как napster, gnutella и inf rase arch. Но к сожалению, лишь немногие из современных программистов освоили фундаментальные принципы проектирования сетей. Откровенно говоря, слишком часто те, кому поручены столь важные разработки, не знают даже основ, и это неизбежно приводит к тому, что вновь созданные приложения недостаточно хорошо масштабируются или даже просто не работают. Именно по этой причине для сообщества пользователей Internet имеет особо важное значение выпуск третьего тома, "Разработка приложений типа клиент/сервер для Linux/POSIX", написанного Дугласом совместно с Дэвидом Л. Стивенсом. По этой замечательной книге можно научиться проектировать и строить приложения типа клиент/сервер и, что важнее всего, оценивать преимущества и недостатки каждого проектного решения. Я надеюсь, что уважаемый читатель сумеет по достоинству оценить педагогический талант доктора Камера. Маршалл Т. Роуз (Marshall T. Rose) Теоретик и агент-реализатор г. Петалума, шт. Калифорния Июнь 2000 года
Пролог Операционная система Linux становится все более популярной и все чаще применяется как система для управления работой многих серверов. Эта новая версия тома 3, в которой используется система Linux, предназначена для программистов, стремящихся изучить все тонкости создания сетевых приложений. В целом, эта книга отвечает на вопрос: "Как применяются протоколы TCP/IP для обеспечения взаимодействия прикладных программ по объединенной сети?" В ней в основном рассматриваются принципы взаимодействия типа клиент/сервер и приведены алгоритмы работы клиентских и серверных компонентов распределенных программ. Каждый проект проиллюстрирован практическим примером, а также описаны все необходимые методы, включая шлюзы прикладного уровня и туннелирование. Кроме того, в книге рассматривается несколько стандартных прикладных протоколов, на примере которых описаны алгоритмы и методы реализации. Хотя этот том можно изучать и использовать отдельно, важным дополнением к нему служат два других тома данной серии. В томе 1 можно найти ответ на вопрос: "Что такое объединенная сеть TCP/IP?" Том 2 посвящен изучению темы "Как функционирует программное обеспечение TCP/IP". В нем представлены дополнительные подробности, описано практическое применение кода и даны более подробные сведения по рассматриваемой теме, чем в томе 1. Итак, хотя программист может научиться создавать сетевые приложения только на основе изучения тома 3, два других тома могут обеспечить более глубокое понимание основополагающих технологий. В этой версии тома 3 описаны новейшие сетевые технологии. Например, в одной главе показано, как использовать в программе Linux средства потоков POSIX для создания параллельного сервера. В главе, посвященной протоколу NFS, описана версия 3, которая, по нашему мнению, должна получить широкое признание среди разработчиков Linux. Кроме того, в эту книгу включены разделы с описанием принципов работы таких программ, как slirp, предоставляющих доступ к Internet по коммутируемому телефонному соединению без наличия на каждом компьютере уникального IP-адреса. Две главы, которые можно охарактеризовать как особенно своевременные, посвящены описанию потоковых и смежных с ними технологий, применяемых для передачи аудио- и видеоинформации по Internet. В главе 28 описаны такие фундаментальные понятия, как протокол RTP (Real-time Transport Protocol — Транспортный протокол передачи в реальном времени), кодирование и флуктуа- ционные буферы. В главе 29 описана реализация протокола RTP, применимая для приема и воспроизведения аудиоинформации в формате МРЗ. Код всех примеров книги доступен в оперативном режиме. Для получения его копии через Web найдите ссылку на том 3 в списке книг по сетям на узле: http://www.cs.purdue.edu/homes/comer/netbooks.html Для получения этого кода по FTP воспользуйтесь URL: ftp://ftp.cs.purdue.edu/pub/Xinu/TCPIP-vol3.linux.dist.tar.Z Книга построена следующим образом. В начальных главах дано вводное описание принципов взаимодействия типа клиент/сервер и интерфейса сокетов, который применяется в прикладных программах для доступа к программному обеспечению протоколов TCP/IP. В них также описаны параллельные процессы и функции операционной системы, которые служат для создания таких процессов. В главах, которые следуют за вступительным материалом, рассматриваются проекты клиентов и серверов. Предисловие 25
Изучение этой книги позволяет понять, что бесчисленное множество возможных проектов сетевых приложений возникло не случайно. Эти проекты подчиняются строгим закономерностям, которые можно постичь, рассмотрев все разнообразие средств организации параллельной работы и сетевого транспорта. Например, в одной главе описан проект непараллельного сервера, в котором используется транспортный протокол с установлением логического соединения (например, TCP), а в другой — аналогичный проект, основанный на использовании транспортного протокола без установления логического соединения (например, UDP). Б книге также описано, как каждый проект вписывается в пространство возможных реализаций, но не предпринимается попытка разработки абстрактной "теории" взаимодействия типа клиент/сервер. В ней сделан акцент на практических принципах и методах проектирования, наиболее важных для программистов. Каждый метод при определенных обстоятельствах имеет свои преимущества и может применяться в прикладном программном обеспечении. Авторы надеются, что понимание концептуальных связей между описанными проектами поможет читателю оценить сильные и слабые стороны каждого подхода и сделать выбор между ними. В книге приведены примеры программ, которые демонстрируют практическое воплощение каждого проекта. В большинстве примеров реализуются стандартные прикладные протоколы TCP/IP. В каждом случае сделана попытка выбрать прикладной протокол, позволяющий довести до читателя рассматриваемую идею проекта без дополнительной сложной информации, затрудняющей понимание. Поэтому каждый пример программы иллюстрирует важное понятие, а некоторые из них превосходны сами по себе. В этой версии тома 3 во всех примерах программ используется механизм сокетов Linux (т.е. API-интерфейс сокетов); две другие версии этого тома содержат часть тех же примеров, выполненных с использованием интерфейса сокетов системы Windows компании Microsoft и интерфейса ТЫ компании AT&T. Последние главы в основном посвящены описанию промежуточного программного обеспечения. В них рассматривается принцип дистанционного вызова удаленных процедур и описано, как этот принцип может применяться для построения распределенных программ. В этих главах показано, как связаны между собой модель вызова удаленных процедур и модель взаимодействия типа клиент/сервер, а также описано программное обеспечение, которое может применяться для формирования клиентских и серверных программ на основе описания интерфейса вызова удаленных процедур. В главах, посвященных протоколу TELNET, показано, насколько важными при разработке производственной программы могут быть даже мелкие подробности и насколько сложным иногда становится код реализации даже самого простого протокола, предназначенного для обмена символьными данными. Соответствующая часть книги заканчивается двумя главами по потоковым транспортным протоколам, упомянутым ранее. Книга в основном посвящена описанию параллельной обработки. Многие описанные понятия покажутся знакомыми студентам, которым уже приходилось разрабатывать параллельные программы, поскольку эти понятия относятся ко всем параллельным программам, а не только к сетевым приложениям. А студентам, которым не приходилось заниматься разработкой параллельных программ, придется сделать определенные усилия по освоению этих понятий. Книга может лечь в основу курса по программированию сокетов продолжительностью в один семестр, предназначенного для студентов старших курсов, или курса по распределенным вычислениям для аспирантов первых лет обучения. Поскольку она в основном посвящена описанию способов использования объединенной сети, а не принципов ее работы, от студентов для понимания этого материала не требуется предварительная подготовка по сетям. Ни одно из рассматриваемых понятий не покажется студентам слишком трудным при том ус- 26 Предисловие
ловии, что преподаватель сумеет найти правильный темп изложения материала. Наилучшей основой для освоения книги может служить базовый курс, состоящий в изложении принципов работы операционных систем, или опыт в области параллельного программирования. Студенты не смогут оценить важность излагаемого материала до тех пор, пока не приступят к его практическому применению. Поэтому любой курс, основанный на этой книге, должен начинаться с упражнений по программированию, которые поставят студентов перед необходимостью применять излагаемые идеи в практических программах. Студенты старших курсов могут изучать излагаемые основы, повторяя описанные проекты в программах реализации других прикладных протоколов. Аспиранты должны строить более сложные распределенные программы, в которых сделан акцент на некоторых более сложных методах (например, методах управления параллельной работой, которые рассматриваются в главе 16, и методах межсоединения, описанных в главах 18 и 19). Авторы хотели бы выразить свою благодарность многим людям за их помощь. Члены исследовательской группы по Internet Университета Пердью предоставили техническую информацию и внесли предложения по улучшению рукописи книги. Майкл Эвангелиста (Michael Evangelista) прочитал корректуру книги и разработал код реализации протокола RTP. Густаво Родригес-Ривера (Gustavo Rodriguez-Rivera) исправил отдельные главы, провел эксперименты для проверки некоторых сведений и отредактировал Приложение 1. Деннис Брилоу (Dennis Brylow) прокомментировал часть глав. Кристина Камер (Christine Comer) отредактировала весь текст, уточнила формулировки и устранила неточности. Дуглас Э. Камер Дэвид Л. Стивене Июль 2000 года Об авторах Доктор Дуглас Камер (Douglas Comer)— признанный в мире специалист по протоколам TCP/IP и сети Internet. Он стоял у истоков Internet и внес огромный вклад в развитие и становление глобальной сети в конце 70-х и начале 80-х годов XX века. Он был членом архитектурного совета по Internet (Internet Architecture Board — IAB) — группы специалистов, определявших стратегию развития глобальной сети. Дуглас также являлся председателем технического комитета и членом исполнительного совета CSNET. Доктор Дуглас Камер консультирует различные компании по вопросам разработки и развертывания сетей, а также проводит по всему миру семинары по протоколам TCP/IP и созданию сетей на их основе. Его лекции рассчитаны как на профессионалов, так и на рядовых пользователей Internet. Дуглас написал операционную систему Xinu и создал собственную реализацию протоколов TCP/IP. Все это он отразил в своих книгах. Написанное им программное обеспечение используется во многих коммерческих продуктах. Доктор Камер является профессором компьютерных наук Университета Пердью (Purdue University), где он преподает и занимается научно-исследовательской работой в области локальных и глобальных компьютерных сетей и операционных систем. Кроме написания серии научно-технических книг, ставших бестселлерами во всем мире, Дуглас редактирует североамериканский журнал Software — Practice and Experience. Доктор Камер является также действительным членом ассоциации ACM (Association of Computing Machinery — Ассоциация пользователей вычислительных машин). Предисловие 27
Дополнительную информацию о нем вы можете получить в Internet по адресу: www.cs.purdue.edu/people/comer Дэвид Стивене получил степень бакалавра наук A985) и магистра наук A993) по информатике в Университете Пердью. С 1983 года он был системным программистом UNIX и работал в основном с версиями ядра BSD UNIX. Он подготовил варианты реализации основной части набора протоколов для Internet и выпустил несколько учебников по компьютерным наукам совместно с доктором Камером. Область его профессиональных интересов включает операционные системы, компьютерные сети и проектирование крупномасштабных программных систем. В последние годы Стивене работал в области масштабируемых сетей на базе высокоэффективных многопроцессорных систем по поручению компании Sequent Computer Systems и корпорации IBM. Он также является почетным членом АСМ и IEEE (Institute of Electrical and Electronics Engineers — Институт инженеров по электротехнике и электронике). Мнения специалистов по поводу версии Linux/POSIX тома 3 серии "Сети TCP/IP" Дастин Босуэлл (Dustin Boswell), компания Caltech: — Безусловно, это самая лучшая книга по данной теме, которую я когда-либо читал. Спасибо авторам за то, что теперь можно легко понять, как действуют сокеты. Джон Лин (John Lin), компания Bell Labs: — Превосходная книга для тех, кто изучает разработку клиентских и серверных приложений TCP/IP. В ней наглядно описаны важные понятия и приведены примеры действующих программ; такое сочетание становится чрезвычайно эффективным способом изучения данной темы. Джейкоби Свэйтс (Jacoby Thwaites): — Ваша книга оказалась для меня исключительно ценной. Примите мою искреннюю благодарность. Роб Молони (Rob Moloney): — Наслаждаюсь ясностью и глубиной изложения! Маршалл Роуз (Marshall Rose): — Для сообщества пользователей Internet имеет особо важное значение выпуск третьего тома (Разработка приложений типа клиент/сервер для Linux/ POSIX), написанного Дугласом совместно с Дэвидом Л. Стивенсом. По этой замечательной книге можно научиться проектировать и строить приложения типа клиент/сервер и, что важнее всего, понять, в чем состоят преимущества и недостатки каждого проектного решения. 28 Предисловие
1 Введение и краткий обзор 1.1. Межсетевые приложения на основе протоколов TCP/IP Протоколы TCP/IP лежат в основе технологий, которые обеспечивают бесперебойное функционирование объединенных сетей. Существует огромное количество разнообразных приложений, которые могут применяться в объединенных сетях TCP/IP. В их число входят не только приложения, основанные на стандартных протоколах, но и такие программы, о существовании которых известно только узкому кругу специалистов. Наиболее широко распространенные приложения связаны с глобальной сетью Internet и системой World Wide Web. В их числе можно назвать приложения просмотра ресурсов Web, комнаты чата, а также потоковые приложения, известные под общим названием средств Web-вещания. На предприятиях технологии TCP/IP используются также и в других целях. Например, в одной из компаний на протоколах TCP/IP основаны программы, используемые для контроля и управления морскими нефтяными платформами, а в другой — для управления складскими запасами. Ряд отелей использует TCP/IP в своей системе резервирования номеров, где каждый заказ передается по частной объединенной сети TCP/IP. Кроме того, во многих крупных сетях приложения TCP/IP применяются для контроля и управления сетевым оборудованием. И наконец, постоянно появляются все новые и новые приложения. Безусловно, технология объединенных сетей получила широкое распространение. В настоящее время в рамках этой технологии создается основной объем трафика общедоступных и частных сетей всего мира. Область ее применения в Европе, Азии, Африке, Америке и в странах Тихоокеанского региона постоянно расширяется. 1.2. Проектирование приложений для распределенной среды Поскольку сетевые средства вошли в состав всего программного обеспечения, программисты обязаны знать их основы и понимать, какие принципы и методы используются при проектировании и реализации распределенных приложений. В этой книге авторы стремились показать, что одной из основных целей распределенных вычислений является обеспечение прозрачности доступа. В задачу программиста входит создание распределенных программ, которые действуют в максимально возможной степени аналогично обычным версиям тех же программ. Поэтому цель развития распределенных вычислений состоит в создании среды, которая скрывает сам факт территориальной удаленности компьютеров и служб и создает впечатление того, что они являются локальными.
1.3. Стандартные и нестандартные прикладные протоколы Набор протоколов TCP/IP и так уже включает много прикладных протоколов, но буквально ежедневно появляются все новые и новые протоколы. По существу, создание двух не встречавшихся ранее программ, в которых для связи применяются протоколы TCP/IP, равносильно изобретению нового прикладного протокола. Безусловно, некоторые прикладные протоколы описаны, стандартизированы и включены в состав официально утвержденного набора протоколов TCP/IP. Такие протоколы принято называть стандартными прикладными протоколами. Другие протоколы, разработанные прикладными программистами для частного использования, называются нестандартными прикладными протоколами. Большинство сетевых администраторов стремятся по мере возможности использовать стандартные прикладные протоколы; не имеет смысла изобретать новый прикладной протокол, если все необходимое предоставляет уже существующий протокол. Например, набор протоколов TCP/IP включает стандартные прикладные протоколы для таких служб, как передача файлов, дистанционный доступ к данным и электронная почта. Поэтому программисту следует использовать для таких служб стандартный протокол. 1.4. Пример использования стандартного прикладного протокола Хотя в сеансах дистанционного доступа данные генерируются только со скоростью, соответствующей возможностям человека по вводу данных и восприятию информации, объем трафика в таких сеансах находится на десятом месте среди наиболее интенсивных источников пакетов в объединенной сети Internet. Многие пользователи выполняют основную часть своей работы в сеансах дистанционного доступа; они не имеют непосредственного подключения к компьютерам, используемым для выполнения основной части вычислений. Набор протоколов TCP/IP включает стандартный прикладной протокол дистанционного доступа под названием TELNET. Протокол TELNET определяет формат данных, которые должны быть переданы прикладной программой на удаленный компьютер для регистрации в системе, а также формат сообщений, отправляемых в ответ удаленным компьютером. В нем указано, как должны быть закодированы символьные данные для передачи, и какие специальные сообщения передаются для управления сеансом или аварийного прерывания дистанционно выполняемой операции. Для большинства пользователей не являются существенными подробные сведения о том, как кодируются данные, передаваемые по протоколу TELNET; пользователь может просто вызвать программное обеспечение и обратиться с его помощью на удаленный компьютер, не зная и не задумываясь о том, как именно работает это программное обеспечение. По сути, применение удаленной службы обычно является не более сложным, чем локальной. Например, в компьютерных системах, на которых работают протоколы TCP/IP, обычно предусмотрена команда, предназначенная для вызова на выполнение программного обеспечения TELNET. Во многих системах эта команда называется telnet. Для ее вызова пользователь должен ввести следующее: telnet machine Здесь параметр machine обозначает доменное имя компьютера, к которому должен быть получен дистанционный доступ. Поэтому для создания соединения TELNET с компьютером example.com пользователь вводит: telnet example.com 30 Глава 1. Введение и краткий обзор
С точки зрения пользователя, работающая программа telnet открывает окно, в котором можно ввести команду для выполнения на удаленном компьютере. После установления соединения приложение telnet обеспечивает двухстороннюю связь. До тех пор, пока окно программы остается открытым, telnet передает каждый символ, введенный пользователем, на удаленный компьютер и выводит каждый символ, поступающий с удаленного компьютера, на экран компьютера пользователя. Обычно после подключения пользователя программы telnet к удаленному компьютеру операционная система этого компьютера передает пользователю запрос, чтобы он назвал себя, введя идентификатор регистрационной записи и пароль. Это приглашение к вводу регистрационных данных, отображаемое для пользователя telnet, выглядит на экране точно так же, как и приглашение, отображаемое при регистрации пользователя на локальном компьютере. Поэтому протокол TELNET обеспечивает создание для пользователя иллюзии того, что он подключен непосредственно к удаленному компьютеру. 1.5. Пример сеанса TELNET Например, рассмотрим, что происходит, когда пользователь вызывает программу telnet и подключается к компьютеру purdue.edu: telnet purdue.edu Trying... Connected to purdue.edu. Escape character is ,A]'. SunOS 5.6 login: Самое первое выводимое сообщение, т.е. Trying..., появляется после того, как программа telnet преобразует имя компьютера в IP-адрес и предпримет попытку создать действительное соединение TCP с компьютером, имеющим этот адрес. После установления соединения программа telnet выводит вторую и третью строки, сообщая пользователю, что попытка подключения была успешной, и указывая специальный символ, который пользователь может ввести, чтобы выйти на время из приложения telnet в случае необходимости (например, если возникнет сбой и пользователю потребуется аварийно прервать соединение). Обозначение А] показывает, что пользователь должен удерживать нажатой клавишу <Ctrl>, нажимая клавишу закрывающей квадратной скобки <]>. Последние строки вывода поступают от удаленного компьютера. Они сообщают, что на нем работает операционная система SunOS версии 5.6, и предоставляют стандартное приглашение к регистрации. Курсор останавливается после сообщения login:, а программа переходит в состояние ожидания ввода пользователем действительного идентификатора учетной записи. Для того чтобы можно было продолжить работу в сеансе TELNET, пользователь должен иметь учетную запись на удаленном компьютере. После ввода пользователем действительного идентификатора учетной записи удаленный компьютер запрашивает пароль и разрешает доступ, только если идентификатор регистрационной учетной записи и пароль являются действительными. 1.5. Пример сеанса TELNET 31
1.6. Применение протокола TELNET для доступа к другой службе В протоколах TCP/IP для обозначения прикладных служб на конкретном компьютере используются номера портов протокола. Программное обеспечение, реализующее определенную службу, ожидает постушшшя запросов через заранее определенный порт протокола, который принято Использовать для данной службы. Например, службе дистанционного доступа, в которой используется протокол TELNET, присвоен общепринятый номер порта 23. Если пользователь вызывает программу telnet, по умолчанию выполняется подключение к порту 23 указанного компьютера. Следует отметить, что протокол TELNET может также применяться для работы со службами, отличными от стандартной службы дистанционного доступа. Для этого пользователь должен указать номер порта протокола необходимой службы. В большинстве реализаций предусмотрена возможность указывать при вызове программы telnet из командной строки необязательный второй параметр, который позволяет пользователю назначить другой порт протокола. При использовании версий telnet, имеющих графический интерфейс, пользователь должен выбрать пункт меню, в котором указан номер порта. В любом случае, если порт не задан, программа telnet использует порт 23. Но если пользователь указывает номер порта, программа telnet подключается не к порту 23, а к указанному порту. Например, если пользователь введет telnet purdue.edu 13 программа telnet создаст соединение с номером порта протокола 13 на компьютере purdue.edu. Порт 13 не соответствует обычной службе дистанционного доступа, поскольку удаленный компьютер перенаправляет все соединения TCP с портом 13 к службе дневного времени, которая сообщает местную дату и время. Непосредственно после подключения программы telnet к порту 13 не поступает приглашение к вводу данных от удаленного компьютера. Однако на экране отображаются три строки вывода, поскольку они поступают of программы telnet, а не от службы на удаленном компьютере: telnet purdue.edu 13 Trying... Connected to purdue.edu. Escape character is '*]'. Рассматриваемая служба удаленного компьютера вырабатывает только одну строку вывода, содержащую дату и время. После передачи этих данных удаленная система закрывает соединение: Sun Jan 16 21:52:08 2000 Connection closed by foreign host. Служба FINGER, к которой можно получить доступ через порт 79, может служить примером интерактивной службы, доступ к которой предоставляет протокол TELNET. Служба FINGER позволяет получить информацию о пользователях, зарегистрированных на компьютере, или информацию о конкретном пользователе. При работе с этой службой можно ввести пустую строку (чтобы передать запрос на получение списка пользователей компьютера) или набрать имя пользователя, а затем ввести символ возврата каретки, т.е. нажать клавишу <Enter> (чтобы передать запрос на получение информации о конкретном пользователе). 32 Глава 1. Введение и краткий обзор
Хотя служба FINGER является интерактивной, она не выводит приглашение автоматически, а ожидает, пока пользователь не введет запрос, на который она должна ответить. Например, чтобы получить с помощью службы FINGER через порт 79 компьютера purdue.edu информацию о пользователе по имени John Rice, необходимо вызвать программу telnet, подождать установления соединения, а затем ввести это имя. В ответ служба предоставит информацию об этом пользователе. Пример сеанса с интерактивной службой приведен ниже: telnet purdue.edu 79 Trying... Connected to purdue.edu. Escape character is ,A]'. John Rice Output of your query: John Rice Name Dept/School Phone Status Email John Richard Rice Computer Science +1 765 49-46007 staff jrr8 cs.purdue.edu 1.7. Прикладные протоколы и функциональные возможности программного обеспечения Как показывают приведенные выше примеры, иногда единственный компонент программного обеспечения, в данном случае программа telnet, может применяться для доступа к нескольким разным службам. Проект протокола TELNET и пример его использования для доступа к службам DAYTIME и FINGER иллюстрируют два важных принципа. Во-первых, задача всех проектов протоколов состоит в поиске фундаментальных подходов, которые могут повторно применяться в нескольких приложениях. С практической точки зрения, протокол TELNET приемлем для широкого круга служб, поскольку он предоставляет основные функциональные средства интерактивной связи. По сути, протокол, используемый для доступа к службе, остается независимым от самой службы. Во-вторых, разработчики прикладных служб стремятся при любой возможности использовать стандартные прикладные протоколы. К службе FINGER, описанной выше, можно легко получить доступ, поскольку в ней для связи используется стандартный протокол TELNET. Кроме того, поскольку большая часть программного обеспечения TCP/IP включает ту или иную прикладную программу, которую пользователи могут вызвать для работы по протоколу TELNET, для доступа к дополнительным службам не требуется дополнительное клиентское программное обеспечение. Проектировщики, разрабатывающие новые интерактивные приложения, могут повторно использовать значительную часть программного обеспечения, если они в качестве протокола доступа выбирают TELNET. Протокол TELNET обеспечивает максимальную гибкость, поскольку определяет только средства интерактивного взаимодействия, а не подробные сведения о службе, к которой предоставляется доступ, поэтому он может использоваться в качестве механизма связи для многих интерактивных служб, а не только для службы дистанционного доступа. 1.7. Прикладные протоколы... 33
1.8. Службы с точки зрения того, кто их предоставляет В примерах прикладных служб, приведенных выше, было показано, как они выглядят с точки зрения отдельного пользователя. Пользователь вызывает на выполнение программу, которая обращается к службе дистанционного доступа, и рассчитывает на то, что получит ответ без задержки или с небольшой задержкой. С точки зрения провайдера, который предоставляет пользователю возможность обратиться к службе, ситуация выглядит совсем иначе. Например, может случиться так, что пользователи многих сетевых узлов одновременно захотят воспользоваться конкретной службой. Понятно, что каждый пользователь будет стремиться получить ответ без задержки. Для обеспечения возможности быстро выдавать ответы и обрабатывать много запросов в компьютерной системе, предоставляющей доступ к прикладной службе, необходимо использовать параллельную обработку. Это означает, что провайдер службы не может заставлять нового пользователя ожидать, пока выполняются запросы предыдущих пользователей. Таким образом, программное обеспечение должно одновременно обрабатывать сразу несколько запросов. Работа параллельных прикладных программ внешне кажется непостижимой. Создается впечатление, что единственная прикладная программа выполняет сразу несколько действий. Например, если речь идет о службе TELNET, то программное обеспечение службы дистанционного доступа должно предоставлять многочисленным пользователям возможность подключиться к указанному компьютеру, управляя при этом несколькими активными сеансами дистанционного доступа. Обмен данными в одном сеансе дистанционного доступа должен выполняться без помех со стороны других сеансов. Аналогичным образом, программное обеспечение службы FINGER должно позволять многочисленным пользователям запрашивать информацию в одно и то же время, не нарушая работу друг друга. В связи с необходимостью обеспечения параллельной работы, проектирование, реализация и сопровождение сетевого программного обеспечения в значительной степени усложняются. Поэтому приходится применять специализированные алгоритмы и методы программирования. Кроме того, поскольку параллельная организация работы усложняет отладку, программисты должны уделять особое внимание документированию своих проектов и следовать принципам качественного программирования. И наконец, программисты должны тщательно соблюдать принципы организации параллельной работы, в частности, выбирать такую степень распараллеливания, которая позволяет обеспечить максимальную производительность, поскольку выбор слишком высокой или слишком низкой степени распараллеливания приводит к снижению производительности. 1.9. Основное назначение данной книги Настоящая книга должна помочь прикладным программистам изучить принципы проектирования, создания и оптимизации сетевого прикладного программного обеспечения. В ней описаны фундаментальные алгоритмы, применяемые и в последовательных, и в параллельных вариантах реализации прикладных протоколов, а также приведены подробные примеры для каждой реализации. Хотя в примерах используются протоколы TCP/IP, изложение в основном направлено на описание принципов, алгоритмов и методов общего назначения, которые распространяются на большинство сетевых протоколов. В ней рассматриваются преимущества и недостатки каждого метода и показано значение средств параллельной работы в проекте сервера. В последних главах рассматриваются некоторые тонкости управления параллельной работой и дан обзор методов, позволяющих программисту автоматически оптимизировать производительность. 34 Глава 1. Введение и краткий обзор
Задача обеспечения параллельного доступа к прикладным службам является важной и сложной, поэтому во многих главах настоящей книги подробно описаны параллельные версии программного обеспечения прикладных протоколов. В каждой главе этой книги рассматривается один из возможных проектов. В первых главах представлена модель взаимодействия типа клиент/сервер, описаны транспортные протоколы с установлением логического соединения и без его установления, а также описан интерфейс прикладного программирования (API — Application Program Interface) сокетов. В следующих главах рассматриваются конкретные алгоритмы и методы реализации, применяемые в клиентском и серверном программном обеспечении, а также наиболее интересные сочетания алгоритмов и методов управления параллельной работой. Кроме описания алгоритмов клиентского и серверного программного обеспечения, в книге представлены такие общие методы, как туннелирование, шлюзы прикладного уровня и дистанционный вызов удаленных процедур. И наконец, в ней рассмотрено несколько стандартных прикладных протоколов (NFS и TELNET) и описаны такие протоколы, как RTP. В большинстве глав приведены примеры программного обеспечения, которые позволяют пояснить рассматриваемые принципы. Это программное обеспечение неразрывно связано с описанием в книге. Оно наглядно показывает, как взаимодействуют между собой отдельные компоненты и как рассматриваемые концепции реализуются в работающих программах. 1.10. Резюме При создании распределенных приложений в качестве транспортного механизма широко применяются основные протоколы Internet (протоколы TCP/IP). Программисты, которые приступают к проектированию и реализации такого программного обеспечения, должны иметь представление о модели взаимодействия типа клиент/сервер, знать функциональные возможности транспортных протоколов, изучить интерфейсы операционной системы, применяемые в прикладных программах для доступа к программному обеспечению протокола, освоить фундаментальные алгоритмы, которые служат для реализации клиентского и серверного программного обеспечения, и овладеть методами, основанными на применении прикладных шлюзов. Большинство сетевых служб обеспечивают одновременный доступ к ним большого числа пользователей. Для поддержки одновременного доступа многих пользователей применяется параллельное серверное программное обеспечение. Значительная часть книги посвящена описанию методов обеспечения параллельного функционирования прикладных протоколов и анализу проблем управления параллельными процессами. Материал для дальнейшего изучения Руководства, которые входят в поставку операционных систем, содержат информацию о том, как вызывать команды доступа к службам, подобным TELNET. На многих узлах набор стандартных команд дополняется командами, которые были разработаны для этих узлов. Проконсультируйтесь с администратором узла, чтобы узнать, какие программные интерфейсы находятся в распоряжении пользователей. 1.10. Резюме 35
Упражнения 1.1. Воспользуйтесь протоколом TELNET на своем локальном компьютере для подключения к другому компьютеру. Была ли вами обнаружена задержка при подключении к компьютеру, находящемуся в той же локальной сети? Появилась ли задержка при подключении к удаленному компьютеру? 1.2. Прочитайте руководство по операционной системе, чтобы узнать, допускает ли реализованная в ней версия программного обеспечения TELNET подключение к порту удаленного компьютера, отличному от стандартного порта, который используется для дистанционной регистрации. 1.3. Определите набор служб TCP/IP, доступных на вашем локальном компьютере. 1.4. Воспользуйтесь программой FTP для получения файла с удаленного узла. Если это программное обеспечение не предоставляет статистических данных, оцените скорость передачи большого файла иным способом. Была ли скорость передачи выше или ниже, чем вы ожидали? 1.5. Воспользуйтесь командой finger для получения информации о пользователях удаленного узла. 36 Глава 1. Введение и краткий обзор
2 Модель взаимодействия типа клиент/сервер и проектирование программного обеспечения 2.1. Введение С точки зрения прикладного программиста, протоколы TCP/IP, как и большинство других протоколов компьютерной связи, просто предоставляют основные механизмы передачи данных. В частности, протоколы TCP/IP позволяют установить соединение между двумя прикладными программами и передавать данные в прямом и обратном направлениях. Поэтому принято считать, что протоколы TCP/IP обеспечивают одноранговую или прямую связь. Взаимодействующие приложения могут выполняться на одном и том же или на разных компьютерах. Хотя протоколы TCP/IP подробно регламентируют процесс передачи данных между двумя взаимодействующими приложениями, они не определяют требований ко времени или содержанию обмена между приложениями, а также не включают требований к организации таких прикладных программ в распределенной среде. На практике широко распространен определенный метод организации работы прикладных программ на основе протоколов TCP/IP, который реализуется почти во всех приложениях. Он известен под названием метода организации взаимодействия типа клиент/сервер1. По существу, взаимодействие типа клиент/сервер приобрело в одноранговых сетевых системах такое важное значение, что стало основой большинства способов компьютерной связи. В настоящей книге взаимодействие типа клиент/сервер применяется для описания всех методов прикладного программирования. Оно считается основой модели клиент/сервер, служит для описания функций клиентских и серверных компонентов и позволяет показать, как строить клиентское и серверное программное обеспечение. Прежде чем перейти к описанию принципов разработки программного обеспечения, необходимо определить основные понятия и терминологию модели клиент/сервер. В следующих разделах определена терминология, применяемая во всей книге. Хотя в современной маркетинговой литературе вычисления, организованные по принципу взаимодействия типа клиент/сервер, часто называют "вычислениями по принципу приложение/сервер", мы придерживаемся первоначального термина.
2.2. Основная модель сетевого взаимодействия Необходимость применения принципа организации взаимодействия типа клиент/сервер связана с решением проблемы согласования условий соединения. Чтобы понять, в чем состоит эта проблема, представьте себе, что перед программистом стоит задача запустить две программы на отдельных компьютерах и обеспечить взаимодействие между ними. Необходимо также учитывать, что компьютеры выполняют свои действия на несколько порядков быстрее, чем люди. После запуска первая программа приступает к выполнению и отправляет сообщение программе, с которой она должна вступить во взаимодействие. Через несколько миллисекунд она определит, что другая программа еще не запущена, поэтому выдаст сообщение об ошибке и завершит свою работу. Тем временем программист запустит вторую программу. К сожалению, после запуска на выполнение вторая программа обнаружит, что не функционирует (поскольку уже завершила свою работу) та программа, с которой она должна взаимодействовать. Но даже если эти две программы не завершат свое выполнение, а будут продолжать посылать друг другу сообщения, вероятность того, что обе они отправят сообщения друг другу своевременно и смогут их получить, является очень низкой, поскольку компьютеры имеют высокое быстродействие и при такой организации взаимодействия переходят в состояние готовности к приему данных лишь на очень короткое время. Модель взаимодействия типа клиент/сервер предусматривает решение проблемы согласования условий соединения наиболее простым способом: она требует, чтобы для обеспечения взаимодействия любой пары приложений один из участников соединения приступал к работе заранее и ждал (в течение неопределенно долгого времени) до тех пор, пока к нему не обратится второй участник соединения. Это очень важное решение; оно позволяет упростить соответствующее программное обеспечение протокола, поскольку не требует, чтобы в нем самом было предусмотрено формирование ответов на входящие запросы по установлению связи. Поскольку модель взаимодействия типа клиент/сервер предусматривает, что за согласование условий соединения отвечают приложения, в протоколах TCP/IP не нужно создавать механизмы автоматического запуска на выполнение программы при получении сообщения. Вместо этого предусмотрено, что одна из программ должна быть переведена в состояние готовности приема запросов на установление связи еще до того, как они поступят. Для обеспечения того, чтобы такие программы были всегда готовы к приему запросов на установление связи, большинство системных администраторов предусматривают автоматический запуск программ связи во время начальной загрузки операционной системы. После запуска программа работает неопределенно долгое время и ожидает поступления запросов к предоставляемой ею службе. 2.3. Терминология и основные понятия Модель взаимодействия типа клиент/сервер предусматривает разбиение приложений связи на две широкие категории в зависимости от того, должно ли приложение ожидать запросов на установление связи или самостоятельно их инициировать. В настоящей главе приведено краткое, исчерпывающее определение этих двух категорий, а в следующих главах приведены примеры приложений обоих категорий и описаны многие нюансы, касающиеся данной темы. 38 Глава 2. Модель взаимодействия типа клиент/сервер...
2.3.1. Клиенты и серверы Принцип взаимодействия типа клиент/сервер предусматривает использование направления активизации взаимодействия для определения того, какая из двух программ является клиентом или сервером. В общем, приложение, являющееся инициатором одноранговой связи, называется клиентом. Клиентское программное обеспечение обычно вызывают на выполнение конечные пользователи, когда у них возникает необходимость обратиться к сетевой службе (например, клиентской программой является Web-броузер). Основная часть клиентского программного обеспечения реализована в виде обычных прикладных программ. При каждом запуске на выполнение клиентское приложение обращается к серверу, передает запрос и ожидает ответ. После получения ответа клиент продолжает свою работу. Клиентские программы часто бывают проще в реализации по сравнению с серверными, и для их запуска на выполнение обычно не требуются специальные системные привилегии. В отличие от этого, сервером является любая программа2, которая ожидает поступления запросов на установление соединения от клиента. Сервер получает запрос клиента, выполняет все необходимые операции и возвращает результаты клиенту. 2.3.2. Привилегии и сложность Для выполнения необходимых операций и возврата результатов серверное программное обеспечение часто нуждается в доступе к объектам, которые защищены операционной системой (например, к файлам, базам данных, устройствам или портам протоколов). В связи с этим серверное программное обеспечение обычно работает с особыми системными привилегиями. Поскольку работающий сервер имеет особые привилегии, необходимо следить за тем, чтобы он не мог непреднамеренно передать свои привилегии клиентам, которые его используют. Например, в файловом сервере, который функционирует как привилегированная программа, необходимо обеспечить тщательный контроль за тем, чтобы доступ к конкретному файлу мог получить только определенный клиент. При разработке серверной программы нельзя полагаться на обычные средства контроля доступа операционной системы, поскольку привилегированный статус этой программы позволяет получить доступ к любому файлу. Как правило, серверная программа содержит код, выполняющий многие действия, перечисленные ниже, которые относятся к сфере защиты: ¦ аутентификация — проверка подлинности клиента; ¦ авторизация — определение того, разрешено ли данному клиенту обращаться к службе, предоставляемой сервером; ¦ защита данных — предотвращение возможности непреднамеренного раскрытия или компрометации конфиденциальных данных; ¦ секретность — предотвращение возможности несанкционированного доступа; ¦ защита — предотвращение возможности недопустимого использования системных ресурсов сетевыми приложениями. Как показано в следующих главах, серверы, выполняющие трудоемкие вычисления или обрабатывающие большие объемы данных, функционируют более С формальной точки зрения, сервер представляет собой программу, а не аппаратное устройство. Однако пользователи компьютеров часто ошибочно применяют этот термин к компьютеру, на котором работает та или иная серверная программа. Например, иногда можно услышать: "Этот компьютер — наш файловый сервер". Но в таких случаях следует говорить: "На этом компьютере работает программа нашего файлового сервера". 2.3. Терминология и основные понятия 39
эффективно, если обладают способностью обрабатывать одновременно несколько запросов. Необходимость соблюдения требований по безопасному использованию привилегий и организации параллельной работы обычно влечет за собой то, что серверы становятся более сложными в проектировании и реализации по сравнению с клиентами. В следующих главах приведено много примеров, которые показывают, в чем состоит различие между клиентами и серверами. 2.3.3. Стандартное и нестандартное клиентское программное обеспечение В главе 1 были упомянуты два широких класса клиентских прикладных программ: применяющие стандартные службы TCP/IP (например, электронную почту) или службы, установленные только на данном сетевом узле (например, собственную систему базы данных организации). К стандартным принадлежат такие службы, которые определены в наборе протоколов TCP/IP и которым присвоены широко известные и принятые во всем мире идентификаторы портов протоколов (для обозначения которых в дальнейшем применяется термин общепринятый). В этой книге все прочие службы именуются определенными локально прикладными службами или нестандартными прикладными службами. Различия между стандартными и нестандартными службами имеют значение только при организации взаимодействия вне пределов локальной среды. В каждой конкретной вычислительной среде системные администраторы обычно предусматривают определение имен служб таким образом, чтобы пользователи не могли различить между собой локальные и стандартные службы. Однако программисты, которые разрабатывают сетевые приложения, предназначенные для использования на других сетевых узлах, должны понимать, в чем состоит различие между этими службами, и следить за тем, чтобы не возникала ненужная зависимость от служб, доступ к которым можно получить только в определенном месте. Хотя в набор протоколов TCP/IP входит много стандартных прикладных протоколов, большинство поставщиков компьютеров предоставляют в составе программного обеспечения TCP/IP лишь небольшую часть стандартных прикладных клиентских программ. Например, программное обеспечение TCP/IP обычно включает: клиент дистанционного доступа, в котором используется стандартный протокол TELNET, клиент электронной почты, в котором для передачи или доступа к электронной почте используется стандартный протокол SMTP или POP, клиент службы передачи файлов, в котором применяется стандартный протокол FTP для передачи файлов с одного компьютера на другой, и Web-броузер, в котором используется стандартный протокол HTTP для доступа к документам Web. Безусловно, во многих организациях по отдельному заказу разрабатываются также приложения, в которых для связи используется Internet. Заказные, нестандартные приложения могут быть простыми или сложными и предоставлять доступ к таким разнообразным службам, как передача аудио- или видеоинформации, речевая связь, дистанционный сбор данных в реальном времени, оперативное резервирование, а также могут обеспечивать доступ к распределенным базам данных, распространение информации о погоде и дистанционное управление устройствами или оборудованием. 2.3.4. Параметризация клиентов Одни клиентские программы предоставляют более широкий набор функциональных средств по сравнению с другими. В частности, некоторые клиентские программы позволяют пользователю указывать не только удаленный компьютер, на котором функционирует сервер, но и номер порта протокола, через который сервер принима- 40 Глава 2. Модель взаимодействия типа клиент/сервер...
ет запросы. Например, в главе 1 показано, что стандартная прикладная клиентская программа позволяет использовать протокол TELNET для доступа к службам, отличным от обычной терминальной службы дистанционного доступа по протоколу TELNET, при условии, что эта программа позволяет пользователю указывать не только удаленный компьютер, но и порт назначения протокола. По сути, программное обеспечение, позволяющее пользователю дополнительно указывать номер порта протокола, имеет больше входных параметров по сравнению с другим программным обеспечением, поэтому для его описания можно использовать термин полностью параметризованный клиент. Например, полностью параметризованный клиент TELNET позволяет пользователю указывать номер порта протокола. Не все поставщики предусматривают полную параметризацию своего клиентского прикладного программного обеспечения. Поэтому в некоторых системах могут возникать сложности или может быть даже исключена возможность использовать клиент TELNET для работы с любым портом, отличным от порта, официально зарезервированного для дистанционного доступа. В действительности, может даже возникнуть необходимость доработать клиентскую программу TELNET поставщика или разработать новое клиентское программное обеспечение TELNET, которое принимает в качестве параметра номер порта и использует этот порт. Безусловно, при разработке клиентского программного обеспечения всегда рекомендуется предусматривать полную параметризацию. При разработке клиентского прикладного программного обеспечения необходимо предусмотреть применение параметров, позволяющих пользователю полностью указывать компьютер назначения и номер порта протокола назначения. Полная параметризация особенно удобна при проверке нового клиентского или серверного программного обеспечения, поскольку она позволяет выполнять проверку, не нарушая работы существующего программного обеспечения. Например, программист может разработать пару программ TELNET, клиент и сервер, вызвать их с использованием нестандартных портов протоколов и перейти к проверке программного обеспечения, не нарушая работы стандартных служб. Другие пользователи могут по-прежнему обращаться к существующей службе TELNET, не испытывая неудобств во время испытания нового программного обеспечения. 2.3.5. Серверы с установлением и без установления логического соединения При разработке программного обеспечения клиент/сервер необходимо выбрать один из двух типов взаимодействия: с установлением или без установления логического соединения. Эти два типа взаимодействия соответствуют двум основным типам транспортных протоколов. В набор протоколов TCP/IP входят транспортные протоколы обоих типов, а прикладному программисту предоставляется возможность выбрать один из них. Если клиент и сервер обмениваются данными по протоколу UDP (User Datagram Protocol — Протокол пользовательских дейтаграмм), взаимодействие осуществляется без установления логического соединения, а если используется протокол TCP (Transmission Control Protocol — Протокол управления передачей), взаимодействие происходит с установлением логического соединения. С точки зрения прикладного программиста, различие между взаимодействием с установлением и без установления логического соединения является очень важным, поскольку во многом определяет выбор алгоритмов, применяемых при организации взаимодействия клиента и сервера. Если речь идет о наборе протоколов TCP/IP, этот выбор определяет также степень надежности, которую обеспечивает 2.3. Терминология и основные понятия 41
соответствующая система передачи данных. Протокол TCP устраняет все проблемы, возникающие в процессе передачи, и обеспечивает полную надежность. Этот протокол предусматривает проверку поступающих данных и автоматическую повторную передачу неполученных сегментов. Протокол TCP вычисляет контрольную сумму данных для проверки отсутствия искажения данных во время передачи. В этом протоколе используется последовательная нумерация пакетов для контроля поступления данных в исходном порядке и автоматического уничтожения дубликатов пакетов. Протокол TCP обеспечивает управление.потоком данных, что позволяет исключить возможность передачи отправителем данных быстрее, чем может принять получатель. И наконец, протокол TCP информирует отправителя, если по каким-то причинам сеть передачи данных становится неработоспособной. С другой стороны, клиенты и серверы, использующие протокол UDP, не имеют никаких гарантий надежной доставки. После отправки клиентом запросы могут быть потеряны, продублированы, задержаны или доставлены не в том порядке, в каком были отправлены. Аналогичным образом, могут быть потеряны, продублированы, задержаны или доставлены не в том порядке ответы, отправленные сервером клиенту. Следовательно, клиентское или серверное программное обеспечение, в котором используется протокол UDP, должно включать код, позволяющий обнаруживать и исправлять подобные ошибки. Надежда на то, что протокол UDP успешно выполнит доставку всех отправленных пакетов, может оказаться неоправданной, поскольку он выполняет лишь все от него зависящее, но не более того. Сам протокол UDP не вносит ошибок, он просто полагается на протокол более низкого уровня, т.е. IP (Internet Protocol — Межсетевой протокол), который выполняет доставку пакетов. Успешное функционирование протокола IP, в свою очередь, зависит от работы базовых аппаратных сетей и промежуточных шлюзов. С точки зрения программиста, применение протокола UDP ведет к тому, что успешное функционирование разработанной им программы зависит от бесперебойной работы базовой объединенной сети. Например, протокол UDP хорошо работает в локальной сети, поскольку в ней редко возникают ошибки, которые приводят к снижению надежности. Ошибки обычно возникают, только если сетевое соединение проложено через глобальную объединенную сеть. Программисты иногда совершают ошибку, выбрав транспортный протокол без установления логического соединения (т.е. UDP), а затем выполнив проверку своего клиентского и серверного программного обеспечения только в локальной сети. Поскольку в локальной сети почти не происходит задержка пакетов, их потеря или доставка с нарушением порядка, создается впечатление, что программное обеспечение работает хорошо. Однако после перехода к использованию того же программного обеспечения в глобальной объединенной сети оно иногда начинает давать сбои или вырабатывать неправильные результаты. Не только начинающие разработчики, но и более опытные профессионалы предпочитают использовать не UDP, a TCP. Организация работы протокола TCP по принципу установления логического соединения упрощает программирование, а надежность этого протокола позволяет программисту снять с себя ответственность за обнаружение и исправление ошибок. По сути, попытка повысить надежность протокола UDP представляет собой нетривиальную задачу, решение которой обычно требует значительного опыта проектирования протоколов. Как правило, протокол UDP используется в прикладных программах только при следующих условиях. Во-первых, в спецификации прикладного протокола должно быть обусловлено применение протокола UDP (возможно, сам прикладной протокол был разработан с учетом требований обеспечения надежности и устранения ошибок передачи), во-вторых, прикладной протокол требует использования средств широковещательной или групповой рассылки, и в-третьих, при- 42 Глава 2. Модель взаимодействия типа клиент/сервер...
ложение работает в надежной локальной среде и нет смысла расходовать лишние ресурсы для повышения надежности. Начинающим разработчикам, которые занимаются проектированием приложений клиент/сервер, настоятельно рекомендуется использовать TCP, поскольку он полностью обеспечивает надежную связь и обмен данными по принципу установления логического соединения. В программах протокол UDP следует использовать только в тех случаях, когда прикладной протокол обеспечивает достижение требуемого уровня надежности, в приложении необходимо применять аппаратную широковещательную (или групповую) рассылку или нет смысла затрачивать ресурсы для повышения надежности. 2.3.6. Серверы, не поддерживающие и поддерживающие состояние Обновляемая сервером информация о ходе взаимодействия с клиентами называется гшфо/шацией о состоянии. Серверы, которые не хранят какой-либо информации о состоянии, называются серверами, не поддерживающими состояние, а серверы, хранящие такую информацию, называются серверами, поддерживающими состояние. Проектировщики стремятся предусмотреть сопровождение информации о состоянии в серверах в целях повышения эффективности. Иногда наличие на сервере даже небольшого объема необходимой информации позволяет уменьшить размеры сообщений, которыми обмениваются клиент и сервер, и обеспечить повышение скорости отклика сервера на запросы. В частности, информация о состоянии позволяет хранить на сервере данные о том, какие запросы поступали от того или иного клиента ранее, и формировать обновляемые ответы по мере поступления каждого нового запроса. В отличие от сказанного выше, основным стимулом к отказу от хранения информации о состоянии является стремление повысить надежность протокола, поскольку информация о состоянии, хранящаяся на сервере, может стать ошибочной, если сообщения будут потеряны, продублированы или доставлены не в исходном порядке, а также если произойдет аварийный останов или перезагрузка клиентского компьютера. Если на сервере при формировании ответа используется ошибочная информация о состоянии, сам ответ может оказаться ошибочным. 2.3.7. Пример файлового сервера, не поддерживающего состояние Поясним различие между серверами с поддержкой состояния и без поддержки состояния на примере. Рассмотрим файловый сервер, который позволяет клиентам обращаться с удаленных компьютеров к данным, хранящимся в файлах на локальном диске. Сервер функционирует как прикладная программа. Он ожидает поступления запроса от клиента по сети. Клиент посылает запросы двух типов, которые позволяют либо извлечь данные из указанного файла, либо записать данные в указанный файл. Сервер выполняет затребованную операцию и отвечает клиенту. Если файловый сервер не поддерживает состояние, он не хранит информацию о выполняемых им операциях. В каждом запросе клиента должна быть приведена полная информация. В сообщении должно быть указано, нужно ли данные записать в файл или прочитать из файла, имя файла, позиция в файле, с которой должна начаться передача, и число передаваемых байтов. Сообщение, в котором передается запрос на запись данных, должно также содержать записываемые данные. В табл. 2.1 перечислены поля сообщения, необходимые для передачи запроса на сервер, не поддерживающий состояние. Сервер обрабатывает сообщения независимо друг от друга. 2.3. Терминология и основные понятия 43
Таблица 2.1. Поля, которые должны быть предусмотрены в сообщении, передаваемом с клиента на файловый сервер, не поддерживающий состояние Элемент Описание ор Операция (чтение или запись) name Имя файла роз Позиция в файле size Число передаваемых байтов data Данные (присутствуют только в запросе на запись) Имя файла, используемое в запросе к серверу, не поддерживающему состояние, должно быть полным и однозначным, чтобы сервер имел возможность определить требуемый файл только из самого сообщения. В связи с этим имя файла может иметь произвольную длину. 2.3.8. Пример файлового сервера, поддерживающего состояние В качестве альтернативы рассмотрим содержимое сообщений, используемых файловым сервером, который поддерживает информацию о состоянии. Поскольку сервер распознает отдельных клиентов и хранит информацию о предыдущих запросах каждого клиента, в конкретном сообщении нет необходимости задавать всю информацию. В частности, как только клиент начнет операцию с файлом, сервер может записать в память имя файла, текущую позицию и предыдущую операцию. После чтения из файла информации, обусловленной предыдущим запросом, в последующих запросах на выполнение операции чтения достаточно предусмотреть только одно поле: число считываемых байтов. Аналогичным образом, как только клиент приступит к записи данных, в каждый последующий запрос на запись достаточно включить только два поля: целое число, которое указывает число записываемых байтов, а затем сами данные. Как можно обеспечить сопровождение информации о состоянии в файловом сервере, поддерживающем состояние? На сервере имеется таблица, в которой хранится информация о каждом клиенте и о том файле, к которому в данный момент выполняется доступ. В табл. 2.2 показана одна из возможных компоновок полей с информацией о состоянии. При такой организации программы клиент может повторно не передавать данные тех полей сообщения, которые хранятся на сервере. Таблица 2.2. Пример таблицы с информацией о состоянии для файлового сервера, поддерживающего состояние Клиент Имя файла Текущая позиция Последняя операция 1 test.program.с 0 read 2 tcp.book.text 456 read 3 dept.budget.text 38 write 4 tetris.exe 128 read После выполнения клиентом каждой операции сервер наращивает указатель позиции файла в своей таблице состояний таким образом, чтобы на следующий запрос от клиента предоставлялась очередная часть данных. 44 Глава 2. Модель взаимодействия типа клиент/сервер...
2.3.9. Идентификация клиента В серверах, поддерживающих состояние, применяются два основных подхода к идентификации клиентов: оконечные точки и дескрипторы. Преимущество идентификации с помощью оконечных точек состоит в том, что такая операция может выполняться автоматически, поскольку используемые при этом механизмы реализованы в транспортных протоколах, а не в прикладном протоколе. Для применения идентификации по оконечным точкам сервер передает в программное обеспечение транспортного протокола требование предоставить информацию идентификации при поступлении запроса от клиента (например, IP-адрес и номер порта протокола клиента). Затем сервер использует информацию об оконечной точке для обозначения конкретной записи в таблице состояний. К сожалению, информация об оконечной точке может изменяться. Например, если в результате нарушения в работе сети клиент будет вынужден открыть новое соединение TCP, сервер не сможет найти соответствие между новым соединением и накопленной ранее информацией о состоянии. Преимущество метода с применением дескриптора, альтернативного методу идентификации с помощью оконечной точки, состоит в том, что созданное с его помощью обозначение остается постоянным после перехода к другому транспортному соединению. Однако этот метод имеет тот недостаток, что в его реализации должно принимать участие само приложение. По существу, дескриптор — это соглашение, заключенное только между клиентом и сервером. Во время передачи первоначального запроса клиентом должна быть указана полная информация. Сервер распределяет запись в своей таблице состояний и формирует краткий идентификатор для этой записи, называемый дескриптором (он обычно представляет собой небольшое целое число). Затем сервер отправляет такой дескриптор клиенту для использования в последующих запросах. При отправке каждого следующего запроса клиент использует короткий дескриптор вместо длинного имени файла. Следует отметить, что данный метод не зависит от применяемого транспортного протокола, поэтому при смене транспортного соединения дескриптор остается действительным. Сервер, поддерживающий состояние» не может хранить информацию о состоянии неопределенно долгое время. Сразу после получения первоначального запроса клиента сервер выделяет небольшой объем локальных ресурсов (например, памяти) для этого клиента. Если не происходит отмена выделения таких ресурсов, сервер в конечном итоге исчерпает все свои ресурсы. Поэтому прикладные протоколы с поддержкой состояния требуют выполнения операции завершения работы. В рассматриваемом примере файлового сервера клиент после завершения работы с файлом должен отправить серверу сообщение с указанием, что файл ему больше не нужен. В ответ на это сервер удаляет хранимую информацию о состоянии и предоставляет возможность использовать ту же запись в таблице другому клиенту. Основным преимуществом принципа организации работы с учетом информации о состоянии является эффективность. Действительно, при условии бесперебойной доставки всех сообщений, которыми обмениваются клиент и сервер, проект, предусматривающий поддержку состояния, позволяет уменьшить объем передаваемых данных. Кроме того, проекты с поддержкой состояния более доступны для понимания программистов, поскольку информация о состоянии используется в большинстве нераспределенных программ. В идеальных условиях, когда сети бесперебойно доставляют все сообщения, а работа компьютеров никогда не завершается аварийно, можно предусмотреть сопровождение на сервере небольшого объема информации о состоянии для каждого текущего сеанса взаимодействия, что позволяет уменьшить размер сообщений и создавать распределенные приложения, в большей степени подобные нераспределенным приложениям. 2.3. Терминология ц основные понятия 45
Информация о состоянии позволяет повысить эффективность, но правильное сопровождение этой информации может оказаться сложным или невозможным, если базовая сеть допускает дублирование, задержку или доставку сообщений с нарушением исходного порядка (например, если для взаимодействия клиента и сервера по Internet используется протокол UDP). Рассмотрим, что произойдет в приведенном выше примере с файловым сервером, если сеть продублирует запрос на чтение. Напомним, что сервер в своей информации о состоянии хранит данные о позиции указателя в файле. Предположим, что сервер обновляет эти данные при выполнении клиентом каждой операции чтения данных из файла. Если сеть продублирует запрос на чтение, то сервер получит две копии одного запроса. При поступлении первой копии сервер извлечет данные из файла, обновит данные о позиции указателя в своей информации о состоянии и возвратит результат клиенту. После поступления второй копии сервер извлечет дополнительные данные, снова обновит информацию о позиции указателя в файле и возвратит клиенту очередную часть данных. Клиентская программа может воспринять второй ответ как дубликат и отбросить его или сообщить об ошибке, поскольку получено два разных ответа на один запрос. В любом случае информация о состоянии, хранящаяся на сервере, может стать неправильной, поскольку не будет совпадать с истинными данными о состоянии, хранящимися в клиентской программе. Информация о состоянии может стать неправильной и в случае перезагрузки компьютеров. Предположим, что клиент обращается к серверу, устанавливает информацию о состоянии, а затем работа клиентского компьютера завершается аварийно. В этом случае сервер может не получить сообщений, позволяющих ему отбросить информацию о состоянии. В конечном итоге накопленная информация о состоянии займет всю память сервера. Если в рассматриваемом примере файлового сервера клиентская программа откроет сто файлов, а затем ее работа завершится аварийно, в таблице состояний сервера навсегда останутся 100 ненужных записей. Работа сервера, поддерживающего состояние, в котором используется идентификация по оконечной точке, также может нарушаться (или могут вырабатываться неправильные ответы) в случае аварийного завершения работы и перезагрузки клиентского компьютера. Если новой клиентской программе, которая будет вызвана на выполнение после аварийного завершения предыдущего сеанса работы, будет назначен тот же номер порта протокола, что и предыдущему экземпляру клиентской программы, который действовал до аварийного завершения, сервер не сможет обнаружить различия между этими двумя экземплярами программы. Может показаться, что эту проблему можно легко решить, предусмотрев уничтожение в сервере информации, полученной ранее от клиента, при получении каждого нового запроса на возобновление работы. Однако следует помнить, что базовая объединенная сеть может дублировать и задерживать сообщения, поэтому любое решение проблемы повторного использования портов протокола новыми экземплярами клиентской программы после перезагрузки должно также учитывать случай, когда запуск клиента происходит нормально, но его первое сообщение на сервер дублируется, а одна из копий задерживается. В целом, задача правильного сопровождения информации о состоянии может быть решена только с помощью сложных протоколов, в которых учтены проблемы ненадежной доставки сообщений и перезапуска компьютерных систем. В реальной объединенной сети, где компьютеры могут завершать работу аварийно и перезагружаться, а сообщения могут быть потеряны, задержаны, продублированы или доставлены не в исходном порядке, проекты, предусматривающие сопровождение информации о состоянии, требуют применения сложных прикладных протоколов, которые трудно проектировать, анализировать и правильно реализовывать. 46 Глава 2. Модель взаимодействия типа клиент/сервер...
2.3.10. Отказ от сопровождения информации о состоянии — это проблема протокола Хотя в предыдущих разделах отказ от сопровождения информации о состоянии рассматривался в контексте серверов, вопрос о том, должен ли сервер поддерживать или не поддерживать информацию о состоянии, в большей степени касается прикладного протокола, чем реализации. Если в прикладном протоколе предусмотрено, что толкование какого-то конкретного сообщения в той или иной степени зависит от предыдущих сообщений, то попытка обеспечить взаимодействие без учета информации о состоянии может оказаться безуспешной. По существу, ответ на вопрос о том, следует ли сопровождать на сервере информацию о состоянии, зависит от того, обеспечивает ли прикладной протокол надежную доставку. Для предотвращения возникновения проблем и обеспечения надежного взаимодействия разработчик прикладного протокола должен позаботиться о том, чтобы каждое сообщение было полностью однозначным. Это означает, что смысл сообщения не должен зависеть от того, доставлено ли оно не в том порядке, в каком было отправлено, а также от ранее доставленных сообщений. По существу, проектировщик протокола должен организовать взаимодействие клиента и сервера таким образом, чтобы сервер всегда давал один и тот же ответ независимо от числа полученных копий запроса. Для описания операции, которая всегда приводит к одному и тому же результату, применяется термин идемпотентный. Мы используем этот термин для описания протоколов, которые обеспечивают формирование сервером одного и того же ответа на данное сообщение, независимо от числа полученных копий сообщения. В объединенной сети, в которой базовая сеть может дублировать, задерживать или доставлять сообщения не в том порядке, в каком они были отправлены, а компьютеры с клиентскими приложениями могут неожиданно завершать работу аварийно, сервер должен быть не поддерживающим информацию о состоянии. В сервере можно отказаться от сопровождения информации о состоянии только в том случае, если проект прикладного протокола обеспечивает идемпотентность операций. 2.3.11. Функционирование серверов в качестве клиентов Программы не всегда полностью соответствуют определению клиента или сервера. Серверу при формировании ответа на запрос может потребоваться доступ к другим сетевым службам. По существу, сервер сам может действовать в качестве клиента. Например, предположим, что программа файлового сервера должна получить информацию о времени суток, чтобы проставить в каталоге отметку времени доступа к файлу. Предположим также, что в системе, в которой функционирует сервер, отсутствуют часы, отсчитывающие время суток. В таком случае для получения отметки времени сервер должен действовать в качестве клиента и отправить запрос на сервер службы текущего времени, как показано на рис. 2.1. После получения ответа от сервера текущего времени файловый сервер завершит формирование ответа и возвратит результат клиенту, приславшему первоначальный запрос. В сетевой среде, в которой работает много доступных серверов, нередко можно обнаружить, что сервер одного приложения действует как клиент другого. Безусловно, проектировщики должны тщательно следить за тем, чтобы не возникали циклические зависимости между серверами. 2.4. Резюме Принцип взаимодействия типа клиент/сервер предусматривает определение прикладной программы связи как клиента или как сервера в зависимости от 2.4. Резюме 47
того, является ли она инициатором установления соединения. В дополнение к/ клиентскому и серверному программному обеспечению для стандартных npib ложений, многие программисты разрабатывают клиентское и серверное программное обеспечение для нестандартных приложений, которые соответствуют конкретным потребностям. Рис. 2.1. Программа файлового сервера, действующая в качестве клиента сервера службы текущего времени Не только начинающие, но и опытные программисты для транспортировки сообщений между клиентом и сервером чаще всего используют протокол TCP, поскольку он обеспечивает надежность, необходимую в объединенной среде. Программисты обращаются к протоколу UDP только в тех случаях, когда задача не может быть решена с помощью TCP (например, для поддержки широковещательной или групповой рассылки). Сопровождение информации о состоянии на сервере позволяет повысить эффективность. Однако если неожиданно произойдет аварийное завершение работы клиентских программ или базовая транспортная система допустит дублирование, задержку или потерю пакета, то сопровождение информации о состоянии может потребовать лишние ресурсы, а сама информация может стать неточной. Поэтому большинство проектировщиков прикладных протоколов стремится свести к минимуму объем информации о состоянии. Реализация протокола, предусматривающая отказ от сопровождения информации о состоянии, может оказаться невозможной, если прикладной протокол не в состоянии обеспечить идемпотентность операций. Программы не всегда можно однозначно отнести к категории клиента или сервера, поскольку многие программы выполняют и те, и другие функции. Программа, действующая как сервер одной службы, может выполнять функции клиента при доступе к другим службам. 48 Глава 2. Модель взаимодействия типа клиент/сервер...
Материал для дальнейшего изучения В литературе [151] кратко описана модель взаимодействия клиент/сервер и даны примеры для операционной системы UNIX. Другие примеры можно найти, изучив приложения, которые входят в поставку операционных систем разных поставщиков. Упражнения 3.1. Какая из реализаций стандартных прикладных клиентских программ на вашем компьютере является полностью параметризованной? Для чего нужна полная параметризация? 2.2. Являются ли такие стандартные прикладные протоколы, как TELNET, FTP, SMTP и NFS (Network File System — Сетевая файловая система), протоколами с установлением или без установления логического соединения? 2.3. Что должно произойти согласно спецификации TCP/IP, если при получении клиентского запроса компьютером на нем не функционирует соответствующий сервер? (Подсказка: прочитайте информацию о протоколе ICMP.) Что произойдет в локальной системе? 2.4. Разработайте структуры данных и форматы сообщений, необходимые для файлового сервера, не поддерживающего состояние. Что произойдет, если два или более клиентов обратятся к одному и тому же файлу? Что произойдет, если работа клиентской программы завершится аварийно до закрытия файла? 2.5. Является ли сервер, в котором используется идентификация по оконечной точке, более защищенным по сравнению с сервером, в котором используются дескрипторы? Объясните ваш ответ. (Подсказка: напомним, что на промежуточных маршрутизаторах может быть получен доступ к содержимому пакетов по мере их прохождения через Internet, но компьютер может завершить работу аварийно и выполнить перезагрузку.) 2.6. Если дескриптор содержит только индекс таблицы, то один и тот же дескриптор будет сформирован повторно при каждом назначении для определенного компьютера конкретной записи в таблице. Разработайте схему, которая позволяет эффективно связать дескриптор с записью в таблице и одновременно формировать для него новое значение при каждом повторном назначении одной и той же записи в таблице. 2.7. Разработайте структуры данных и форматы сообщений, необходимые для файлового сервера, поддерживающего состояние. Используйте операции открытия, чтения, записи и закрытия для доступа к файлам. Предусмотрите, чтобы операция открытия возвращала целое число, используемое для доступа к файлу при выполнении операции чтения и записи. Как распознать дубликаты запросов на открытие файла, поступающих от клиента, который отправляет запрос на открытие, аварийно завершает работу, перезагружается и снова посылает запрос на открытие файла? 2.8. Исходя из условий предыдущего упражнения, ответьте, что произойдет согласно вашему проекту, если два или более клиентов предпримут попытку доступа к одному и тому же файлу? Объясните, что произойдет, если работа клиента завершится аварийно до закрытия файла? Материал для дальнейшего изучения 49
2.9. Внимательно изучите протокол NFS дистанционного доступа к файлам, чтобы определить, какие операции являются идемпотентными. Какие сообщения об ошибках могут появляться, если происходит потеря, дублирование или задержка сообщений? 2.10. Должны ли в идемпотентном протоколе обязательно использоваться сообщения большей длины по сравнению с эквивалентным ему протоколом, не являющимся идемпотентным? Объясните ваш ответ. Глава 2. Модель взаимодействия типа клиент/сервер...
3 Параллельная обработка в программном обеспечении клиента/сервера 3.1. Введение В предыдущей главе были определены принципы взаимодействия типа клиент/сервер. В настоящей главе эти принципы рассматриваются более подробно, с описанием принципа параллельной работы, который обеспечивает значительное расширение возможностей взаимодействия типа клиент/сервер, но вместе с тем усложняет проектирование и реализацию программного обеспечения. Описанию принципа параллельной работы посвящены также последующие главы, где подробно описаны способы обеспечения параллельного доступа, применяемые в серверах. Кроме описания общего принципа параллельной работы, в настоящей главе приведен также обзор средств, предусмотренных в операционной системе для обеспечения параллельного выполнения процессов. Важно понять все функции, описанные в этой главе, поскольку они будут применяться во многих реализациях серверов в следующих главах. 3.2. Обеспечение параллельной работы в сетях Термин параллельное выполнение применяется для обозначения вычислений, которые действительно выполняются одновременно или только создается видимость этого. Например, в многопользовательской компьютерной системе параллельная работа может обеспечиваться с использованием метода разделения времени; этот метод предусматривает достаточно быстрое переключение процессора с одного вычисления к другому для того, чтобы создавалось впечатление одновременного выполнения этих вычислений. Другой способ обеспечения параллельной работы может состоять в использовании метода многопроцессорной обработки, при использовании которого несколько процессоров одновременно выполняют несколько вычислений. Параллельная обработка является основой распределенных вычислений и применяется во многих формах. Многочисленные пары прикладных программ, выполняемых на компьютерах в одной сети, могут одновременно обмениваться данными, совместно используя соединяющую их сеть. Например, приложение А на одном компьютере может обмениваться данными с приложением В на другом компьютере одновременно с тем, как приложение С на третьем компьютере обменивается данными с приложением D на четвертом. Хотя все они совместно
используют общую сеть, создается впечатление, что функционирование всех этих приложений происходит независимо друг от друга. Сетевое аппаратное обеспечение предписывает выполнение правил доступа, позволяющих каждой паре взаимодействующих компьютеров обмениваться сообщениями. Эти правила доступа исключают возможность для любой конкретной пары приложений нарушить работу других приложений, потребляя все ресурсы сети. Параллельная работа может также осуществляться в любой отдельно взятой компьютерной системе. Например, каждый из многочисленных пользователей системы с разделением времени может вызвать клиентское приложение, которое будет обмениваться данными с приложением на другом компьютере. Один пользователь может передавать файл одновременно с тем, как другой пользователь проводит сеанс дистанционного доступа. С точки зрения пользователя, создается впечатление, что все клиентские программы работают одновременно. Рис, 3,1, Параллельная работа клиентских программ Кроме параллельной работы клиентов на одном компьютере, одновременно может функционировать целый ряд клиентских программ на многих компьютерах, подключенных к сети. На рис. 3.1 показана параллельная работа клиентских программ, которая происходит, когда пользователи запускают их на выполнение одновременно на нескольких компьютерах или когда многозадачная операционная система позволяет одновременно использовать несколько программ на одном компьютере. Разработка клиентской программы не всегда требует какого-то особого внимания или усилий со стороны программиста для обеспечения возможности использовать ее параллельно с другими программами. Прикладной программист проектирует и разрабатывает каждую клиентскую программу без учета требований параллельного выполнения; параллельная работа многочисленных клиентских программ обеспечивается автоматически, поскольку операционная система 52 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера
позволяет каждому из многочисленных пользователей вызывать клиентскую программу одновременно с другими пользователями. Поэтому отдельные клиенты работают во многом аналогично любой обычной программе. В основной части клиентского программного обеспечения параллельная работа обеспечивается за счет того, что базовая операционная система позволяет пользователям одновременно вызывать на выполнение несколько клиентских программ, или благодаря тому, что пользователи многих компьютеров могут одновременно вызывать на выполнение клиентское программное обеспечение. Каждая отдельная клиентская программа действует как обычная программа; в ней не нужно явно управлять параллельной работой. 3.3. Обеспечение параллельной работы в серверах В отличие от параллельно работающих клиентских программ, обеспечение параллельного выполнения задач сервером требует значительных усилий. Как показано на рис. 3.2, единственная серверная программа должна параллельно обрабатывать все входящие запросы, поскольку к серверу обращаются многочисленные клиенты через его единственный, общепринятый порт протокола. Чтобы понять, почему так важно обеспечить параллельную работу, рассмотрим операции, выполняемые сервером, которые требуют большого объема вычислений или продолжительных сеансов связи. Например, рассмотрим сервер дистанционного доступа. Если он не в состоянии обеспечить параллельную работу, то сможет поддерживать одновременно только один сеанс дистанционного доступа. После подключения одного клиента сервер будет вынужден игнорировать или отвергать последующие запросы до тех пор, пока первый пользователь не завершит работу. Безусловно, такой проект ограничивает возможности сервера и не позволяет нескольким удаленным пользователям обращаться к данному конкретному компьютеру одновременно. В главе 8 описаны алгоритмы и проблемы проектирования параллельных серверов, а также показаны принципы их работы. В главах с 9 по 13 каждый из алгоритмов демонстрируется на примерах, более подробно рассматриваются проекты и анализируется код практически применимых серверов. Оставшаяся часть настоящей главы в основном посвящена описанию терминологии и основных понятий, используемых в этой книге. 3.4. Терминология и основные понятия Поскольку лишь немногие прикладные программисты имеют опыт проектирования параллельных программ, изучение параллельной работы серверов может оказаться сложной задачей. В настоящем разделе описаны основные понятия параллельной обработки и показано, как осуществляется ее поддержка в операционной системе. Здесь приведены примеры, иллюстрирующие принципы параллельной работы, и определена терминология, используемая в следующих главах. 3.4.1. Понятие процесса В системах параллельной обработки для определения основной единицы вычислительной работы служит абстрактное понятие процесса1. Мы определяем 1 В некоторых системах вместо термина "процесс" используются термины "задача" или "задание", а в других системах этот термин пишется с прописной буквы: "Процесс". 3.3. Обеспечение параллельной работы в серверах 53
процесс как некоторое адресное пространство в сочетании, по меньшей мере, с одним потоком выполнения. Наиболее важной информацией, связанной с потоком, является указатель команд, который определяет адрес в памяти команды, выполняемой в данном процессе. С процессом связана также информация, определяющая идентификатор пользователя, владеющего процессом, оттранслированная программа, выполняемая в процессе, и местонахождение кода программы и областей данных процесса в оперативной памяти. 00 Рис. 3.2. Серверное программное обеспечение должно быть специально запрограммировано на одновременную обработку запросов Процесс нужно отличать от программы, поскольку понятие процесса охватывает только активное выполнение вычислений, а не статическую версию программы. После создания пользователем процесса операционная система загружает копию программы в память компьютера, а затем запускает поток управления, выполняющий эту программу. 6 частности, система параллельной обработки позволяет нескольким потокам выполнять один и тот же фрагмент кода "одновременно" (в одном или нескольких процессах). Каждый поток выполнения движется со своей скоростью, и каждый может начаться или закончиться в произвольное время. Поэтому каждый поток может выполняться в той точке кода, к которой не обращается какой-либо иной поток. Поскольку в каждом потоке имеется отдельный указатель команд, который определяет следующую выполняемую команду, удается избежать путаницы. Безусловно, в однопроцессорной архитектуре единственный процессор способен выполнять в любой момент времени только один поток. В результате работы операционной системы создается впечатление, что компьютер выполняет сразу несколько вычислений, быстро переключая процессор с одного потока выполнения на другой. 54 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера
Может даже показаться, что многочисленные потоки работают одновременно. В действительности, в течение короткого времени работает один поток, затем также ненадолго управление снова переходит к другому потоку и т.д. Для описания этого принципа используется термин параллельное выполнение. Он означает "выполнение, которое внешне кажется одновременным". В однопроцессорном компьютере параллельную работу обеспечивает операционная система, а в многопроцессорном компьютере все процессоры могут одновременно выполнять разные потоки. Прикладные программисты разрабатывают программы для параллельной среды, не задумываясь над тем, каким именно является используемый ими компьютер: однопроцессорным или многопроцессорным. 3.4.2. Совместное использование локальных и глобальных переменных В системе параллельной обработки обычная прикладная программа представляет собой просто частный случай: она представлена фрагментом кода, который выполняется одним и только одним потоком одновременно. Понятие потока во многом отличается от обычного понятия программы. Например, большинство прикладных программистов рассматривают набор переменных, определенных в программе, как связанный с кодом этой программы. Но если некоторый код выполняется несколькими потоками одновременно, необходимо, чтобы каясдый процесс имел свою собственную копию переменных. Чтобы понять, с чем это связано, рассмотрим следующий фрагмент кода на языке С, который выводит целые числа от 1 до 10: for (i = 1; i <= 10? i++) printf(H% d\n\ i); В этом цикле используется индексная переменная i. Разрабатывая обычную программу, программист может считать, что место для хранения переменной i распределено в самом коде. Однако если этот участок кода выполняется двумя или более потоками одновременно, то один из них может выполнять шестую итерацию, тогда как другой приступает к первой итерации. Каждый поток должен иметь разные значения переменной i. Это означает, что каждый поток должен иметь свою собственную копию переменной i, так как в противном случае может возникнуть путаница. Итак, если несколько потоков одновременно выполняют определенный фрагмент кода, то каждый поток должен иметь свою собственную, независимую копию переменных, связанных с этим кодом. В модели процесса понятие отдельных копий переменных было расширено в целях включения глобальных переменных. Например, программа, которая начинается со следующих объявлений, содержит глобальную переменную х: int х; main (argc, argv) int argc; char *[] argvj { Операционная система создает отдельную копию переменной х для каждого процесса, который выполняет программу. Однако все потоки в пределах конкретного процесса совместно используют одну и ту же копию. Каждый процесс включает отдельную копию глобальных переменных; если в одном процессе выполняется несколько потоков, то каждый из них имеет собственную копию локальных переменных, но все они совместно используют копию глобальных переменных, принадлежащих процессу. 3.4. Терминология и основные понятия 55
3.4.3. Вызовы процедур В процедурно-ориентированном языке, таком как Pascal или С, выполняемый код может содержать вызовы подпрограмм (процедуры или функции). Подпрограммы принимают параметры, вычисляют результат, а затем возвращают управление оператору, расположенному непосредственно после точки вызова. Если код выполняется одновременно несколькими потоками, то каждый из них может находиться на разных этапах последовательности вызовов процедур. Один из потоков, например, А, может начать выполнение, вызвать процедуру, а затем вызвать процедуру второго уровня еще до того, как начнет выполняться другой поток, допустим, В. Поток В может выполнить возврат из процедуры первого уровня так же, как поток А выполнит возврат после вызова процедуры второго уровня. В системе поддержки выполнения программ процедурно-ориентированных языков программирования для выполнения вызовов процедур используется механизм стека. Система поддержки выполнения программ проталкивает в стек запись активизации процедуры при каждом вызове процедуры. Кроме всего прочего, запись активизации содержит информацию о том, в каком месте кода произошел вызов процедуры. После завершения выполнения процедуры система поддержки выполнения программ выталкивает запись активизации, находящуюся на вершине стека, и возвращается к выполнению процедуры, в которой произошел вызов другой процедуры. По аналогии с правилом для локальных переменных, в системах параллельного программирования предусмотрено разграничение между вызовами процедуры в потоках выполнения. Если несколько потоков выполняют один фрагмент кода одновременно, то в каждом из них имеется свой собственный стек записей активизации процедур поддержки выполнения программ. 3.5. Пример создания параллельного процесса 3.5.1. Пример последовательной обработки на языке С Следующий пример демонстрирует параллельную обработку в операционной системе UNIX2. Как и при описании большинства понятий программирования, в этой книге используется простейший синтаксис; программа занимает всего несколько строк кода. Например, ниже приведена обычная программа на языке С, которая выводит целые числа от 1 до 5 вместе с их суммой: /* Файл sum.с - типичная программа на языке С, которая */ /* суммирует целые числа от 1 до 5'*/ ¦include <stdlib.h> ¦include <stdio.h> int sum; /* sum - глобальная переменная */ main() { int i; /* i - локальная переменная */ Во всей этой книге общий термин UNIX используется для обозначения всех многочисленных разновидностей системы с разделением времени UNIX, а такие термины, как Linux, служат для обозначения конкретной версии системы UNIX. В примерах кода авторы старались придерживаться того подмножества функций Linux, которое соответствует стандарту POSIX. 56 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера
sum =0; for (i я 1 ; i <= 5 ; i++) { /* Наращивать в цикле значение */ /* переменной i от 1 до 5 */ printf(" The value of i is %d\n", i); fflush(stdout); /* Вывести содержимое буфера на*/ /* устройство вывода */ sum += i; } printf(" The sum is %d\nM, sum)? exit@); /* Завершить программу */ } После вызова на выполнение программа выводит на экран шесть строк: The value of i is 1 The value of i is 2 The value of i is 3 The value of i is 4 The value of i is 5 The sum is 15 3.5.2. Параллельная версия Для создания нового процесса в программе вызывается системная функция fork3. По существу, fork делит работающую программу на два (почти) одинаковых процесса и запускает поток на выполнение в новом процессе с того же места в коде. Потоки в двух процессах продолжают работу так, как если бы два пользователя одновременно запустили две копии этого приложения. Например, в следующей модифицированной версии приведенного выше примера запускается функция fork для создания нового процесса. (Следует отметить, что введение средств параллельной работы полностью меняет смысл программы, несмотря на то, что вызов функции fork занимает только одну строку кода.) tinclude <stdlib.h> ¦include <stdio.h> int sum; main() { int i; sum =0; fork(); /* Создать новый процесс */ for (i ¦ 1 ; i <- 5 ? i++) { printf(" The value of i is %d\n", i); fflush(stdout); sum += i; } printf(M The sum is %d\n", sum); exit@); С точки зрения программиста, вызов функции fork выглядит и действует так же, как обычный вызов функции в языке С. Этот вызов записывается как f ork (). Однако во время выполнения управление передается операционной системе, которая создает новый процесс. 3.5. Пример создания параллельного процесса 57
После вызова на выполнение этой параллельной версии программы система создает процесс с единственным потоком, выполняющим данный код. Однако после достижения потоком выполнения вызова функции fork система формирует дубликат процесса, создает один поток в новом процессе и разрешает продолжить выполнение и первоначальному, и вновь созданному потоку. Безусловно, каждый поток имеет собственную копию локальных переменных, используемых в программе. По существу, лучше всего можно представить себе, что происходит, если предположить, что система создает вторую копию всей работающей программы. Затем представьте себе, что начинают работать обе копии (точно так же, как если бы два пользователя вызвали на выполнение эту программу одновременно). Чтобы понять, как работает функция fork, представьте себе, что эта функция "вынуждает" операционную систему сделать копию выполняемой программы и разрешить обеим копиям работать одновременно. В одной конкретной однопроцессорной системе выполнение параллельной программы, представленной в этом примере, привело к получению двенадцати строк вывода: The value of i is 1 The value of i is 2 The value of i is 3 The value of i is 4 The value of i is 5 The sum is 15 The value of i is 1 The value of i is 2 The value of i is 3 The value of i is 4 The value of i is 5 The sum is 15 На используемом компьютере поток в первом процессе выполнялся настолько быстро, что смог закончить работу еще до того, как начал действовать поток во втором процессе. Сразу после окончания работы последнего потока операционная система завершает весь процесс. После закрытия первого процесса операционная система переключает процессор на поток второго процесса, который также работает до завершения. Все выполнение программы заняло меньше секунды. Издержки операционной системы, связанные с переключением между потоками, завершением процессов и обработкой системных вызовов, включая вызов функции fork и вызовы, необходимые для вывода данных на устройство вывода, составили менее 20% общих затрат времени. 3.5.3. Квантование времени В приведенном выше примере программы каждый поток выполнял простейшие вычисления: пять итераций цикла. Поэтому как только один поток в процессе получил контроль над процессором, он быстро подошел к завершению. При исследовании параллельных потоков, выполняющих гораздо более сложные вычисления, наблюдается интересное явление: операционная система выделяет на короткое время все имеющиеся вычислительные мощности процессора одному из потоков, а затем переходит к следующему. Мы используем термин квантование времени для описания систем, которые позволяют распределить все доступные ресурсы процессора между параллельно работающими потоками. Например, если система с квантованием времени имеет только один процессор, ресурсы ко- 58 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера
торого она может выделить потокам, а программа разделилась на два потока, то некоторое время будет выполняться один из потоков, затем в течение короткого времени будет работать второй поток, затем опять первый и т.д. Если в системе с квантованием времени имеется много потоков, в ней в течение короткого времени выполняется каждый из этих потоков, а затем процессор снова передается в распоряжение первого потока. Механизм квантования времени пытается распределять все доступные ресурсы процессора равным образом между всеми имеющимися потоками. Если вызваны на выполнение только два потока и компьютер имеет один процессор, то каждый из потоков получает приблизительно 50% процессорного времени. Если же на компьютере с одним процессором вызвано на выполнение N потоков, то каждый получает приблизительно 1/N процессорного времени4. Поэтому создается впечатление, что все потоки функционируют с одинаковой скоростью, независимо от их количества. Если в системе выполняется много потоков, скорость работы каждого из них становится низкой; если потоков мало, скорость повышается. Для ознакомления с эффектом квантования времени требуется пример программы, в которой каждый поток выполняется дольше по сравнению с выделенным квантом времени. Дополним приведенную выше параллельную программу таким образом, чтобы в ней происходило 10000 итераций цикла вместо 5: ¦include <stdlib.h> ¦include <stdio.h> int sum; main() { int i; sura = 0; fork(); for (i = 1 ; i <= 10000 ; i++) { printf(" The value of i is %d\nH, i); fflush(stdout); sum += i; } printf(" The total is %d\n", sum); exit(O); } Если полученная параллельная программа будет вызвана на выполнение в той же системе, что и прежде, она выдаст 20002 строки вывода. Однако теперь в начале распечатки не будет находиться весь вывод потока первого процесса, за которым следует весь вывод потока второго процесса; вывод этих потоков будет чередоваться. В течение одного кванта времени первый поток выполнит 74 итерации еще до того, как начнет выполняться второй поток. Затем второй поток выполнит 63 итерации до того, как система снова переключится на первый поток. В течение следующих квантов времени каждый из потоков будет получать количество процессорного времени, достаточное для выполнения от 60 до 90 итераций. Безусловно, эти два потока будут конкурировать со всеми другими потоками, работающими на компьютере, поэтому фактическая скорость выполнения будет немного колебаться в зависимости от состава смеси активных программ. Некоторые механизмы квантования времени предусматривают равномерное распределение процессорного времени между процессами, а затем дальнейшее распределение времени процесса между его потоками. 3.5. Пример создания параллельного процесса 59
3.5.4. Понятие однопотокового процесса Хотя выше было сказано, что процесс может содержать несколько потоков выполнения, функция fork не дублирует все эти потоки. Вместо этого при создании функцией fork копии работающего процесса вновь созданный процесс содержит один и только один поток — точную копию потока, в котором была выполнена функция fork. Поэтому вновь созданный процесс является однопотоковым процессом. По существу, однопотоковые процессы имеют очень широкое распространение. Например, однопотоковый процесс создается при вызове пользователем команды. Командный интерпретатор для выполнения каждой команды создает один (и только один) процесс и запускает в этом процессе единственный поток выполнения. Поэтому если программист явно не создал несколько потоков, то каждый процесс является однопотоковым. Хотя процесс может содержать несколько потоков, вновь созданный процесс, возникающий в результате вызова функции fork, является однопотоковым. 3.5.5. Проявление различий между процессами Как было сказано выше, функция fork может применяться для создания нового процесса, выполняющего точно такой же код и имеющего точно такие же значения переменных, что и первоначальный процесс. Однако создание полностью идентичной копии работающей программы не дает ничего нового и не приносит пользы, т.к. означает, что обе копии проводят совершенно одинаковые вычисления. Тем не менее, новый процесс, созданный функцией fork, не является абсолютно идентичным первоначальному процессу; он отличается одной небольшой особенностью. Дело в том, что fork — это функция, поэтому она возвращает значение в вызвавший ее оператор. После возврата управления этой функцией значение, возвращенное в текущий процесс, отличается от значения, возвращенного во вновь созданный процесс. Во вновь созданном процессе функция fork возвращает нуль, а в первоначальном процессе — небольшое положительное целое число, которое служит обозначением для вновь созданного процесса. Формально, возвращенное ненулевое значение называется идентификатором процесса5. В параллельных программах значение, возвращенное функцией fork, используется для определения порядка дальнейших действий. В наиболее общем случае код содержит условный оператор, позволяющий проверить, является ли возвращенное значение отличным от нуля: tinclude <stdlib.h> int sum; main() { int pid; sum =0; pid = fork(); if (pid != 0) { /* Первоначальный процесс */ printf(" The original process prints this.\nM); } else { /* Вновь созданный процесс */ printf(H The new process prints this.\n"); } Многие программисты вместо термина "идентификатор процесса" применяют сокращение pid. 60 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера
exit(O); } В этом примере кода в переменную pid записывается значение, возвращенное в результате вызова функции fork. Напомним, что каждый процесс имеет собственную копию всех переменных и что функция fork возвращает либо нуль (во вновь созданном процессе), либо ненулевое значение (в первоначальном процессе). Вслед за вызовом функции fork применяется оператор if для проверки переменной pid и определения того, какой именно процесс выполняется: первоначальный или вновь созданный. Каждый из этих двух процессов выводит идентифицирующее его сообщение и завершает работу. После вызова программы на выполнение появляется два сообщения: одно из первоначального процесса, а другое из вновь созданного процесса. Функция fork возвращает в первоначальном и вновь созданном процессах разные значения; в параллельных программах возвращаемое значение используется для вызова на выполнение в новом процессе иного кода, чем в первоначальном процессе. 3.6. Выполнение нового кода В системе UNIX предусмотрен механизм, позволяющий в любом процессе вызвать на выполнение независимую, отдельно оттранслированную программу. В основе этого механизма лежит системный вызов execve6, который принимает три параметра: имя файла, содержащего выполняемую объектную программу (т.е. программу, обработанную транслятором), указатель на список строковых параметров, которые должны быть переданы программе, и указатель на список строк, которые представляют так называемую среду. Функция execve заменяет код процесса, выполняемого в настоящее время, кодом новой программы. Этот вызов не влияет на какие-либо иные процессы. Поэтому для создания нового процесса, который выполняет объектный код, полученный из файла, процесс должен вызвать функции fork и execve. Например, каждый раз, когда пользователь вводит команду для выполнения в одном из доступных командных интерпретаторов, действующий командный интерпретатор использует функцию fork для создания нового процесса выполнения команды и функцию execve — для вызова на выполнение кода. Функция execve особенно важна для серверов, которые поддерживают различные службы. Чтобы иметь возможность разрабатывать программу для каждой службы, не учитывая влияния программ других служб, программист может разработать, написать и оттранслировать код каждой службы в виде отдельной программы. После получения сервером запроса к конкретной службе он может вызвать функции fork и execve для создания процесса, выполняющего одну из этих программ. В следующих главах этот метод описан более подробно и приведены примеры использования функции execve в серверах. В некоторых версиях UNIX используется прежнее название этой функции — exec. 3.6. Выполнение нового кода 61
3.7. Переключение контекста и проектирование программного обеспечения протокола Хотя средства параллельной обработки, предоставляемые операционными системами, позволяют создавать более мощные и простые программы, их применение все-таки связано с определенными вычислительными издержками. Для обеспечения того, чтобы все потоки выполнялись параллельно, в операционной системе используется квантование времени и осуществляется переключение одного (или нескольких) процессоров между потоками настолько быстро, что может показаться будто эти процессы выполняются одновременно. В тот момент, когда операционная система на время останавливает выполнение одного потока и переключается на другой, происходит переключение контекста; переключение между потоками одного и того же процесса требует меньших издержек по сравнению с переключением с потока одного процесса на поток другого. И в том, и в другом случае переключение контекста требует использования ресурсов процессора, и пока процессор выполняет переключение контекста, ни один из прикладных потоков не получает процессорного времени. Поэтому мы рассматриваем переключение контекста как издержки, необходимые для обеспечения параллельной работы. В целях предотвращения ненужных издержек программное обеспечение протокола должно быть спроектировано с учетом минимизации переключения контекста. В частности, программисты должны всегда тщательно следить за тем, чтобы преимущества введения средств параллельной работы в сервер оставались выше по сравнению с затратами на переключение контекста. В следующих главах описано применение средств параллельной работы в серверном программном обесцечении, представлены непараллельные, а также параллельные проекты, в которых используются и о дно потоковые, и многопотоковые процессы, а также описаны обстоятельства, при которых является целесообразным использование того или иного из указанных процессов. 3.8. Параллельная работа и асинхронный ввод/вывод Кроме предоставления поддержки параллельного использования процессора, некоторые операционные системы позволяют одному прикладному потоку инициировать и управлять параллельными операциями ввода и вывода. Системный вызов select предоставляет базовую операцию, на основе которой программисты могут разрабатывать программы управления параллельным вводом/выводом. В принципе, работу функции select понять несложно: она позволяет программе запрашивать у операционной системы информацию о том, какие устройства ввода/вывода готовы к использованию. В качестве примера рассмотрим прикладную программу, которая считывает символы из соединения TCP и выводит их на экран дисплея. Допустим, что программа позволяет также пользователю вводить с клавиатуры команды для определения способа отображения данных. Поскольку пользователь редко вводит (или вообще может не вводить) команды, программа не может ждать ввода с клавиатуры, поэтому она должна продолжать чтение и вывод текста, полученного из соединения TCP. Но если в какой-то момент программа выполнит очередную попытку чтения из соединения TCP, но не получит данных, она заблокируется. Пользователь может ввести команду в то время, как программа заблокирована на период ожидания новых данных в соединении TCP. Проблема состоит в том, что приложение не имеет информации о том, поступит ли раньше ввод с клавиатуры или из соединения TCP. Для решения этой проблемы выполняемая программа вызывает 62 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера
функцию select и указывает дескрипторы, которые соответствуют двум возможным источникам ввода. Тем самым программа передает операционной системе запрос на то, чтобы ей была предоставлена информация, когда один из двух возможных источников ввода получит данные, доступные для чтения. Как только станет доступен один из источников, из этого вызова будет выполнен возврат и поток приступит к чтению из данного источника. На данный момент достаточно просто понять принцип работы функции select; в следующих главах представлены дополнительные сведения и даны примеры ее использования. 3.9. Резюме Параллельное функционирование является основой приложений TCP/IP, поскольку оно позволяет одному пользователю обращаться к службам, не ожидая окончания работы другого пользователя. Параллельную работу клиентских программ организовать несложно, поскольку многочисленные пользователи вполне могут одновременно вызывать на выполнение клиентское прикладное программное обеспечение. Параллельную работу серверных программ обеспечить гораздо сложнее, поскольку серверное программное обеспечение должно быть специально разработано в целях обеспечения одновременного выполнения многочисленных запросов. В системе UNIX программа создает дополнительный процесс с использованием системного вызова fork. Неформально это можно описать так, что вызов функции fork заставляет операционную систему создать дубликат программы, в результате чего вновь созданный поток начинает выполнять второй процесс, в то время как первоначальный поток продолжает выполнять первый процесс. Формально, вызов fork представляет собой вызов функции, поскольку он возвращает значение. Единственное различие между первоначальным процессом и процессом, созданным функцией fork, заключается в том, какое именно значение возвращает этот вызов. Во вновь созданном процессе вызов возвращает нуль; в первоначальном процессе он возвращает небольшой положительный целочисленный идентификатор вновь созданного процесса. В параллельных программах возвращаемое значение используется для обеспечения выполнения в новых процессах иной части программы, чем в первоначальном процессе. В процессе можно вызвать в любое время функцию execve для вызова на выполнение кода отдельно оттранслированной программы. Организация параллельной работы требует затрат ресурсов. В частности, операционная система при переключении контекста с одного потока на другой расходует процессорное время. Программисты, которые вводят средства обеспечения параллельной работы в проект сервера, должны убедиться в том, что преимущества параллельного проекта перевешивают дополнительные издержки, связанных с переключением контекста. Вызов функции select позволяет управлять параллельным вводом/выводом в одном процессе. Функция select используется в процессе для определения того, какое устройство ввода/вывода становится доступным в первую очередь. Материал для дальнейшего изучения Параллельная обработка описана во многих книгах по операционным системам. В книге [61] рассматривается вся эта тема в целом. В книге [33] обсуждается реализация процессов, передачи сообщений и механизмов координации процессов в операционной системе Xinu. В книге [92] описана операционная система BSD UNIX версии 4.3 , от которой происходят многие современные разновидности операционных систем, включая Linux. 3.9. Резюме 63
Упражнения 3.1. Вызовите на выполнение приведенные примеры программ в своей локальной компьютерной системе. Определите приблизительное число итераций цикла вывода, которое могло быть выполнено в потоке в течение одного кванта времени. 3.2. Напишите параллельную программу, которая запускает пять процессов. Пусть поток каждого процесса выведет несколько строк, а затем остановится. 3.3. Прочитайте литературу о примитивах потока POSIX и напишите программу, которая создает пять параллельных потоков в одном процессе. Сравните время ее работы с программой, которая создает для выполнения той же задачи пять отдельных процессов. Напишите математическое выражение для оценки разницы в стоимости выполнения как функцию трех параметров: размера глобальных переменных, размера локальных переменных и затрат времени на выполнение потоков. 3.4. Ознакомьтесь со сведениями о том, как создаются параллельные процессы или потоки в других операционных системах. 3.5. Прочитайте дополнительную литературу о функции fork. Какую информацию вновь созданный процесс использует совместно с первоначальным процессом? 3.6. Напишите программу, в которой функция execve применяется для смены кода выполняемого процесса. 3.7. Напишите программу, в которой функция select используется для чтения с двух терминалов (т.е. последовательных линий передачи данных) и отображения результатов на экране с обозначением источника. 3.8. Перепишите программу, составленную по условиям предыдущего упражнения, таким образом, чтобы в ней не использовалась функция select. Какая версия проще для понимания? Какая версия эффективнее? Укажите версию, которая позволяет легче добиться корректного завершения. 64 Глава 3. Параллельная обработка в программном обеспечении клиента/сервера
4 Прикладной интерфейс к протоколам 4.1. Введение В предыдущих главах описана модель взаимодействия типа клиент/сервер для программ, обменивающихся данными, и показаны зависимости между параллельной работой и сетевой связью. В настоящей главе рассматриваются общие свойства интерфейса, применяемого в прикладной программе для обмена данными в рамках модели клиент/сервер. В следующей главе эти свойства демонстрируются на практических примерах и предоставляются подробные сведения о конкретном интерфейсе. 4.2. Неформальная спецификация программного интерфейса протокола Протоколы TCP/IP были спроектированы с учетом необходимости их применения в среде, которая включает аппаратное и программное обеспечение многих поставщиков. Проектировщики протоколов TCP/IP, стремясь обеспечить их совместимость с многочисленными разновидностями компьютеров, операционных систем или сетевых интерфейсных устройств, тщательно следили за тем, чтобы была исключена какая-либо зависимость от внутреннего представления данных, применяемого любым поставщиком. Кроме того, в стандартах TCP/IP полностью исключены какие-либо спецификации интерфейса прикладного программирования в терминах средств, доступных в операционной системе только одного поставщика. Таким образом, интерфейс между TCP/IP и приложениями, в которых эти протоколы используются, был определен неформально, в виде ряда рекомендаций, а не точной спецификации. В стандартах TCP/IP не даны подробные сведения о том, каким образом прикладное программное обеспечение должно взаимодействовать с программным обеспечением протоколов TCP/IP; в них описаны только необходимые функциональные средства, а возможность определять конкретные требования к реализации API-интерфейса предоставлена системным проектировщикам.
4.2.1. Преимущества и недостатки неформальной спецификации Применение неформальной спецификации API-интерфейса имеет свои преимущества и недостатки. Преимуществом такой сцецификации является гибкость и широкая применимость. Она позволяет проектировщикам реализовывать протоколы TCP/IP для любых операционных систем, начиная от простейших версий, работающих на персональных компьютерах, и заканчивая сложнейшими системами супер-ЭВМ. Еще более важно то, что наличие неформальной спецификации означает, что проектировщики имеют возможность использовать организацию интерфейса, наиболее подходящую для операционной системы: процедурную или основанную на передаче сообщений. Недостатком неформальной спецификации является то, что ее применение может привести к появлению различий в отдельных деталях реализации интерфейса для каждой операционной системы. По мере того как поставщики предлагают все новые API-интерфейсы, которые отличаются от существующих, прикладное программирование становится все сложнее, а приложения теряют свойства переносимости с одного компьютера на другой. Поэтому хотя системные проектировщики одобряют такую неформальную спецификацию, прикладные программисты желают иметь более жесткую спецификацию, поскольку она позволяла бы переносить приложения на новые компьютеры без внесения изменений. На практике существует лишь небольшое число API-интерфейсов, позволяющих использовать протоколы TCP/IP во многих приложениях. Калифорнийский университет в г. Беркли (Berkeley) определил API-интерфейс для своей версии операционной системы UNIX; этот API-интерфейс, который был адаптирован для многих систем, разработанных позже, включая Linux, известен под названием API-интерфейса сокетов9 интерфейса сонетов или просто сонетов. Компания Microsoft приспособила API-интерфейс сокетов для использования в своей операционной системе и присвоила этому варианту название Windows Sockets. Компания AT&T определила API- интерфейс для системы UNIX версии System V, который получил известность под аббревиатурой ТЫ1. Определено также несколько других API-интерфейсов, но пока еще ни один из них не получил широкого признания. 4.3. Функциональные средства интерфейса Хотя в стандартах TCP/IP не определен интерфейс прикладного программирования, в них описаны необходимые функциональные средства. Интерфейс должен поддерживать следующие концептуальные операции: ¦ распределение локальных ресурсов для связи; ¦ задание локальной и удаленной оконечных точек связи; ¦ инициирование соединения (клиентский компонент); ¦ передача дейтаграммы (клиентский компонент); ¦ ожидание входящего запроса на установление соединения (серверный компонент); ¦ передача или прием данных; ¦ определение момента поступления данных; ¦ выработка срочных данных; ¦ обработка входящих срочных данных; ТЫ является сокращением от Transport Layer Interface (Интерфейс транспортного уровня). 66 Глава 4. Прикладной интерфейс к протоколам
¦ корректное завершение соединения; ¦ обработка запроса на завершение соединения от удаленного участника соединения; ¦ аварийное прекращение связи; ¦ устранение последствий аварийных ситуаций или аварийного прекращения соединения; ¦ освобождение локальных ресурсов после завершения связи. 4.4. Концептуальная спецификация интерфейса Не следует думать, что стандарты TCP/IP не содержат никаких указаний о том, что именно должен обеспечивать API-интерфейс. Для протоколов TCP/IP в них определен концептуальный интерфейс, который применяется в качестве пояснительного примера. Поскольку в большинстве операционных систем для передачи управления из прикладной программы в систему используется процедурный механизм, стандарт определяет концептуальный интерфейс как набор процедур и функций. Стандарт задает параметры, необходимые для каждой процедуры или функции, а также определяет семантику выполняемых ими операций. Например, в стандарте TCP рассматривается процедура SEND и перечисляются параметры, которые должны быть предоставлены приложением для передачи данных по существующему соединению TCP. В основе определения концептуальных операций лежит простой принцип. Концептуальный интерфейс, определенный в стандартах TCP/IP, не регламентирует форматы представления данных и не содержит подробных сведений о программной реализации; он просто представляет собой пример одного из возможных API-интерфейсов, который может быть предложен в операционной системе для прикладных программ, использующих протоколы TCP/IP. Поэтому данный концептуальный интерфейс служит неформальной иллюстрацией того, как приложение взаимодействует с протоколом TCP. Поскольку он не определяет точных требований, проектировщики операционной системы вправе выбирать другие имена процедур или другие параметры, при том условии, что будут обеспечены эквивалентные функциональные возможности. 4.5. Системные вызовы На рис. 4.1 показан механизм системных вызовов, используемый в большинстве операционных систем для передачи управления между прикладной программой и процедурами операционной системы, которые предоставляют доступ к требуемым службам. С точки зрения программиста, каждый системный вызов выглядит и действует как обращение к функции. Системные вызовы действуют аналогично другим вызовам функций, за исключением того, что управление передается в операционную систему. Как показано на этом рисунке, при использовании в приложении системного вызова управление передается от приложения к интерфейсу системного вызова. Затем интерфейс передает управление операционной системе, которая направляет входящий вызов внутренней процедуре, выполняющей затребованную операцию. После завершения работы внутренней процедуры управление через интерфейс системного вызова возвращается к приложению, которое затем продолжает выполняться. По существу, каждый раз при возникновении в прикладной про- 4.4. Концептуальная спецификация интерфейса 67
грамме необходимости в получении доступа к службам операционной системы, процесс, выполняющий приложение, переходит на уровень операционной системы, выполняет необходимую операцию, а затем возвращается на уровень приложения. При прохождении через интерфейс системного вызова процесс приобретает привилегии, которые позволяют ему читать или модифицировать структуры данных в операционной системе. Однако операционная система остается защищенной, поскольку каждый системный вызов выполняет переход к процедуре, написанной проектировщиками операционной системы. Приложения в пользовательском адресном пространстве •^— Интерфейс системного вызова Программное обеспечение протокола в системном адресном пространстве Рис. 4.1. Приложения, взаимодействующие с программным обеспечением протоколов TCP/IP через интерфейс системного вызова 4.6. Два основных подхода к организации сетевой связи При установке программного обеспечения в операционной системе проектировщики операционной системы должны четко определить набор процедур, используемых для доступа к протоколам TCP/IP. Во всех реализациях таких процедур используется один из двух подходов: ¦ создаются принципиально новые системные вызовы, используемые в приложениях для доступа к протоколам TCP/IP; ¦ для доступа к протоколам TCP/IP используются обычные вызовы функций ввода/вывода. При первом подходе составляется перечень всех концептуальных операций, подбираются имена и определяются параметры для каждой из них, а затем операции реализуются в виде системных вызовов. Поскольку многие проектировщики считают, что новые системные вызовы должны создаваться только в случае крайней необходимости, такой подход используется редко. При втором подходе используются обычные примитивы ввода/вывода, но предусматривается их перегрузка (переопределение) с тем, чтобы они могли применяться для работы не только с обычными устройствами ввода/вывода, но и с сетевыми протоколами. Вполне естественно, что многие проектировщики выбирают комплексный подход, в котором по возможности используются существующие функции ввода/вывода, а дополнительные функции вводятся для тех операций, которые не могут быть удобно представлены с помощью существующих функций. Системные функции, вызываемые приложениями Ядро операционной системы, содержащее программное обеспечение протокола TCP/IP 68 Глава 4. Прикладной интерфейс к протоколам
4.7. Основные функции ввода/вывода, применяемые в Linux В качестве иллюстрации того, как могут быть расширены обычные системные вызовы для обеспечения возможности применения протоколов TCP/IP, рассмотрим основные функции ввода/вывода Linux. В системе Linux, как и в других версиях операционной системы UNIX, предусмотрен основной набор из шести системных функций, применяемых для выполнения операций ввода/вывода на устройствах или файлах. В табл. 4.1 перечислены эти операции и указано их обычное назначение. Таблица 4.1. Основные операции ввода/вывода, доступные в Linux Операция Назначение open Подготовить устройство или файл для выполнения операций ввода или вывода close Завершить использование ранее открытого устройства или файла read Получить данные из устройства ввода данных или файла и поместить их в память прикладной программы . write Передать данные из памяти прикладной программы в устройство вывода или файл lseek Перейти в конкретную позицию в файле или на устройстве (эта операция применяется только к файлам или устройствам наподобие дисков) ioctl2 Выполнить операцию управления устройством или программным обеспечением, используемым для доступа к нему (например, указать размер буфера или изменить отображение набора символов) При вызове в прикладной программе для инициирования операции ввода или вывода функция open возвращает небольшое целое число, называемое дескриптором файла, которое используется в приложении во время выполнения следующих операций ввода/вывода. Вызов функции open принимает три параметра: имя открываемого файла или устройства, набор битовых флажков, который определяет правила поведения в особых случаях (например, должен ли быть создан файл, если он не существует), и режим доступа, который в свою очередь определяет права доступа на чтение/запись для вновь созданных файлов. Например, следующий фрагмент кода int desc; desc = ореп(иШепатеи, OJIDWR, 0); позволяет открыть существующий файл filename в режиме, допускающем и чтение, и запись. После получения целочисленного дескриптора desc приложение использует его в дальнейших операциях ввода/вывода, выполняемых с этим файлом. Например, оператор read(desc, buffer, 128); выполняет чтение 128 байтов данных из файла в массив buffer. И наконец, после завершения использования файла в приложении вызывается функция close для освобождения дескриптора и связанных с ним ресурсов (например, внутренних бустеров): close(desc); Название функции ioctl является сокращением от Input Output ConTroL (Управление вводом/выводом). 4.7. Основные функции ввода/вывода, применяемые в Linux 69
4.8. Применение функций ввода/вывода Linux для работы с протоколами TCP/IP На этапе доработки систем UNIX в целях расширения средств работы с протоколами TCP/IP были дополнены обычные средства ввода/вывода UNIX. Во- первых, расширен набор файловых дескрипторов и предусмотрена возможность создавать в приложениях дескрипторы, применимые для сетевой связи. Во- вторых, дополнены системные вызовы read и write с тем, чтобы они могли работать не только с обычными файловыми дескрипторами, но и новыми сетевыми дескрипторами. Поэтому в случае необходимости передать данные через соединение TGP в приложении создается соответствующий дескриптор, а затем для передачи данных используется функция write. Однако не всю сетевую связь можно легко уложить в схему open-read-write-close (открыть-читать-писать-закрыть), применяемую в системе UNIX. В приложении приходится задавать такие параметры, необходимые для протоколов, как номера локального и удаленного портов протокола, а также удаленный IP-адрес, сообщать, какой транспортный протокол (TCP или UDP) был выбран в приложении, а также указывать, должно ли приложение инициировать передачу или ждать входящих запросов на установление соединения (т.е. необходимо определить, в каком качестве оно должно функционировать: в качестве клиента или сервера). Если приложение предназначено для использования в качестве сервера, то в нем необходимо указать, сколько входящих запросов на установление соединения должна поставить в очередь операционная система, прежде чем начать отклонять очередные запросы. Кроме того, если в приложении решено использовать протокол UDP, то в нем необходимо предусмотреть способность передавать дейтаграммы UDP, a не просто поток байтов. Проектировщики API-интерфейса сокетов включили дополнительные функции, позволяющие учесть эти особые случаи. В системе Linux, как и в первых версиях системы UNIX, каждая из дополнительных функций реализована путем введения нового системного вызова в операционную систему. Подробные сведения об этом проекте приведены в следующей главе. 4.9. Резюме Поскольку протоколы TCP/IP предназначены для работы в среде, которая включает программное и аппаратное обеспечение многих поставщиков, стандарты этих протоколов определяют только неформальный интерфейс, который может использоваться в прикладных программах, и предоставляют проектировщикам операционной системы свободу выбора способа его реализации. В стандартах описан концептуальный интерфейс, но он предназначен только в качестве пояснительного примера. Хотя в стандартах этот концептуальный интерфейс представлен в виде набора процедур, проектировщики имеют право выбирать другие процедуры или использовать совершенно иной принцип взаимодействия (например, передачу сообщений). Операционные системы часто предоставляют доступ к службам с помощью механизма, известного как интерфейс системного вызова. При введении средств поддержки протоколов TCP/IP проектировщики операционной системы стремятся, по возможности, свести к минимуму число новых системных вызовов путем расширения существующих системных вызовов. Но т.к. сетевая связь требует выполнения операции, которые невозможно легко совместить с существующими процедурами ввода/вывода, при создании большинства интерфейсов к протоколам TCP/IP приходится предусматривать несколько новых системных вызовов. 70 Глава 4. Прикладной интерфейс к протоколам
Материал для дальнейшего изучения В разделе 2 руководства программиста, которое входит в поставку операционной системы Linux, подробно описан каждый из вызовов сокетов; в разделе 4Р можно ознакомиться с дополнительными сведениями о протоколах и сетевых интерфейсных устройствах. В книге [4] описан интерфейс ТЫ компании AT&T, альтернативный по отношению к интерфейсу сокетов, используемому в версии System V UNIX. Упражнения 4.1. Ознакомьтесь с операционной системой, действующей по принципу передачи сообщений. Как можно было бы расширить интерфейс прикладного программирования с учетом сетевой связи? 4.2. Сравните интерфейс сокетов системы Berkeley UNIX с интерфейсом ТЫ компании AT&T. В чем состоят основные различия? Чем эти два интерфейса похожи? Какие соображения заставили проектировщиков предпочесть один проект другому? 4.3. Некоторые аппаратные архитектуры ограничивают число возможных системных вызовов (например, их число может составлять 64 или 128). Сколько системных вызовов уже назначено в вашей локальной операционной системе? 4.4. Проанализируйте, с чем связано налагаемое аппаратным обеспечением ограничение числа системных вызовов, которое рассматривалось в предыдущем упражнении. Как может системный проектировщик создать операционную систему, в которой число системных вызовов превышает аппаратное ограничение, не дорабатывая аппаратное обеспечение? 4.5. Выясните, как можно воспользоваться в сценарии командного интерпретатора таким устройством, как /dev/tcp, в качестве основы API- интерфейса для протокола TCP. Напишите пример сценария. 4.6. Ознакомьтесь с языком сценариев Perl. Какой API-интерфейс, позволяющий использовать в сценарии протоколы TCP/IP, предоставляется языком Perl? Материал для дальнейшего изучения 71
5 API-интерфейс сокетов 5.1. Введение В предыдущей главе описан интерфейс между прикладными программами и программным обеспечением TCP/IP и показано, как в большинстве систем используется механизм системных вызовов для передачи управления программному обеспечению TCP/IP в операционной системе. В ней также рассмотрено шесть основных функций ввода/вывода, предоставляемых в системах UNIX: open, close, read, write, lseek и ioctl. В этой главе даны подробные сведения об API- интерфейсе сокетов и показано, как функции сокетов объединяются с функциями ввода/вывода UNIX. В ней рассматриваются общие понятия и описана область назначения каждого вызова. В следующих главах показано, как эти вызовы могут применяться в клиентах и серверах, и представлены примеры, которые иллюстрируют многие из описанных здесь подробностей реализации. 5.2. Сокеты Berkeley В начале 1980-х годов агентство ARPA (Advanced Research Projects Agency — агентство перспективных научных исследований) финансировало работу группы разработчиков Калифорнийского университете в г. Беркли, которые должны были создать программное обеспечение транспортных протоколов TCP/IP для операционной системы UNIX и предоставить созданное в результате программное обеспечение в распоряжение представителей других организаций. В составе этого проекта разработчики создали интерфейс, используемый в приложениях для обмена данными. Они решили применять уже существующие системные вызовы и вводить новые только для поддержки функций TCP/IP, которые нельзя легко включить в существующий набор функций. Результаты этого проекта получили широкую известность под названием API-интерфейса сокетов или просто интерфейса сонетов1, а разработанная операционная система стала называться Berkeley UNIX или просто UNIX. (Протокол TCP впервые появился в выпуске 4.1 программного дистрибутива BSD — Berkeley Software Distribution; функции сокетов, описанные в настоящей книге, относятся к более поздним выпускам.) В дальнейшем система Berkeley UNIX была адаптирована для своих целей многими поставщиками компьютеров, в частности, такими изготовителями рабочих станций, как Sun Microsystems, Tektronix и Digital Equipment Corporation, поэтому интерфейс сокетов стал применяться на многих компьютерах. Таким образом, интерфейс сокетов получил настольно широкое распро- 1 Интерфейс сокетов иногда называют интерфейсом сокетов Berkeley.
странение, что стал фактически рассматриваться как стандарт. Компания Microsoft внесла свой вклад в дальнейшее распространение этого интерфейса, разработав версию сокетов для своей операционной системы. И наконец, соке- ты были включены в систему Linux. 5.3. Определение интерфейса протокола При решении вопроса о том, как ввести в операционную систему функции, которые обеспечивают прикладным программам доступ к программному обеспечению протоколов TCP/IP, прежде всего необходимо выбрать имена для функций и параметры, применяемые в каждой функции. Эта работа связана с определением набора услуг, предоставляемых каждой функцией, а также форм их использования в приложениях. Необходимо также решить, должен ли этот интерфейс применяться только с протоколами TCP/IP или в нем нужно предусмотреть возможность работы с дополнительными протоколами. Таким образом, должен быть принят один из двух общих подходов: ¦ определить функции, предназначенные только для поддержки связи по протоколам TCP/IP; ¦ определить функции, которые поддерживают сетевую связь в целом, и ввести параметры, с помощью которых связь по протоколам TCP/IP можно обозначить как частный случай. Различия между этими двумя подходами проще всего понять, рассмотрев, как они влияют на выбор имен системных функций и параметров, необходимых для этих функций. Например, при первом подходе проектировщик может создать системную функцию maketcpconnection, а при втором — создать общую функцию makeconnection и использовать параметр для обозначения протокола TCP. Поскольку проектировщики из г. Беркли решили учесть возможность применения различных наборов протоколов связи, они выбрали второй подход. По существу, в своем проекте они предусмотрели степень обобщения, далеко выходящую за рамки протоколов TCP/IP. Они допустили возможность применения множества семейств протоколов, среди которых протоколы TCP/IP представлены единственным семейством (PF_INET). Они также предусмотрели, чтобы в приложениях необходимые операции связи определялись путем указания требуемого типа службы, а не имени протокола. Поэтому вместо указания на то, что ему требуется соединение TCP, приложение запрашивает службу типа потоковой передачи с использованием параметра, обозначающего семейство протоколов Internet. API-интерфейс сокетов предоставляет обобщенные функции, которые поддерживают сетевую связь с помощью многих возможных протоколов. В вызовах функций сокетов все протоколы TCP/IP упоминаются как одно семейство протоколов. Эти вызовы позволяют программистам указывать тип требуемой службы, а не имя конкретного протокола. Общий проект интерфейса сокетов и предоставляемый этим интерфейсом широкий подход критиковался с момента его создания. Некоторые специалисты в области компьютерных наук утверждают, что такая общность не нужна и только затрудняет изучение прикладных программ, а другие доказывают, что предоставление программистам возможности задавать тип службы, а не конкретный протокол упрощает программирование, поскольку освобождает программиста от необходимости понимать все тонкости функционирования каждого семейства протоколов. И наконец, некоторые коммерческие постав- 74 Глава 5. API-интерфейс сокетов
щики программного обеспечения TCP/IP выступают в пользу применения альтернативных интерфейсов, поскольку интерфейсы сокетов нельзя вводить в операционную систему, если у заказчика нет исходного кода, поэтому поставка операционной системы обычно требует заключения специального лицензионного соглашения и дополнительных расходов. 5.4. Понятие сокета 5.4.1. Дескрипторы сокетов и дескрипторы файлов Приложение, которому требуется выполнить ввод/вывод, вызывает функцию open, чтобы создать дескриптор файла, применяемый для доступа к файлу. Как показано на рис. 5.1, в операционной системе дескриптор файла реализован как массив указателей на внутренние структуры данных. Система сопровождает отдельную таблицу дескрипторов файлов для каждого процесса. При открытии файла процессом система помещает указатель на внутренние структуры данных для этого файла в таблицу дескрипторов файлов процесса и возвращает индекс таблицы в вызывающий оператор. В прикладной программе достаточно только присвоить этот дескриптор переменной и использовать его в последующих вызовах, которые запрашивают операции с файлами. Операционная система использует дескриптор как индекс таблицы дескрипторов процесса и переходит по указателю к структурам данных, содержащим всю информацию о файле. Таблица дескрипторов процесса используется для хранения указателей на внутренние структуры данных для файлов, открытых процессом. Процесс (приложение) использует, дескриптор при обращении к файлу. Операционная система Таблица дескрипторов (одна на весь процесс) Внутренняя структура данных для файла О Внутренняя структура данных для файла 1 Внутренняя структура данных для файла 2 Внутренняя структура данных для файла 3 Рис. 5.1. Таблица дескрипторов, распределяемая для каждого процесса В API-интерфейсе сокетов реализовано новое абстрактное понятие для сетевой связи — сокет. Как и файл, каждый активный сокет обозначается небольшим целым числом, называемым дескриптором сокета. Операционная система размещает дескрипторы сокетов в той же таблице дескрипторов, что и дескрипторы файлов. Поэтому в приложении не может присутствовать и дескриптор файла, и дескриптор сокета с одним и тем же значением. В операционной системе предусмотрена отдельная системная функция socket, вызываемая приложением для создания сокета; функция open используется в приложении только для создания дескрипторов файлов. Общий замысел, кото- 5.4. Понятие сокета 75
рый лег в основу разработки интерфейса сокетов, состоял в том, чтобы для создания любого сокета было достаточно одного системного вызова. Сразу после создания сокета приложение должно выполнить дополнительные системные вызовы для указания точных сведений о его назначении. Этот принцип можно пояснить, рассмотрев структуры данных, сопровождаемые системой. 5.4.2. Системные структуры данных для сокетов Проще всего можно понять принцип работы сокета, рассмотрев связанные с ним структуры данных в операционной системе. При вызове сокета приложением операционная система распределяет в оперативной памяти структуру данных для хранения информации, необходимой для связи, и заполняет новую запись в таблице дескрипторов, включив в нее указатель на структуру данных. Например, на рис. 5.2 показано, что происходит с таблицей дескрипторов, приведенной на рис. 5.1, после вызова функции socket2. В этом примере в качестве параметров вызова функции socket указано семейство протоколов PF_INET и тип службы SOCK_STREAM. В системе используется общая таблица дескрипторов для сокетов и других средств ввода/вывода. Операционная система Таблица дескрипторов (одна на весь процесс) Структура данных для сокета Семейство: PFJNET Служба: SOCK.STREAM Локальный IP-адрес: Удаленный IP-адрес: Локальный порт: Удаленный порт: Рис. 5.2. Концептуальная схема структур данных операционной системы после вызова функции socket Хотя внутренняя структура данных для сокетов содержит много полей, операционная система во время создания сокета оставляет основную часть из них незаполненной. Как будет показано ниже, приложение, создавшее сокет, должно использовать дополнительные системные вызовы для заполнения полей в структуре данных сокета перед его использованием. S.4.3. Перевод сокета в активный или пассивный режим Сразу после создания сокета приложение должно указать, как он будет использоваться. Сам сокет является универсальным средством ввода/вывода и может применяться для любой связи. Например, сервер может настроить сокет на ожидание входящих запросов на установление соединения. Клиент может настроить сокет для инициирования соединения. * По сути, структуры данных намного сложнее, чем показано на рис. 5.1; приведенная здесь схема иллюстрирует общий принцип, но не показывает все нюансы. 0" 1: 2* 3* <1* *" Как .^ и гшвжде ^ 76 Глава 5. API-интерфейс сокетов
Сокет, переведенный сервером в режим ожидания входящих запросов на установление соединения, называется пассивным сонетом. В отличие от него, сокет, применяемый клиентом для инициирования соединения, называется активным сонетом. Единственное различие между активным и пассивным сонетами состоит в том, какие именно приложения их используют; для первоначального создания сонета всегда применяется; один и тот же способ. 5.5. Определение адреса оконечной точки Сразу после своего создания сокет не содержит никакой информации о том, как он будет использоваться. В частности, сокет не содержит информации о номерах портов протокола или IP-адресах локального или удаленного компьютеров. Прежде чем в приложении можно будет использовать сокет, необходимо указать один или оба эти адреса. В протоколах TCP/IP оконечная точка связи определена как сочетание IP- адреса и номера порта протокола. В других семействах протоколов такие адреса оконечных точек определены иначе. Поскольку абстрактное понятие сокета распространяется на многие семейства протоколов, в сокете не указано, как должны быть определены адреса оконечных точек, и даже не задан конкретный формат протокольного адреса. Вместо этого сокет разрешает указывать оконечные точки в соответствии с каждым семейством протоколов. Для обеспечения свободы выбора представлений используемых адресов в каждом семействе протоколов спецификация сокета определяет семейство адресов для каждого типа адреса. В семействе протоколов для определения формата представления адреса может использоваться одно или несколько семейств адресов. Во всех протоколах TCP/IP применяется единственное представление адреса, а семейство адресов обозначается символической константой AF_INET. На практике часто возникает путаница между семейством протоколов TCP/IP, которое обозначается константой PF_INET, и применяемым в нем семейством адресов, обозначенным константой AF_INET. Основная проблема состоит в том, что обе символические константы имеют одинаковое числовое значение B), поэтому если в программе одна из этих констант непреднамеренно заменена другой, программа все равно работает правильно. Примеры неправильного использования встречаются даже в оригинальном исходном коде Berkeley UNIX. Однако программисты должны учитывать различия между этими константами, поскольку при таком условии будет понятен смысл переменных, а программы станут более переносимыми. 5.6. Обобщенная структура адреса Разработчики некоторых программ выполняют манипуляции с протокольными адресами, не учитывая подробных сведений о том, как определено представление адреса в каждом семействе протоколов. Например, может возникнуть необходимость написать процедуру, которая принимает в качестве параметра определение произвольной оконечной точки протокола и выбирает одно из нескольких возможных действий в зависимости от типа адреса. Для обеспечения возможности создания таких программ в системе сокетов определен обобщенный формат, используемый во всех адресах оконечных точек. Такой обобщенный формат состоит из следующей пары значений: (address^family, endpoint_address) Здесь поле address^family (семейство адресов) содержит константу, которая определяет один из заранее назначенных типов адресов, а поле endpoint_ б.б. Определение адреса оконечной точки 77
address (адрес оконечной точки) содержит адрес оконечной точки, в котором используется стандартное представление для указанного типа адреса. На практике программное обеспечение сокетов содержит объявления заранее определенных структур С для адреса оконечных точек. В прикладных программах эти заранее определенные структуры используются, когда возникает необходимость сохранить в памяти адреса оконечных точек или использовать оверлейную структуру для определения местонахождения отдельных полей в структуре. Наиболее общая структура известна под названием sockaddr. Она содержит 2-байтовый идентификатор семейства адресов и 14-байтовый массив для хранения адреса3: struct sockaddr { /* Структура для хранения адреса */ u_char sa_len; /* Общая длина */ u^short sa_family; /* Тип адреса */ cHar sa data[14]; /* Значение адреса */ }? К сожалению, не во всех семействах адресов определены оконечные точки, которые вписываются в структуру sockaddr. Например, в некоторых системах UNIX предусмотрена возможность определять в семействе адресов AFJJNIX программную конструкцию, которую принято называть именованным каналом. Адреса оконечной точки в семействе AF_UNIX состоят из имени пути в файловой системе, которое может намного превышать по длине 14 байтов. Поэтому в прикладных программах нельзя использовать структуру sockaddr при объявлении переменных, поскольку переменная, объявленная с типом sockaddr, не имеет достаточной длины для хранения всех возможных адресов оконечных точек. На практике часто возникает путаница, поскольку структура sockaddr как раз и предназначена для хранения адресов семейства AF_INET. Поэтому программное обеспечение TCP/IP работает правильно, даже если программист объявляет переменные как относящиеся к типу sockaddr. Однако для обеспечения переносимости и удобства сопровождения программ в коде работы с протоколами TCP/IP не следует использовать в объявлениях структуру sockaddr. Вместо этого структура sockaddr должна применяться только в качестве оверлейной структуры, а код должен ссылаться только на поле sa_f amily этой структуры. В каждом семействе протоколов, в котором используются сокеты, определено точное представление адресов оконечных точек, а в программном обеспечении сокетов предусмотрены соответствующие объявления структур. Каждый адрес оконечной точки TCP/IP состоит из 2-байтового поля, которое обозначает тип адреса (в нем должна быть задана константа AF_INET), 2-байтового поля номера порта, 4-байтового поля IP-адреса и 8-байтового поля, которое остается неиспользуемым. Этот формат определен в заранее заданной структуре sockaddr_in: struct sockaddrjln { /* Структура для хранения адреса */ u_char sin_len; /* Общая длина */ iTshort sin_familyj /* Тип адреса */ u_short sinjport; /* номер порта протокола */ struct in_addr sin_addr; /* IP-адрес (в некоторых системах */ /* объявляется с типом u_long) */ char sin_zero[8]; /* Не используется (устанавливается */ /* равным нулю) */ У. 3 В этой книге указанная структура описана в соответствии со спецификацией выпуска 4.4 программного обеспечения Berkeley и последующих версий систем; более старые версии структуры sockaddr не включают поле sa_len. 78 Глава 5. API-интерфейс сокетов
В приложении, в котором используются исключительно только протоколы TCP/IP, можно применять структуру sockaddr_in; в нем никогда не потребуется использовать структуру sockaddr4. В прикладной программе для представления оконечной точки связи протоколов TCP/IP используется структура sockaddr^in, которая содержит и IP-адрес, и номер порта протокола. Программисты должны соблюдать осторожность, разрабатывая программы, в которых используется сочетание разных семейств протоколов, поскольку для адресов оконечных точек некоторых протоколов, отличных от TCP/IP, требуется структура больших размеров. 5.7. Основные системные вызовы в API-интерфейсе сокетов Вызовы сокетов можно разделить на две группы: основные вызовы, которые предоставляют доступ к базовым функциональным средствам, и вспомогательные процедуры, которые упрощают работу программиста. В настоящем разделе описаны вызовы, которые предоставляют основные функциональные средства, необходимые для работы клиентов и серверов. Объем информации, связанный с описанием системных вызовов сокетов, их параметров и семантики, может показаться ошеломляющим. Сложность этого интерфейса в основном связана с тем, что сокеты имеют параметры, позволяющие использовать их в программах многими способами. Сокет может применяться клиентом или сервером для потоковой передачи (TCP) или дейтаграммной связи (UDP), причем он может использоваться с конкретным адресом удаленной оконечной точки (обычно необходимым для клиента) или с неопределенным адресом удаленной оконечной точки (обычно необходимым для сервера). Чтобы упростить задачу освоения интерфейса сокетов, начнем с изучения основных вызовов сокетов и описания того, как они могут использоваться в простых клиентских и серверных программах для обмена данными с помощью протокола TCP. В каждой из следующих глав рассматривается один из способов использования сокетов и иллюстрируются многие подробности и тонкости, не описанные в настоящей главе. 5.7.1. Вызов функции socket В приложении функция socket вызывается для создания нового сокета, который может использоваться для сетевой связи. После вызова эта функция возвращает дескриптор вновь созданного сокета. Параметры вызова обозначают семейство протоколов, которое будет применяться в приложении (например, PF_IflET для TCP/IP), и необходимый протокол или тип службы (например, потоковый или дейтаграммный). Для сокета, в котором используется семейство протоколов Internet, параметр с указанием протокола или типа службы определяет, какой протокол (TCP или UDP) будет применяться в сокете. 4 Структура sockaddr используется для приведения (т.е. смены типа) указателей или результатов вызовов системных функций для обеспечения применения в программах строгого контроля соответствия типов. 5.7. Основные системные вызовы в API-интерфейсе сокетов 79
5.7.2. Вызов функции connect После создания сокета клиент вызывает функцию connect для установления активного соединения с удаленным сервером. Параметр функции connect, который включает IP-адрес удаленного компьютера и номер порта протокола, позволяет клиенту указать удаленную оконечную точку. После установления соединения клиент может передавать с его помощью данные5. 5.7.3. Вызов функции send Функция send используется и в клиентах, и в серверах для передачи данных через соединение TCP. Клиенты обычно используют функцию send для передачи запросов, а серверы — для передачи ответов. При вызове функции send необходимо задать три параметра. Приложение передает системе в этом вызове дескриптор сокета, в который должны быть отправлены данные, адрес передаваемых данных и их длину. Обычно функция send копирует исходящие данные в буферы, находящиеся в ядре операционной системы, и позволяет приложению продолжать выполнение, в то время как происходит передача данных по сети. Если буферы системы переполняются, вызов функции send может быть временно заблокирован, т.е. до тех пор, пока не появится возможность передать по протоколу TCP данные по сети и освободить в буфере место для новых данных. 5.7.4. Вызов функции recv И клиенты, и серверы используют функцию recv для приема данных из соединения TCP. Обычно после установления соединения сервер использует функцию recv для получения запроса, отправляемого клиентом путем вызова функции send. После отправки запроса клиент исйользует функцию recv для получения ответа. Для чтения данных из соединения приложение вызывает функцию recv с тремя параметрами. Первый параметр задает используемый дескриптор сокета, второй определяет адрес буфера, а третий указывает длину буфера. Функция recv извлекает байты данных, поступающие через сокет, и копирует их в область буфера, определяемую пользователем. Если данные не поступают, вызов функции recv блокируется до тех пор, пока они не начнут поступать. Если поступает больше данных, чем помещается в буфере, функция recv извлекает из сокета только тот объем, который является достаточным для заполнения буфера. Если поступает меньше данных, чем помещается в буфере, функция recv извлекает все данные и возвращает число полученных байтов. В клиентах и серверах функция recv может также использоваться для получения сообщений из сокетов, в которых применяется протокол UDP. Как и при использовании протокола с установлением логического соединения, в вызывающем операторе должно быть задано три параметра, которые обозначают дескриптор сокета, адрес буфера для размещения данных и размер буфера. При каждом вызове функции recv извлекается одно входящее сообщение UDP (т.е. одна пользовательская дейтаграмма). Если буфер не может вместить полностью все сообщение, функция recv заполняет буфер и отбрасывает оставшиеся данные. 5.7.5. Вызов функции close После завершения использования сокета в клиентской или серверной программе необходимо вызвать функцию close для его освобождения. Если сокет 5 В следующей главе подключенные и неподключенные сокеты рассматриваются более подробно. 80 Глава 5. API-интерфейс сокетов
используется только одним процессом, функция close немедленно разрывает соединение и освобождает сокет, а если сокет находится в совместном использовании нескольких процессов, функция close уменьшает число ссылок на сокет и освобождает его, как только число ссылок достигает нуля. 5.7.6. Вызов функции bind Сразу после создания сокета в нем отсутствует информация об адресах оконечных точек (ему не присвоен ни локальный, ни удаленный адрес). В приложении для указания адреса локальной оконечной точки вызывается функция bind. Этот вызов принимает параметры, в которых задаются дескриптор сокета и адрес оконечной точки. В протоколах TCP/IP для задания адреса оконечной точки используется структура sockaddr_in, которая включает и IP-адрес, и номер порта протокола. В сервере функция bind используется в основном для указания общепринятого номера порта, через который он будет принимать запросы на установление соединения. 5.7.7. Вызов функции listen Сразу после создания сокет не является ни активным (т.е. готовым для использования клиентом), ни пассивным (т.е. готовым для использования сервером) до тех пор, пока в приложении не будут осуществлены дальнейшие действия. Серверы с установлением логического соединения вызывают функцию listen, чтобы перевести сокет в пассивный режим и подготовить его для приема входящих запросов на установление соединения. Большая часть серверных программ состоит из бесконечного цикла, в котором принимается очередной входящий запрос на установление соединения, выполняется его обработка, а затем происходит возврат к выполнению операции приема очередного входящего соединения. Даже если обработка каждого входящего соединения занимает только несколько миллисекунд, может оказаться, что новый запрос на установление соединения поступит в тот момент, когда сервер занимается обработкой текущего запроса. Для обеспечения того, чтобы не был потерян ни один запрос на установление соединения, сервер должен передать функции listen параметр, который указывает операционной системе, что запросы на установление соединения, поступающие в сокет, нужно ставить в очередь. Поэтому один параметр вызова функции listen указывает сокет, который должен быть переведен в пассивный режим, а другой —размер очереди, предназначенной для этого сокета. 5.7.8. Вызов функции accept При работе с сокетами TCP после вызова в серверной программе функций socket (для создания сокета), bind (для указания адреса локальной оконечной точки) и listen (для перевода сокета в пассивный режим) вызывается функция accept для извлечения из очереди следующего входящего запроса на установление соединения. Параметр функции accept указывает сокет, из очереди которого должен быть принят запрос на соединение. Функция accept создает новый сокет для каждого нового запроса на соединение и возвращает дескриптор нового сокета в вызывающий оператор. В сервере новый сокет используется только для нового соединения, а первоначальный сокет служит для приема следующих запросов на соединение. Сразу после приема запроса на соединение сервер может передавать данные через новый сокет. Закончив использование нового сокета, сервер его закрывает. 5.7. Основные системные вызовы в API-интерфейсе сокетов 81
5.7.9. Применение функций read и write с сокетами В системе Linux, как и в большинстве других систем UNIX, программисты могут использовать функцию read вместо recv, и write вместо send. Это означает, что применительно к сокетам TCP или UDP функция read имеет тот же смысл, что и recv, а функция write равнозначна send. Основным преимуществом использования функций read и write является то, что программисты с ними уже знакомы, а основным преимуществом функций send и recv можно назвать то, что их можно легко отличить от других функций ввода/вывода в коде, 5.7.10. Сводные данные по вызовам функций сокетов В табл. 5.1 перечислены системные вызовы, относящиеся к сокетам. Таблица 5.1. Функции сокета и их назначение; функции read и write эквивалентны функциям recv и send Функция Назначение socket Создать дескриптор для использования в сетевой связи connect Подключиться к удаленному участнику соединения (клиент) send (write) Передать исходящие данные в соединение TCP recv (read) Принять входящие данные из соединения TCP close Завершить связь и освободить дескриптор bind Привязать локальные IP-адрес и порт протокола к сокету listen Перевести сокет в пассивный режим и установить число входящих запросов на установление соединения TCP, которые должны быть поставлены в очередь операционной системой (сервер) accept Принять очередной входящий запрос на соединение (сервер) recv (read) Принять следующую входящую дейтаграмму UDP recvmsg Принять следующую входящую дейтаграмму UDP (разновидность функции recv) recvf roro Принять следующую входящую дейтаграмму и записать исходный адрес ее оконечной точки send (write) Передать исходящую дейтаграмму UDP sendmsg Передать исходящую дейтаграмму UDP (разновидность функции send) sendto Передать исходящую дейтаграмму UDP, обычно по заранее записанному адресу оконечной точки shutdown Завершить передачу данных через соединение TCP в одном или обоих направлениях getpeername После поступления запроса на установление соединения получить адрес оконечной точки удаленного компьютера из сокета getsockopt Получить текущие опции сокета setsockopt Изменить опции сокета 82 Глава 5. API-интерфейс сокетов
5.8. Вспомогательные процедуры преобразования В стандартах TCP/IP определено стандартное представление для двоичных целых чисел, применяемых в заголовках протоколов. Это представление, известное под названием сетевого порядка байтов, предусматривает форму представления целых чисел со старшим байтом в начале. Хотя программное обеспечение протоколов скрывает от прикладных программ основную часть значений, используемых в заголовках, программист должен знать о существовании указанных стандартов, поскольку некоторые процедуры сокетов требуют записи параметров в сетевом порядке байтов. Например, сетевой порядок байтов применяется в поле порта протокола структуры sockaddr_in. Процедуры сокетов включают несколько функций, выполняющих прямое и обратное преобразование из сетевого порядка байтов в порядок байтов локального хоста. В программах следует всегда вызывать процедуры преобразования, даже если порядок байтов локального компьютера совпадает с сетевым порядком байтов, поскольку соблюдение этого правила обеспечивает переносимость исходного кода на компьютеры с иной архитектурой. Процедуры преобразования подразделяются на два набора, с суффиксами s (сокращение от слова short) и 1 (сокращение от слова long), предназначенные для работы, соответственно, с 16-битовыми и 32-битовыми целыми числами. Функции htons (сокращение от host to network short) и ntohs (сокращение от network to host short) выполняют прямое и обратное преобразование целых чисел в коротком формате из порядка байтов хоста в сетевой порядок байтов. Аналогичным образом, функции htonl и ntohl выполняют прямое и обратное преобразование целых чисел в длинном формате из порядка байтов хоста в сетевой порядок байтов. В программном обеспечении, в котором используются протоколы TCP/IP, необходимо вызывать функции htons, ntohs, htonl и ntohl для прямого и обратного преобразования двоичных целых чисел из порядка байтов хоста в стандартный сетевой порядок байтов. Выполнение этого требования обеспечивает переносимость исходного кода на любой компьютер независимо от предусмотренного в нем порядка байтов. 5.9. Применение вызовов функций сокетов в программе На рис. 5.3 показана последовательность вызовов, выполняемых клиентом и сервером с использованием протокола TCP. В клиентской программе создается сокет с помощью функции socket, вызывается функция connect для подключения к серверу, а затем осуществляется обмен данными с использованием функции send (или write) для передачи запросов и recv (или read)— для получения ответов. По окончании использования соединения в клиентской программе вызывается функция close. В серверной программе используется функция bind для указания локального (известного клиенту или общепринятого) порта протокола, который будет применяться в сервере; вызывается функция listen для установки длины очереди запросов на соединение, а затем серверная программа входит в цикл. Сервер в цикле вызывает функцию accept для перехода к ожиданию поступления очередного входящего запроса на установление соединения, затем в нем используются функции recv и send (или read и write) для взаимодействия с клиентом, и наконец, вызывается функция close для разрыва соединения. После этого сервер возвращается в цикле к выполнению вызова функции accept, в котором он ожидает поступления следующего запроса на установление соединения. Сервер функционирует неопределенно долгое время. Он ожидает поступления но- 5.8. Вспомогательные процедуры преобразования 83
вого запроса на соединение через общепринятый порт, принимает запрос, взаимодействует с клиентом, а затем закрывает соединение. 5.10. Символические константы для параметров вызова функций сокетов Кроме системных функций, которые реализуют протоколы взаимодействия через сокеты, в большинстве систем предусмотрен набор заранее определенных символических констант и объявлений структур данных, которые могут использоваться в программах для объявления данных и задания параметров. Например, при указании применяемой службы (дейтаграммной или потоковой) в прикладной программе используются символические константы S0CK_DGRAM или S0CKJSTREAM. Для этого в црограмму необходимо включить соответствующие определения с помощью оператора include препроцессора языка программирования С. Обычно операторы include присутствуют в начале файла исходного кода; они должны быть обработаны препроцессором перед тем, как в программе будут использоваться какие-либо определяемые в них константы. Операторы include, необходимые для работы с сокетами, обычно имеют вид: ¦include <sys/types.h> ¦include <sys/socket.h> Клиентская часть socket j connect i cenH ^^ * ID \ close Серверная часть socket \ bind \ ticton iioioi i * accept \ recv \ send \ close Рис. б.З. Пример последовательности системных вызовов сонетов, выполняемых клиентом и сервером при использовании протокола TCP В последующем изложении подразумевается, что программа всегда начинается с этих операторов, даже если они явно не показаны в примерах. В конкретных реализациях API-интерфейса сокетов обычно предусмотрены заранее определенные символические константы и объявления структур данных, применяемые в системных вызовах функций сокетов. Программы, в которых имеются ссылки на эти константы, должны начинаться с one- 84 Глава 5. API-интерфейс сокетов
раторов include препроцессора С с указанием файлов, в которых находятся эти определения, 5.11. Резюме В операционной системе BSD UNIX было впервые введено понятие сокета как механизма, позволяющего создать интерфейс между прикладными программами и программным обеспечением протокола в операционной системе. Поскольку этот механизм в дальнейшем был адаптирован для своих программных продуктов многими другими поставщиками, API-интерфейс сокетов фактически стал стандартом. В программе для создания сокета и получения его дескриптора сокета вызывается функция socket. Параметры вызова функции socket должны задавать используемое семейство протоколов и тип требуемой службы. Все протоколы TCP/IP входят в состав семейства протоколов Internet, что обозначается символической константой PF_INET. Система создает для сокета внутреннюю структуру данных, заполняет поле семейства протоколов и использует параметр с указанием типа службы для выбора конкретного протокола (обычно либо UDP, либо TCP). Дополнительные системные вызовы позволяют задавать в приложении адрес локальной оконечной точки (bind), переводить сокет в пассивный режим для его использования в сервере (listen) или переводить сокет в активный режим для его использования в клиенте (connect). В серверах могут быть выполнены дополнительные вызовы для получения входящих запросов на установление соединения (accept), а клиенты и серверы могут передавать или принимать информацию (recv и send). И наконец, и в клиентах, и в серверах могут быть освобождены выделенные для сокета ресурсы по окончании его использования (close). Структура сокета позволяет определить в каждом семействе протоколов одно или несколько представлений адреса. Во всех протоколах TCP/IP используется семейство адресов Internet, обозначенное константой AF_INET, которое определяет, что адрес оконечной точки содержит и IP-адрес, и номер порта протокола. При задании в приложении оконечной точки связи для функции socket используется заранее определенная структура sockaddr_in. Если в клиентской программе указано, что ей нужен любой неиспользуемый локальный порт протокола, программное обеспечение TCP/IP выбирает для нее такой порт. Прежде чем в прикладной программе, написанной на языке программирования С, можно будет использовать заранее определенные структуры и символические константы, относящиеся к сокетам, в нее необходимо включить несколько файлов с определениями этих структур и констант. В частности, в этой книге предполагается, что все приведенные в ней исходные программы начинаются с операторов, которые включают файлы <sys/types.h> и <sys/socket.h>. Материал для дальнейшего изучения В книге [92] подробно описана система Berkeley UNIX и рассмотрены внутренние структуры данных, используемые для сокетов. В статье [129] интерфейс для протоколов TCP/IP представлен с точки зрения файловой системы UNIX. Оперативная документация, которая входит в поставку Linux, содержит спецификации функций сокетов, в том числе точное описание параметров и кодов возврата. Заслуживает внимания раздел этой документации, озаглавленный The IPC Tutorial. Большой объем информации о вызовах сокетов можно также найти в Приложении 1. 5.11. Резюме 85
Упражнения 5.1. Рассмотрите включаемый файл для сокетов в вашей системе (обычно он называется /usr/include/sys/socket.h). Какие типы сокетов разрешены для применения? Какие типы сокетов не имеют смысла для протоколов TCP/IP? 5.2. Если в вашей компьютерной системе имеются аппаратные часы с точностью, по меньшей мере, до одной микросекунды, измерьте время, необходимое для выполнения каждого из системных вызовов сокетов. Почему некоторые вызовы требуют на несколько порядков больше времени по сравнению с другими? 5.3. Внимательно прочитайте справочное руководство по функции connect. Какой сетевой трафик создается при вызове функции connect с соке- том типа SOCK_DGRAM? 5.4. Проследите за пакетами в локальной сети при первом вызове в приложении функции connect с сокетом типа S0CKJ5TREAM. Какое число пакетов вами обнаружено? 5.5. Как было сказано выше, для передачи данных в сокет могут с одинаковым успехом использоваться функции send или write, а для приема данных из сокета — функции read или recv. Изучите производственный код. Какая из двух равнозначных функций применяется в нем чаще всего? Какую предпочитаете вы? Почему? 86 Глава 5. API-интерфейс сокетов
б Алгоритмы и задачи проектирования клиентского программного обеспечения 6.1. Введение В предыдущих главах приведены общие сведения о сокетах, которые используются в приложениях для обеспечения взаимодействия с программным обеспечением TCP/IP, и описаны связанные с ними системные вызовы. В настоящей главе рассматриваются алгоритмы, лежащие в основе клиентского программного обеспечения. В ней показано, как приложения, инициируя связь, становятся клиентами, как в них используются протоколы TCP или UDP для взаимодействия с сервером и вызовы функций сокетов для взаимодействия с протоколами. В следующей главе это описание продолжено и приведены законченные клиентские программы, в которых реализованы представленные здесь понятия. 6.2. Изучение алгоритмов, а не конкретных реализаций Поскольку протоколы TCP/IP предоставляют широкий набор функциональных средств, позволяющих обеспечить взаимодействие программ с помощью различных способов, в приложении, в котором используются протоколы TCP/IP, необходимо определить большое число сведений о желаемом способе связи. Например, в приложении необходимо указать его назначение (клиент или сервер), ввести адрес оконечной точки (или адреса обеих оконечных точек), задать применяемый протокол (с установлением или без установления логического соединения), определить способ предписания правил авторизации и защиты, а также привести такие сведения, как размер применяемых буферов. До сих пор в этой книге рассматривался только набор операций, предназначенных для использования в приложениях, без описания способов их конкретного использования. К сожалению, изучение подробных сведений обо всех возможных системных вызовах и их точных параметрах не позволяет понять, каким образом можно построить хорошо спроектированную распределенную программу. В действительности, лишь немногие разработчики помнят все эти сведения, хотя, безусловно, требуется общее понимание функционирования системных вызовов, предназначенных для сетевой связи. Вместо изучения конкретных деталей следует изучать и запоминать все возможные способы взаимодействия программ по сети и постараться понять преимущества и недостатки каждого возможного проекта. По сути, для успешной работы важнее всего изучить алгоритмы, лежа-
щие в основе распределенных вычислений, что позволяет принимать обоснованные проектные решения и быстро выбирать наиболее подходящие из всех возможных алгоритмов. Только после принятия таких решений следует обращаться к руководству по программированию, чтобы узнать все подробности, необходимые для написания программы, реализующей конкретный алгоритм в конкретной системе. Основной вывод из сказанного состоит в том, что если программист знает, что должна делать программа, то для него несложно будет определить, как осуществить эти действия. Хотя программисты обязаны понимать концептуальные возможности API-интерфейса, они должны сосредоточиться на изучении способов структуризации программ связи, а не усваивать подробные сведения о конкретном API-интерфейсе. 6.3. Архитектура клиента Приложения, действующие в качестве клиентов, концептуально проще приложений, выполняющих функции серверов, по нескольким причинам. Во- первых, в основной части клиентского программного обеспечения явно не предусмотрено обеспечение параллельного взаимодействия с несколькими серверами. Во-вторых, основная часть клиентского программного обеспечения применяется в виде обычных прикладных программ. В отличие от серверного, для клиентского программного обеспечения обычно йе требуются специальные привилегии, поскольку оно, как правило, не обращается к привилегированным портам протокола. В-третьих, в значительной части клиентского программного обеспечения не нужно предписывать соблюдение правил защиты, поскольку клиентские программы могут полагаться на операционную систему, которая обеспечивает выполнение таких правил автоматически. В действительности, задачи проектирования и реализации клиентского программного обеспечения являются столь простыми, что опытный прикладной программист может быстро научиться разрабатывать несложные клиентские приложения. В следующих разделах дано общее описание клиентского программного обеспечения, а затем описаны различия между клиентами, в которых используется протокол TCP, и клиентами, основанными на использовании протокола UDP. 6.4. Определение местонахождения сервера В клиентском программном обеспечении может использоваться один из перечисленных ниже методов для определения IP-адреса и номера порта протокола сервера: ¦ доменное имя или IP-адрес сервера могут быть заданы в виде константы во время трансляции клиентской программы; ¦ клиентская программа может требовать у пользователя указывать сервер при вызове ее на выполнение; ¦ информация о местонахождении сервера предоставляется из постоянного хранилища данных (например, из файла на локальном диске); ¦ для поиска сервера используется отдельный протокол (например, групповой или широковещательной рассылки сообщения, на которое отвечают все серверы). Если адрес сервера задан в виде константы, клиентское программное обеспечение работает быстрее и в меньшей степени зависит от конкретной локальной вычислительной среды. Однако применение такого способа означает также, что клиентская программа должна быть перетранслирована при изменении местонахождения сервера. Еще более важно то, что клиент не может быть на время подключен 88 Глава 6. Алгоритмы и задачи проектирования...
к другому серверу даже в период отладки. В качестве компромиссного решения в некоторых клиентских программах предусматривается применение константы с именем компьютера вместо IP-адреса. Если вместо IP-адреса применяется константа с именем компьютера, то привязка адреса к порту протокола откладывается до момента вызова программы на выполнение. Это позволяет выбрать на узле обобщенное имя для сервера и ввести в систему доменных имен псевдоним для этого имени. Использование псевдонимов позволяет сетевому администратору узла менять местонахождение серверов без смены клиентского программного обеспечения. Чтобы переместить сервер в другое место, администратору достаточно сменить только псевдоним компьютера. Например, в локальном домене можно ввести псевдоним для почтового хоста и предусмотреть, чтобы все клиенты искали в системе доменных имен строку "mailhost" вместо имени конкретного компьютера. Поскольку все клиенты ссылаются на общее имя, а не на имя конкретного компьютера, системный администратор может изменить местонахождение почтового хоста без перетрансляции клиентского программного обеспечения. Способ, предусматривающий хранение адреса сервера в файле, обеспечивает повышение гибкости клиентского программного обеспечения, но означает также, что выполнение клиентской программы зависит от наличия такого файла. Поэтому клиентское программное обеспечение нельзя перенести на другой компьютер без дополнительных сложностей. Хотя способ с использованием широковещательного протокола для поиска серверов хорошо работает в небольшой, локальной среде, его нельзя распространить на большие объединенные сети. Кроме того, применение механизма динамического поиска создает сложности при проектировании клиентов и серверов, а также приводит к появлению дополнительного широковещательного трафика в сети. В целях устранения ненужной сложности и зависимости от компьютерной среды в большинстве клиентских программ задача определения местонахождения сервера решается с помощью наиболее простого способа: клиентские программы требуют от пользователя задать параметр с указанием сервера во время их вызова. Если при разработке клиентского программного обеспечения предусматривается возможность задания адреса сервера в качестве параметра, то клиентское программное обеспечение становится более универсальным и устраняется его зависимость от вычислительной среды. Если пользователю предоставляется возможность указать адрес сервера при вызове клиентского программного обеспечения, то клиентская программа становится более универсальной и появляется возможность менять местонахождение сервера. Необходимо сделать следующее важное замечание: применение параметра для задания адреса сервера обеспечивает наибольшую гибкость. Программу, принимающую параметр с адресом, можно использовать в сочетании с другими программами, которые считывают адрес сервера с диска, находят адрес с помощью удаленного сервера имен или определяют его с использованием широковещательного протокола. Разработка клиентского программного обеспечения, принимающего адрес сервера в качестве параметра, обеспечивает также создание расширенных версий ПО, в которых используются дополнительные способы поиска адреса сервера (например, чтение адреса из файла на диске). Для некоторых служб требуется определенный сервер, тогда как другие позволяют использовать любой доступный сервер. Например, если пользователь вызывает клиентскую программу дистанционной регистрации, он имеет в виду конкретный целевой компьютер; для него регистрация на другом компьютере обычно не имеет смысла. Но если пользователь просто хочет узнать текущее 6.4. Определение местонахождения сервера 89
время, то для него не имеет значения, от какого сервера будет получен ответ. Для предоставления доступа к подобным службам проектировщик может так приспособить один из описанных выше способов поиска сервера, чтобы в результате применения этого способа клиент получал целый ряд имен серверов, а не одно имя. Для этого необходимо также внести изменения в клиентскую программу таким образом, чтобы она последовательно обращалась к каждому серверу в указанном наборе до тех пор, пока один из них не ответит. 6.5. Синтаксический анализ параметра адреса Обычно пользователь при вызове клиентской программы может задавать параметры в командной строке. В большинстве систем каждый параметр, передаваемый в клиентскую программу, представляет собой символьную строку. В клиенте используется информация о синтаксисе каждого параметра для интерпретации его значения. Например, большая часть клиентского программного обеспечения позволяет пользователю задавать либо доменное имя компьютера, на котором работает сервер (например, merlin.cs.purdue.edu), либо IP-адрес в точечной десятичной форме (например, 128.10.2.3). Для определения того, что именно было задано пользователем (имя или адрес), клиентская программа проверяет, содержит ли параметр алфавитные символы. При положительном ответе предполагается, что было задано имя. Если же параметр содержит только цифры и десятичные точки, то клиентская программа принимает предположение, что задан точечный десятичный адрес, и выполняет его синтаксический анализ соответствующим образом. Безусловно, для клиентских программ иногда требуется не только доменное имя компьютера или IP-адрес сервера, но и некоторая дополнительная информация. Например, полностью параметризованное клиентское программное обеспечение позволяет пользователю не только задавать имя компьютера, но и указывать порт протокола. Для этого может применяться дополнительный параметр; эта информация может быть также закодирована в одной строке. Например, для указания порта протокола, связанного со службой smtp, на компьютере с именем merlin.cs.purdue.edu, клиент может принимать два параметра: merlin.cs.purdue.edu smtp В другом варианте вызова имя компьютера и порт протокола могут быть объединены в один параметр: merlin.cs.purdue.edu:smtp Хотя для каждого клиента конкретный синтаксис параметров может быть выбран независимо от других, применение многочисленных клиентских программ со своим собственным синтаксисом может привести к путанице. С точки зрения пользователя, одним из самых важных требований является единообразие. Поэтому программистам рекомендуется следовать соглашениям по разработке клиентского программного обеспечения, установленным в их локальной системе. Например, во многих утилитах Linux для указания хост-компьютера сервера и порта протокола используются отдельные параметры. 6.6. Поиск доменного имени В клиентской программе адрес сервера должен быть указан с помощью структуры sockaddr_in. Это означает, что адрес, заданный в точечной десятичной форме (или в текстовом виде, как доменное имя), должен быть преобразован в 32- битовый IP-адрес, представленный в двоичном коде. Задача преобразования то- 90 Глава 6. Алгоритмы и задачи проектирования...
чечного десятичного обозначения в двоичную форму является несложной. Однако преобразование его из доменного имени требует значительно больших усилий. API-интерфейс сонетов включает библиотечные процедуры inet_addr и gethostbyname, которые выполняют необходимые преобразования1. Процедура inet_addr принимает в качестве параметра строку в коде ASCII, содержащую точечный десятичный адрес, и возвращает эквивалентный IP-адрес в виде двоичного кода. Процедура gethostbyname принимает строку в коде ASCII, содержащую доменное имя компьютера. Она возвращает адрес структуры hostent, которая, кроме всего прочего, содержит IP-адрес хоста в двоичном коде. Структура hostent определена во включаемом файле netdb.h: struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h addr list; ¦define h_addr h_addrj.ist[0] Поля, содержащие имена и адреса, должны представлять собой списки, поскольку хосты, которые имеют несколько сетевых интерфейсов, имеют также несколько имен и адресов. Кроме того, для совместимости с предыдущими версиями в файле определен идентификатор h_addr, который указывает на первый элемент в списке адресов хостов. Поэтому в программе идентификатор h_addr может использоваться так, как если бы он обозначал поле структуры. Рассмотрим простой пример преобразования имени. Предположим, что клиентской программе передано доменное имя merlin.cs.purdue.edu в форме строки и она должна получить IP-адрес. Клиентская программа должна вызвать процедуру gethostbyname следующим образом: struct hostent *hptr; char *examplenam = "merlin.cs.purdue.edu"; if ( hptr = gethostbyname( examplenam ) ) { /* Теперь IP-адрес - в поле hptr->h_addr */ } else { /* Ошибка в имени - обработать ошибку */ } В случае успешного вызова процедура gethostbyname возвращает указатель на действительную структуру hostent. Если имя не удалось преобразовать в IP- адрес, вызов процедуры возвращает нуль. Поэтому в клиентской программе значение, возвращенное процедурой gethostbyname, проверяется для определения того, не произошла ли ошибка. Библиотечные процедуры, которые возвращают указатели на статические структуры данных, использовать с потоками небезопасно; в главе 12 описана связь между выполнением потока и вызовом библиотечных процедур. /* Доменное имя хоста */ /* Другие псевдонимы */ /* Тип адреса */ /* Длина адреса */ /'* Список адресов */ 6.6. Поиск доменного имени 91
6.7. Поиск общепринятого порта по имени В большинстве клиентских программ необходимо предусмотреть поиск порта протокола для конкретной службы, которая должна быть вызвана. Например, в клиенте почтового сервера SMTP необходимо выполнить поиск общепринятого порта, назначенного для протокола SMTP. Для этого клиент вызывает библиотечную функцию getservbyname, которая принимает два параметра: строку с указанием желаемой службы и строку с указанием используемого протокола. Функция возвращает указатель на структуру типа servent, которая также определена во включаемом файле netdb.h: struct servent { char *s_name; /* Стандартное имя службы */ char **s_aliases; /* Другие псевдонимы */ int sjport; /* Порт службы */ char *sj?roto? /* Используемый протокол */ У, Если в клиенте TCP необходимо найти официально назначенный номер порта протокола для SMTP, в нем вызывается функция getservbyname, как показано в следующем примере: struct servent *sptr; if ( sptr = getservbyname( "smtp", "tcp" )) { /* Теперь номер порта - в поле sptr->sj?ort */¦ } else { /* Возникла ошибка - обработать ошибку */ } 6.8. Номера портов и сетевой порядок байтов Функция getservbyname возвращает номер порта протокола службы в сетевом порядке байтов. В главе 5 рассмотрено понятие сетевого порядка байтов и описаны библиотечные процедуры, позволяющие преобразовать данные из сетевого порядка байтов в порядок, используемый на локальном компьютере. Этого достаточно для понимания того, что функция getservbyname возвращает обозначение порта именно в той форме, которая требуется для использования в структуре sockaddr_in, но это внутреннее представление может не соответствовать представлению данных на локальном компьютере. Поэтому возвращаемое этой функцией значение нельзя выводить на печать без преобразования в локальный порядок байтов, т.к. оно может показаться неправильным. 6.9. Поиск протокола по имени В интерфейсе сокетов предусмотрен механизм, позволяющий преобразовать в клиентской или серверной программе имя протокола в целочисленную константу, назначенную этому протоколу. Поиск соответствия между этими значениями выполняет библиотечная функция getprotobyname. Вызов этой функции предусматривает передачу имени протокола в виде строкового параметра, а функция getprotobyname при ее вызове возвращает адрес структуры типа protoent. Если функция getprotobyname не может обратиться к базе данных для преобразования или указанное имя не существует, она возвращает нуль. База данных с именами 92 Глава 6. Алгоритмы и задачи проектирования...
протоколов позволяет определить на сетевом узле псевдонимы для каждого имени. Структура protoent имеет поле для официально назначенного имени протокола, а также поле, которое указывает на список псевдонимов. Объявление структуры содержится во включаемом файле netdb.h на языке С: struct protoent { char *p_name? /* Стандартное имя протокола */ char **p_aliases; /* Список допустимых псевдонимов */ int p proto; /* Стандартный номер протокола */ }? Если в клиентской программе необходимо выполнить поиск официально назначенного номера протокола для UDP, в ней вызывается функция getprotobyname, как показано в следующем примере: struct protoent *pptr; if ( pptr = getprotobyname( "udp" )) { /* Теперь стандартный номер протокола - в поле pptr->p_proto */ } else { /* Возникла ошибка - обработать ошибку */ } 6.10. Алгоритмы клиентов TCP Разработка клиентского программного обеспечения обычно проще по сравнению с разработкой серверного программного обеспечения. Поскольку все проблемы обеспечения надежности и управления потоком данных решаются с помощью протокола TCP, задача построения клиента с использованием TCP является наиболее простой из всех задач сетевого программирования. В клиенте TCP применяется алгоритм 6.1 для установления соединения с сервером и обмена с ним данными. В соответствии с этим алгоритмом, клиентское приложение распределяет сокет и подключает его к серверу. Затем оно отправляет через установленное соединение запросы и получает ответы. В следующих разделах каждый из этапов данного алгоритма рассматривается более подробно. Алгоритм 6.1. Клиент с установлением логического соединения 1. Найти IP-адрес и номер порта протокола сервера, с которым необходимо установить связь. 2. Распределить сокет. 3. Указать, что для соединения нужен произвольный, неиспользуемый порт протокола на локальном компьютере, и позволить программному обеспечению TCP выбрать такой порт. 4. Подключить сокет к серверу. 5. Выполнять обмен данными с сервером по протоколу прикладного уровня (для этого обычно требуется передавать запросы и принимать ответы). в. Закрыть соединение. 6.11. Распределение сокета В предыдущих разделах уже рассматривались способы, используемые для поиска IP-адреса сервера, а также была описана функция socket, которая применя- 6.10. Алгоритмы клиентов TCP 93
ется для распределения сокета связи. В клиентах, использующих протокол TCP, необходимо указать семейство протоколов PF_INET и службу SOCK_STREAM. Программа начинается с операторов include, которые ссылаются на файлы, содержащие определения символических констант, используемых в вызове, и объявление переменной, предназначенной для хранения дескриптора сокета. Если служба, обозначенная вторым параметром, предоставляется несколькими протоколами в семействе протоколов, обозначенным первым параметром, то третий параметр вызова функции socket должен указывать конкретный протокол. В случае семейства протоколов Internet доступ к службе SOCK_STREAM предоставляет только протокол TCP. Поэтому третий параметр не должен применяться; вместо него необходимо заДать 0: ¦include <sys/types.h> ¦include <sys/socket.h> int s; /* Дескриптор сокета */ s = socket( PF_INET, S0CKJ3TREAM, 0); 6.12. Выбор локального номера порта протокола В приложении необходимо задать адреса удаленной и локальной оконечных точек для сокета, прежде чем появится возможность использовать его для связи. Сервер работает с общепринятым адресом порта протокола, который должны знать все клиенты. Однако клиент TCP не работает с заранее назначенным портом. Вместо этого необходимо выбрать локальный порт протокола для использований в качестве адреса оконечной точки. Как правило, для клиента может быть выбран любой порт при соблюдении следующих условий: во-первых, порт не должен совпадать с портами, которые уже используются другими процессами на компьютере, и во-вторых, порт не должен быть предназначен для какой-либо службы в соответствии с общепринятым соглашением. Безусловно, если клиенту требуется локальный порт протокола, он может выбирать случайным образом произвольный порт, пока не найдет тот, который соответствует критериям, приведенным выше. Однако интерфейс сокетов в значительной степени упрощает выбор клиентского порта, поскольку в нем предусмотрен способ, в соответствии с которым клиент позволяет программному обеспечению TCP выбрать локальный порт автоматически. Выбор локального порта, соответствующего вышеуказанным критериям, происходит как побочный эффект вызова функции connect. 6.13. Фундаментальная проблема выбора локального IP-адреса При формировании оконечной точки соединения клиент должен выбрать локальный IP-адрес, а также локальный номер порта протокола. Если хост подключен только к одной сети, то задача выбора локального IP-адреса становится тривиальной. Но поскольку маршрутизаторы или многоадресные хосты имеют несколько IP-адресов, в них такая задача выбора может оказаться сложной. Как правило, сложность выбора IP-адреса связана с тем, что правильный выбор зависит от маршрутизации, а приложения редко имеют доступ к информации о маршрутах. Чтобы понять, с чем это связано, рассмотрим компьютер с несколькими сетевыми интерфейсами, и поэтому с несколькими IP-адресами. Прежде чем в при- 94 Глава 6. Алгоритмы и задачи проектирования...
ложении можно будет использовать протокол TCP, в нем необходимо иметь адрес оконечной точки для связи. При обмене данными по протоколу TCP с удаленным хостом каждый сегмент TCP инкапсулируется в дейтаграмму IP и передается программному обеспечению IP. Это программное обеспечение использует адрес назначения удаленного хоста и свою таблицу маршрутизации для выбора адреса следующего участка маршрута и сетевого интерфейса, который может применяться для достижения места назначения следующего участка маршрута. Вот здесь и возникает проблема: IP-адрес источника в исходящей дейтаграмме должен совпадать с IP-адресом сетевого интерфейса, через который протокол IP направляет дейтаграмму. Но если приложение выбирает один из IP-адресов компьютера случайным образом, оно может выбрать адрес, не соответствующий интерфейсу, через который программное обеспечение IP направляет этот трафик. На практике может создаваться впечатление, что клиентская программа работает правильно, даже если выбран неправильный адрес локальной оконечной точки, поскольку пакеты все равно передаются на клиентский компьютер, хотя и по маршруту, отличному от того, по какому они передаются на сервер. Однако использование неправильного адреса является нарушением спецификации протоколов, затрудняет и вносит путаницу в управление сетью, а также приводит к снижению надежности программы. Для решения этой проблемы предусмотрена возможность оставлять в приложении незаполненным поле локального IP-адреса при вызове функции socket и предоставлять программному обеспечению TCP/IP возможность выбрать локальный IP-адрес автоматически во время подключения клиента к серверу. Поскольку для выбора правильного локального IP-адреса необходимо обеспечить взаимодействие приложения с программным обеспечением маршрутизации IP, в клиентском программном обеспечении TCP поле адреса локальной оконечной точки обычно остается незаполненным, а автоматический выбор правильного локального IP-адреса и неиспользуемого локального номера порта протокола выполняется программным обеспечением TCP/IP. 6.14. Подключение сокета TCP к серверу Системный вызов connect позволяет клиенту TCP инициировать соединение. С точки зрения основополагающего протокола, вызов функции connect инициирует трехэтапное квитирование TCP. Этот вызов не возвращает управление до тех пор, пока не будет установлено соединение TCP или программное обеспечение TCP не исчерпает установленный тайм-аут и не откажется от дальнейших попыток установить соединение. Вызов возвращает 0, если соединение установлено успешно, или -1, если такая попытка окончилась неудачей. Функция connect принимает три параметра: retcode = connect(s, remaddr, remaddrlen) Здесь s — дескриптор сокета, remaddr — адрес структуры типа sockaddr_in, в которой задан адрес удаленной оконечной точки, с которой должно быть установлено соединение, a remaddrlen — длина второго параметра (в байтах). Функция connect выполняет четыре задачи. Во-первых, проверяет, является ли действительным указанный сокет и не был ли он уже подключен. Во-вторых, заполняет поле адреса удаленной оконечной точки в дескрипторе сокета, взяв это значение из второго параметра. В-третьих, выбирает адрес локальной оконечной точки для соединения (IP-адрес и номер порта протокола), если в деск- 6.14. Подключение сокета TCP к серверу 95
рипторе сокета это значение еще не было задано. В-четвертых, инициирует соединение TCP и возвращает э вызывающую программу значение, по которому можно судить, была ли попытка соединения успешной. 6.15. Взаимодействие с сервером с использованием протокола TCP При условии, что вызов функции connect привел к успешному созданию соединения, клиент может использовать это соединение для обмена данными с сервером. Обычно прикладной протокол определяет взаимодействие по принципу запрос/ответ, согласно которому клиент посылает ряд запросов и ожидает ответа на каждый из них. Обычно в клиентской программе вызывается функция send для передачи каждого запроса и функция recv для получения ответа. В случае простейших прикладных протоколов клиент посылает только один запрос и получает только один ответ. Более сложные прикладные протоколы требуют, чтобы клиент выполнял эту задачу в цикле, отправляя запрос и дожидаясь ответа, прежде чем отправить следующий запрос. В приведенном ниже коде иллюстрируется взаимодействие по принципу запрос/ответ на примере того, как программа передает простой запрос через соединение TCP и принимает ответ: /* Пример фрагмента кода */ ¦define BLEN 120 /* Длина используемого буфера */ char *req = "request of some sort"; char buf[BLEN]; /* Буфер для ответа */ char *bptr; /* Указатель на буфер */ int n; /* Количество считанных байтов */ int buflen; /* Место, оставшееся в буфере */ bptr = buf; buflen = BLEN; /* Отправить запрос */ send( s, req, strlen(req), 0 ); /* Получить ответ (может состоять из нескольких фрагментов) */ while ( ( n = recv(s, bptr, buflen,0) )> 0 ) { bptr += n; buflen -= n; } 6.16. Получение ответа из соединения TCP В коде предыдущего примера показан клиент, который посылает небольшое сообщение на сервер и ожидает короткий ответ (менее 120 байтов). Код содержит единственный вызов функции send, но предусматривает повторное выполнение вызовов функции recv. До тех пор, пока вызовы функции recv возвращают данные, в коде уменьшается счетчик пространства, доступного в буфере, а указатель буфера 96 Глава 6. Алгоритмы и задачи проектирования...
продвигается вслед за полученными данными. Такая итерация безусловно необходима, даже если приложение на другом конце соединения передает каждый раз только небольшой объем данных, поскольку протокол TCP не обеспечивает передачу информации в виде блоков. В действительности, протокол TCP является потоковым: он гарантирует доставку последовательности байтов, переданных отправителем, но не гарантирует доставку их в виде таких же фрагментов, какие были отправлены. Программное обеспечение TCP может принять решение разбить любой блок данных на части и передать каждую часть в отдельном сегменте (например, данные могут быть разбиты на части таким образом, чтобы каждая из них заполнила сегмент максимального размера, или для этого программного обеспечения может потребоваться отправка небольших фрагментов, если получатель не имеет достаточного объема буферного пространства для приема крупных фрагментов). Иным образом, программное обеспечение TCP может принять решение накопить больше байтов в своем выходном буфере перед передачей сегмента (например, для заполнения дейтаграммы). В результате может оказаться, что в приложение- получатель данные будут поступать небольшими фрагментами, даже если в приложении-отправителе они были переданы программному обеспечению TCP в одном вызове функции send. С другой стороны, в приложение-получатель данные могут поступить в виде одного большого фрагмента, даже несмотря на то, что в приложении-отправителе они были переданы программному обеспечению TCP в виде ряда вызовов функции send. В основе разработки программ, использующих протокол TCP, лежит следующий принцип. Поскольку программное обеспечение TCP не учитывает границ между записями, то в любой программе, принимающей данные из соединения TCP, необходимо предусмотреть возможность получения данных в виде фрагментов, составляющих лишь несколько байтов. Это правило остается в силе, даже если приложение-отправитель передает данные в виде больших блоков. 6.17. Закрытие соединения TCP 6.17.1. Потребность в частичном закрытии После того как приложение полностью заканчивает использование соединения, оно может вызвать функцию close для корректного завершения соединения и освобождения сокета. Однако закрытие соединения нередко превращается в сложную задачу, поскольку протокол TCP допускает двухстороннюю связь. Поэтому для закрытия соединения обычно требуется координация действий клиента и сервера. Чтобы понять, в чём состоит проблема, рассмотрим клиентскую и серверную программы, в которых используется принцип взаимодействия запрос/ответ, описанный выше. Клиентское программное обеспечение неоднократно выдает запросы, на которые отвечает сервер. С одной стороны, сервер не может закрыть соединение, поскольку не имеет информации о том, будет ли клиент присылать дополнительные запросы. С другой стороны, даже если клиентская программа имеет информацию о том, что она больше не должна передавать запросы, ей может быть неизвестно, получены ли все данные от сервера. Последнее особенно важно для прикладных протоколов, которые предусматривают передачу произвольных объемов данных в ответ на запрос (например, к базе данных). 6.17.2. Операция частичного закрытия Для решения проблемы закрытия соединения в большинство реализаций API- интерфейса сокетов включен дополнительный программный примитив, позволяющий 6.17. Закрытие соединения TCP 97
закрывать в приложениях соединение TCP в одном направлении. Соответствующий системный вызов shutdown принимает два параметра (дескриптор сокета s и спецификацию направления direction), и закрывает сокет в указанном направлении: errcode = shutdown(s, direction); Параметр direction с указанием направления предоставляет собой целое число. Бели он содержит 0, то дальнейший ввод будет запрещен. Если он содержит 1, будет запрещен дальнейший вывод. И наконец, если значение этого параметра равно 2, соединение закрывается в обоих направлениях. Теперь преимущество частичного закрытия должно быть очевидно: после того как клиент заканчивает отправку запросов, он может использовать функцию shutdown для указания того, что не имеет больше данных для передачи, не освобождая сокет. Соответствующий протокол сообщает о выполнении функции shutdown на удаленный компьютер таким образом, что серверная прикладная программа получает сигнал об обнаружении конца файла. Как только сервер обнаружит конец файла, он получит информацию о том, что прекратилось поступление запросов. После отправки последнего ответа сервер может закрыть соединение. Механизм частичного закрытия позволяет устранить неопределенность в работе прикладных протоколов, которые передают произвольный объем информации в ответ на запрос. В таких случаях клиент выполняет операцию частичного закрытия после передачи последнего запроса; затем сервер закрывает соединение после передачи последнего ответа. 6.18. Программирование клиента UDP На первый взгляд программирование клиента UDP может показаться простой задачей. Алгоритм 6.2 показывает, что основные принципы работы с клиентом UDP во многом напоминают работу с клиентом TCP (алгоритм 6.1). Процесс-отправитель создает подключенный сокет и использует его для последовательной отправки одного или нескольких запросов. В этом алгоритме проблема надежности игнорируется. Алгоритм 6.2. Клиент без установления логического соединения 1. Найти IP-адрес и номер порта протокола сервера, с которым необходимо установить связь. 2. Распределить сокет. 3. Указать, что для соединения нужен произвольный, неиспользуемый порт протокола на локальном компьютере, и позволить программному обеспечению UDP выбрать такой порт. 4. Указать сервер, на который должны передаваться сообщения. 5. Выполнять обмен данными с сервером по протоколу прикладного уровня (для этого обычно требуется передавать запросы и принимать ответы). 6. Закрыть сокет. Первые несколько этапов алгоритма работы клиента UDP во многом аналогичны соответствующим этапам алгоритма клиента TCP. Клиент UDP получает адрес и номер порта протокола сервера, а затем распределяет сокет для связи. 98 Глава 6. Алгоритмы и задачи проектирования...
6.19. Подключенные и неподключенные сокеты UDP В клиентском приложении сокет UDP может использоваться в одном из двух основных режимов: подключенном и неподключенном. Для перехода в подключенный режим клиент вызывает функцию connect для задания адреса удаленной оконечной точки (т.е. IP-адреса и номера порта протокола сервера). После задания удаленной оконечной точки клиентская программа может передавать и принимать сообщения, не указывая повторно удаленный адрес. В неподключенном режиме клиентская программа не подключает сокет к указанной удаленной оконечной точке, а задает удаленный адрес назначения при отправке каждого сообщения. Основное преимущество подключенных сокетов UDP связано с тем, что они упрощают разработку обычного клиентского программного обеспечения, взаимодействующего только с одним сервером одновременно: в приложении достаточно только один раз указать сервер, независимо от числа передаваемых в дальнейшем дейтаграмм. Основное преимущество неподключенных сокетов обусловлено предоставляемой ими свободой выбора адресата, поскольку решение о передаче данных на конкретный сервер может быть отложено до того, как поступит запрос, предназначенный для отправки. Кроме того, клиентская программа вполне может отправлять каждый запрос на другой сервер. Сокеты UDP могут быть подключенными, и в этом случае их удобно применять для взаимодействия с конкретным сервером, или неподключенными, в результате чего возникает необходимость указывать в приложении адрес сервера при передаче каждого сообщения. 6.20. Применение функции connect для работы с сокетом UDP Хотя в клиентской программе можно подключить сокет типа S0CK__DGRAM, вызов функции connect не приводит к инициализации какого-либо обмена пакетами, а также к проверке допустимости адреса удаленной оконечной точки. В действительности, вызов функции connect приводит просто к регистрации информации об удаленной оконечной точке в структуре данных сокета для дальнейшего использования. Таким образом, при ее применении к сокетам S0CK_DGRAM функция connect только записывает адрес в структуру данных в памяти. Даже если вызов функции connect оказался успешным, это еще не означает, что адрес удаленной оконечной точки действителен или сервер является достижимым. 6.21. Обмен данными с сервером с использованием протокола UDP После вызова функции connect в клиентской программе UDP может использоваться функция send для передачи сообщения или функция recv для получения ответа. В отличие от TCP, протокол UDP предусматривает передачу не потока данных, а сообщений. При каждом вызове в клиентской программе функции send программное обеспечение UDP передает на сервер единственное сообщение, содержащее все данные, переданные функции send. Аналогичным образом, каждый вызов функции recv приводит к получению одного полного сообщения. При условии, что в клиентской программе определен достаточно большой буфер, вы- 6.19. Подключенные и неподключенные сокеты UDP 99
зов функции recv возвращает все данные из последнего входящего сообщения2. Поэтому в клиенте UDP нет необходимости предусматривать повторные вызовы функции recv для получения одного сообщения. 6.22. Закрытие сокета, в котором используется протокол UDP Клиент UDP вызывает функцию close для закрытия сокета и освобождения связанных с ним ресурсов. Как только сокет будет закрыт, программное обеспечение UDP начнет отбрасывать дальнейшие поступающие сообщения, адресованные в порт протокола, который был распределен для этого сокета. Однако компьютер, на котором была выполнена функция close, не информирует хост с адресом удаленной оконечной точки о том, что сокет закрыт. Поэтому приложения, в которых используется транспортный протокол без установления логического соединения, должны быть спроектированы таким образом, чтобы удаленный участник соединения знал, как долго сокет должен оставаться доступным до его закрытия. 6.23. Частичное закрытие сокета UDP На подключенном сокете UDP может быть выполнена функция shutdown для прекращения дальнейшей передачи в указанном направлении. К сожалению, в отличие от частичного закрытия соединения TCP, применение функции shutdown к сокету UDP не приводит к отправке каких-либо сообщений удаленному участнику соединения. Вместо этого локальный сокет просто отмечается как неприменимый для передачи данных в указанном направлении (направлениях). Таким образом, если клиент останавливает свой дальнейший вывод в сокет, сервер не получает никаких указаний о том, что связь прекратилась. 6.24. Предупреждение о ненадежности протокола UDP В приведенном выше упрощенном алгоритме работы клиента UDP игнорируется неотъемлемая характеристика UDP, а именно то, что принцип его работы предусматривает ненадежную доставку (по принципу выполнения этим протоколом всего от него зависящего, но не более того). Хотя Простейший клиент UDP может работать в локальной сети, характеризующейся низкими потерями, небольшими задержками и отсутствием переупорядочения пакетов, клиенты, в которых реализован описанный алгоритм, не смогут работать в сложной объединенной сети. Для работы в среде объединенной сети клиентская программа должна обеспечить надежную работу с использованием тайм-аута и повторной передачи. В клиентской программе необходимо также решить проблему дублирования пакетов или их доставки с нарушением исходного порядка. Задача повышения надежности может оказаться сложной, и для ее решения необходимо иметь опыт проектирования протоколов. Клиентское программное обеспечение, в котором используется протокол UDP, должно обеспечивать надежность с помощью таких методов, как контроль за последовательностью поступления пакетов, подтверждения, тайм-ауты и повторная передача. Проектирование правильных, надеж- Если пользовательский буфер не имеет достаточной длины, программное обеспечение UDP отбрасывает те байты сообщения, которые не помещаются в буфер. 100 Глава 6. Алгоритмы и задачи проектирования...
ных и эффективных протоколов для среды объединенной сети требует большого опыта. 6.25. Резюме Клиентские программы относятся к категории наиболее простых сетевых программ. Клиент должен получить IP-адрес и номер порта протокола сервера, прежде чем он сможет вступить с ним во взаимодействие; для расширения области применения клиентской программы часто возникает необходимость определить требование, согласно которому пользователь при ее вызове должен указывать сервер. Затем клиентская программа преобразует адрес сервера из точечного десятичного обозначения в двоичное или использует систему доменных имен для преобразования символического имени компьютера в IP-адрес. Алгоритм работы клиента TCP является несложным: клиент TCP распределяет сокет и подключает его к серверу. Затем в клиентской программе используется функция send для передачи запросов на сервер и функция recv для получения ответов. Закончив использование соединения, либо клиент, либо сервер вызывает функцию close для его закрытия. Хотя клиент должен явно указать адрес оконечной точки сервера, с которым он должен вступить во взаимодействие, он может позволить программному обеспечению TCP/IP выбрать неиспользуемый номер порта протокола и заполнить в дескрипторе сокета поле локального IP-адреса, автоматически выбрав правильное значение. Это позволяет избежать проблем, которые могут возникнуть в маршрутизаторе или в многоадресном хосте, если клиентской программой будет ошибочно выбран IP-адрес, отличный от IP-адреса интерфейса, по которому протокол IP отправляет трафик в нужном направлении. В клиентской программе используется функция connect для задания адреса удаленной оконечной точки для сокета. При использовании с протоколом TCP функция connect инициирует трехэтапное квитирование и проверяет возможность связи. При использовании с протоколом UDP функция connect просто регистрирует адрес оконечной точки сервера для последующего использования. Закрытие соединения TCP может оказаться сложным, если ни клиент, ни сервер не имеют точной информации о том, когда должна закончиться связь. Для решения этой проблемы в интерфейсе сокетов предусмотрена функция shutdown, которая позволяет выполнить частичное закрытие и передать второму участнику соединения сообщение о том, что поступление данных к нему прекращено. В клиенте функция shutdown применяется для закрытия пути, ведущего к серверу; сервер получает из соединения сигнал о конце файла, указывающий на то, что клиент завершил свою работу. После завершения отправки сервером последнего ответа, в нем для прекращения соединения используется функция close. Материал для дальнейшего изучения Во многих документах RFC, в которых определены протоколы, наряду с этим предложены алгоритмы или методы реализации клиентских программ. Обзор методов реализации клиентских программ приведен также в литературе [151]. Упражнения 6.1. Прочитайте литературу о вызовах сокетов sendto и recvfrom. Применяются ли они с сокетами, в которых используется протокол TCP или UDP? Объясните ваш ответ. 6.25. Резюме 101
6.2. Напишите на языке С программу, которая без получения или отправки каких-либо пакетов определяет, используется ли в компьютере сетевой порядок байтов. 6.3. После преобразования имени компьютера система доменных имен возвращает набор из одного или нескольких IP-адресов. Почему? 6.4. Разработайте клиентское программное обеспечение, в котором применяется функция gethostbyname для поиска имен компьютеров на сетевом узле, а затем выполняется вывод всей полученной информации. Вас не удивляют некоторые полностью определенные доменные имена, если они имеются? Используются ли на вашем узле полностью определенные доменные имена или псевдонимы? Опишите возможные обстоятельства, при которых псевдонимы могут неправильно преобразовываться в IP-адреса. 6.5. Определите, сколько времени потребуется для поиска имени компьютера (функция gethostbyname) и службы (функция getservent). Выполните эту проверку для действительных и недействительных имен. Требует ли поиск недействительного имени намного больше времени, чем действительного? Объясните обнаруженную разницу. 6.6. Воспользуйтесь сетевым монитором для наблюдения за сетевым трафиком, который вырабатывается компьютером при поиске IP-адреса по имени с помощью функции gethostbyname. Проведите этот эксперимент несколько раз для каждого искомого имени компьютера. Объясните, с чем связаны различия в сетевом трафике при первой и последующей попытках поиска. 6.7. Для определения того, совпадает ли локальный порядок байтов компьютера с сетевым порядком, напишите программу, в которой используется функция getservbyname для поиска номера порта службы ECHO протокола UDP, а затем выполняется вывод полученного номера порта протокола. Если локальный и сетевой порядок байтов совпадают, это значение будет равно 7. 6.8. Напишите программу, которая распределяет локальный порт протокола, закрывает сокет, ожидает несколько секунд, а затем распределяет другой локальный порт. Выполните эту программу на простаивающем компьютере и на компьютере с интенсивно функционирующей системой с разделением времени. Какие номера портов получает программа на каждом из этих компьютеров? Если они не совпадают, объясните почему. 6.9. При каких обстоятельствах в клиентской программе можно использовать функцию close вместо shutdown? 6.10. Должен ли с началом работы клиентской программы каждый раз использоваться один и тот же номер порта протокола? Объясните ваш ответ. 102 Глава 6. Алгоритмы и задачи проектирования...
7 Примеры клиентского программного обеспечения 7.1. Введение В предыдущей главе описаны основные алгоритмы, лежащие в основе клиентских приложений, а также конкретные методы, применяемые для реализации этих алгоритмов. В настоящей главе приведены примеры законченных, работоспособных клиентских программ, которые иллюстрируют рассмотренные понятия более подробно. В этих примерах используется не только протокол UDP, но и TCP. Еще более важно то, что в этой главе показано, как может быть создана библиотека, состоящая из процедур, которые скрывают тонкости выполнения вызовов функций сокетов и упрощают разработку переносимого и легко сопровождаемого клиентского программного обеспечения. 7.2. Значимость небольших примеров Набор протоколов TCP/IP определяет множество служб и стандартных прикладных протоколов для доступа к ним. Эти службы отличаются по своей сложности, начиная от самых простейших (таких как служба генератора символов, применяемая только для проверки программного обеспечения протокола) и заканчивая исключительно сложными (такими как служба передачи файлов, которая обеспечивает аутентификацию и защиту). Примеры, приведенные в этой и нескольких следующих главах, в основном посвящены реализации программного обеспечения клиентов и серверов для простых служб. За ними следуют главы, в которых описаны клиентские и серверные приложения для некоторых сложных служб. Хотя на первый взгляд может показаться, что протоколы, используемые в этих примерах, не предоставляют интересных или полезных служб, необходимо уделить время на их изучение. Во-первых, поскольку для реализации самих этих служб требуется лишь небольшой объем кода, на их примере легко понять, как устроено клиентское и серверное программное обеспечение, которое обеспечивает их работу. Еще более важно то, что эти небольшие программы наглядно представляют фундаментальные алгоритмы и четко демонстрируют использование системных функций в клиентских и серверных программах. Во-вторых, изучая простые службы, читатель получает представление о том, какой примерно объем кода требуется для реализации служб и каково количество возможных служб. Глубокое понимание работы простых служб станет особенно важным при изучении глав, в которых рассматриваются ситуации, требующие применения мультипротокольных и мультисервисных проектов.
7.3. Сокрытие тонкостей реализации Большинство программистов понимает, в чем состоит преимущество разделения крупных, сложных программ на набор процедур: модульная программа проще для понимания, отладки и модификации, чем эквивалентная ей монолитная программа. Если процедуры тщательно разработаны, они могут повторно применяться в других программах. И наконец, продуманный выбор процедур упрощает перенос программы на новые компьютерные системы. По сути, процедуры повышают уровень языка, используемого программистами, скрывая тонкости реализации. Разработка программы с применением средств низкого уровня, предусмотренных в большинстве языков программирования, является весьма трудоемкой и способствует возникновению ошибок. К тому же, в программах, создаваемых с помощью средств низкого уровня, часто повторяются одни и те же фрагменты кода. Применение процедур позволяет избежать таких повторений путем использования операций более высокого уровня. После оформления конкретного алгоритма в виде процедуры ее можно повторно использовать во многих программах без необходимости повторно продумывать все подробности реализации. Широкое использование процедур становится особенно важным при создании клиентских и серверных программ. Во-первых, сетевое программное обеспечение включает объявления таких элементов данных, как адреса оконечных точек, поэтому разработка программ, в которых используются сетевые службы, требует учета огромного числа мелких подробностей, с которыми не приходится сталкиваться в обычных программах. Применение процедур для сокрытия этих тонкостей позволяет уменьшить вероятность ошибки. Во-вторых, основная часть кода, необходимого для распределения сокета, привязки адресов и формирования сетевого соединения, повторяется в каждом клиенте; размещение этого кода в процедурах позволяет программисту вызывать его повторно, а не вводить в каждую программу. В-третьих, поскольку протоколы TCP/IP проектировались в целях обеспечения взаимодействия разнотипных компьютеров, сетевые приложения часто функционируют на разных компьютерных архитектурах. Программисты могут использовать процедуры, чтобы включить в них код, зависящий от операционной системы, в результате чего перенос программы на новый компьютер значительно упрощается. 7.4. Пример библиотеки процедур для клиентских программ Для того чтобы понять, как можно с помощью процедур упростить программирование, рассмотрим задачу разработки клиентских программ. Для установления соединения с сервером клиент должен выбрать протокол (такой как TCP или UDP), определить доменное имя сервера, найти и преобразовать имя требуемой службы в номер порта протокола, распределить и подключить сокет. Составление кода для каждого из этих этапов с нуля в каждом приложении было бы напрасной потерей времени. Кроме того, если бы в дальнейшем потребовалось внести изменения в код реализации какого-либо из этих этапов, то пришлось бы корректировать каждое приложение. Чтобы свести к минимуму трудоемкость программирования, можно один раз написать нужный код, поместить его в процедуру, а затем просто вызывать процедуру из каждой клиентской программы. Первым этапом проектирования библиотеки процедур является теоретические исследования; на этом этапе необходимо определить перечень операций высокого уровня, применение которых позволит упростить разработку программ. Например, допустим, что прикладной программист решил создать две процедуры, которые выполняют всю работу по распределению и подключению сокета: 104 Глава 7. Примеры клиентского программного обеспечения
socket = connectTCP(machine, service); и socket = connectUDP(machine, service); Важно понять, что нет готового рецепта того, как должен быть составлен "правильный" набор абстрактных процедур; можно только наметить один из возможных способов формирования такого набора. Процедуры позволяют определить операции высокого уровня, создать код, совместно используемый в разных приложениях, и уменьшить вероятность ошибок, возникающих в связи с тем, что не учтены какие-то тонкости. Примеры процедур, приведенные в настоящей книге, просто иллюстрируют один из возможных подходов; программисты вправе выбирать собственные варианты процедур. 7.5. Реализация процедуры connectTCP Поскольку обе из предложенных процедур, connectTCP и connectUDP, должны выполнять задачу распределения сокета и заполнения основных полей дескриптора сокета, мы решили разместить весь код низкого уровня в третьей процедуре, т.е. в connectsock, и реализовать обе операции высокого уровня в виде простых вызовов. Этот подход иллюстрируется в файле connectTCP.с: /* connectTCP.с - процедура connectTCP */ int connectsock(const char *host, const char *service, const char *transport); /* _ * Процедура connectTCP - выполняет подключение к указанной службе TCP на * указанном хосте * .... ... . .... .. . ... */ int connectTCP(const char *host, const char *service) /* * Параметры: * host - имя хоста, к которому должно быть выполнено подключение * service - служба, связанная с требуемым портом */ { return connectsock(host, service, "tcp"); } 7.6. Реализация процедуры connectUDP Файл connectUDP.с показывает, как может применяться процедура connectsock для создания подключенного сокета, в котором используется протокол UDP: /* connectUDP.с - программа connectUDP */ int connectsock(const char *host, const char *service, const char *transport); /* ™ * Процедура connectUDP - выполняет подключение к указанной службе UDP на 7.5. Реализация процедуры connectTCP 105
* указанном хосте • >_« т */ int connectUDP(const char *host, const char *service) /* * Параметры: * host - имя хоста, к которому должно быть выполнено подключение * service - служба, связанная с требуемым портом */ { return connectsock(host, service, "udp")j } 7.7. Процедура, которая формирует соединения Процедура connectsock содержит весь код, необходимый для распределения сокета и его подключения. В вызывающем операторе должно быть указано, какой именно сокет создается (UDP или TCP). /* Файл connectsock.с - процедура connectsock */ ¦include <sys/types.h> ¦include <sys/socket.h> ¦include <netinet/in.h> ¦include <arpa/inet.h> ¦include <netdb.h> ¦include <string.h> ¦include <stdlib.h> ¦ifndef INADDRJTONE ¦define . INADDR NONE Oxffffffff ¦endif /* INADDRJTONE */ extern int errno; int errexit(const char *format, ...)? /* * Процедура connectsock - распределяет и подключает сокет с использованием * протокола TCP или UDP * . » */ int connectsock(const char *host, const char *service, const char *transport) /* * Параметры: * host - имя хоста, к которому должно быть выполнено подключение * service - служба, связанная с требуемым портом * transport - имя используемого транспортного протокола ("tcp" или "udp") */ 106 Глава 7. Примеры клиентского программного обеспечения
struct hostent *phe; /* указатель на запись с информацией о хосте */ struct servent *pse; /* указатель на запись с информацией о службе */ struct protoent *ppe; /* указатель на запись с информацией о протоколе */ struct sockaddr_in sin; /* IP-адрес оконечной точки */ int s, type; /* дескриптор сокета и тип сокета */ memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; /* Преобразовать имя службы в номер порта */ if ( pse = getservbyname(service, transport) ) sin.sinjport = pse->s_port; else if ((sin.sin_port=htons((unsigned short)atoi(service))) == 0) errexit("can't get \4s\H' service entry\nM, service); /* Преобразовать имя хоста в IP-адрес, предусмотрев возможность */ /* представить его в точечном десятичном формате */ if ( phe = gethostbyname(host) ) memcpy(&sin.sin_addr, phe->h_addr, phe->h_length); else if ( (sin.sin_addr.s_addr = inet_addr(host)) =- INADDR_NONE ) errexit("can't get \M%s\M host entry\n", host); /* Преобразовать имя транспортного протокола в номер протокола */ if ( (рре = getprotobyname(transport)) ~ 0) errexit("can't get \"%s\" protocol entry\n", transport); /* Использовать имя протокола для определения типа сокета */ if (strcmp(transport, "udp") =- 0) type = SOCK_DGRAM; else type = SOCKJTREAM; /* Распределить сокет */ s = socket(PF_INET, type, ppe->p_proto); if (s <0) errexit("can't create socket: %s\n", strerror(errno)); /* Подключить сокет */ if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) <0) errexit(Mcan't connect to %s.%s: %s\n", host, service, strerror(errno)); return s; } Хотя основные этапы выполнения этой процедуры являются достаточно простыми, некоторые фрагменты кода могут показаться непонятными. Во-первых, язык С допускает использование сложных выражений. В связи с этим в некоторых условных операторах используются выражения, содержащие вызов функции, присваивание и сравнение, причем весь этот код размещается в одной строке. Например, вызов процедуры getprotobyname присутствует в выражении, в котором результат присваивается переменной рре, а затем сравнивается со значением 0. Если возвращаемое значение равно нулю (т.е. произошла ошибка), 7.7. Процедура, которая формирует соединения 107
в операторе if выполняется вызов функции errexit. В ином случае, процедура продолжает выполнение. Во-вторых, в этом коде используются две библиотечные процедуры, которые определены в стандарте ANSI С, — memset и memcpy1. Процедура memset помещает байты с заданным значением в блок памяти; это —самый быстрый способ обнуления большой структуры или массива. Процедура memcpy копирует блок байтов из одного места в памяти в другое без учета его содержимого . В процедуре connectsock процедура memset используется для заполнения всей структуры sockaddrJLn нулями, а затем для копирования байтов IP-адреса сервера в поле sin_addr используется процедура memcpy. И наконец, процедура connectsock вызывает процедуру connect для подключения сокета. При возникновении ошибки она вызывает функцию errexit. /* Файл errexit.с - процедура errexit */ ¦include <stdarg.h> ¦include <stdio.h> ¦include <stdlib.h> /* * Процедура errexit - выводит сообщение об ошибке и завершает работу *_____ -—- - */ int errexit(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); exlt(l)? } Функция errexit принимает переменное число параметров и передает их функции vfprintf для вывода. В функции errexit для форматирования вывода могут применяться соглашения, применяемые при работе с функцией printf. Первый параметр указывает, как должен быть отформатирован вывод; остальные параметры задают значения, выводимые в соответствии с указанным форматом. 7.8. Применение примеров библиотечных процедур После того как программист определил прототипы процедур и сформировал из них библиотеку процедур, он может приступить к созданию клиентских приложений. Если прототипы процедур выбраны правильно, они упрощают прикладное программирование и скрывают многие тонкости реализации. Для иллюстрации применения примеров библиотечных процедур воспользуемся ими для построения примеров клиентских приложений. Поскольку каждый из клиентов В ранних версиях UNIX для этих процедур использовались имена bzero и Ьсору. Для копирования IP-адресов не может применяться функция strcpy, поскольку IP- адреса могут содержать нулевые байты, интерпретируемые функцией strcpy как обозначение конца строки. 108 Глава 7. Примеры клиентского программного обеспечения
обращается к одной из стандартных служб TCP/IP, они также демонстрируют некоторые наиболее простые прикладные протоколы. 7.9. Служба DAYTIME Стандарты TCP/IP определяют прикладной протокол, который позволяет пользователю получить сведения о дате и времени суток в формате, предназначенном для восприятия человеком. Эта служба носит официальное название DAYTIME. Для доступа к службе DAYTIME пользователь вызывает клиентское приложение. Клиент обращается к серверу для получения информации, а затем выводит ее на экран. Хотя в стандарте не определен точный синтаксис, в нем предложено несколько возможных форматов. Например, служба DAYTIME может вывести дату в формате, представляющем информацию о дне недели, месяце и числе, годе, времени суток и часовом поясе weekday, month day, year time-timezone примерно так: Thursday, February 22, 1996 17:37:43-PST Стандарт определяет, что служба DAYTIME может применяться для работы и с протоколом TCP, и с протоколом UDP. В обоих случаях в ней используется порт протокола 133. В версии службы DAYTIME для протокола TCP сигналом к активизации вывода является появление запроса на установление соединения TCP: сразу после получения нового запроса на установление соединения сервер формирует текстовую строку, содержащую текущую дату и время, передает эту строку, а затем закрывает соединение. Таким образом, клиент вообще не должен передавать никаких запросов. По существу, стандарт даже определяет, что сервер должен отбрасывать любые данные, переданные клиентом. Версия службы DAYTIME для протокола UDP требует отправки клиентом запроса. Запрос состоит из произвольной дейтаграммы UDP. При получении сервером каждой дейтаграммы он форматирует строку с отметкой текущей даты и времени, помещает ее в исходящую дейтаграмму и отправляет этот ответ клиенту. После передачи ответа сервер отбрасывает дейтаграмму, активизировавшую его ответ. 7.10. Реализация клиента TCP для службы DAYTIME Код клиента TCP для работы со службой DAYTIME приведен в файле TCPdaytime.c. /* Файл TCPdaytime.c - главная процедура программы TCPdaytime */ tinclude <unistd.h> ¦include <stdlib.h> ¦include <string.h> ¦include <stdio.h> extern int errno; int TCPdaytime(const char *host, const char *service); Пространства номеров портов TCP и UDP являются независимыми; применение одного и того же номера порта для данной службы при работе с обоими протоколами не является обязательным, а также не все службы доступны через оба протокола. 7.9. Служба DAYTIME 109
int errexit(const char *format, ...); int connectTCP(const char *host, const char *service); ¦define LINELEN 128 /* ............... * Главная процедура - клиент TCP для службы DAYTIME * . , . , т«. . - .. ... - */ int main(int argc, char *argv[]) { char *host = "localhost"; /* Имя хоста, применяемое по умолчанию */ char *service - "daytime"; /* Применяемый по умолчанию порт службы */ switch (argc) { case 1: host = "localhost"; break; case 3: service = argv[2]; /* Выполняется также следующая ветвь оператора switch */ case 2: host = argv[l]; break; default: fprintf(stderr, "usage: TCPdaytime [host [port]]\n"); exit(l); } TCPdaytime(host, service); exit(O); /* ......... * Процедура TCPdaytime - вызывает службу DAYTIME на указанном хосте и выводит * результаты * .. . . . ^ */ TCPdaytime(const char *host, const char *service) { char buf[LINELEN+l]; /* Буфер для одной строки текста */ int s, n; /* Дескриптор сокета и число операций чтения */ s = connectTCP(host, service); while( (n = read(s, buf, LINELEN))> 0) { buf[n] = '\0'; /* Обеспечить наличие нулевого символа */ /* в конце строки */ (void) fputs( buf, stdout ); } } 110 Глава 7. Примеры клиентского программного обеспе
Обратите внимание, насколько упрощается код благодаря использованию процедуры connectTCP. Сразу после установления соединения служба DAYTIME просто считывает и выводит входные данные, полученные из соединения, повторяя эту операцию в цикле до тех пор, пока не будет обнаружен признак конца файла. 7.11. Чтение данных из соединения TCP Пример службы DAYTIME иллюстрирует один важный принцип: протокол TCP предоставляет потоковую службу, которая не гарантирует соблюдение границ между записями. На практике такая потоковая организация передачи данных означает, что протокол TCP вносит "рассогласование" между передающими и принимающими приложениями. Например, предположим, что передающее приложение отправило 64 байта данных в одном вызове функции send, а затем отправило 64 байта во втором вызове. Принимающее приложение может получить все 128 байтов в одном вызове функции read или же получить 10 байтов в первом вызове, 100 байтов во втором и 18 — в третьем. Число байтов, возвращенных в результате вызова, зависит от размера дейтаграмм в базовой объединенной сети, от наличия места в буфере и от задержек, возникающих при прохождении данных по объединенной сети. Поскольку потоковая служба TCP не гарантирует доставки данных в виде таких же блоков, в каких они были отправлены, в приложении, принимающем данные из соединения TCP, нельзя полагаться на то, что все данные будут доставлены в одной передаче; в нем необходимо повторно вызывать функцию recv (или read) до тек пор, пока не будут получены все данные. 7.12. Служба TIME В стандартах TCP/IP определена служба, позволяющая одному компьютеру получить значение текущей даты и времени суток от другого. Эта служба, которая носит официальное название TIME, является исключительно простой: клиентская программа, выполняемая на одном компьютере, отправляет запрос на сервер, работающий на другом компьютере. При поступлении запроса сервер получает текущую дату и время суток от локальной операционной системы, представляет эту информацию в стандартном формате и передает ее клиенту в виде ответа. Для предотвращения проблем, возникающих в том случае, если клиентский и серверный компьютер находятся в разных часовых поясах, протокол TIME определяет, что вся информация времени и даты должна быть представлена во всемирном координированном времени (Universal Coordinated TimeL, для обозначения которого применяется аббревиатура UCT или UT. Таким образом, сервер перед передачей ответа преобразовывает локальное время во всемирное, а клиент преобразовывает всемирное время в локальное после получения ответа. В отличие от службы DAYTIME, которая возвращает результаты в формате, предназначенном для восприятия человеком, служба TIME возвращает результаты в формате, предназначенном для программ, которые записывают в память или выполняют другие действия с отметками времени. Программное обеспечение протокола TIME всегда задает время в виде 32-битовых целых чисел, представ- 4 Всемирное координированное время прежде называлось гринвичским средним поясным временем. 7.11. Чтение данных из соединения TCP 111
ляющих число секунд, истекших с начала эпохи. В протоколе TIME началом эпохи считается полночь 1 января 1900 года5. Применение целочисленного представления позволяет быстро передавать время с одного компьютера на другой, без преобразования его вначале в текстовую строку, а затем снова в целое число. Поэтому служба TIME позволяет устанавливать аппаратные часы на одном компьютере по часам другого компьютера. 7.13. Доступ к службе TIME В клиентах для доступа к службе TIME через порт протокола 37 может использоваться протокол TCP или UDP (формально, в стандартах определены две отдельные службы: одна для UDP, а другая для TCP). В версии службы TIME для протокола TCP сигналом к активизации вывода является появление запроса на установление соединения, во многом аналогично службе DAYTIME, описанной выше. Клиент формирует соединение TCP с сервером TIME и ожидает поступления данных, предназначенных для чтения. При обнаружении нового запроса на установление соединения сервер передает текущее время, закодированное в виде целого числа, а затем закрывает соединение. Клиент не передает никаких данных, поскольку сервер никогда не выполняет чтения из соединения. Клиенты могут также получить доступ к службе TIME с помощью протокола UDP. Для этого клиент отправляет запрос, состоящий из одной дейтаграммы. Сервер не обрабатывает входящую дейтаграмму, не считая того, что извлекает из нее адрес отправителя и номер порта протокола для использования в ответе. Сервер представляет текущее время в виде целого числа, помещает его в дейтаграмму и отправляет ее в ответ клиенту. 7.14. Точные показания времени и сетевые задержки Хотя служба TIME устраняет различия в часовых поясах, она не позволяет решить проблему запаздывания при передаче по сети. Если для прохождения сообщения от сервера к клиенту требуется 3 секунды, то клиент получит значение времени, отстающее на 3 секунды от показаний часов сервера. Поэтому для синхронизации часов используются другие, более сложные протоколы. Однако служба TIME остается популярной по трем причинам. Во-первых, она является чрезвычайно простой по сравнению со службами, основанными на использовании протоколов синхронизации часов. Во-вторых, большинство клиентов обращаются к серверам в локальной сети, где сетевое запаздывание составляет всего лишь несколько миллисекунд. В-третьих, кроме как при использовании программ, в которых отметки времени служат для синхронизации управления обработкой данных в реальном времени, мало кого волнует отклонение показаний часов компьютера на небольшую величину. В тех случаях, когда требуется более высокая точность, можно усовершенствовать протокол TIME или использовать иной протокол. Простейшим способом повышения точности показаний часов, получаемых с помощью службы TIME, является вычисление аппроксимированного значения сетевой задержки между сервером и клиентом, а затем суммирование этой аппроксимации со значением времени, полученным от сервера. Например, один из способов аппроксимации запаздывания требует от клиента вычисления времени, истекшего в процессе прохождения сигнала от клиента к серверу и обратно. В клиентской программе В одном из упражнений мы предлагаем вычислить максимальное значение даты, которое может быть представлено 32-битовым целым числом. 112 Глава 7. Примеры клиентского программного обеспечения
предполагается наличие равной задержки в обоих направлениях и формируется оценка времени прохождения в обратном направлении путем деления общего времени кругового обращения на два. Затем в клиентской программе аппроксимация задержки складывается со значением времени, полученным от сервера. 7.15. Клиент UDP для службы TIME Код реализации клиента UDP для службы TIME содержится в файле UDPtime.c. /* Файл UDPtime.c - главная процедура */ ¦include <sys/types.h> ¦include <unistd.h> ¦include <stdlib.h> ¦include <string.h> ¦include <stdio.h> ¦define BUFSIZE 64 ¦define UNIXEPOCH 2208988800UL /* Время наступления эпохи UNIX: число */ /* секунд с начала эпохи Internet */ ¦define MSG "what time is it?\n" extern int errno; int connectUDP(const char *host, const char *service); int errexit(const char *format, ...)? /* * Главная процедура - клиент UDP для службы TIME, который выводит полученное * значение времени * «.—« - —— —-™ — -. — */ int main(int argc, char *argv[]) { char *host = "localhost"; /* Имя хоста, применяемое по умолчанию */ char *service * "time"; /* Применяемое по умолчанию имя службы */ time_t now; /* 32-битовое целое число для хранения */ /* значения времени */ int s, n; ¦/* Дескриптор сокета и число операций чтения */ switch (argc) { case 1: host ¦ "localhost"; break; case 3: service * argv[2]; /* Выполняется также следующая ветвь оператора switch */ case 2: host * argv[l]; break; default: fprintf(stderr, "usage: UDPtime [host [port]]\n"); 7.15. Клиент UDP для службы TIME 113
exit(l); } s = connectUDP(host, service); (void) write(s, MSGf strlen(MSG)); /* Прочитать значение времени */ n = read(s, (char *)&now, sizeof(now))j if (n <0) errexit("read failed: %s\n", strerror(errno)); now = ntohl((unsigned long)now); /* Представить с использованием */ /* машинно-зависимого порядка байтов */ now -= UNIXEPOCH; /* Выполнить преобразование из */ /* представления для Internet в */ /* представление для UNIX */ printf("%s", ctime(Snow)); exit(O); } Программа, приведенная в этом примере, обращается к службе TIME, передавая дейтаграмму. Затем в ней вызывается функция read для получения ответа и извлечения из него значения времени. Сразу после получения информации о времени программа UDPtime должна преобразовать значение времени в форму, подходящую для локального компьютера. На первом этапе в ней используется функция ntohl для преобразования 32-битового значения (представленного в переменной типа long языка С) из стандартного сетевого порядка байтов в машинно-зависимый порядок байтов хоста. На втором этапе программа UDPtime должна преобразовать значение времени в локальное представление на компьютере. Программа, приведенная в этом примере, разработана для операционной системы Linux. Как и в протоколах Internet, в системе Linux для представления времени используются 32- битовые целые числа, которые рассматриваются как число секунд. Однако в отличие от протоколов Internet, в системе Linux началом эпохи отсчета времени считается 1 января 1970 года. Поэтому для преобразования значения времени, отсчитанного от начала эпохи, которая применяется в протоколе TIME, в значение времени от начала эпохи Linux, клиентская программа должна вычесть число секунд, прошедших между 1 января 1900 года и 1 января 1970 года. В коде этого примера для преобразования используется константа 2208988800. После преобразования значения времени в представление для локального компьютера, программа UDPtime вызывает библиотечную процедуру ctime, преобразующую значение времени в форму, предназначенную для восприятия человеком. 7.16. Служба ECHO В стандартах TCP/IP определена служба ECHO для протоколов UDP и TCP. На первый взгляд служба ECHO кажется почти бесполезной, поскольку сервер ECHO просто возвращает назад все данные, полученные им от клиента. Но несмотря на ее простоту, служба ECHO является важным инструментальным средством для сетевых администраторов, поскольку позволяет проверять достижимость хостов, отлаживать программное обеспечение протоколов и выявлять проблемы маршрутизации. В стандарте службы ECHO для протокола TCP определено, что сервер должен принимать входящие запросы на установление соединения, получать данные, по- 114 Глава 7. Примеры клиентского программного обеспечения
ступающие из соединения, и передавать те же данные назад через то же соединение до тех пор, пока клиент не прекратит передачу. Между тем, клиентская программа передает данные, введенные пользователем, а затем снова их принимает. 7.17. Клиент TCP для службы ECHO Простая клиентская программа для службы ECHO представлена в файле TCPecho.c. /* Файл TCPecho.c - главная процедура программы TCPecho */ ¦include <unistd.h> ¦include <stdlib.h> ¦include <string.h> ¦include <stdio.h> extern int errno; int TCPecho(const char *host, const char *service); int errexit(const char *format, ...)? int connectTCP(const char *host, const char *service); ¦define LINELEN 128 /* * Главная процедура - клиент TCP для службы ECHO *-——-.——— — - —»—™—.—— - - ._ — */ int main(int argc, char *argv[]) { char *host = "localhost"; /* Имя хоста, применяемое по умолчанию */ char *service = "echo"; /* Применяемое по умолчанию имя службы */ switch (argc) { case 1: host « "localhost"; break; case 3: service = argv[2]; /* Выполняется также следующая ветвь оператора switch */ case 2: host = argv[l]; break; default: fprintf(stderr, "usage: TCPecho [host [port]]\n"); exit(l); } TCPecho(host, service); exit(O); } /* * Процедура TCPecho - передает введенные данные в службу ECHO на указанном 7.17. Клиент TCP для службы ECHO 115
* хосте и выводит ответ * . .. .... —.—.——. ...... —... ... ... *...... */ irit TCPecho(const char *host, const char *service) { char buf[LINELEN+l]; /* Буфер для одной строки текста */ int s, n; /* Дескриптор сокета и число операций чтения */ int outchars, inchars; /* Число отправленных и полученных символов */ s = connectTCP(host, service); while (fgets(buf, sizeof(buf), stdin)) { buf[LINELEN] ¦ '\0'; /* Обеспечить наличие нулевого символа */ /* в конце строки */ outchars = strlen(buf); (void) write(s, buf, outchars); /* Выполнить прием ранее переданной строки */ for (inchars = 0; inchars <outchars; inchars+=n ) { n ¦ read(s, &buf[inchars], outchars - inchars); if (n <0) errexit("socket read failed: %s\n", strerror(errno)); } fputs(buf, stdout); > } После открытия соединения программа TCPecho входит в цикл, в котором повторно выполняется построчное чтение введенных данных, передача строки через соединение TCP на сервер ECHO, прием возвращенной строки и ее вывод. После успешной передачи всех введенных строк на сервер, их приема и вывода клиент завершает свою работу. 7.18. Клиент UDP для службы ECHO Клиентская программа, в которой для доступа к службе ECHO используется протокол UDP, приведена в файле UDPecho.c. /* Файл UDPecho.c - главная процедура программы UDPecho */ ¦include <unistd,h> ¦include <stdlib.h> ¦include <string.h> ¦include <stdio.h> extern int errno; int UDPechofconst char *host, const char *service); int errexitjconst char *format, ...); int connectUDP(const char *host, const char *service); ¦define LINELEN 128 116 Глава 7. Примеры клиентского программного обеспечения
/* */ int main(int argc, char *argv[]) { char *host.« "localhost"; char *service = "echo"; switch (argc) { case 1: host = "localhost"; break; case 3: service « argv[2]; /* Выполняется также следующая ветвь оператора switch */ case 2: host •¦ argv[l]; break; default: fprintf(stderr, "usage: UDPecho [host [port]]\n"); exit(l); } UDPecho(host, service); exit(O); } /* ..... .. . ................ . . * Процедура UDPecho - передает введенные данные в службу ECHO на указанном * хосте и выводит ответ *..... —..———._.—_.. ... . -...—— —... ._——.. */ int UDPecho(const char *host, const char *service) { char buf[LINELEN+l]; /* Буфер для одной строки текста */ int s, nchars; /* Дескриптор сокета и число операций чтения s я connectUDP(host, service); while (fgets(buf, sizeof(buf), stdin)) { buf[LINELEN] = '\0'; /* Обеспечить наличие нулевого символа */ /* в конце строки */ nchars = strlen(buf); (void) write(s, buf, nchars)? if (read(s, buf, nchars) <0) errexitj"socket read failed: %s\n% strerror(errno))? fputs(buf, stdout); } } 7.18. Клиент UDP для службы ECHO
В этом примере клиента службы ECHO для работы по протоколу UDP используется тот же общий алгоритм, что и в версии для протокола TCP. В этой программе повторно выполняется чтение строки ввода, передача ее на сервер, прием с сервера и вывод. Наиболее существенное различие между версиями для UDP и TCP заключается в том, как в них обрабатываются данные, полученные от сервера. Поскольку протокол UDP предоставляет дейтаграммную службу передачи данных, в клиентской программе каждая строка ввода рассматривается как единое целое и помещается в отдельную дейтаграмму. Аналогичным образом, сервер ECHO также получает и возвращает целые дейтаграммы. Таким образом, клиент TCP принимает входящие данные в виде потока байтов, а клиент UDP либо получает от сервера всю строку, либо вообще ее не получает; каждый вызов функции read возвращает всю строку, но только если не возникла ошибка. 7.19. Резюме Процедуры позволяют повысить гибкость и удобство сопровождения программ, скрыть тонкости реализации и упростить перенос программ с одного компьютера на другой. Цосле разработки и отладки процедура может быть помещена в библиотеку для повторного использования во многих программах. Библиотека процедур приобретает особо важное значение при разработке программ, в которых используются протоколы TCP/IP, поскольку эти протоколы часто применяются на компьютерах разных типов. В настоящей главе представлен пример библиотеки процедур, используемых для создания клиентского программного обеспечения. Основные процедуры этой библиотеки, connectTCP и connectUDP, упрощают распределение и подключение сокета к конкретной службе на конкретном хосте. В настоящей главе приведено несколько примеров клиентских приложений. Каждый пример содержит код законченной программы С, которая реализует стандартный прикладной протокол: DAYTIME (применяется для получения и вывода времени суток в формате, воспринимаемом человеком), TIME (служит для получения значения времени в виде 32-битового целого числа) и ECHO (используется для проверки связи по сети). Как показывают эти примеры кода, библиотека процедур скрывает многие тонкости, связанные с распределением сокета, и упрощает разработку клиентского программного обеспечения. Материал для дальнейшего изучения Каждый из описанных здесь прикладных протоколов составляет часть стандартов TCP/IP. Стандарт протокола DAYTIME содержится в документе [126], стандарт протокола TIME представлен в [127], а стандарт протокола ECHO — в [120]. В документе [100] определена версия 3 протокола NTP (Network Time Protocol — Синхронизирующий сетевой протокол). Упражнения 7.1. Воспользуйтесь программой TCPdaytime для передачи запросов на несколько серверов. Как каждый из них форматирует значение времени и даты? 7.2. Стандарт широко применяемой в Internet службы TIME предусматривает, что время должно быть указано в виде 32-битового целого числа, содержащего число секунд, истекших с начала эпохи, которым считается полночь 1 января 1900 года. В большинстве систем UNIX также предусмотрено представление времени в виде 32-битового целого чис- 118 Глава 7. Примеры клиентского программного обеспечения
л а секунд, но началом эпохи считается 1 января 1970 года. Какое максимальное значение даты и времени может быть представлено в соответствии с каждым из этих соглашений? 7.3. Усовершенствуйте клиент TIME, чтобы он выполнял проверку полученной даты для определения того, следует ли она за датой 1 января 2002 (или какой-то другой датой в недавнем прошлом). 7.4. Модифицируйте клиент TIME, чтобы он вычислял время Е9 истекшее с момента передачи запроса до получения ответа, и складывал половину значения Е со значением времени, полученным от сервера. 7.5. Разработайте клиент TIME, который обращается к двум серверам TIME, а затем сообщает разницу между полученными от них показаниями времени. 7.6. Объясните, как может возникнуть тупиковая ситуация, если программист выберет слишком большой размер строки в клиенте ECHO протокола TCP (например, 20000). 7.7. В клиентах ECHO, представленных в данной главе, не предусмотрена проверка того, совпадает ли отправленный ими текст с текстом, полученным от сервера. Откорректируйте эти программы, чтобы они проверяли полученные данные. 7.8. Клиенты ECHO, представленные в этой главе, не предусматривают подсчета числа переданных или полученных символов. Что произойдет, если сервер по ошибке отправит назад один дополнительный символ, который не был передан клиентом? 7.9. В примерах клиентов ECHO в данной главе не используется функция shutdown. Откорректируйте код таким образом, чтобы в нем эта функция использовалась. 7.10. Дополните предыдущее упражнение, объяснив, почему использование функции shutdown позволяет повысить производительность клиента. 7.11. Перепишите программу UDPecho.c таким образом, чтобы она проверяла возможность получения доступа к компьютеру по сети, формируя сообщение, передавая его, а затем устанавливая тайм-аут для получения ответа. Если ответ не поступит в течение 5 секунд, программа должна объявить, что хост назначения недостижим. Обязательно предусмотрите, по меньшей мере, однократную повторную передачу запроса на тот случай, если в объединенной сети дейтаграмма случайно потеряется. 7.12. Перепишите программу UDPecho.c таким образом, чтобы в ней создавалось и передавалось новое сообщение через каждую секунду, выполнялась проверка ответов для определения того, совпадают ли они с запросами, и выводилось только время кругового обращения каждого сообщения без вывода содержимого самого сообщения. 7.13. Объясните, что произойдет в программе UDPecho, если в базовой сети: продублируется запрос, отправленный с клиента на сервер; продубли- руется ответ, отправленный с сервера на клиента; потеряется запрос, отправленный клиентом серверу; или потеряется ответ, отправленный сервером клиенту. Откорректируйте программу таким образом, чтобы она могла справиться с каждой из этих проблем. >ажнения 119
8 Алгоритмы и задачи проектирования серверного программного обеспечения 8.1. Введение В настоящей главе рассматривается проектирование серверного программного обеспечения. В ней описаны такие важные темы, как доступ к серверу с установлением и без установления логического соединения, приложения с поддержкой и без поддержки состояния, а также последовательные и параллельные реализации сервера. В ней рассматриваются преимущества и недостатки каждого подхода и даны примеры приложений, в которых может применяться рассматриваемый проект. В следующих главах описанные принципы показаны на примерах законченных серверных программ, в которых реализованы все эти основные проектные решения. 8.2. Концептуальный алгоритм сервера По сути, каждый сервер функционирует в соответствии со следующим простым алгоритмом: в нем создается сокет и выполняется привязка сокета к общепринятому порту, через который должны приниматься запросы. Затем сервер входит в бесконечный цикл, в котором он принимает очередной запрос, поступающий от клиента, обрабатывает этот запрос, формирует ответ и отправляет его клиенту. К сожалению, этот несложный концептуальный алгоритм может непосредственно применяться для создания только самых простейших служб. Чтобы понять, с чем это связано, рассмотрим, например, службу передачи файлов, которой для выполнения запроса может потребоваться значительное время. Предположим, что первый клиент, обратившийся к серверу, затребовал передачу гигантского файла (например, 200 мегабайт), а второй клиент передал запрос на получение очень небольшого файла (например, 20 байт). Если сервер должен полностью закончить передачу первого файла, преясде чем приступить к передаче второго, то второму клиенту придется ждать получения своего небольшого файла неоправданно долгое время. Поскольку второй клиент запрашивает такой малый объем информации, он рассчитывает на то, что ответ сервера поступит немедленно. И действительно, большинство серверов быстро выполняют небольшие запросы, поскольку обладают способностью обрабатывать все запросы параллельно.
8.3. Сравнение параллельных и последовательных серверов Мы используем термин последовательный сервер для описания сервера, выполняющего обработку одного запроса за другим, а термин параллельный сервер — для описания сервера, который обрабатывает сразу несколько запросов. По существу, в большинстве серверов не нужно применять какие-либо дополнительные средства для того, чтобы обеспечить одновременную обработку нескольких запросов. В них применяется так называемая параллельная обработка, обычно основанная на использовании нескольких потоков выполнения, при которой каждый поток обрабатывает отдельный запрос. Как будет показано ниже, вполне возможны и другие реализации параллельной работы; выбор конкретного метода зависит от прикладного протокола. В частности, если сервер выполняет небольшой объем обработки по сравнению с объемом выполняемых операций ввода/вывода, существует возможность реализовать такой сервер в виде единого потока выполнения, в котором используется асинхронный ввод/вывод для обеспечения одновременного применения нескольких каналов связи. При этом создается впечатление, что сервер обменивается данными сразу с несколькими клиентами одновременно. Параллельным называется сервер, обеспечивающий одновременное выполнение нескольких запросов. Определение параллельного сервера не относится лишь к тому серверу, в котором применяется несколько потоков выполнения. Как правило, в отличие от последовательных серверов, при проектировании и разработке параллельных серверов приходится решать более трудные задачи, созданный в результате код намного сложнее, а его сопровождение требует больше усилий. Однако большинство программистов выбирают параллельные версии сервера, поскольку последовательные вызывают неоправданные задержки в работе распределенных приложений и могут создавать узкие места, снижающие производительность и негативно влияющие на работу многих клиентских приложений. Последовательные реализации сервера, которые являются более простыми для изучения и разработки, могут привести к снижению производительности, поскольку клиентам приходится ожидать доступа к службе. В отличие от последовательных, параллельные реализации сервера обеспечивают лучшую производительность, хотя и являются более сложными для проектирования и разработки. 8.4. Сравнение методов доступа с установлением и без установления логического соединения В основе решения проблемы обеспечения обмена данными по сети лежит правильный выбор транспортного протокола, применяемого в клиенте для доступа к серверу. Набор протоколов TCP/IP предусматривает возможность использования в приложениях одного из двух транспортных протоколов. Протокол TCP (Transmission Control Protocol — Протокол управления передачей) предоставляет транспортную службу с установлением логического соединения, а протокол UDP (User Datagram Protocol — Протокол пользовательских дейтаграмм) предоставляет службу без установления логического соединения. Поэтому серверы, в которых используется протокол TCP, являются по определению серверами с уста- 122 Глава 8. Алгоритмы и задачи проектирования...
новлением логического соединения, а серверы, в которых применяется протокол UDP, — серверами без установления логического соединения1. 8.5. Характеристики транспортных протоколов Протоколы TCP и UDP — два основных транспортных протокола в наборе TCP/IP. Эти протоколы во многом отличаются друг от друга. Как уже было сказано, протокол TCP предоставляет службу с установлением логического соединения, a UDP — без установления логического соединения. Однако наиболее важные различия между этими двумя протоколами заключаются в том, какие возможности они предоставляют приложениям. 8.5.1. Характеристики протокола TCP ¦ Двухточечная связь. Как было указано выше, протокол TCP предоставляет приложениям только службу с установлением логического соединения. Соединение TCP относится к типу двухточечного соединения, поскольку в нем имеются только две оконечные точки: с одной стороны находится клиентское приложение, а с другой — серверное. ¦ Надежное установление соединения. Протокол TCP требует, чтобы клиентское приложение устанавливало соединение с сервером до начала обмена данными, и гарантирует надежное установление соединения. Сам факт установления соединения может служить подтверждением наличия сетевой связности. Если какое-либо нарушение в работе исключает возможность поступления пакетов в удаленную систему, или сервер не готов принять запрос на установление соединения, то попытка соединения завершается неудачей и клиент информируется об этом. ¦ Надежная доставка. Протокол TCP гарантирует, что после установления соединения данные будут доставляться в том же порядке, в каком они были отправлены, без потерь и дублирования. Если какое-либо нарушение в работе исключает возможность надежной доставки, отправитель информируется об этом. ¦ Передача с управлением потоком данных. Протокол TCP предусматривает управление скоростью передачи данных, поэтому исключает возможность для отправителя передавать данные быстрее, чем их может принять получатель. Поэтому протокол TCP может использоваться для передачи данных с быстродействующего компьютера на компьютер с низким быстродействием. ¦ Дуплексная передача. Единственное соединение TCP обеспечивает одновременную передачу данных в любом направлении и в любое время, причем эта передача происходит беспрепятственно. Поэтому по одному и тому же соединению клиент может отправлять на сервер запросы, а сервер — передавать ответы. ¦ Потоковая организация. Как было описано выше, протокол TCP предусматривает передачу потока байтов от отправителя к получателю; границы между сообщениями не устанавливаются. Хотя интерфейс сокетов позволяет подключать в приложении сокет UDP к удаленной оконечной точке, эта операция влияет только на демультиплексирование входящих дейтаграмм. Поскольку UDP не является протоколом с установлением логического соединения, сетевой узел не информируется о подключении сокета и соединение транспортного уровня не создается. 8.5. Характеристики транспортных протоколов 123
8.5.2. Характеристики протокола UDP ¦ Связь "многие ко многим". В отличие от TCP, протокол UDP обеспечивают большую гибкость в определении числа приложений, взаимодействующих друг с другом. Он допускает отправку многими приложениями сообщений одному получателю или передачу одним приложением нескольким получателям. Еще более важно то, что протокол UDP позволяет использовать в приложениях для доставки сообщений средства групповой или широковещательной рассылки базовой сети. ¦ Ненадежная доставка. Протокол U?)P характеризуется ненадежной доставкой. Это означает, что сообщения, отправляемые по этому протоколу, могут быть потеряны, продублированы или доставлены не в том порядке, в каком они были отправлены. В нем не предусмотрены средства повторной передачи, а отправитель не информируется о возникновении подобных нарушений в работе. ¦ Отсутствие управления потоком данных. Протокол UDP не предусматривает управления потоком данных: если дейтаграммы поступают быстрее, чем их может обработать принимающая система или приложение, дейтаграммы отбрасываются без предупреждения или извещения. ¦ Дейтаграммная организация. Как уже было сказано, протокол UDP предоставляет дейтаграммную службу передачи данных. Отправитель, вызывая на выполнение функцию передачи данных, указывает точное число передаваемых байтов данных. Программное обеспечение UDP помещает эти данные в одно исходящее сообщение. С другой стороны, протокол UDP предусматривает доставку данных на принимающий компьютер по одному сообщению за один раз. Поэтому доставленные данные укладываются точно в такие же границы сообщений, которые были установлены приложением-отправителем. 8.6. Выбор транспортного протокола Мы употребляем термин с установлением логического соединения по отношению к серверам, в которых используется протокол TCP, а без установления логического соединения — к серверам, в которых применяется протокол UDP, но было бы точнее употреблять этот термин, когда речь идет о прикладных протоколах, а не о серверах, поскольку выбор транспортного протокола приводит к гораздо более значимым последствиям, чем подробности реализации. По существу, выбор транспортного протокола зависит от применяемого прикладного протокола. Если прикладной протокол рассчитан на использование надежной доставки, характерной для протокола TCP, он может работать неправильно или неэффективно в случае передачи сообщений по протоколу UDP. Поскольку протоколы TCP и UDP резко различаются своими характеристиками, проектировщик не может сделать выбор между транспортными протоколами с установлением и без установления логического соединения, не учитывая требований прикладного протокола, 8.7. Серверы с установлением логического соединения Основным преимуществом использования протокола с установлением логического соединения является простота программирования. В частности, поскольку такой транспортный протокол автоматически решает проблемы потери пакетов и их доставки с нарушением исходного порядка, в серверной программе уже можно не заниматься решением таких проблем. Вместо этого в программе сервера с уста- 124 Глава 8. Алгоритмы и задачи проектирования...
новлением логического соединения достаточно реализовать только функции управления и использования соединений. Сервер принимает от клиента входящие запросы на установление соединения, а затем передает через это соединение все необходимые данные. Он принимает запросы от клиента и отправляет ответы. И наконец, после завершения сеанса взаимодействия сервер закрывает соединение. До тех пор, пока соединение остается открытым, надежность работы обеспечивается протоколом TCP. Программное обеспечение этого протокола выполняет повторную передачу потерянных данных, проверяет, получены ли данные без ошибок при передаче и, в случае необходимости, переупорядочивает входящие пакеты. При отправке клиентом запроса программное обеспечение TCP либо выполняет его надежную доставку, либо информирует клиента о том, что соединение разорвано. Аналогичным образом, при разработке сервера можно рассчитывать на то, что программное обеспечение TCP будет надежно доставлять ответы или информировать о том, что доставка невозможна. Но серверы с установлением логического соединения имеют также и свои недостатки. Проект с установлением логического соединения требует создания отдельного сокета для каждого соединения, тогда как проект без установления логического соединения допускает обмен данными с несколькими хостами через один сокет. Издержки по распределению сокета и управлению соединением могут иметь особое значение, если речь идет об операционной системе, которая должна работать неопределенно долгое время на компьютере с ограниченными ресурсами. Что касается простых приложений, то издержки трехэтапного квитирования, применяемого для установления и прекращения соединения, приводят к тому, что протокол TCP требует больших затрат по сравнению с UDP, которые могут оказаться неоправданными. Наиболее важный недостаток протокола TCP связан с тем, что простаивающее соединение, по которому не проходят пакеты, напрасно потребляет ресурсы. Предположим, что клиентская программа установила соединение с сервером, обменялась с ним несколькими запросами и ответами, а затем завершилась аварийно. Поскольку работа клиентской программы прекратилась преждевременно, она больше не будет отправлять запросы, а в связи с тем, что сервер уже ответил на все запросы, полученные до сих пор, он больше не будет передавать клиенту данные. В такой ситуации возникает проблема, связанная с непроизводительным использованием ресурсов: сервер распределил для соединения в памяти структуры данных (включая буферное пространство), а освободить эти ресурсы невозможно. Напомним, что сервер должен работать неопределенно долгое время. Если подключающиеся к нему клиентские программы будут завершаться аварийно одна за другой, сервер исчерпает все доступные ресурсы (такие как сокеты, буферное пространство и соединения TCP) и в конечном итоге прекратит функционирование. 8.8. Серверы без установления логического соединения Серверы без установления логического соединения также имеют свои преимущества и недостатки. Хотя такие серверы не порождают проблем, связанных с исчерпанием ресурсов, они не могут полагаться на базовый транспортный протокол в отношении надежной доставки. Ответственность за обеспечение надежной доставки должен взять на себя один из участников соединения. Обычно функции по обеспечению повторной передачи запросов в случае неполучения ответа возлагаются на клиентские программы. Если в сервере необходимо разбивать ответы на несколько пакетов данных, в нем также может потребоваться реализовать механизм повторной передачи. Задача обеспечения надежности с использованием тайм-аутов и повторной передачи может оказаться чрезвычайно сложной. По существу, для ее решения 8.8. Серверы без установления логического соединения 125
требуется значительный опыт проектирования протоколов. Поскольку протоколы TCP/IP работают в среде объединенной сети, которая характеризуется резким изменением сквозных задержек, применение постоянных значений тайм-аута становится неоправданным. Многие программисты, разрабатывающие собственные схемы обеспечения надежности, усвоили этот урок на своем горьком опыте. Приложения обычно проверяются в локальной сети, отличающейся высокой надежностью и небольшим изменением задержки. После переноса приложений в глобальную объединенную сеть, характеризующуюся более низкой надежностью и значительным изменением задержки, простые схемы определения тайм-аута повторной передачи перестают работать. Стратегия повторной передачи, которую можно применить в среде объединенной сети, должна быть адаптивной. Поэтому для обеспечения работы приложений, основанных на использовании протокола UDP, в глобальной сети Internet в них необходимо реализовать такую же сложную схему повторной передачи, какая используется в протоколе TCP. Поэтому начинающим программистам рекомендуется применять транспортный протокол с установлением логического соединения. Поскольку протокол UDP не гарантирует надежной доставки, применение транспортного протокола без установления логического соединения связано с необходимостью обеспечения достаточно высокой надежности за счет использования прикладного протокола, основанного на применении сложной и развитой методики, известной под названием адаптивной повторной передачи. Задача внедрения средств адаптивной повторной передачи в уже существующее приложение является сложной и требует значительного опыта. Еще один фактор, который может повлиять на выбор проекта без установления или с установлением логического соединения, связан с тем, требуется ли для данной службы широковещательная или групповая рассылка. Поскольку протокол TCP обеспечивает только двухточечную связь, он не дает возможности использовать в приложениях средства широковещательной или групповой связи; для таких служб требуется протокол UDP. Поэтому любой сервер, который принимает или отвечает на запросы, передаваемые с помощью средств групповой связи, должен использовать протокол без установления логического соединения. На практике администраторы большинства сетевых узлов стараются, по возможности, исключить применение широковещательной рассылки. К тому же ни один из стандартных прикладных протоколов TCP/IP в настоящее время не требует групповой рассылки. Однако в дальнейшем могут появиться приложения, в которых будут шире применяться средства групповой связи. 8.9. Нарушения в работе, надежность и отказ от поддержки состояния Как было описано в главе 2, сопровождаемая сервером информация о том, как проходят существующие сеансы взаимодействия с клиентами, называется информацией о состоянии. Серверы, не сопровождающие информацию о состоянии, называются серверами, не поддерживающими состояние^ а серверы, сопровождающие информацию о состоянии, называются серверами, поддерживающими состояние. Задача выбора проекта с поддержкой или без поддержки состояния связана с решением проблемы обеспечения надежности, особенно если в проекте используется транспортный протокол без установления логического соединения. Напомним, что в объединенной сети сообщения могут быть продублированы, задержаны, потеряны или доставлены в ином порядке по сравнению с исходным. Если транспортный про- 126 Глава 8. Алгоритмы и задачи проектирования...
токол не гарантирует надежной доставки, как в случае применения протокола UDP, прикладной протокол должен быть разработан таким образом, чтобы он смог обеспечить надежную доставку. Кроме того, должна быть очень тщательно продумана реализация сервера, чтобы в серверной программе не возникла зависимость от состояния (и не произошло снижение эффективности работы). 8.10. Оптимизация серверов, не поддерживающих состояние Чтобы понять, какие тонкости связаны с решением задачи оптимизации, рассмотрим сервер без установления логического соединения, который позволяет клиентам считывать информацию из файлов, хранящихся на диске, подключенном к компьютеру, на котором работает сервер. Чтобы исключить необходимость использования в протоколе информации о состоянии, проектировщик должен предусмотреть, чтобы в каждом клиентском запросе были указаны имя файла, позиция в файле и число считываемых байтов. Наиболее простая реализация сервера предусматривает обработку каждого запроса независимо от других: сервер открывает указанный файл, переводит указатель в заданную позицию, считывает указанное число байтов, отправляет требуемую информацию клиенту, а затем закрывает файл. Например, предположим, что опытный программист, получивший задание разработать серверную программу, заметил следующее: во-первых, высоки издержки, связанные с открытием и закрытием файлов, во-вторых, иногда клиенты, использующие этот сервер, считывают лишь несколько байтов в каждом запросе, и в- третьих, клиенты обычно считывают файлы последовательно. Кроме того, программист знает по опыту, что сервер может считывать данные из буфера в памяти на несколько порядков быстрее по сравнению с чтением с диска. Поэтому для оптимизации производительности сервера программист решил вести небольшую таблицу с информацией о файле, например, такую, как показано на рис. 8.1: В сервере для поиска записи в этой таблице используются IP-адрес и номер порта протокола клиента. Такая оптимизация равносильна ведению информации о состоянии. Таблица с информацией о файлах, Для доступа «записям используемых клиентами таблицы применяются IP-адрес и порт начинающихся с байта 1024 Имя файла: X Смещение: 512 Указатель буфера: Имя файла: Y Смещение: 1024 Указатель буфера: 1 Буфер для данных файла X, начинающихся с байта 512 1 Буфер для данных файла Y, 8.10. Оптимизация серверов, не поддерживающих состояние 127
Рис. 8.1. Таблица с информацией, предназначенная для повышения производительности сервера Программист использует IP-адрес и номер порта протокола клиента в качестве индекса в таблице и предусматривает хранение в каждой записи таблицы указателя на большой буфер данных из считываемого файла. При выдаче клиентом первого запроса сервер обращается к таблице и находит, что в ней нет записи для данного клиента. Он распределяет большой буфер для хранения данных из файла, вводит новую запись в таблицу, которая указывает на буфер, открывает указанный файл и считывает данные в буфер. Затем сервер копирует информацию из буфера при формировании ответа. При поступлении следующего запроса от того же клиента сервер находит соответствующую запись в таблице, проходит по указателю к буферу и извлекает из него данные, не открывая файл. Как только клиент прочитает весь файл, сервер освобождает буфер и удаляет запись из таблицы, в результате чего эти ресурсы становятся доступными для использования другим клиентом. Безусловно, наш опытный программист тщательно строит программу, чтобы в ней предусматривалась проверка наличия затребованных данных в буфере и чтение новых данных в буфер из файла по мере необходимости. Сервер также сравнивает имя файла, указанное в запросе, с именем файла в записи таблицы для проверки того, продолжает ли клиент обращаться к тому же файлу, как и в предыдущем запросе. Если клиенты действуют в соответствии с перечисленными выше предположениями и программа составлена правильно, то распределение на сервере больших файловых буферов и применение простой таблицы позволяет намного повысить производительность. Кроме того, с учетом сделанных предположений, оптимизированная версия сервера будет работать, по меньшей мере, не хуже, чем первоначальная версия, поскольку сервер будет затрачивать очень мало времени на сопровождение структур данных по сравнению с затратами времени на чтение с диска. Таким образом, создается впечатление, что такой вариант оптимизации позволяет повысить производительность без каких-либо сложностей. Однако введение предложенной таблицы влечет за собой внешне малозаметное, но существенное изменение проекта сервера, поскольку оно равносильно введению поддержки информации о состоянии. Безусловно, если поддержка информации о состоянии будет введена непродуманно, она может привести к появлению ошибок, связанных с формированием ответа сервером. Например, если в сервере для поиска буфера используется IP-адрес и номер порта протокола, полученных от клиента, но не предусматривается проверка в запросе имени файла или смещения в файле, то запросы, поступившие как дубликаты или с нарушением исходного порядка, могут привести к тому, что сервер вернет неправильные данные. Но напомним, что программист, разработавший эту оптимизированную версию, достаточно опытен и предусмотрел проверку сервером имени файла и смещения в каждом запросе, хотя бы на тот случай, что в сети какой-то запрос будет продублирован или потерян, или в клиентской программе будет принято решение перейти к чтению из нового файла, а не продолжать последовательное чтение из старого файла. Таким образом, может показаться, что введение информации о состоянии не меняет способа формирования ответов сервером. И действительно, если программист тщательно проработал все детали, протокол по-прежнему будет работать правильно. Если это действительно так, то почему бы постоянно не использовать информацию о состоянии? К сожалению, применение даже небольшого объема информации о состоянии может вызвать нарушение в работе сервера в случае отказа компьютеров, клиентских программ или сетей. Чтобы понять, с чем это связано, рассмотрим, что произойдет, если работа одной из клиентских программ закончится аварийно и придется выполнить ее перезапуск. Весьма велика вероятность того, что клиент запросит 128 Глава 8. Алгоритмы и задачи проектирования...
произвольный номер порта протокола и программное обеспечение UDP назначит новый номер, отличный от того, который был назначен в связи с выполнением предыдущих запросов. После поступления запроса от того же клиента сервер не будет иметь информации о том, что произошло аварийное завершение и перезапуск клиентской программы, поэтому распределит новый буфер для файла и новую запись в таблице. Следовательно, сервер не будет знать, что старая запись в таблице, которая использовалась для этого клиента, должна быть удалена. А если сервер не будет удалять старые записи, в конечном итоге в его таблице не останется свободного места. Может показаться, что появление неиспользуемых записей в таблице не составляет проблемы, если в сервере будет предусмотрено удаление ненужных записей, когда потребуется место для новых записей. Например, в сервере может использоваться алгоритм удаления записей с наиболее давним использованием (LRU — Least Recently Used), во многом аналогичный алгоритму замены страниц, применяемому во многих системах виртуальной памяти. Однако в сети, где множество клиентов обращаются к одному серверу, частые аварийные завершения могут привести к тому, что из-за повторных попыток подключения одного и того же клиента вся таблица будет заполнена записями, которые никогда больше не будут использоваться. В наихудшем случае может оказаться, что каждый поступающий на сервер запрос будет вызывать удаление записи для освобождения места. Если достаточно часто будет происходить аварийное завершение работы программы и перезагрузка одного и того же клиентского компьютера, это может привести к тому, что сервер начнет удалять записи успешно функционирующих* клиентов. В результате сервер будет затрачивать все больше ресурсов на управление таблицей и буферами, чем на формирование ответов на запросы2. Программист должен тщательно продумывать решения по оптимизации работы сервера, не поддерживающего состояние, поскольку сопровождение даже небольших объемов информации о состоянии может потребовать использования дополнительных ресурсов, если часто происходит аварийный останов и перезагрузка клиентских компьютеров или если базовая сеть дублирует или задерживает сообщения. 8.11. Четыре основных типа серверов Серверы могут быть последовательными или параллельными, и в них может использоваться транспортный протокол с установлением или без установления логического соединения. На рис. 8.2 показано, как можно классифицировать серверы с этими свойствами по четырем основным типам. В системах управления виртуальной памяти такое явление называется пробуксовкой. 8.11. Четыре основных типа серверов 129
Последовательный без установления логического соединения Параллельный без установления логического соединения Последовательный с установлением логического соединения Параллельный с установлением логического соединения Рис. 8.2. Четыре основных типа серверов 8.12. Время обработки запроса Как правило, последовательные серверы могут применяться только для реализации самых простейших прикладных протоколов, поскольку они заставляют клиентов ждать своей очереди. Для проверки того, применима ли в конкретном случае последовательная реализация, необходимо определить время отклика, которое измеряется в масштабах локальной или глобальной сети. Мы определяем время обработки запроса сервером, как общее количество времени, которое требуется серверу для обработки одного отдельно взятого запроса, и определяем наблюдаемое клиентом время отклика как общую задержку между временем отправки клиентом запроса и временем получения ответа от сервера. Безусловно, время отклика, наблюдаемое клиентом, будет всегда больше времени обработки запроса сервером. Но если на сервере имеется очередь запросов, подлежащих обработке, то наблюдаемое время отклика может быть намного больше времени обработки запроса. Последовательные серверы одновременно обрабатывают лишь один запрос. Если очередной запрос поступает в то время, когда сервер занимается обработкой предыдущего запроса, система ставит новый запрос в очередь. Как только сервер заканчивает обработку запроса, он обращается к очереди для определения того, содержится ли в ней новый запрос. Если N обозначает среднюю длину очереди запросов, то наблюдаемое время отклика на поступающий запрос будет примерно в N/2 + 1 выше по сравнению со временем обработки запроса сервером. Поскольку наблюдаемое время отклика растет пропорционально N9 большинство реализаций предусматривает ограничение N небольшим значением (например, 5) с тем условием, что будет выполнен переход к использованию параллельных серверов в том случае, если такая небольшая очередь не удовлетворяет потребности приложений. Еще один способ поиска ответа на вопрос о том, может ли применяться последовательный сервер, связан с оценкой общей нагрузки, которую этот сервер должен обрабатывать. Сервер, предназначенный для поддержки К клиентов, каждый из которых отправляет R запросов в секунду, должен характеризоваться временем обработки запросов, составляющим менее 1/KR секунд на один запрос. Если сервер не способен обрабатывать запросы с требуемой скоростью, то в конечном итоге очередь 130 Глава 8. Алгоритмы и задачи проектирования...
ожидающих запросов переполнится. Поэтому для предотвращения переполнения в серверах, которые могут характеризоваться большим значением времени обработки запросов, проектировщик должен предусмотреть параллельную реализацию. 8.13. Алгоритмы последовательного сервера Задача проектирования, программирования, отладки и корректировки последовательного сервера является наиболее простой. Поэтому большинство программистов выбирает последовательный проект, если он обеспечивает достаточно быстрый отклик при ожидаемой нагрузке. Обычно последовательные серверы лучше всего работают с простыми службами, доступ к которым предоставляется с помощью протокола без установления логического соединения. Но как показано в следующих разделах, последовательные реализации могут использоваться с транспортными протоколами без установления и с установлением логического соединения. 8.14. Алгоритм последовательного сервера с установлением логического соединения В алгоритме 8.1 представлен алгоритм работы последовательного сервера, доступ к которому осуществляется с помощью транспортного протокола TCP с установлением логического соединения. Согласно этому алгоритму, в одном потоке выполнения последовательно обрабатываются запросы на установление соединения, поступающие от клиентов. В следующих разделах каждый этап работы алгоритма рассматривается более подробно. Алгоритм 8.1. Последовательный сервер с установлением логического соединения 1. Создать сокет и установить связь с общепринятым адресом предоставляемой службы. 2. Перевести сокет в пассивный режим, подготавливая его для использования сервером. 3. Принять из сокета следующий запрос на установление соединения и получить новый сокет для соединения. 4. Считывать в цикле запросы от клиента, формировать ответы и отправлять клиенту в соответствии с прикладным протоколом. 5. После завершения обмена данными с конкретным клиентом закрыть соединение и возвратиться к этапу 3 для приема нового запроса на установление соединения. 8.15. Привязка к общепринятому адресу с использованием шаблона INADDR_ANY В сервере необходимо создать сокет и привязать его к общепринятому порту, соответствующему предоставляемой службе. Как и в клиентах, в серверных программах используется процедура getportbyname для преобразования имени службы в соответствующий общепринятый номер порта. Например, в стандартах протоколов TCP/IP определена служба ECHO. В серверной программе, реализующей службу ECHO, процедура getportbyname устанавливает соответствие строки "echo" с портом 7, назначенным для этой службы. 8.13. Алгоритмы последовательного сервера 131
Напомним, что при использовании процедуры bind для определения оконечной точки подключения сокета в ней используется структура sockaddr_in, которая содержит и IP-адрес, и номер порта протокола. Поэтому в процедуре bind нельзя задать номер порта протокола для сокета, не указав при этом IP-адрес. К сожалению, выбор конкретного IP-адреса, через который сервер будет принимать запросы на установление соединения, может оказаться сложным. Для хостов, имеющих одно сетевое соединение, выбор является очевидным, поскольку хост имеет только один IP-адрес. Однако маршрутизаторы и многоадресные хосты имеют несколько IP-адресов. Если в сервере задан один конкретный IP-адрес для привязки сокета к номеру порта протокола, то сокет не будет принимать запросы на установление соединения, направляемые клиентами по другим IP-адресам компьютера. Для решения этой проблемы в интерфейсе сокетов определена специальная константа INADDR_ANY, которая может использоваться вместо IP-адреса. Константа INADDR_ANY задает шаблон адреса, который соответствует любому из IP-адресов хоста. Применение этой константы позволяет одному серверу, установленному на многоадресном хосте, принимать входящие запросы на установление соединения, адресованные по любому из IP-адресов хоста. При указании локальной оконечной точки для сокета в серверах вместо конкретного IP-адреса используется константа INADDR_ANY, которая позволяет принимать в сокете дейтаграммы, отправленные по любому из IP-адресов компьютера. 8.16. Перевод сокета в пассивный режим В сервере, использующем протокол TCP, для перевода сокета в пассивный режим применяется вызов процедуры listen. Процедура listen принимает также параметр с указанием длины внутренней очереди запросов для сокета. Очередь запросов содержит ряд входящих запросов на установление соединения TCP от клиентов, которые потребовали подключения к серверу. 8.17. Прием входящих запросов на установление соединения и их использование В сервере, использующем протокол TCP для получения следующего входящего запроса на установление соединения (т.е. извлечения его из очереди запросов), вызывается процедура accept. В результате этого вызова возвращается дескриптор сокета, который будет использоваться для нового соединения. После приема запроса на установление нового соединения сервер использует процедуру recv (или read) для получения от клиента запросов прикладного протокола и процедуру send (или write) для передачи ответов. И наконец, после завершения работы с этим соединением сервер вызывает процедуру close для освобождения сокета. 8.18. Алгоритм последовательного сервера без установления логического соединения Напомним, что последовательные серверы в наибольшей степени подходят для создания служб, характеризующихся минимальными затратами времени на обработку запроса. Такие транспортные протоколы с установлением логического соединения, как TCP, имеют более высокие издержки по сравнению с транспортными протоколами без установления логического соединения, например, с UDP, поэтому в большинстве последовательных серверов используется транспортный протокол без 132 Глава 8. Алгоритмы и задачи проектирования...
установления логического соединения. В алгоритме 8.2 приведен общий алгоритм работы последовательного сервера, в котором применяется протокол UDP. Создание сокета для последовательного сервера без установления логического соединения осуществляется таким же образом, как и для сервера с установлением логического соединения. Сокет сервера остается неподключенным и может принимать входящие дейтаграммы от любого клиента. Для последовательной обработки запросов (дейтаграмм), полученных от клиентов, применяется один поток. Алгоритм 8.2. Последовательный сервер без установления логического соединения 1. Создать сокет и установить связь с общепринятым адресом предоставляемой службы. 2. Считывать в цикле запросы от клиента, формировать ответы и отправлять клиенту в соответствии с прикладным протоколом. 8.19. Формирование адреса ответа в сервере без установления логического соединения В интерфейсе сокетов предусмотрено два способа задания адреса удаленной оконечной точки. В главах 6 и 7 описано, как использовать в клиентах функцию connect для задания адреса сервера. После вызова клиентом функции connect он может применить функцию send (или write) для передачи данных, поскольку внутренняя структура данных сокета содержит не только адрес удаленной оконечной точки, но и адрес локальной оконечной точки. Однако в сервере без установления логического соединения нельзя использовать функцию connect, поскольку это приведет к ограничению применения сокета для связи с другими удаленными хостами и портами (кроме того хоста и порта, которые указаны в вызове этой функции), поэтому в сервере нельзя будет использовать сокет для получения дейтаграммы от произвольных клиентов. Таким образом, в сервере без установления логического соединения применяется неподключенный сокет. В нем явно формируются адреса ответов и используется вызов функции сокетов sendto для указания и передаваемой дейтаграммы, и адреса, по которому она должна быть передана. Вызов функции sendto имеет следующую форму: retcode = sendto(s, message, len, flags, toaddr, toaddrlen); Здесь s — неподключенный сокет, message — адрес буфера, содержащего передаваемые данные, len — число байтов в буфере, flags -*— опции отладки или управления, toaddr — указатель на структуру sockaddr_in, содержащую адрес оконечной точки, по которому должно быть отправлено сообщение, и toaddrlen — целое число, обозначающее длину структуры адреса. Функции сокетов предоставляют удобный способ получения адреса клиента в серверах без установления логического соединения: сервер получает адрес для ответа из адреса источника, находящегося в запросе. По существу, интерфейс сокетов предоставляет вызов, который может применяться для получения адреса отправителя вместе с очередной поступающей дейтаграммой. Этот вызов (recvf rom) принимает два параметра с указанием двух буферов. Система помещает поступающую дейтаграмму в один буфер, а адрес отправителя — в другой. Вызов функции recvf rom имеет следующую форму: retcode = recvfrom(s, buf, len, flags, from, fromlen); 8.19. Формирование адреса ответа в сервере без установления логического соединения 133
Здесь параметр s указывает используемый сокет, but обозначает буфер, в который система должна поместить очередную дейтаграмму, J ел определяет место, доступное в буфере, flags управляет действиями функции в особых случаях (например, указывает, нужно ли выполнять предварительный просмотр данных без извлечения их из сокета), from указывает второй буфер, в который система должна поместить адрес источника, a fromlen указывает адрес целочисленной переменной. Первоначально целочисленная переменная, на которую указывает параметр fromlen, определяет длину буфера from; после возврата управления функцией recvfrom он содержит длину адреса источника (т.е. длину элемента данных в буфере from). При формировании ответа сервер использует адрес, записанный функцией recvfrom в буфер from во время получения запроса. 8.20. Алгоритмы параллельного сервера Основная причина применения принципов параллельной обработки в сервере связана с необходимостью сократить время отклика на запросы, поступающие сразу от нескольких клиентов. Параллельная организация работы способствует сокращению времени отклика при следующих условиях: ¦ для формирования ответа требуется выполнение значительного объема операций ввода/вывода; ¦ требуемое время обработки запросов резко изменяется от одного запроса к другому; ¦ сервер работает на компьютере с несколькими процессорами. В первом случае применение в сервере средств параллельной обработки запросов дает возможность более рационально использовать ресурсы процессора и периферийных устройств, даже если на компьютере установлен только один процессор. Пока процессор занимается вычислениями при формировании одного ответа, устройства ввода/вывода могут передавать в память данные, которые требуются для формирования других ответов. Во втором случае квантование времени позволяет использовать один процессор для обработки запросов, требующих небольшого объема вычислений, не задерживая ответы на них на то время, которое требуется для обработки более трудоемких запросов. В третьем случае параллельное выполнение задач на компьютере с несколькими процессорами позволяет использовать один процессор для формирования ответа на один запрос, в то время как другой процессор формирует ответ на другой. В действительности, большинство параллельных серверов адаптируется к существующему аппаратному обеспечению автоматически: получая доступ к большему объему аппаратных ресурсов (например, к большему числу процессоров), они работают быстрее. Параллельные серверы обеспечивают повышение производительности за счет более рационального использования ресурсов процессора и устройств ввода/вывода. Такие серверы обычно проектируются так, что их производительность повышается автоматически при эксплуатации с аппаратными средствами, предоставляющими больше ресурсов. 8.21. Один ведущий и несколько ведомых потоков Хотя и существует возможность обеспечить определенную степень распараллеливания работы сервера с использованием одного потока выполнения, в большинстве параллельных серверов применяется несколько потоков. Вначале к работе приступает один поток, известный под названием ведущего; он открывает 134 Глава 8. Алгоритмы и задачи проектирования...
сокет в общепринятый порт, ожидает поступления следующего запроса и создает ведомый поток (возможно, в новом процессе) для обработки запроса. Ведомые потоки создаются для каждого запроса. Ведущий поток не вступает непосредственно во взаимодействие с клиентом, а передает эти функции ведомому потоку. Каждый ведомый поток обеспечивает взаимодействие с одним клиентом. После формирования и передачи ответа клиенту ведомый поток завершает свою работу. В следующих разделах принципы использования ведущего и ведомых потоков рассматриваются более подробно. В них показано, как эти принципы применяются в параллельных серверах без установления и с установлением логического соединения, и описаны альтернативные реализации. 8.22. Алгоритм параллельного сервера без установления логического соединения Наиболее простой вариант параллельного сервера без установления логического соединения может быть реализован на основе алгоритма 8.3. Ведущий поток сервера принимает входящие запросы (дейтаграммы) и формирует ведомые потоки, возможно в новом процессе, для обработки каждого из них. Алгоритм 8.3. Параллельный сервер без установления логического соединения 1. Ведущий поток. Создать сокет и выполнить его привязку к общепринятому адресу предоставляемой службы. Оставить сокет неподключенным. 2. Ведущий поток. Вызывать в цикле функцию recvfrom для приема очередного запроса от клиента и создавать новые ведомые потоки (возможно, в новом процессе) для формирования ответа. 1. Ведомый поток. Работа потока начинается с получения конкретного запроса от ведущего потока, а также доступа к сокету. 2. Ведомый поток. Сформировать ответ согласно прикладному протоколу и отправить его клиенту с использованием функции sendto. 3. Ведомый поток. Завершить работу (т.е. работа ведомого потока завершается после обработки одного запроса). Продумывая проект параллельного сервера, необходимо учитывать, что операция создания нового потока или процесса может оказаться весьма дорогостоящей, хотя общий объем затрат может зависеть от операционной системы и базовой архитектуры. При использовании протокола без установления логического соединения необходимо тщательно взвесить, оправдывают ли затраты, связанные с обеспечением параллельной работы, достигнутое повышение скорости. Поскольку операция процесса или потока является весьма дорогостоящей, параллельные реализации серверов без установления логического соединения встречаются редко. 8.22. Алгоритм параллельного сервера без установления логического соединения 135
8.23. Алгоритм параллельного сервера с установлением логического соединения В прикладных протоколах с установлением логического соединения основным средством связи является соединение. Такие протоколы позволяют клиенту установить соединение с сервером, обменяться данными через это соединение, а затем его закрыть. В большинстве случаев соединение между клиентом и сервером используется для последовательной обработки нескольких запросов, поскольку протокол позволяет клиенту неоднократно отправлять запросы и получать ответы, не разрывая существующее соединение и не создавая новое. Серверы с установлением логического соединения обеспечивают параллельное использование соединений, а не параллельную обработку отдельных запросов. В алгоритме 8.4 перечислены этапы работы параллельного сервера, в котором применяется протокол с установлением логического соединения. Ведущий поток сервера принимает входящие запросы на установление соединения и создает ведомые потоки или процессы для выполнения каждого из них. После завершения своей работы ведомый поток закрывает соединение. Алгоритм 8.4. Параллельный сервер с установлением логического соединения 1. Ведущий поток. Создать сокет и выполнить его привязку к общепринятому адресу предоставляемой службы. Оставить сокет неподключенным. 2. Ведущий поток. Перевести сокет в пассивный режим, подготовив его для использования сервером. 3. Ведущий поток. Вызывать в цикле функцию accept для получения очередного запроса от клиента и создавать новый ведомый поток или процесс для формирования ответа. 1. Ведомый поток. Работа потока начинается с получения доступа к соединению, полученному от ведущего потока (т.е. к сокету соединения). 2. Ведомый поток. Выполнять обмен данными с клиентом через соединение: принимать запрос (запросы) и передавать ответ (ответы). 3. Ведомый поток. Закрыть соединение и завершить работу. Ведомый поток завершает свою работу после обработки всех запросов от одного клиента. Как и в случае с сервером без установления логического соединения, ведущий поток не вступает непосредственно во взаимодействие с клиентом. Сразу после получения нового запроса на установление соединения ведущий поток создает ведомый поток для поддержки этого соединения. Ведущий поток ждет поступления следующих запросов на установление соединения одновременно с тем, как ведомый поток взаимодействует с клиентом. 8.24. Способы обеспечения параллельной работы сервера Поскольку в операционной системе Linux предусмотрены две формы организации параллельной работы (процессы и потоки), чаще всего применяются два способа реализации принципа работы "ведущий/ведомый". В одной реализации сервер создает несколько процессов, содержащих по одному потоку выполнения. 136 Глава 8. Алгоритмы и задачи проектирования...
В другой реализации сервер создает несколько потоков выполнения в одном процессе. Эти две формы реализации показаны на рис. 8.3. Ведущий процесс \ г О Ведомые процессы А О л ^ J Ведущий поток (а) Ведомые потоки (б) Рис. 8.3. Две реализации принципа "ведущий/ведомый": (а) несколько однопотоковых процессов и (б) один процесс, содержащий несколько потоков выполнения Эти две реализации описаны в главах 11 и 12. В обеих главах приведены примеры сервера, соответствующего алгоритму 8.4. В реализации, описанной в главе 11, используется форма, показанная на рис. 8.3а, где каждый из потоков (один ведущий и несколько ведомых) реализован в виде однопотокового процесса. Реализация, описанная в главе 12, соответствует форме, показанной на рис. 8.36, в которой и ведущий, и ведомые потоки реализованы в одном процессе. В главе 12 приведено сравнение этих двух реализаций и описаны преимущества и недостатки каждой из них. 8.25. Применение в качестве ведомых потоков отдельных программ Алгоритм 8.4 показывает, как в параллельном сервере создается новый ведомый поток для каждого соединения. В реализации с однопотоковыми процессами ведущий поток сервера выполняет это действие путем вызова системной функции fork. В случае простых прикладных протоколов весь код, необходимый для обеспечения работы и ведущего, и ведомого потоков, может содержаться в одной серверной программе. После вызова функции fork первоначальный процесс переходит по циклу к приему следующего входящего запроса на установление соединения, а новый процесс становится ведомым и поддерживает работу вновь созданного соединения. Однако в некоторых случаях может оказаться более удобным вызов серверным процессом на выполнение кода программы, которая была написана и оттранслирована отдельно. Такие операционные системы, как Linux, позволяют легко выполнить подобное требование, поскольку в них предусмотрена возможность вызывать в ведомом процессе функцию execve после вызова функции fork. Функция execve перекрывает код ведомого процесса кодом новой программы. Общий принцип состоит в следующем: 8.25. Применение в качестве ведомых потоков отдельных программ 137
В реализациях многих служб код выполнения функций и ведущего, и ведомого процессов может содержаться в одной программе. Однако в некоторых случаях применение независимой программы позволяет упростить программирование или сопровождение ведомого компонента, поэтому ведущая программа включает вызов функции execve после вызова функции fork. 8.26. Псевдопараллельная организация работы с применением одного потока В предыдущих разделах рассматривались параллельные серверы, реализованные с использованием параллельных потоков или процессов. Однако в некоторых случаях для параллельной обработки клиентских запросов целесообразно применять только один поток выполнения. В частности, в некоторых операционных системах операция создания потока или процесса является столь дорогостоящей, что применение в сервере принципа создания нового потока для каждого запроса или каждого соединения является неоправданным. Не менее важно то, что некоторые прикладные протоколы требуют, чтобы сервер предоставлял во всех соединениях совместный доступ к определенной информации. Чтобы понять, какие соображения могут свидетельствовать в пользу создания сервера, обеспечивающего псевдопараллельную организацию работы с использованием одного потока, рассмотрим систему X Window. Сервер X позволяет многочисленным клиентам выводить текст и графику в окна, открытые на растровом дисплее. Каждый клиент управляет одним окном, посылая запросы на обновление его содержимого. Все клиенты работают независимо друг от друга, причем одни из них могут не менять изображение в окне несколько часов подряд, а другие — часто обновлять изображение на дисплее. Например, приложение, которое показывает время, выводя изображение часов, обновляет картинку на дисплее через каждую минуту. Между тем, приложение, которое показывает состояние почтового ящика пользователя, должно дождаться поступления новой электронной почты и только после этого обновить изображение на дисплее. Сервер для системы X Window объединяет информацию, полученную от клиентов, в одном непрерывном разделе памяти, называемом дисплейным буфером. Данные, поступающие от всех клиентов, складываются в одну разделяемую структуру данных. Кроме того, система X Window была первоначально спроектирована для использования в такой версии UNIX, где каждый процесс выполнялся в отдельном адресном пространстве без разделяемой памяти. Однако сервер X должен предоставлять доступ к параллельно работающей службе. Хотя и существует возможность добиться требуемого распараллеливания с помощью потоков, совместно использующих память, в равной степени возможно обеспечить псевдопараллельную организацию работы, если суммарная нагрузка по обработке запросов, поступающих на сервер, не превышает его пропускную способность. Таким образом, сервер функционирует в виде одного потока выполнения, в котором используется системный вызов select для обеспечения асинхронного ввода/вывода. В алгоритме 8.5 описаны этапы работы однопотокового сервера, которые должны быть выполнены для обеспечения параллельной поддержки многочисленных соединений. Поток выполнения ожидает перехода в состояние готовности очередного дескриптора, что может означать поступление нового запроса на установление соединения или передачу клиентом запроса через существующее соединение1. В главе 13 представлен пример сервера, который обеспечивает параллельную работу с использованием только одного потока выполнения. 138 Глава 8. Алгоритмы и задачи проектирования...
Алгоритм 8.5. Параллельный сервер с установлением логического соединения, реализованный в виде одного потока выполнения 1. Создать сокет и привязать его к общепринятому порту предоставляемой службы. Добавить сокет к списку сокетов, через которые может осуществляться ввод/вывод. 2. Использовать функцию select для получения информации о готовности существующих сокетов к вводу/выводу. 3. Если готов первоначальный сокет, использовать функцию accept для получения очередного запроса на установление соединения и добавить новый сокет к списку сокетов, через которые может осуществляться ввод/вывод. 4. Бели готов сокет, отличный от первоначального, использовать функцию recv или read для получения очередного запроса, сформировать ответ и передать ответ клиенту с использованием функции send или write. 5. Продолжить обработку запросов, начиная с приведенного выше этапа 2. 8.27. Области применения серверов различных типов ¦ Последовательный или параллельный. Последовательные серверы проще в проектировании, реализации и сопровождении, но параллельные серверы могут обеспечить более быстрый отклик на запросы. Последовательная реализация применяется, если время обработки запросов невелико и последовательное решение обеспечивает достаточно быстрое время отклика для рассматриваемого приложения. ¦ Действительно параллельный или псевдопараллельный. В сервере с одним потоком выполнения для поддержки сразу нескольких соединений может применяться асинхронный ввод/вывод, а многопотоковая реализация, основанная на использовании нескольких однопотоковых процессов или нескольких потоков в одном процессе, позволяет автоматически обеспечить параллельную работу с использованием средств операционной системы. Однопотоко- вое, псевдопараллельное решение применяется, если создание или переключение между потоками требует больших затрат, а сервер должен обеспечить совместное использование данных или обмен данными между соединениями. Многопотоковое решение применяется, если сервер должен обеспечить совместное использование или обмен данными между соединениями, а использование потоков не требует больших затрат. Мультипроцессное решение применяется, если каждый ведомый поток может работать в изоляции от других; такое решение может также применяться для достижения максимального распараллеливания (например, в многопроцессорной системе). ¦ С установлением или без установления логического соединения. Поскольку доступ с установлением логического соединения подразумевает использование протокола TCP, он также подразумевает и надежную доставку, а транспортный протокол без установления логического соединения (UDP) не может обеспечить надежную доставку. При использовании транспортного протокола без установления логического соединения требуемая надежность может быть достигнута, только если функции повышения надежности реализованы в прикладном протоколе (что почти никогда не встречается) или каждый клиент обращается к своему серверу по локальной сети, которая характеризуется исключительно низкими потерями и отсутствием переупорядочения пакетов. Ес- 8.27. Области применения серверов различных типов 139
ли между клиентом и сервером лежит распределенная сеть, для связи между ними должен применяться транспортный протокол с установлением логического соединения. Клиент и сервер без установления логического соединения ни в коем случае нельзя переносить в среду глобальной сети без проверки способности прикладного протокола обеспечить требуемую надежность. 8.28. Сводные данные по типам серверов Последовательный сервер без установления логического соединения Наиболее широко распространенная форма сервера без установления логического соединения, которая особенно часто используется в службах, требующих незначительного времени для обработки каждого запроса. Последовательные серверы в основном не поддерживают состояние, поэтому программы таких серверов проще для изучения, а сами серверы менее восприимчивы к отказам. Последовательный сервер с установлением логического соединения Менее распространенный тип сервера, используемый для реализации служб, требующих незначительного времени обработки каждого запроса, для которых тем не менее необходим надежный транспортный протокол. При достаточно больших издержках, связанных с установлением и завершением соединений, среднее время отклика иногда становится весьма значительным. Параллельный сервер без установления логического соединения Довольно редко применяемый сервер, в котором создается новый поток или процесс для обработки каждого запроса. Во многих системах дополнительные затраты на создание потоков или процессов не оправдывают повышения эффективности, достигнутого за счет параллельной организации работы. Для того чтобы затраты на обеспечение параллельной работы были оправданы, время, необходимое для создания нового потока/процесса, должно быть намного меньше по сравнению с временем, необходимым для формирования ответа, или же должна существовать возможность использования нескольких устройств ввода/вывода для параллельной обработки запросов. Параллельный сервер с установлением логического соединения Наиболее распространенный тип сервера, поскольку он обеспечивает использование надежного транспортного протокола (т.е. может применяться в глобальной объединенной сети), а также обладает способностью параллельно обрабатывать сразу несколько запросов. Существует две основных реализации таких серверов. В наиболее распространенной реализации для поддержки соединений используются параллельные однопотоковые процессы или параллельные потоки в одном процессе; гораздо менее распространенная реализация основана на использовании одного потока и асинхронного ввода/вывода для поддержки параллельных соединений. В реализации с параллельными процессами ведущий поток сервера создает ведомый процесс для поддержки каждого соединения. При использовании параллельных процессов появляется возможность вызывать на выполнение для поддержки каждого соединения отдельно оттранслированную программу, а не вкладывать весь код в одну большую серверную программу. В однопотоковой реализации один поток выполнения управляет параллельными соединениями. В этом случае достигается псевдопараллельная организация работы с использованием асинхронного ввода/вывода. Сервер в цикле ожидает перехода в состояние готовности к вводу/выводу любого из открытых соединений и обрабатывает каждый запрос. Поскольку все соединения поддерживаются в одном потоке, сервер может обеспечить их совместный доступ к общим дан- 140 Глава 8. Алгоритмы и задачи проектирования...
ным. Но поскольку существует только один поток, сервер не может обрабатывать запросы быстрее по сравнению с последовательным сервером, даже на компьютере с несколькими процессорами. Такая реализация сервера может быть оправдана, если только приложение требует совместного доступа к данным разных соединений или на обработку каждого запроса не требуется много времени. 8.29. Важная проблема тупиковой ситуации в работе сервера Многие реализации серверов имеют один важный недостаток, а именно — сервер может легко оказаться в тупиковой ситуации2. Чтобы понять, как может возникнуть тупиковая ситуация, рассмотрим последовательный сервер с установлением логического соединения. Предположим, что клиентское приложение (например, С) выполняет недопустимые действия. В простейшем случае предположим, что приложение С формирует соединение с сервером, но не присылает никаких запросов. Сервер принимает запрос на установление соединения и вызывает функцию recv или read для получения ожидаемого запроса. Поэтому серверная программа заблокируете^ в этом системном вызове, ожидая запроса, который никогда не поступит. Тупиковая ситуация в сервере может возникнуть при еще менее очевидных обстоятельствах, если клиенты действуют неправильно, не считывая отправленные сервером ответы. Например, предположим, что клиент С установил соединение с сервером, передал ему ряд запросов, но не прочитал ни одного ответа. Сервер принял один за другим запросы, сформировал ответы и отправил их клиенту. Применяемое на сервере программное обеспечение протокола TCP выполнило передачу нескольких первых байтов через соединение с клиентом. В этот момент вступает в действие механизм управления потоком данных TCP. После заполнения буфера приема данных клиента протокол TCP прекращает передачу данных. Серверная прикладная программа продолжает формировать ответы. Локальный выходной буфер TCP, применяемый программным обеспечением этого протокола для хранения исходящих данных соединения, заполняется, а сервер блокируется. Тупиковая ситуация возникает в связи с тем, что при отсутствии возможности выполнения операционной системой системного вызова вызывающая программа блокируется. В частности, при вызове функции send или write вызывающая программа блокируется, если программное обеспечение TCP не имеет свободного локального буферного пространства для размещения передаваемых данных, а при вызове функции recv или read вызывающая программа блокируется до тех пор, пока программное обеспечение TCP не получит данные. В параллельных серверах блокируется только один ведомый поток, связанный с конкретным клиентом, если этот клиент окажется не в состоянии отправлять запросы или читать ответы. Однако в реализациях, основанных на использовании одного потока выполнения, блокируется весь сервер. После возникновения блокировки сервер теряет способность поддерживать остальные соединения. Это означает, что любой сервер, в котором используется только один поток, может оказаться в тупиковой ситуации. Неправильно функционирующий клиент может вызвать тупиковую ситуацию в однопотоковом сервере, если в сервере используются системные функции, которые могут заблокироваться в процессе обмена данными с клиентом. При разработке серверов необходимо уделять большое внимание предотвращению тупиковых ситуаций в серверах, поскольку при наличии 2 Тупиковой ситуацией называется состояние, в котором программа или ряд программ не могут продолжать работу, поскольку они заблокированы в ожидании события, которое никогда не произойдет. Применительно к серверу, тупиковая ситуация означает, что сервер прекращает отвечать на запросы. 8.29. Важная проблема тупиковой ситуации в работе сервера 141
предпосылок возникновения тупиковых ситуаций действия одного клиента могут привести к тому, что сервер прекратит обработку запросов других клиентов. 8.30. Альтернативные реализации Примеры реализации серверных алгоритмов, описанных в этой главе, приведены в главах с 9 по 13. В главах 14 и 15 эти принципы дополнены двумя важными практическими методами реализации, которые не рассматривались в этой главе; в них описаны мультипротокольные и мультисервисные серверы. Хотя эти методы предоставляют привлекательные преимущества для некоторых приложений, они не были здесь описаны, поскольку их легче понять, рассматривая как простые обобщения алгоритма однопотокового сервера, которые описаны в главе 13. 8.31. Резюме В основе работы сервера лежит простой алгоритм: в программе выполняется бесконечный цикл, в котором сервер ожидает поступления следующего запроса от клиента, обрабатывает запрос и передает ответ. Однако на практике для достижения надежности, гибкости и эффективности используется целый ряд реализаций серверов. Последовательные реализации могут применяться для служб, требующих небольшого объема вычислений. При использовании транспортного протокола с установлением логического соединения последовательный сервер одновременно поддерживает только одно соединение, а при использовании транспортного протокола без установления логического соединения последовательный сервер одновременно обрабатывает только один запрос. Для повышения эффективности в серверах часто применяется параллельная организация работы, позволяющая обрабатываясь сразу несколько запросов. В сервере с установлением логического соединения параллельная поддержка соединений достигается путем создания отдельного потока или процесса для поддержки каждого соединения. В сервере без установления логического соединения параллельная организация работы обеспечивается путем создания нового потока или процесса для обработки каждого нового запроса. В тупиковой ситуации может оказаться любой сервер, реализованный на основе одного потока выполнения, в котором применяются такие синхронные системные функции, как recv, read, send или write. Тупиковая ситуация может возникать в последовательных серверах, а также в параллельных серверах, в которых используется однопотоковая реализация. Проблема тупиковых ситуаций является особенно важной в связи с тем, что если не будут приняты меры, то единственный неправильно действующий клиент может исключить возможность обработки сервером запросов других клиентов. Материал для дальнейшего изучения В поставку операционной системы Linux и других версий UNIX входят серверы, которые могут служить прекрасными образцами реализации многих серверных алгоритмов; программисты, изучая методы программирования, охотно знакомятся с исходным кодом этих серверов. 142 Глава 8. Алгоритмы и задачи проектирования...
Упражнения 8.1. Рассчитайте, сколько времени потребуется последовательному серверу для передачи файла объемом 200 Мбайт, если объединенная сеть имеет пропускную способность 2,3 Кбайт/с. 8.2. Бели в среднем 20 клиентов посылают на последовательный сервер по 2 запроса в секунду, то какое максимальное время может затрачиваться сервером на обработку каждого запроса? 8.3. Подключаясь с разных клиентских компьютеров к параллельному серверу с установлением логического соединения, определите, какое время в среднем затрачивается сервером для приема запросов на установление соединения и создания для их обслуживания новых процессов? Новых потоков? 8.4. Напишите алгоритм для параллельного сервера без установления логического соединения, который создает новый процесс для каждого запроса. 8.5. Доработайте алгоритм, описанный в условиях предыдущего упражнения, таким образом, чтобы сервер создавал по одному новому процессу для каждого клиента, а не создавал новый процесс в ответ на каждый запрос. Как обеспечить с помощью этого алгоритма завершение работы процесса? 8.6. Серверы с установлением логического соединения обеспечивают параллельную- поддержку соединений. Есть ли смысл еще больше повысить степень распараллеливания работы параллельного сервера с установлением логического соединения, предусмотрев создание в ведомых потоках дополнительных потоков для каждого запроса? Объясните ваш ответ. 8.7. Перепишите программу клиента эхо-сервера TCP таким образом, чтобы в ней использовался один поток выполнения для параллельной обработки ввода с клавиатуры, ввода из соединения TCP и вывода в соединение TCP. 8.8. Могут ли клиенты вызывать тупиковую ситуацию или нарушать обслуживание других клиентов параллельными серверами? Объясните ваш ответ. 8.9. Тщательно изучите системный вызов select. Как можно использовать функцию select в однопотоковом сервере для предотвращения тупиковой ситуации? 8.10. Вызов функции select принимает параметр, в котором указано число проверяемых этой функцией дескрипторов ввода/вывода. Объясните, как с использованием этого параметра обеспечить переносимость программы однопотокового сервера на многие системы UNIX. Упражнения 143
9 Последовательные серверы без установления логического соединения (UDP) 9.1. Введение В предыдущей главе было рассмотрено много возможных проектов серверов и описаны преимущества и недостатки каждого из них. В настоящей главе приведен пример реализации последовательного сервера, в которой используется транспортный протокол без установления логического соединения. Этот пример сервера создан в соответствии с алгоритмом 8.21. В следующих главах описание этой темы продолжено на примерах реализации других алгоритмов серверов. 9.2. Создание пассивного сокета Этапы создания пассивного сокета аналогичны этапам создания активного сокета. При этом приходится учитывать много тонкостей и предусматривать в программе поиск имени службы для получения номера порта протокола, применяемого в соответствии с общепринятым соглашением. В целях упрощения кода сервера программисты используют процедуры, которые скрывают сложности процесса распределения сокета. Как и в примерах клиентов, в этих примерах реализации серверов применяются две процедуры высокого уровня, passiveUDP и passiveTCP, которые распределяют пассивный со- кет и привязывают его к общепринятому порту сервера. Каждый сервер вызывает одну из этих процедур, причем выбор конкретной процедуры зависит от того, используется ли в сервере транспортный протокол с установлением или без установления логического соединения. В настоящей главе рассматривается процедура passiveUDP, а в следующей главе приведен код процедуры passiveTCP. Поскольку эти две процедуры имеют много общего, в них предусмотрен вызов процедуры низкого уровня passivesock для выполнения одинаковых действий. В сервере без установления логического соединения для создания сокета, который используется в предоставляемой им службе, вызывается функция passiveUDP. Если для сервера требуется один из портов, зарезервированных для общепринятых служб (т.е. порт с низким номером), серверный процесс должен Описание алгоритма 8.2 приведено на стр. 133.
иметь особые привилегии2. Для создания сокета непривилегированной службы вызов процедуры passiveUDP может быть выполнен в любой прикладной программе. Процедура passiveUDP вызывает процедуру passivesock для создания сокета протокола без установления логического соединения, а затем возвращает дескриптор сокета в вызывающий оператор. Для упрощения проверки клиентского и серверного программного обеспечения в процедуре passivesock предусмотрено смещение всех номеров портов путем сложения с глобальной целочисленной переменной portbase. По сути это означает, что все номера портов с помощью этой процедуры переносятся в область старших номеров. Необходимость применения переменной portbase и смещения номеров портов станет очевидной в следующих главах. Однако основной замысел можно легко понять. В двух серверах, находящихся на одном и том же компьютере, нельзя одновременно использовать одинаковый номер порта протокола. Перенеся на время все номера портов в область более высоких значений, программист получает возможность проверить новую версию программного обеспечения клиент/сервер на компьютере, не нарушая работу производственной версии программы. Основное преимущество использования переменной portbase связано с ее безопасностью и общностью. Во-первых, поскольку программисту не нужно корректировать все ссылки на номера портов во всем коде, применение переменной portbase способствует снижению вероятности ошибок (например, связанных с непреднамеренным пропуском одного из исправлений при вставке кода перед отладкой или при удалении кода после отладки). Во-вторых, применение переменной portbase служит общим решением проблемы. Она не только позволяет проверять новую версию во время работы производственного сервера, но и дает возможность одновременно проверять сразу несколько новых версий сервера. Для этого программист просто присваивает каждой версии уникальное, ненулевое значение portbase. Следовательно, номер порта, передаваемый конкретной версии сервера API- интерфейсу сокетов, не будет конфликтовать с номерами портов, которые используются для других отладочных версий или для производственной версии. Применение глобальной переменной для смещения номеров портов обеспечивает безопасную отладку программы, поскольку позволяет программисту одновременно проверять несколько версий сервера, не внося изменений во всем коде. I* passiveUDP.с - процедура passiveUDP */ int passivesock(const char *service, const char *transport, int qlen); /* —- —- * Процедура passiveUDP - создает пассивный сокет для использования * в сервере UDP * . */ int passiveUDP(const char *service) 2 В системе Linux, как и в большинстве систем UNIX, приложение должно выполняться с правами root (т.е. от имени суперпользователя), чтобы иметь достаточные привилегии для привязки порта с номером менее 1024. 146 Глава 9. Последовательные серверы без установления логического соединения...
/" * Параметры: * service - служба, связанная с требуемым портом */ { return passivesock(service, "udp", 0); } Все тонкости процесса распределения сокета, включая использование переменной portbase, учтены в процедуре passivesock. Она принимает три параметра. Первый параметр указывает службу, второй — имя протокола, а третий (используется только для сокетов TCP) задает требуемую длину очереди запросов на установление соединения. Первый параметр, символьная строка, может содержать либо имя службы, либо номер порта протокола для этой службы; если используется номер порта, он должен быть представлен в виде символьной строки. Процедура passivesock распределяет либо дейтаграммный, либо потоковый сокет, привязывает сокет к общепринятому порту данной службы и возвращает дескриптор сокета в вызывающий оператор. Напомним, что при привязке сокета к общепринятому порту в серверной программе необходимо указать адрес с использованием структуры sockaddr_in, которая включает IP-адрес и номер порта протокола. Вместо задания конкретного локального IP-адреса в процедуре passivesock используется константа INADDR_ANY, как описано в главе 8. Применение константы INADDR_ANY обеспечивает работу сервера не только на хостах, имеющих один IP-адрес, но и на маршрутизаторах и многоадресных хостах, которые имеют несколько IP-адресов. Следует отметить, что процедура passivesock требует указания конкретного номера порта протокола; применение в качестве параметра порта константы INADDR_ANY означает, что сервер будет принимать запросы, направленные в данный порт че^ез любой из IP-адресов компьютера: ** /* Файл passivesock.с - процедура passivesock */ ¦include <sys/types.h> ¦include <sys/socket.h> ¦include <netinet/in.h> ¦include <stdlib.h> ¦include <string.h> ¦include <netdb.h> extern int errno; int errexit(const char *format, ...); unsigned short portbase =0; /* Точка отсчета номера порта, для */ /*использования в серверах, не работающих с привилегиями пользователя root */ /* - - * Процедура passivesock - распределяет и подключает серверный сокет * с использованием протокола TCP или UDP * . */ int passivesock(const char *service, const char *transport, int qlen) /* 9.2. Создание пассивного сокета 147
* Параметры: * service - служба, связанная с требуемым портом * transport - имя используемого транспортного протокола ("tcp" или "udp") * glen - максимальная длина очереди запросов на подключение * к серверу */ { struct servent *pse; /* указатель на запись с информацией о службе */ struct protoent *ppe; /* указатель на запись с информацией о протоколе*/ struct sockaddr_in sin; /* IP-адрес оконечной точки */ int s, type; " /* Дескриптор сокета и тип сокета */ memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin~addr.s_addr = INADDR_ANY; /* Преобразовать имя службы в номер порта¦*/ if ( pse = getservbyname(service, transport) ) sin.sinjport = htons(ntohs((unsigned short)pse->s_port) + portbase); else if ((sin.sinj?ort=htons((unsigned short)atoi(service))) == 0) errexit("can't""get \"%s\" service entry\n", service); /* Преобразовать имя протокола в номер протокола */ if ( (рре ¦ getprotobyname(transport)) ==0) errexit(Hcan't get \"%s\" protocol entry\n", transport); /* Использовать имя протокола для определения типа сокета */ if (strcmp(transport, "udp") ==0) type = SOCK_DGRAM; else type « SOCKJ3TREAM; /* Распределить сокет */ s ¦ socket(PF INET, type, ppe->p proto); if (s <0) errexit("can't create socket: %s\n", strerror(errno)); /* Выполнить привязку сокета */ if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) <0) errexitj"can't bind to %s port: %s\n", service, strerror(errno)); if (type ¦¦ SOCK^STREAM && listen(s, qlen) <0) errexit(Mcan't listen on %s port: %s\n", service, strerror(errno)); return s; 9.3. Организация работы На рис. 9.1 показана упрощенная схема организации работы последовательного сервера без установления логического соединения. Требуется только один поток выполнения, который обеспечивает взаимодействие сервера со многими клиентами с использованием одного сокета. 148 Глава 9. Последовательные серверы без установления логического соединения...
I Прикладной ^ процесс сервера Операционная система Рис. 9.1. Схема организации работы последовательного сервера без установления логического соединения Функционирование одного потока сервера продолжается неопределенно долгое время. В нем используется один пассивный сокет, привязанный к общепринятому порту протокола предоставляемой им службы. Сервер получает запрос из сокета, формирует ответ и передает ответ клиенту с использованием того же сокета. Сервер использует адрес источника в запросе в качестве адреса назначения в ответе. 9.4. Пример сервера TIME Рассмотрим на примере, как используются в сервере без установления логического соединения процедуры распределения сокета, описанные выше. Как было сказано в главе 7, клиенты используют службу TIME для получения текущего времени суток с сервера, функционирующего на другом компьютере. Поскольку для службы TIME требуется небольшой объем вычислений, реализация последовательного сервера работает хорошо. Код последовательного сервера службы TIME без установления логического соединения находится в файле UDPtimed.c. /* UDPtimed.c - главная процедура */ ¦include <sys/types.h> ¦include <sys/socket.h> ¦include <netinet/in.h> ¦include <stdio.h> ¦include <time.h> ¦include <string.h> extern int errno; int passiveUDP(const char *service); int errexit(const char *format, .,.)? ¦define UNIXEPOCH 2208988800UL /* Время наступления эпохи UNIX: число */ Сервер т Сокет открыт в общепринятый порт, который используется во всех операциях обмена данными 9.4. Пример сервера TIME 149
/* /* секунд от начала эпохи Internet */ * Главная процедура - последовательный сервер UDP для службы TIME * *. */ int main(int argc, char *argv[]) { struct sockaddr^in fsin; char *service = "time"; char buf[l]j int sock; time_t now; unsigned int alen; switch (argc) { case 1: break; case 2: service = argv[l]; break; default: errexit("usage: UDPtimed [port]\nH); } sock « passiveUDP(service); while A) { alen = sizeof(fsin); if (recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&fsin, balen) <0) errexit("recvfrom: %s\n", strerror(errno)); (void) time(Snow); now = htonl((unsigned long)(now + UNIXEPOCH)); (void) sendto(sock, (char *)&now, sizeof(now), 0, (struct sockaddr *)&fsin, sizeof(fsin)); } } Как и любой сервер, процесс UDPtimed должен функционировать неопределенно долгое время. Поэтому в основе этой программы лежит бесконечный цикл, который принимает запросы, определяет текущее время и отправляет ответ клиенту, приславшему запрос. В этой программе есть несколько тонкостей. После интерпретации параметров программа UDPtimed вызывает процедуру passiveUDP для создания пассивного сокета службы TIME. Затем она входит в бесконечный цикл. Согласно спецификации протокола TIME, клиент для активизации процесса формирования ответа может прислать произвольную дейтаграмму. Дейтаграмма может иметь любую длину и содержать любые значения, поскольку сервер не интерпретирует ее содержимое. В этом примере для чтения основной дейтаграммы используется функция recvf rom. Эта функция помещает входящую дейтаграмму в буфер buf, а адрес оконечной 150 Глава 9. Последовательные серверы без установления логического соединения... /* Адрес источника в дейтаграмме, */ /* полученной от клиента*/ /* Имя службы или номер порта */ /* Входной буфер; может иметь любой размер> 0 */ /* Сокет сервера */ /* Текущее время */ /* Длина адреса источника */
точки клиентской программы, отправившей дейтаграмму, записывает в структуру f sin. Поскольку в этой реализации не требуется обработка данных, в ней используется односимвольный буфер. Если дейтаграмма содержит более одного байта данных, функция recvf rom отбрасывает все оставшиеся байты. В программе UDPtimed для получения текущего значения времени используется системная функция time. Как было указано в главе 7, в системе Linux, как и в других системах UNIX, для представления времени, измеряемого с начала эпохи (полночь 1 января 1970 года) используется 32-битовое целое число. После получения этого значения времени из операционной системы программа UDPtimed должна преобразовать его в значение, измеренное с начала эпохи Internet, и привести результат к сетевому порядку байтов. Для выполнения этого преобразования с полученным значением времени складывается константа UNIXEPOCH, имеющая по определению значение 2208988800, равное разнице в секундах между эпохой Internet и эпохой Linux. Затем вызывается функция htonl для представления результата в сетевом порядке байтов. И наконец, в программе UDPtimed вызывается sendto для передачи полученного результата клиенту. В функции sendto в качестве адреса назначения используется адрес оконечной точки, хранящийся в структуре f sin (т.е. используется адрес клиента, который прислал дейтаграмму). 9.5. Резюме Для простых служб, в которых сервер при обработке каждого запроса выполняет небольшой объем вычислений, вполне приемлема последовательная реализация. В настоящей главе приведен пример последовательного сервера для службы TIME, в котором используется протокол UDP для доступа без установления логического соединения. Этот пример служит подтверждением того, что применение процедур позволяет скрыть тонкости процесса распределения сокета и разработать более простую и легкую для понимания серверную программу. Материал для дальнейшего изучения Стандарт протокола TIME содержится в документе [63]. В документе [100] определен протокол NTP (Network Time Protocol — Синхронизирующий сетевой протокол), в статье [101] дано сводное описание проблем, связанных с использованием протокола NTP в действующих сетях, а в документе [102] описаны методы применения протокола NTP для синхронизации часов. В статье [94] обсуждаются также вопросы синхронизации показаний часов в распределенной среде. Упражнения 9.1. Проведите измерения характеристик программы UDPtimed, чтобы определить, сколько времени в ней затрачивается на обработку каждого запроса. Если у вас есть доступ к сетевому анализатору, измерьте также интервал времени между прохождением пакетов запроса и ответа. 9.2. Предположим, что в программе UDPtimed будет случайно искажен адрес клиента в интервале времени между получением запроса и передачей ответа (т.е. в серверной программе будет непреднамеренно записано в структуру fsin случайное значение перед ее использованием в вызове функции sendto). Что в этом случае произойдет? Почему? 9.5. Резюме 151
9.3. Проведите эксперимент для определения того, что произойдет, если N клиентов отправят запросы на сервер UDPtimed одновременно. Варьируйте и значение N (число отправителей), и значение S (размер передаваемых ими дейтаграмм). Объясните, почему сервер не сможет ответить на все запросы. (Подсказка: прочитайте справочное руководство по функции listen.) 9.4. В примере кода программы UDPtimed.с в вызове функции recvfrom определен размер буфера, равный 1. Что произойдет, если в программе будет задан размер буфера О? 9.5. Рассчитайте разницу между эпохой отсчета времени в Linux и эпохой отсчета времени в Internet. He забудьте учесть високосные годы. Совпадает ли полученное вами значение с константой UNIXEPOCH, которая определена в файле UDPtimed? Если нет, объясните причину. (Подсказка: прочитайте в литературе о високосных секундах.) 9.6. В качестве меры защиты системный администратор попросил вас доработать программу UDPtimed таким образом, чтобы она записывала в журнал информацию обо всех клиентах, обратившихся к службе. Откорректируйте программу таким образом, чтобы она выводила на консоль строку при поступлении каждого запроса. Объясните, как повлияет на работу этой службы применение средств ведения журналов. 9.7. Если вы имеете доступ к паре компьютеров, подключенных к глобальной объединенной сети, воспользуйтесь клиентом UDPtime из главы 7 и сервера UDPtimed из этой главы для проверки того, происходит ли в вашей объединенной сети потеря или дублирование пакетов. 152 Глава 9. Последовательные серверы без установления логического соединения...
10 Последовательные серверы с установлением логического соединения (TCP) 10.1. Введение В предыдущей главе приведен пример последовательного сервера, в котором используется UDP в качестве транспортного протокола без установления логического соединения. В настоящей главе показано применение в последовательном сервере протокола TCP в качестве транспортного протокола с установлением логического соединения. Этот пример сервера соответствует алгоритму 8.11. 10.2. Распределение пассивного сокета TCP Как было указано в главе 9, в сервере с установлением логического соединения для распределения потокового сокета и привязки его к общепринятому порту предлагаемой службы может применяться процедура passiveTCP. Процедура passiveTCP принимает два параметра. Первый параметр, символьная строка, задает имя или номер службы, а второй указывает желаемую длину очереди входящих запросов на установление соединения. Если первый параметр содержит имя, он должен соответствовать одной из записей в базе данных служб, к которой обращается библиотечная функция getservbyname. Если первый параметр задает номер порта, то он должен представлять это число в виде текстовой строки (например, 79). /* Файл passiveTCP.с - процедура passiveTCP */ int passivesock(const char *service, const char *transport, int qlen); /* * Процедура passiveTCP - создает пассивный сокет для использования * в сервере TCP *__ —™- .- « ™ */ Описание алгоритма 8.1 приведено на стр. 131.
int passiveTCP(const char *service, int qlen) /* * Параметры: * service - служба, связанная с требуемым портом * qlen - максимальная длина очереди запросов сервера */ { return passivesock(service, "tcp", qlen); } 10.3. Сервер службы DAYTIME Как было отмечено в главе 7, служба DAYTIME позволяет пользователю одного компьютера получить значение текущей даты и времени суток с другого компьютера. Поскольку служба DAYTIME предназначена для использования людьми, ее спецификация определяет, что сервер при отправке ответа должен отформатировать дату в виде удобной для восприятия строки текста ASCII. Поэтому клиентская программа может отобразить ответ для пользователя точно в таком же формате, в каком он был получен. В главе 7 показано, как используется в клиентской программе протокол TCP для доступа к серверу DAYTIME и отображения текста, возвращенного сервером. Поскольку операции получения и форматирования даты требуют небольшого объема обработки, а предполагаемая интенсивность использования этой службы невелика, то нет необходимости оптимизировать скорость работы сервера DAYTIME. Если новые клиенты попытаются выполнить свои запросы на установление соединения в то время, когда сервер занят обработкой другого запроса, программное обеспечение протокола поставит эти запросы в очередь. Поэтому для данного сервера вполне подходит последовательная реализация. 10.4. Схема организации процессов Как показано на рис. 10.1, в последовательном сервере с установлением логического соединения используется один поток выполнения, который функционирует в цикле неопределенно долгое время, используя один сокет для обработки входящих запросов, а второй, временный сокет — для взаимодействия с клиентом. Поток выполнения ожидает поступления запросов на установление соединения через общепринятый порт, а затем обменивается данными с клиентом через соединение. Сервер, в котором используется транспортный протокол с установлением логического соединения, выполняет обработку запросов на установление соединения в цикле: он ожидает поступления очередного запроса на установление соединения от клиента через общепринятый порт, принимает этот запрос, выполняет его, закрывает соединение, а затем снова переходит в состояние ожидания. Реализация службы DAYTIME становится особенно простой, поскольку сервер не должен принимать от клиента какой-либо определенный запрос: в этой службе для активизации ответа используется сам факт появления входящего запроса на установление соединения. Поскольку клиент не присылает какой-то определенный запрос, сервер не читает данные из соединения. 154 Глава 10. Последовательные серверы с установлением логического соединения...
Сокет для приема запросов на установление соединения Сокет для отдельного соединения Прикладной процесс сервера Операционная система Рис. 10.1. Схема организации процессов последовательного сервера с установлением логического соединения 10.5. Пример сервера DAYTIME Пример кода последовательного сервера DAYTIME с установлением логического соединения приведен в файле TCPdaytimed.c. /* TCPdaytimed.c - главная процедура */ ¦include <sys/types.h> ¦include <sys/socket.h> ¦include <netinet/in.h> ¦include <unistd.h> ¦include <stdio.h> ¦include <string.h> extern int errno; int errexit(const char *format, ...); void TCPdaytimed(int fd); int passiveTCP(const char *service, int qlen); ¦define QLEN 32 /* . . . * Главная процедура - последовательный сервер TCP для службы DAYTIME *. . . «. */¦ int main(int argc, char *argv[]) { struct sockaddr_in fsinj /* Адрес источника в сообщении, полученном */ /* от клиента */ 10.5. Пример сервера DAYTIME 155
char *service = "daytime"? /* Имя службы или номер порта */ int msock, ssock; /* Ведущий и ведомый сокеты */ unsigned int alen; /* Длина адреса источника */ switch (argc) { case 1: break; case 2: service = argv[l]; break ; default: errexit("usage: TCPdaytimed [port]\n"); } msock = passiveTCP(service, QLEN); while A) { alen = sizeof(fsin); ssock = accept(msock, (struct sockaddr *)&fsin, &alen); if (ssock <0) errexit("accept failed: %s\n", strerror(errno)); TCPdaytimed(ssock); (void) close(ssock); } } /*i .._.——___ ... * Процедура TCPdaytimed - предоставляет доступ к службе DAYTIME * по протоколу TCP */ void TCPdaytimed(int fd) { char *pts; time_t now; char *ctime(); (void) time(finow); pts = ctime(Snow); (void) write(fd, pts, strlen(pts)); } Как и последовательный сервер без установления логического соединений, описанный в предыдущей главе, последовательный сервер с установлением логического соединения должен работать неопределенно долгое время. После создания сокета, через который принимаются запросы к общепринятому порту, сервер входит в бесконечный цикл, в котором он принимает и обрабатывает запросы на установление соединения. Объем кода этого сервера невелик, поскольку вызов процедуры passiveTCP позволяет скрыть тонкости распределения и привязки сокета. В результате вызова процедуры passiveTCP создается ведущий сокет, связанный с общепринятым портом службы DAYTIME. Второй параметр вызова процедуры указывает, что ве- 156 Глава 10. Последовательные серверы с установлением логического соединения... /* Указатель на строку со значением времени */ /* Текущее время */
дущий сокет будет иметь очередь запросов с длиной QLEN, что позволяет операционной системе ставить в очередь запросы на установление соединения, поступающие от QLEN других клиентов, пока сервер занимается формированием ответа на запрос текущего клиента. После создания ведущего сокета главная процедура сервера входит в бесконечный цикл. При каждом проходе по циклу сервер вызывает функцию accept для получения очередного запроса на установление соединения из ведущего сокета. Чтобы исключить потребление сервером ресурсов во время ожидания запроса на установление соединения от клиента, вызов функции accept блокирует поток сервера до тех пор, пока не поступит запрос. После получения запроса на установление соединения программное обеспечение протокола TCP выполняет трехэтапное квитирование для установления соединения. Как только будет выполнено квитирование и система распределит новый сокет для входящего запроса на установление соединения, функция accept возвратит дескриптор нового сокета, что позволит серверу продолжить работу. Если запрос на установление соединения не поступит, серверный процесс останется заблокированным на неопределенно долгое время в вызове функции accept. При получении каждого нового запроса на установление соединения в сервере для его обработки вызывается процедура TCPdaytimed. Работа процедуры TCPdaytimed в основном сводится к вызову системных функций time и ctime. Функция time возвращает 32-битовое целое число, которое обозначает текущее время в секундах с начала эпохи Linux. Библиотечная функция ctime принимает целочисленный параметр, обозначающий время в секундах с начала эпохи Linux, и возвращает адрес строки в кодировке ASCII, которая содержит значение времени и даты, представленное в формате, предназначенном для восприятия человеком. Сразу после получения значения времени и даты в виде строки ASCII сервер вызывает функцию write для отправки этой строки клиенту через соединение TCP. Как только произойдет возврат управления после вызова процедуры TCPdaytimed, главная процедура продолжит выполнение цикла и снова встретит оператор вызова функции accept. Функция accept заблокирует сервер до момента поступления следующего запроса. 10.6. Закрытие соединений После вывода ответа происходит возврат управления из процедуры TCPdaytimed. Вслед за этим в главной процедуре происходит явное закрытие сокета, через который проходил обмен данными с клиентом. Вызов функции close представляет собой требование выполнить корректное закрытие соединения. В частности, при выполнении этой функции программное обеспечение протокола TCP гарантирует, что все данные будут надежно доставлены клиенту и подтверждены, прежде чем соединение будет разорвано. Поэтому программист может вызвать функцию close, не беспокоясь о том, будут ли доставлены данные. Безусловно, принцип корректного останова, реализованный в протоколе TCP, означает, что вызов функции close может не быть выполнен немедленно: этот вызов блокируется до тех пор, пока программное обеспечение TCP сервера не получит ответ от программного обеспечения TCP клиента. Как только клиент подтвердит и получение всех данных, и поступление запроса на закрытие соединения, будет выполнен возврат после вызова функции close. 10.в. Закрытие соединений 157
10.7. Закрытие соединения и уязвимость сервера Прикладной протокол определяет применяемый в сервере способ управления соединениями TCP. В частности, прикладной протокол обычно регламентирует выбор способа завершения соединения. Например, вариант, предусматривающий закрытие соединения сервером, вполне может применяться в случае протокола DAYTIME, поскольку сервер имеет информацию о том, закончилась ли передача данных. В приложениях, предусматривающих более сложные сценарии взаимодействия между сервером и клиентом, нельзя допускать закрытия соединения с сервером сразу после обработки одного запроса, поскольку в процессе работы приходится ждать, не будет ли клиентом принято решение о передаче дополнительных сообщений с запросами. Например, рассмотрим сервер службы ECHO. В этом случае работой сервера управляет клиент, поскольку он определяет объем данных, для которого должен быть выполнен эхо-повтор. В связи с тем, что сервер должен обрабатывать произвольные объемы данных, он не может закрыть соединение после приема и передачи только одного запроса и ответа. Поэтому сигнал о завершении должен передать клиент, чтобы сервер имел информацию о том, когда нужно закрыть соединение. Предоставление клиентам возможности устанавливать продолжительность соединения может оказаться опасным, поскольку позволяет им управлять использованием ресурсов сервера. В частности, клиенты, выполняющие недопустимые действия, могут заставить сервер впустую расходовать такие ресурсы, как дескрипторы сокетов и соединения TCP. Может показаться, что сервер, приведенный в рассматриваемом примере, никогда не исчерпает ресурсы, поскольку он явно закрывает соединения. Но даже такая простая стратегия закрытия соединения может оказаться уязвимой по отношению к недопустимым действиям клиентов. Чтобы пЪнять, с чем это связано, напомним, что протокол TCP определяет продолжительность тайм-аута поддержания соединения, равную удвоенному значению максимального времени жизни сегмента (MSL — Maximum Segment Lifetime) после закрытия соединения. В течение этого тайм-аута программное обеспечение TCP проводит регистрацию всех пакетов, передаваемых через соединение, чтобы можно было безошибочно отбросить устаревшие пакеты, которые поступают с задержкой. Поэтому, если клиенты выполняют повторные запросы к серверу в быстрой последовательности, они могут исчерпать ресурсы сервера. Хотя программисты почти не имеют возможности вмешиваться в работу протоколов, они должны понимать, под влиянием каких особенностей протоколов распределенное программное обеспечение может стать уязвимым к нарушениям в работе сети, и попытаться уменьшить такую уязвимость при проектировании серверов. 10.8. Резюме Последовательный сервер с установлением логического соединения выполняет один проход по циклу при обработке каждого соединения. Сервер остается заблокированным в вызове функции accept до момента поступления запроса на установление соединения от клиента. Как только программное обеспечение базового протокола установит новое соединение и создаст новый сокет, вызов функции accept возвращает дескриптор сокета и сервер получает возможность продолжить выполнение. Как было отмечено в главе 7, в протоколе DAYTIME для активизации действий сервера по формированию и передаче ответа используется сам факт появления запроса на установление соединения. Клиент не должен передавать запрос, поскольку сервер отвечает сразу после обнаружения нового запроса на установление соединения. Для формирования ответа сервер получает текущее значение времени от операционной системы, представляет эту информацию в виде строки, 158 Глава 10. Последовательные серверы с установлением логического соединения...
подходящей для восприятия человеком, а затем возвращает ответ клиенту. В этом примере сервера сокет, соответствующий отдельному соединению, закрывается после передачи ответа. В таком случае принцип организации взаимодействия, предусматривающий немедленное закрытие соединения, вполне приемлем, т.к. служба DAYTIME допускает передачу только одного ответа для каждого соединения. Серверы, позволяющие передавать несколько запросов через одно соединение, должны ждать, пока клиент не закроет соединение. Материал для дальнейшего изучения Протокол DAYTIME, используемый в этой главе, описан в документе [126]. Упражнения 10.1. Должен ли процесс иметь особые привилегии для вызова на выполнение сервера DAYTIME на локальном компьютере? Должен ли он иметь особые привилегии для вызова на выполнение клиента DAYTIME? 10.2. Каково основное преимущество использования для активизации ответа от сервера самого факта появления запроса на установление соединения? В чем состоит основной недостаток? 10.3. Некоторые серверы DAYTIME передают в конце строки текста сочетание двух символов: символа возврата каретки (CR — Carriage Return) и перевода строки (LF — LineFeed). Откорректируйте пример серверной программы, чтобы в ней в конце строки передавались символы CR-LF, а не просто символ LF. Какими символами должны оканчиваться строки согласно стандарту протокола DAYTIME? 10.4. Программное обеспечение TCP обычно распределяет очередь постоянной длины для дополнительных запросов на установление соединения, поступающих в то время, пока сервер занят, и позволяет серверу устанавливать размер очереди во время вызова функции listen. Какова длина очереди, предоставляемой программным обеспечением TCP вашего компьютера? Какая длина очереди может быть указана сервером при вызове функции listen? 10.5. Откорректируйте пример кода сервера, приведенный в файле TCPdaytimed.c, таким образом, чтобы он явно не закрывал соединение после передачи ответа. Будет ли эта программа по-прежнему работать правильно? Объясните ваш ответ. 10.6. Сравните сервер с установлением логического соединения, который после передачи ответа явно закрывает каждое соединение, с сервером, позволяющим клиенту держать соединение открытым неопределенно долгое время перед его закрытием. Каковы преимущества и недостатки каждого из этих подходов? 10.7. Предположим, что в программном обеспечении TCP используется тайм-аут соединения, равный 4 минутам (т.е. в нем в течение 4 минут хранится информация о соединении после закрытия). Если сервер DAYTIME работает в системе, имеющей 100 записей для регистрации информации о соединении TCP, то какова максимальная скорость, с которой сервер может обрабатывать запросы без исчерпания числа доступных записей? Материал для дальнейшего изучения 159
11 Параллельные серверы с установлением логического соединения (TCP) 11.1. Введение В предыдущей главе показан пример применения транспортного протокола с установлением логического соединения в последовательном сервере. В настоящей главе приведен пример параллельного сервера, в котором используется транспортный протокол с установлением логического соединения. Этот пример сервера соответствует алгоритму 8.41. Такого проекта чаще всего придерживаются программисты, разрабатывая параллельные серверы TCP. Этот сервер при обеспечении параллельного формирования ответов на запросы опирается на поддержку параллельного выполнения процессов операционной системой. Системный администратор предусматривает автоматический запуск ведущего серверного процесса во время начальной загрузки системы. Ведущий сервер функционирует неопределенно долгое время, ожидая поступления новых запросов на установление соединения от клиентов. Ведущий поток создает новый ведомый поток для обработки запросов каждого нового соединения и предоставляет каждому ведомому потоку возможность взять на себя весь обмен данными с клиентом. Как было описано в главе 8, чаще всего применяются две основные реализации алгоритма 8.4. В настоящей главе рассматривается реализация, в которой используется несколько однопотоко- вых процессов, а в следующей главе описана реализация с использованием нескольких потоков в одном процессе и дано сравнение этих двух реализаций. 11.2. Служба ECHO Рассмотрим службу ECHO, описанную в главе 7. Клиент открывает соединение с сервером, а затем повторно передает данные через соединение и принимает эхо- повтор этих данных, возвращенный сервером. Сервер ECHO отвечает каждому клиенту. Он принимает запрос на установление соединения, получает данные из соединения, а затем посылает назад те же данные, которые были им приняты. Чтобы дать возможность клиенту передавать произвольные объемы данных, сервер не выполняет чтение всей входной информации до начала отправки ответа. Вместо этого сервер чередует прием и передачу. После поступления нового запроса Описание алгоритма 8.4 приведено на стр. 136.
на установление соединения сервер входит в цикл. При каждом проходе по циклу сервер сначала читает данные, поступающие из соединения, а затем снова пишет эти данные в то же соединение. Сервер повторно выполняет эти операции до тех пор, пока не встретит признак конца файла, после чего он закрывает соединение. 11.3. Сравнение последовательных и параллельных реализаций Последовательная реализация сервера службы ECHO может оказаться неудовлетворительной, поскольку клиенты будут вынуждены ждать завершения обработки всех предыдущих запросов на установление соединения. Если клиент решит передать большие объемы данных (например, несколько мегабайт), последовательный сервер отложит обслуживание всех других клиентов до тех пор, пока не выполнит этот запрос. Параллельная реализация сервера службы ECHO дает возможность обойтись без продолжительных задержек, т.к. не позволяет одному клиенту захватить все ресурсы. Вместо этого параллельный сервер поддерживает обмен данными сразу с несколькими клиентами для того, чтобы их запросы выполнялись одновременно. Поэтому, с точки зрения клиента, параллельный сервер обеспечивает лучшее наблюдаемое время отклика по сравнению с последовательным сервером. 11.4. Схема организации процессов На рис. 11.1 показана схема организации процессов параллельного сервера с установлением логического соединения, в котором используются однопотоковые процессы. Как показано на этом рисунке, ведущий процесс непосредственно не взаимодействует с клиентами. По существу, в его задачу входит просто ожидание поступления очередного запроса на установление соединения через общепринятый порт. После поступления такого запроса система возвращает новый дескриптор сокета, предназначенный для использования с этим соединением. Ведущий процесс создает ведомый процесс для обслуживания соединения и предоставляет возможность ведомому процессу работать параллельно с ним. В любое время в сервере функционирует один ведущий процесс, а число ведомых процессов может составлять от нуля и более. Ведущий процесс сервера принимает каждый входящий запрос на установление соединения и создает ведомый процесс для его обслуживания. Для получения очередного запроса на установление соединения из общепринятого порта используется блокирующий вызов функции accept, что позволяет предотвратить бесполезное расходование ресурсов процессора во время ожидания очередных запросов на установление соединения в ведущем процессе сервера. Поэтому, в отличие от последовательного сервера, описанного в главе 10, ведущий процесс сервера в параллельном сервере основную часть времени проводит в состоянии, заблокированном в вызове функции accept. После поступления запроса на установление соединения выполняется возврат управления функцией accept, а ведущий процесс получает возможность продолжить выполнение. Ведущий процесс создает ведомый процесс для обработки запроса и снова вызывает функцию accept. В этом вызове ведущий процесс в очередной раз блокируется до поступления еще одного запроса на установление соединения. 162 Глава 11. Параллельные серверы с установлением логического соединения (TCP)
Сокет Сокеты для запросов для отдельных на установление соединений соединения Серверные прикладные процессы (или потоки) Операционная система Рис. 11.1. Схема организации процессов параллельного сервера с установлением логического соединения, в котором используются однопотоковые процессы 11.5. Пример параллельного сервера ECHO Код сервера ECHO, в котором используются параллельные процессы для одновременного обслуживания нескольких клиентов, приведен в файле TCPechod.c. /* TCPechod.c - главная процедура TCPechod */ tdefine finclude finclude tinclude finclude tinclude finclude finclude finclude JJSE_BSD <sys/types.h> <sys/signal.h> <sys/socket.h> <sys/time.h> <sys/resource.h> <sys/wait.h> <sys/errno.h> <netinet/in.h> finclude <unistd.h> tinclude <stdlib.h> finclude <stdio.h> finclude <string.h> tdefine QLEN 32 tdefine BUFSIZE 4096 /* Максимальная длина очереди соединений */ extern int errno; void reaper(int); int TCPechod(int fd); int errexit(const char *format, .)? 11.5. Пример параллельного сервера ECHO 163
int passiveTCP(const char *servicef int qlen); /* * Главная процедура - параллельный сервер TCP для службы ECHO *..... . .... ... ... . ........... . .-.-...-.......-. */ int main(int argc, char *argv[]) { char *service = "echo"; /* Имя службы или номер порта */ struct sockaddrJLn fsin; /* Адрес клиента */ unsigned int alen; /* Длина адреса клиента */ int msock; /* Ведущий сокет сервера */ int ssock; /* Ведомый сокет сервера */ switch (argc) { case 1: break; case 2: service = argv[l]; break; default: errexit("usage: TCPechod [port]\n"); } msock - passiveTCP(service, QLEN); (void) signal(SIGCHLD, reaper); while A) { alen = sizeof(fsin); ssock = accept(msock, (struct sockaddr *)&fsin, &alen); if (ssock <0) { if (errno == EINTR) continue; errexit("accept: %s\n", strerror(errno)); } switch (fork()) { case 0: /* Дочерний процесс */ (void) close(msock); exit(TCPechod(ssock)); default: /* Родительский процесс */ (void) close(ssock); break; case -1: errexit("fork: %s\n", strerror(errno)); } } } /* * Процедура TCPechod - выполняет эхо-повтор данных до обнаружения признака * конца файла *......... ........ -.-....-.—.-—_-_--..-..—--..--.-...——. ._ */ 164 Глава 11. Параллельные серверы с установлением логического соединения (TCP)
int TCPechodfint fd) { char buf[BUFSIZ]; int cc; while (cc = read(fd, buf, sizeof buf)) { if (cc <0) errexit("echo read: %s\n", strerror(errno)); if (write(fd, buf, cc) <0) errexit("echo write: %s\n", strerror(errno)); } return 0? } /* * Процедура reaper - убирает записи дочерних процессов-зомби из системных * таблиц * . «. « -™ - */ void reaper(int sig) { int status; while (wait3(&status, WNOHANG, (struct rusage *H)>= 0) /* Пустое тело цикла */; } Как показано в этом примере, вызовы функций, обеспечивающих параллельную работу, занимают лишь небольшую часть кода. Ведущий процесс сервера начинает свое выполнение в главной процедуре. После проверки параметров в ведущем процессе сервера вызывается процедура passiveTCP, которая создает пассивный сокет для общепринятого порта протокола, а затем входит в бесконечный цикл. Символическая константа QLEN, передаваемая в качестве второго параметра процедуры passiveTCP, задает максимальное число дополнительных входящих запросов на установление соединений TCP, которые будут поставлены в очередь, пока сервер занимается обслуживанием текущего соединения2. При каждом проходе по циклу ведущий сервер вызывает функцию accept для перехода в состояние ожидания запроса на установление соединения от клиента. Как и в последовательном сервере, этот вызов блокируется до поступления запроса. После получения программным обеспечением базового протокола TCP запроса на установление соединения операционная система создает сокет для нового соединения и вызов функции accept возвращает дескриптор этого сокета. После возврата управления из функции accept ведущий процесс сервера создает ведомый процесс для обслуживания соединения. Для этого ведущий процесс вызывает функцию fork, чтобы разделиться на два процесса . Поток во вновь созданном дочернем процессе вначале закрывает сокет ведущего процесса, 2 В системе Linux максимально допустимая длина очереди задается константой SOMAXCONN. о Напомним, что процесс, созданный функцией fork, содержит один поток выполнения; значение, возвращаемое после вызова этой функции, позволяет отличить друг от друга первоначальный родительский процесс и вновь созданный дочерний процесс. 11.5. Пример параллельного сервера ECHO 165
а затем вызывает процедуру TCPechod для обслуживания соединения. Поток в родительском процессе закрывает сокет, который был создан для обслуживания нового соединения, и продолжает выполнение бесконечного цикла. При следующем проходе по циклу ведущий процесс после вызова функции accept снова переходит в состояние ожидания очередных запросов на установление соединения. Следует отметить, что и первоначальный, и новые процессы имеют доступ к открытым сокетам после вызова функции fork() и что они оба должны закрыть один из этих сокетов, после чего система освобождает связанный с ним ресурс. Поэтому этот сокет закрывается только в ведущем процессе после вызова функции close потоком ведущего процесса для закрытия сокета нового соединения. Аналогичным образом, когда поток в ведомом процессе вызывает функцию close для закрытия сокета ведущего процесса, этот сокет закрывается только в ведомом процессе. Ведомый процесс продолжает получать доступ к сокету нового соединения до тех пор, пока не завершит свою работу, а ведущий сервер продолжает иметь доступ к сокету, который соответствует общепринятому порту. После закрытия сокета ведущего процесса ведомый процесс вызывает процедуру TCPechod, которая предоставляет услуги службы ECHO для одного соединения. Процедура TCPechod состоит из цикла, который повторно вызывает функцию read для приема данных из соединения, а затем вызывает функцию write для обратной передачи тех же данных через это соединение. Как правило, функция read возвращает (положительное) число считанных байтов. Она возвращает значение меньше нуля, если произошла ошибка (например, если разорвано сетевое соединение между клиентом и сервером), или нуль, если обнаружен признак конца файла (т.е. из этого сокета нельзя больше получать данные). Аналогичным образом, функция write 'обычно возвращает число переданных символов, но возвращает значение меньше нуля при возникновении ошибки. В ведомом процессе проверяются коды возврата и используется функция errexit для вывода сообщения, если возникла ошибка. Процедура TCPechod возвращает нуль, если ей удалось выполнить эхо-повтор всех данных без ошибок. После возврата управления процедурой TCPechod ведомый процесс использует возвращенное значение в качестве параметра вызова функции exit. Система Linux интерпретирует вызов функции exit как требование завершить процесс и использует параметр вызова этой функции как код завершения процесса. В соответствии с общепринятым соглашением, в процессе для обозначения нормального завершения используется код завершения нуль. Это означает, что после предоставления услуг службы ECHO ведомый процесс окончился нормально. После прекращения работы ведомого процесса операционная система автоматически закрывает все его открытые дескрипторы, в том числе дескриптор соединения TCP. 11.6. Удаление информации о сбойных процессах из системных таблиц Поскольку в параллельных серверах для динамического создания процессов используется функция fork, они являются потенциальным источником проблемы не полностью завершившихся процессов (процессов, информация о которых остается в системных таблицах). В системе Linux эта проблема решается путем передачи специального сигнала родительскому процессу после завершения работы каждого дочернего процесса. Завершившийся процесс остается в виде так называемого процесса-зомби до тех пор, пока родительским процессом не будет выполнен системный вызов wait3. Для полного завершения дочернего процесса (т.е. 166 Глава 11. Параллельные серверы с установлением логического соединения (TCP)
для уничтожения процесса-зомби) в рассматриваемом примере сервера ECHO перехватывается сигнал завершения дочернего процесса и выполняется функция обработки этого сигнала. Операционной системе дается указание, что для ведущего процесса сервера при получении каждого сигнала о завершении работы дочернего процесса (сигнал SIGCHLD) должна быть выполнена функция reaper, в виде следующего вызова: signal(SIGCHLD, reaper); После вызова функции signal система автоматически вызывает функцию reaper при получении процессом сервера каждого сигнала SIGCHLD. Функция reaper вызывает системную функцию wait3 для полного завершения дочернего процесса, закончившего свою работу. Функция wait3 остается заблокированной до тех пор, пока не произойдет завершение работы одного или нескольких дочерних процессов (по любой причине). Эта функция возвращает значение структуры status, которую можно проанализировать для получения дополнительной информации о завершившемся процессе. Поскольку данная программа вызывает функцию wait3 при получении сигнала SIGCHLD, вызов этой функции всегда происходит только после завершения работы дочернего процесса. Для предотвращения возникновения в сервере тупиковой ситуации в случае ошибочного вызова в программе используется параметр WN0HANG, который указывает, что функция wait3 не должна блокироваться в ожидании завершения какого-либо процесса. Вместо этого возврат управления после вызова происходит немедленно, даже если не произошел выход из какого-либо процесса. 11.7. Резюме Серверы с установлением логического соединения обеспечивают параллельную работу, позволяя сразу нескольким клиентам обмениваться данными с сервером. В настоящей главе описана одна из самых простых реализаций, в которой для создания нового ведомого процесса после получения каждого запроса на установление соединения используется функция fork. Поток в ведущем процессе никогда не вступает во взаимодействие с клиентами; он просто принимает запросы на установление соединения и создает ведомые процессы для обслуживания каждого из них. Работа каждого ведомого процесса начинается в главной процедуре сразу после вызова функции fork. Ведущий процесс закрывает свою копию дескриптора сокета для нового соединения, а ведомый процесс закрывает свою копию дескриптора сокета ведущего процесса. Соединение с клиентом прекращается после выхода из ведомого процесса в связи с тем, что операционная система закрывает копию сокета ведомого процесса. Материал для дальнейшего изучения Протокол ECHO, используемый в примере сервера TCP, определен в документе [120]. Упражнения 11.1. Доработайте сервер ECHO таким образом, чтобы он регистрировал в журнале время создания каждого ведомого процесса и время завершения работы ведомого процесса. Сколько клиентских программ должно быть вызвано на выполнение, прежде чем обнаружится перекрытие между записями для разных ведомых процессов? 11.7. Резюме 167
11.2. Сколько клиентов может одновременно обратиться к параллельному серверу, приведенному в рассматриваемом примере, прежде чем сервер будет вынужден отказать в обслуживании какому-либо клиенту? Сколько клиентов может обратиться к последовательному серверу, описанному в главе 10, прежде чем клиенты начнут получать отказ в предоставлении обслуживания? 11.3. Разработайте последовательную реализацию сервера ECHO. Проведите эксперименты для определения того, сможет ли человек ощутить разницу во времени отклика при работе с параллельной и последовательной версиями. 11.4. Доработайте этот пример сервера, чтобы в нем процедура TCPechod явно закрывала соединение, прежде чем выполнить возврат. Объясните, почему явный вызов функции close позволяет упростить сопровождение кода. 11.5. Разработайте модифицированную версию сервера ECHO, в которой создается новый поток выполнения в том же процессе, а не создается новый процесс, и измерьте разницу во времени выполнения. 11.6. Какие сокеты должны быть закрыты ведущими и ведомыми потоками в модифицированной версии сервера, разработанной по условиям предыдущего упражнения? Почему? 168 Глава 11. Параллельные серверы с установлением логического соединения (TCP)
12 Применение потоков для обеспечения параллельной работы (TCP) 12.1. Введение В предыдущей главе рассматривались параллельные серверы с установлением логического соединения и использовался мультипроцессный проект для иллюстрации одного из способов организации параллельной работы. В настоящей главе показано, как могут использоваться потоки для обеспечения параллельной работы. В ней описаны общие характеристики потоков, их преимущества и недостатки, а также приведен пример параллельного сервера с установлением логического соединения, реализованного по принципу создания нескольких потоков в одном процессе. Этот пример сервера соответствует алгоритму 8.41, который также служил основой проекта, применяемого в предыдущей главе. Как и в проекте реализации параллельного сервера, в котором используется несколько процессов, системный администратор предусматривает создание одного ведущего потока во время начальной загрузки системы. Ведущий поток работает неопределенно долгое время, ожидая поступления от клиентов новых запросов на установление соединения. Ведущий поток создает новый ведомый поток для обслуживания каждого соединения с клиентом, а ведомый поток выполняет все операции обмена данными с этим клиентом. После того как ведомый поток заканчивает отвечать на запросы определенного клиента, его работа завершается. 12.2. Краткий обзор потоков Linux Как было описано выше, поток выполнения представляет собой один из принципов организации отдельных вычислений, а один процесс может содержать от одного и более потоков. Потоки, применяемые в системе Linux, соответствуют стандарту потоков POSIX, известному как стандарт 1003.1с; ему соответствует большинство других систем UNIX. Ниже перечислены характеристики потоков Linux. ¦ Динамическое создание. Новый поток может быть создан в любое время путем вызова функции pthread_create2. Операционная система ограничива- Описание алгоритма 8.4 приведено на стр. 136. 2 Для создания процесса или потока в системе Linux используется базовая функция clone. Функция clone не только лежит в основе реализации функций fork и pthread_create, но также позволяет создавать промежуточные формы параллельной организации работы, которые обеспечивают большую степень разделения ресурсов, чем процессы, но меньшие, чем потоки.
ет максимально допустимое количество параллельных потоков, равно как и максимальное количество параллельных процессов. ¦ Параллельное выполнение. При использовании однопроцессорного компьютера лишь создается впечатление, что все потоки выполняются одновременно. На многопроцессорном компьютере действительно выполняется сразу несколько потоков, поскольку операционная система может назначить для каждого из них по одному процессору. ¦ Многозадачный режим с вытеснением. Операционная система распределяет ресурсы процессора по потокам. В том случае, если число активных потоков превышает число доступных процессоров (например, на однопроцессорном компьютере), система передает ресурсы процессора от одного потока к другому автоматически, разрешая одному потоку выполняться в течение короткого времени перед передачей ресурсов другому. API- интерфейс управления потоками включает также функцию sched_yield, которую поток может вызвать, чтобы добровольно отказаться от ресурсов процессора, прежде чем закончится выделенный ему квант времени. ¦ Приватные локальные переменные. Каждый поток имеет свой собственный, приватный стек. Этот стек используется для распределения локальных переменных и хранения записей активизации процедур (т.е. информации о вызовах процедур). ¦ Разделяемые глобальные переменные. Все потоки процесса разделяют единый набор глобальных переменных. ¦ Разделяемые дескрипторы файлов. Все потоки процесса разделяют единый набор дескрипторов файлов. ¦ Функции координации и синхронизации. API-интерфейс управления потоками включает функции, которые могут использоваться потоками для координации и синхронизации выполнения. 12.3. Преимущества применения потоков Многопотоковые процессы обладают двумя основными преимуществами по сравнению с однопотоковыми процессами: более высокая эффективность и разделяемая память. Повышение эффективности связано с уменьшением издержек на переключение контекста. Переключением контекста называются действия, выполняемые операционной системой при передаче ресурсов процессора от одного потока выполнения к другому. При переключении с одного потока на другой операционная система должна сохранить в памяти состояние предыдущего потока (например, значения регистров) и восстановить состояние следующего потока. Потоки в одном и том же процессе разделяют значительную часть информации о состоянии процесса, поэтому операционной системе приходится выполнять меньший объем работы по сохранению и восстановлению состояния. Вследствие этого переключение с одного потока на другой в одном и том же процессе происходит быстрее по сравнению с переключением между двумя потоками в разных процессах. В частности, поскольку потоки одного и того же процесса разделяют адресное пространство памяти, то переключение между потоками процесса означает, что операционная система не должна менять отображение виртуальной памяти на физическую. Второе преимущество потоков, т.е. разделяемая память, вероятно, является для программистов еще более важным, чем повышение эффективности. Потоки упрощают разработку параллельных серверов, в которых все копии сервера должны взаимодействовать друг с другом или обращаться к разделяемым элементам данных. Кроме того, потоки упрощают разработку систем контроля и 170 Глава 12. Применение потоков для обеспечения параллельной работы (TCP)
управления. В частности, поскольку ведомые потоки в сервере совместно используют память, они могут записывать в глобальную память статистическую информацию, что позволяет контролирующему потоку формировать отчеты об активности ведомых потоков сервера для системного администратора. Пример такого текущего контроля будет рассмотрен ниже. 12.4. Недостатки потоков Хотя потоки имеют свои преимущества над однопотоковыми процессами, они не лишены также определенных недостатков. По нашему мнению, один из наиболее важных недостатков связан с тем, что потоки не только разделяют память, но и имеют общее состояние процесса, поэтому действия, выполненные одним потоком, могут повлиять на другие потоки в том же процессе/Например, если два потока попытаются одновременно обратиться к одной и той же переменной, они могут помешать друг другу. Как будет описано ниже, API-интерфейс потоков предоставляет функции, которые могут использоваться потоками для координации работы. Однако многие библиотечные функции, возвращающие указатели на статические элементы данных, не являются безопасными с точки зрения потоков, а это означает, что результаты вызова таких функций могут оказаться непредсказуемыми. Например, рассмотрим библиотечную функцию gethostbyname, используемую в приложениях для преобразования доменного имени в IP-адрес. Если два потока вызовут функцию gethostbyname одновременно, то ответ на один поисковый запрос может быть перекрыт ответом на другой. Поэтому если несколько потоков вызывают определенную библиотечную функцию, они должны координировать свою работу для обеспечения того, чтобы в любое время ее вызов выполнял только один поток. Еще один недостаток связан с отсутствием надежности. Если одна из параллельно работающих копий однопотокового сервера вызовет серьезную ошибку (например, в ней будет выполнена ссылка на недопустимую область памяти), то операционная система завершит только тот процесс, который вызвал ошибку. С другой стороны, если серьезная ошибка будет вызвана одним из потоков многопотокового сервера, то операционная система завершит весь процесс. 12.5. Дескрипторы, задержка и выход Иногда трудно понять, в чем заключается различие между потоками и процессами, особенно тем программистам, которые привыкли работать с однопотоковыми процессами. К тому же такие статические ресурсы, как глобальные переменные, а также многие динамически распределяемые ресурсы, связаны с процессом, а не с отдельным потоком. Например, поскольку дескрипторы файла принадлежат процессу, то после открытия файла любым потоком тот же дескриптор для доступа к этому файлу может использоваться другими потоками процесса. Еще более важно то, что при закрытии дескриптора одним из потоков, этот дескриптор закрывается для всего процесса (т.е. другие потоки в процессе больше не имеют доступа к дескриптору). Аналогичным образом, хотя некоторые функции операционной системы оказывают влияние только на вызывающий поток, другие влияют на весь процесс. Например, если один из потоков выполнил вызов блокирующейся функции ввода/вывода (например, функции read), то блокируется только один поток. Однако если любой из потоков вызовет функцию exit, немедленно происходит завершение всего процесса. Это означает, что функция exit связана с процессом, а не с отдельным потоком. 12.4. Недостатки потоков 171
Хотя некоторые системные функции оказывают влияние только на вызывающий поток, другие функции, такие как exit, влияют на весь процесс. 12.6. Завершение работы потока Если в потоке нельзя вызвать функцию exit, не завершив всего процесса, то как может поток прекратить свою работу, не останавливая выполнение других потоков в процессе? Для этого могут применяться два способа. Во-первых, рабр- та потока прекращается, если он выполняет возврат из своей процедуры верхнего уровня (т.е. процедуры, с которой началось его выполнение). Во-вторых, для завершения работы потока может быть вызвана функция pthread_exit. API-интерфейс управления потоками включает функции, которые действуют в одном потоке. В частности, для выхода из потока можно вызвать функцию pthread_exit (или выполнить возврат из его процедуры верхнего уровня), не влияя на другие потоки процесса. 12.7. Координация и синхронизация работы потоков Синхронизация работы потоков необходима в связи с тем, что потоки работают параллельно, но каждый поток характеризуется разной скоростью выполнения. Например, если поток заблокирован при выполнении операции ввода/вывода, то продолжительность задержки зависит от работы операционной системы и аппаратных средств; невозможно предугадать, какие потоки будут работать на протяжении этой задержки или какое число машинных команд они выполнят. Поэтому если перед программистом стоит задача координации выполнения потоков, то он должен предусмотреть явные вызовы функций синхронизации. В системе Linux предусмотрено три механизма синхронизации: мьютексы, семафоры и условные переменные. 12.7.1. Мьютекс В потоках мьютексы используются для обеспечения взаимно исключающего доступа к разделяемому элементу данных. Мьютекс инициализируется динамически путем вызова функции pthread_mutex_init3; программист предусматривает применение отдельного мьютекса для каждого элемента данных, который должен быть защищен. Сразу после инициализации мьютекса поток вызывает функцию pthread_mutex_lock перед использованием элемента данных и функцию pthreadjnutexjmlock по окончании его использования. Эти два вызова позволяют одновременно обращаться к элементу данных только одному потоку. Первый поток, вызвавший функцию pthreadjnutex_lock с конкретным мьютексом, продолжает свою работу без задержки. Однако система блокирует каждый следующий поток, вызывающий функцию pthread_mutex_lock с той же блокировкой. В конечном итоге, по окончании работы с данными первый поток вызывает функцию pthread_ mutexjinlock, разрешив другим потокам использовать этот элемент данных. Если другие потоки были заблокированы мьютексом во время вызова функции pthread_ mutexjinlock, операционная система разблокирует один из ожидающих потоков. Взаимоисключающая блокировка может быть инициализирована статически путем присвоения ей константы PTHREAD_MUTEX_INITIALIZER. 172 Глава 12. Применение потоков для обеспечения параллельной работы (TCP)
Мьютекс — это механизм, используемый потоками для синхронизации работы. Каждый мьютекс связан с элементом данных; только один поток в любое время имеет доступ к данным, связанным с определенным мъютексом. 12.7.2. Семафор Семафор (иногда называемый семафором-счетчиком) представляет собой механизм синхронизации, обобщающий мьютекс и применяемый в том случае, если доступно N копий ресурса. Мьютекс предоставляет возможность пройти критический участок только одному потоку, а семафор позволяет работать одновременно вплоть до # потокам. Как и мьютекс, семафор инициализируется динамически. Для инициализации семафора применяется функция sem_init;.B одном из ее параметров должен быть указан начальный счетчик N. Сразу после инициализации семафора поток вызывает функцию sem_wait до начала использования одной копии ресурса, а возвращая копию для использования другими потоками, вызывает функцию semjpost. Функция sem_wait может быть беспрепятственно вызвана потоками, число которых составляет N; после ее вызова каждый из них продолжает свое выполнение. Однако если к семафору попытаются обратиться дополнительные потоки, они будут заблокированы. Потоки остаются заблокированными семафором до тех пор, пока один из выполняющихся потоков не вызовет функцию semjpost для освобождения этого семафора, и с этого момента будет разрешено приступить к работе одному из заблокированных потоков. Семафор — это механизм синхронизации потоков, который является обобщением механизма мьютекса. В любой момент к ресурсу, защищенному семафором, который был инициализирован со счетчиком N, могут обращаться потоки, число которых не превышает N. 12.7.3. Условные переменные Наиболее сложным и затруднительным для понимания механизмом синхронизации потоков являются условные переменные. По существу, условные переменные требуются только в ситуации, при которой одновременно выполняются следующие два условия: ¦ ряд потоков использует мьютекс для получения взаимно исключающего доступа к некоторому ресурсу; ¦ сразу после приобретения ресурса поток должен перейти к ожиданию возникновения определенного события. Без применения условных переменных в программах, которые сталкиваются с описанной выше ситуацией, приходится использовать своего рода активное ожидание, в котором поток повторно приобретает мьютекс, проверяет условие, а затем освобождает мьютекс. Условная переменная обеспечивает значительное повышение эффективности ожидания, поскольку позволяет потоку выполнять оба эти действия атомарно (при атомарном выполнении какого-либо действия исключена возможность прервать это действие). Перед блокировкой на условной переменной поток приобретает мьютекс. При вызове потоком функции pthread_cond_wait для перехода в состояние ожидания изменения условной переменной в потоке должна быть указана и условная переменная, изменения которой он ожидает, и захваченный им мьютекс. Операционная система освобождает мьютекс, захваченный потоком, и одновременно с этим блокирует поток в ожидании изменения условной переменной. 12.7. Координация и синхронизация работы потоков 173
После выполнения функции pthread_cond_wait поток блокируется на указанной условной переменной до тех пор, пока какой-то другой поток не передаст сигнал об изменении этой переменной4. Для передачи сигнала об изменении условной переменной могут применяться два способа; различие между ними определяется тем, что многочисленные ожидающие потоки могут обрабатываться по- разному. Функция pthread_cond_signal позволяет продолжить работу только одному потоку, даже если сигнала об изменении состояния переменной ожидают несколько потоков, а функция pthread_cond_broadcast позволяет продолжить работу всем потокам, заблокированным на этой переменной. Разрешая одному потоку продолжить работу, операционная система одновременно разблокирует этот поток и позволяет ему снова приобрести мьютекс, которым он обладал, прежде чем заблокироваться на условной переменной, об изменении состояния которой получен сигнал. Другими словами, ожидание изменения условной переменной равносильно отказу на время от мьютекса, а затем автоматическому повторному приобретению этого мьютекса после поступления сигнала об изменении условной переменной. В результате блокировка потока на условной переменной не исключает для других потоков возможности пройти через критический участок, поскольку теперь мьютекс могут приобрести другие потоки. Условная переменная — это механизм синхронизации потоков, используемый в сочетании с мъютексом. Ожидая изменения состояния условной переменной, поток на время отказывается от своего права владения мъютексом и приобретает мьютекс повторно после получения сигнала об изменении состояния условной переменной. 12.8. Пример сервера, реализованного с применением потоков Ниже приведен пример использования потоков в серверной программе. Чтобы иметь возможность провести сравнение с реализацией, в которой используются несколько процессов, был выбран вариант службы ECHO, описанный в предыдущей главе. Многопотоковый сервер, рассматриваемый в качестве примера в настоящей главе, действует по такому же алгоритму параллельной работы с установлением логического соединения; единственное различие заключается в реализации. После инициализации ведущий поток, выполняющий основную программу, входит в бесконечный цикл, в котором он снова и снова блокируется в вызове функции accept, ожидая поступления запроса на установление соединения TCP. После получения входящего запроса в ведущем потоке вызывается функция pthread_create для создания нового потока, предназначенного для обслуживания соединения. Ведущий поток продолжает работать в цикле и ожидает поступления очередного запроса на установление соединения. Во вновь созданном потоке выполняется процедура TCPechod, в основе которой лежит цикл, выполняющий прием данных из соединения TCP и передачу тех же данных назад отправителю. Код сервера содержится в файле TCPmtechod.c. /* TCPmtechod.c - главная процедура TCPechod, prstats */ tinclude <unistd.h> Поток, ожидающий сигнала об изменении условной переменной, может также быть разблокированным, если какой-либо сигнал будет обработан потоком. Поэтому на практике программисты заключают вызов функции pthread_cond__wait в цикл, в котором эта функция продолжает вызываться до тех пор, пока условие, ожидаемое поток, остается ложным. 174 Глава 12. Применение потоков для обеспечения параллельной работы (TCP)
tinclude <stdlib.h> ¦include <stdio.h> ¦include <string.h> ¦include <pthread.h> ¦include ¦include ¦include ¦include ¦include ¦include ¦include ¦include ¦define ¦define <sys/types.h> <sys/signal.h> <sys/socket.h> <sys/time.h> <sys/resource.h> <sys/wait.h> <sys/errno.h> <netinet/in.h> QLEN BUFSIZE 32 4096 ¦define INTERVAL 5 /* Максимальная длина очереди соединений */ /* Число секунд */ struct { pthread_mutex_t unsigned int unsigned int unsigned long unsigned long } stats; stjnutex; st_concount; st_contotal; st_contime; st bytecount; void prstats(void); int TCPechodjint fd); int errexitfconst char *format, ...)? int passiveTCPfconst char *service, int qlen); /* * Главная процедура * параллельный сервер TCP для службы ECHO */ int main(int argc, char *argv[]) { pthread__t th; pthread_attr_t ta; char *service = "echo"; struct sockaddr_in fsin; unsigned int alen; int msock; int ssock; /* Имя службы или номер порта */ /* Адрес клиента */ /* Длина адреса клиента */ /* Ведущий сокет сервера */ /* Ведомый сокет сервера */ switch (argc) { case 1: break; case 2: service = argv[l]; break; default: errexitf"usage: TCPechod [port]\n"); 12.8. Пример сервера, реализованного с применением потоков 17
msock * passiveTCP(service, QLEN); (void) pthread attr init(fita); (void) pthread~attr~setdetachstate(&ta, PTHREAD_CREATE_DETACHED); (void) pthreadjnutexjlnit(&stats.st_mutex, 0); if (pthread_create(&th, &ta, (void * (*)(void *))prstats, 0) <0) errexit("pthread_create(prstats): %s\n", strerror(errno)); while A) { alen = sizeof(fsin); ssock ¦ accept(msock, (struct sockaddr *)&fsin, &alen); if (ssock <0) { if (errno « EINTR) continue; errexit("accept: %s\n", strerror(errno)); } if (pthread_create(&th, &ta, (void * (*)(void *))TCPechod, (void *Jssock) <0) errexit("pthread create: %s\n", strerror(errno)); } } /* * Процедура TCPechod - выполняет эхо-повтор данных до обнаружения признака * конца файла */ int TCPechod(int fd) < time t start; char" buf[BUFSIZ]j int cc; start ¦ time@); (void) pthreadjnutexJLock(&stats.stjautex); stats.st_concount++;" (void) pthreadjnutex jinlock(&stats.st jnutex); while (cc ¦ read(fd,"buf, sizeof buf)" { if (cc <0) errexit("echo read: %s\n", strerror(errno)); if (write(fd, buf, cc) <0) errexit("echo write: %s\n", strerror(errno)); (void) pthread jnutex_lock(fistats.stjnutex); stats.stjaytecount +¦ cc; (void) pthread mutex unlock(&stats.st mutex); } (void) close(fd); (void) pthread jnutex J.ock(&stats.st jnutex); stats.etjsontime +¦ time@) - start! stats. st~concount—; stats.st~contotal++; (void) pthreadjnutexjinlock(&stats.st jnutex); return 0; 176 Глава 12. Применение потоков для обеспечения параллельной работы (TCP)
/*—-- - —— * Процедура prstats - выводит статистические данные о работе сервера *_______ __----_-____-^-_- __-_—__.——..__-.._..—___ —_———__ -- */ void prstats(void) { time_t now; while A) { (void) sleep(INTERVAL); (void) pthreadjnutex_lock(&stats.stjnutex); now ¦ time(O);"" (void) printf ("— %s", ctime(&now)); (void) printf("%-32s: %u\n", "Current connections", stats.st_concount); (void) printf("%-32s: %u\n", "Completed connections", stats.st_contotal)? if (stats.st contotal) { (void) printf("%-32s: %.2f (secs)\n", "Average complete connection time", (float)stats.st^contime / (float)stats.st~contotal); (void) printf("%-32s: %.2f\nH, "Average byte count", (float)stats.st^bytecount / (float)(stats.st_contotal + stats.st concount)); } (void) printf("%-32s: %lu\n\n\ "Total byte count", stats.st_bytecount); (void) pthreadjnutexjinlock(&stats.stjnutex); } 12.9. Текущий контроль и управление . В этом примере с серверной программой рассматривается механизм текущего контроля, отсутствовавший в предыдущих примерах, — монитор. Хотя здесь речь идет о простом мониторе, наглядно показано, как может использоваться монитором разделяемая память для взаимодействия с ведомыми потоками. Монитор реализован как отдельный поток, выполняющий процедуру prstats. В рассматриваемом примере кода в основе процедуры prstats лежит бесконечный цикл. При каждом проходе по циклу монитор выводит статистические данные о состоянии соединений, а затем переходит в состояние ожидания на число секунд, заданное константой INTERVAL. Вывод содержит данные о числе действующих соединений, завершенных соединений, общей продолжительности соединений и о среднем числе байтов, передаваемых через каждое соединение. Для взаимодействия потока монитора и ведомых потоков используется разделяемая глобальная структура данных. Каждый ведомый поток вносит в структуру stats информацию о своем соединении, а монитор извлекает эту информацию че- 12.9. Текущий контроль и управление 177
рез каждые INTERVAL секунд. Для обеспечения того, чтобы только один поток одновременно мог получить доступ к разделяемой структуре, в сервере используется мьютекс stats. stjnutex. Поток ожидает освобождения мьютекса, прежде чем обратиться к структуре, и освобождает его после использования этой структуры. В производственном сервере монитор может применяться для обеспечения более сложного взаимодействия с системным администратором. Например, вместо одного лишь вывода статистических данных, монитор может дополнительно предоставить администратору возможность вводить команды с клавиатуры. Тем самым монитор может выводить информацию по требованию и дать возможность администратору управлять сервером (например, динамически устанавливать или корректировать максимальный предел распараллеливания). 12.10. Резюме Параллельный сервер может быть реализован с использованием потоков в одном процессе. Основные преимущества потоков обусловлены меньшими издержками на переключение контекста и способностью использовать разделяемую память. Основной недостаток связан с повышением сложности задач программирования. В программе необходимо применять механизмы синхронизации доступа потоков к глобальным переменным и к некоторым библиотечным процедурам, а также учитывать, что такие системные функции, как exit, воздействуют на весь процесс, а не на отдельный поток. Материал для дальнейшего изучения Функции потоков описаны в оперативном руководстве, которое входит в поставку операционной системы Linux. Упражнения 12.1. Сравните многопотоковый параллельный сервер, рассматриваемый в этой главе, с реализацией сервера в предыдущей главе, в которой используется несколько однопотоковых процессов. Какой из этих двух серверов работает быстрее? Как зависит производительность сервера от числа параллельных соединений? 12.2. Изменится ли ответ на вопрос предыдущего упражнения, если будет исключен поток монитора? 12.3. Прочитайте литературу с описанием функции pthread_attr_init. Для чего она применяется? 12.4. Что произойдет с сервером, если цоток монитора будет подключен к клавиатуре и пользователь введет комбинацию клавиш <Ctrl+S> (т.е. остановит вывод)? 12.5. В примере кода монитора предусмотрен захват монитором мьютекса на то время, пока он форматирует и выводит статистические данные. Откорректируйте код таким образом, чтобы монитор удерживал мьютекс лишь на время, достаточное для копирования структуры stats. 12.6. В рассматриваемом примере программы монитора для периодического вывода статистических данных служит отдельный поток. Воспользуйтесь сигнальным механизмом Linux для реализации тех же функциональных средств. В чем состоят преимущества и недостатки каждого подхода? 12.7. Откорректируйте код монитора таким образом, чтобы он принимал команды с клавиатуры и отвечал на каждую из них. 178 Глава 12. Применение потоков для обеспечения параллельной работы (TCP)
13 Однопотоковые параллельные серверы (TCP) 13.1. Введение В предыдущих главах было показано, как функционирует большинство параллельных серверов с установлением логического соединения. В них для создания каждого отдельного процесса или потока, обслуживающего соединение, используются средства операционной системы. Это позволяет операционной системе распределять ресурсы процессора между потоками по принципу квантования времени. В настоящей главе рассматривается проект, который является очень интересным, но на первый взгляд неосуществимым; здесь описан сервер, который предоставляет клиентам возможность псевдопараллельного выполнения работы с использованием только одного потока управления1. В начале этой главы рассматриваются основные понятия, относящиеся к данному вопросу. В ней показано, почему такой подход является вполне реализуемым, и при каких условиях он может превзойти по своим характеристикам версию, основанную на использовании нескольких потоков. Затем в ней представлен пример с однопотоко- вым сервером. В этом примере показан один из способов использования вызовов системы Linux для поддержки сразу нескольких соединений. 13.2. Активизация обработки данных сервером В приложениях клиент/сервер, характеризующихся тем, что затраты на обеспечение ввода/вывода превышают затраты на подготовку ответа на запрос, в сервере может использоваться асинхронный ввод/вывод для организации псевдопараллельной работы клиентов. В основе этого подхода лежит простой принцип: необходимо предусмотреть, чтобы единственный поток выполнения в сервере держал открытыми соединения TCP с несколькими клиентами и обеспечивал обслуживание сервером того соединения, через которое в определенный момент поступают данные. Таким образом, сам факт поступления данных используется для активизации обработки данных сервером. Чтобы понять, почему такой подход является осуществимым, рассмотрим параллельный сервер службы ECHO, описанный в предыдущей главе. В этом примере для обеспечения параллельного выполнения кода создается отдельный ве- Следует учитывать различия между представленной здесь однопотоковой реализацией, в которой применяется только один поток выполнения в одном процессе, и реализацией, в которой используется несколько однопотоковых процессов (см. главу 11).
домый поток для обслуживания каждого нового соединения. С теоретической точки зрения, в основе организации работы сервера лежит использование механизма квантования времени операционной системы для разделения ресурсов процессора между потоками и, следовательно, между соединениями. Однако на практике в сервере службы ECHO квантование времени применяется редко. Внимательное изучение процесса работы параллельного сервера службы ECHO показывает, что чаще всего для активизации обработки используется сам факт поступления данных. В основе такого решения лежит изучение структуры потоков данных, проходящих по объединенной сети. Данные поступают на сервер в виде пульсирующего трафика, а не устойчивого потока, поскольку базовая объединенная сеть доставляет данные в отдельных пакетах. Неравномерность трафика еще больше возрастает в связи с тем, что клиенты, как правило, отправляют данные в виде крупных блоков, чтобы каждый из сформированных в результате сегментов TCP мог занять всю дейтаграмму IP. На сервере каждый ведомый поток основную часть времени проводит в состоянии, заблокированном в вызове функции read, ожидая поступления следующей порции данных. Сразу после поступления данных происходит возврат управления из функции read, и выполнение ведомого потока возобновляется. Ведомый поток вызывает функцию write для передачи данных обратно клиенту, а затем снова вызывает функцию read для перехода в состояние ожидания поступления следующей порции данных. Процессор сможет выдержать нагрузку по обработке данных, поступающих от многочисленных клиентов, не внося замедления, только в том случае, если он будет способен завершить цикл чтения и записи одного потока до поступления данных для другого ведомого потока. Безусловно, если нагрузка становится столь значительной, что процессор не может завершить выполнение одного запроса до поступления следующего, принцип организации работы с квантованием времени становится более приемлемым. Операционная система переключает процессор между всеми ведомыми потоками, имеющими данные, предназначенные для обработки. Однако в случае простых служб, требующих небольшого объема обработки данных для обслуживания каждого запроса, велика вероятность того, что для активизации процесса обработки будет использоваться сам факт поступления данных. Параллельные серверы, в которых на обработку каждого запроса требуется мало времени, часто выполняют обработку данных последовательно; при этом причиной активизации процесса обработки является сам факт поступления данных. Квантование времени начинает использоваться только с того момента, когда нагрузка становится настолько высокой, что процессор не может обрабатывать запросы последовательно. 13.3. Управляемая данными активизация обработки в одном потоке Поскольку в результате изучения характера работы параллельного сервера стало очевидно, что он часто выполняет обработку данных последовательно, был сделан вывод, что для выполнения той же задачи может применяться один поток. Допустим, что один поток сервера обслуживает соединения TCP одновременно с несколькими клиентами. Поток блокируется, ожидая поступления данных. Сразу после поступления данных через любое соединение поток активизируется, обрабатывает запрос и передает ответ. Затем он снова блокируется, ожидая поступления данных из другого соединения. При условии, что процессор работает достаточно быстро для того, чтобы выдержать нагрузку, возложенную на сервер, однопотоко- вая версия сможет обслуживать соединения с таким же успехом, как и версия с 180 Глава 13. Однопотоковые параллельные серверы (TCP)
несколькими потоками. Действительно, однопотоковая реализация не требует переключения между контекстами потоков или процессов, поэтому она может даже выдержать немного более высокую нагрузку по сравнению с реализацией, в которой используются несколько потоков или процессов. В основе разработки программы однопотокового, параллельного сервера лежит использование асинхронного ввода/вывода, организованного с помощью примитива select операционной системы. Сервер создает сокет для каждого соединения, которое он должен поддерживать, а затем вызывает функцию select, которая ожидает поступления данных через каждое из них. По существу, функция select может ожидать поступления запросов на выполнение операций ввода/вывода через все возможные сокеты, поэтому она может также одновременно ожидать поступления новых запросов на установление соединения. Этапы работы однопотокового сервера подробно описаны в алгоритме 8.52. 13.4. Схема организации работы однопотокового сервера На рис. 13.1 показана схема организации работы и состав сокетов однопотокового, параллельного сервера. Для управления обмена данными через все сокеты применяется один поток. Прикладной процесс сервера _ Операционная щ система Рис. 13.1. Схема организации процессов сервера с установлением логического соединения, в котором параллельная работа обеспечивается с использованием одного потока, управляющего сразу несколькими сокетами По сути, один единственный поток должен выполнять обязанности и ведущего, и всех ведомых потоков. Он сопровождает набор сокетов, причем один из них (так называемый ведущий сокет) остается привязанным к общепринятому порту, через который он должен принимать запросы на установление соединения. Каждый из ведомых сокетов в наборе соответствует соединению, для которого ведомый поток должен обрабатывать запросы. Сервер передает набор дескрипто- 2 Описание алгоритма 8.5 приведено на стр. 139. 13.4. Схема организации работы однопотокового сервера 181 ( Сервер ) t ^\\ L-J Сокет для запросов на установление соединения \ Г \ Г 1 Г IJ . U 1_1 1 Сокеты для отдельных соединений
ров сокетов функции select в качестве параметра и ожидает активизации любого из них. После возврата управления функция select передает в вызывающий оператор битовую маску, которая указывает, какой из дескрипторов в наборе готов к работе. В сервере для принятия решения о том, с каким из дескрипторов нужно продолжить работу, используется порядок их активизации. Для определения того, какие операции (ведущего или ведомого потока) должны быть выполнены для данного дескриптора, в однопотоковом сервере используется сам дескриптор. Если к работе готов дескриптор, соответствующий ведущему сокету, сервер выполняет для него такие же операции, какие выполнил бы ведущий поток: он вызывает функцию accept с этим сокетом для получения нового соединения. Если же к работе готов дескриптор, соответствующий ведомому сокету, сервер выполняет операцию ведомого потока: он вызывает функцию read для получения запроса, а затем отвечает на этот запрос. 13.5. Пример однопотокового сервера службы ECHO Для пояснения описанных выше принципов и иллюстрации того, как один поток может применяться для обеспечения параллельной работы, воспользуемся следующим примером. Рассмотрим файл TCPmechod.c, который содержит код сервера, реализующего службу ECHO. /* Файл TCPmechod.c - главная процедура echo */ ¦include <sys/types.h> ¦include <sys/socket.h> ¦include <sys/time.h> ¦include <netinet/in.h> ¦include <unistd.h> ¦include <string.h> ¦include <stdio.h> ¦define QLEN 32 /* Максимальная длина очереди соединений */ ¦define BUFSIZE 4096 extern int errno; int errexit(const char *format, ...); int passiveTCP(const char *service, int qlen); int echo(int fd); /* * Главная процедура - параллельный сервер TCP для службы ECHO * . */ int main(int argc, char *argv[]) { char *service = "echo"; struct sockaddrJLn fsin; int msock; fd_set rfds; fd set afds; /* Имя службы или номер порта */ /* Адрес источника в сообщении, полученном */ /* от клиента */ /* Ведущий сокет сервера */ /* Набор дескрипторов, готовых к чтению */ /* Набор активных дескрипторов */ 182 Глава 13. Однопотоковые параллельные серверы (TCP)
unsigned int alen; /* Длина адреса источника */ int fd, nfds; switch (argc) { case 1: break; case 2: service = argv[l]; break; default: errexit("usage: TCPmechod (port]\nH); } msock = passiveTCP(service, QLEN); nfds = getdtablesize(); FD_ZERO(&afds)? FDJ5ET(msock, &afds); while A) { memcpy(&rfds, fcafds, sizeof(rfds)); if (select(nfds, Srfds, (fd_set *H, (fd_set *H, (struct timeval *H) <0) errexit("select: %s\nM, strerror(errno)); if (FD_ISSET(msock, &rfds)) { int ssock; alen = sizeof(fsin); ssock = accept(msock, (struct sockaddr *)&fsin, &alen); if (ssock <0) errexit("accept: %s\n", strerror(errno)); FD_SET(ssock, fcafds); } for (fd=0; fd<nfds; ++fd) if (fd != msock && FD_ISSET(fd, fcrfds)) if (echo(fd) == 0) { (void) close(fd); FD_CLR(fd, Safds); } } } /* - * Процедура echo - выполняет эхо-повтор данных, находящихся в одном буфере; * возвращает число полученных байтов * .. . . */ int echo(int fd) 13.5. Пример однопотокового сервера службы ECHO 183
char buf[BUFSIZ]; int cc; cc = read(fd, buf, sizeof buf); if (cc <0) errexit("echo read: %s\n", strerror(errno)); if (cc && write(fd, buf, cc) <0) errexit("echo write: %s\n", strerror(errno))j return cc; } Выполнение потока этого сервера, как и выполнение ведущего потока сервера в параллельной реализации, начинается с открытия пассивного сокета в общепринятый порт. В этом примере используется системная функция getdtablesize для определения максимального числа дескрипторов, а затем применяются макрокоманды FD_ZER0 и FDJ3ET для создания битового вектора, соответствующего дескрипторам сокетов, которые должны контролироваться функцией select. Затем сервер входит в бесконечный цикл, в котором он вызывает функцию select для ожидания готовности к работе одного или нескольких дескрипторов3. Если готов дескриптор ведущего сокета, сервер вызывает функцию accept для получения нового соединения. Он добавляет дескриптор нового соединения к управляемому им набору и снова переходит в состояние ожидания активизации следующих дескрипторов. Если же готов дескриптор ведомого сокета, сервер вызывает процедуру echo, в которой вызывается функция read для получения данных из соединения и функция write для отправки их назад клиенту. Если один из дескрипторов ведомого сокета сообщает о получении признака конца файла, сервер закрывает дескриптор и использует макрокоманду FD_CLR для удаления его из набора дескрипторов, используемых функцией select. 13.6. Резюме Выполнение обработки данных в параллельных серверах часто активизируется в результате поступления самих данных, а не с помощью механизма квантования времени базовой операционной системы. В тех случаях, если предоставляемая служба требует небольшого объема обработки, для такого же эффективного обслуживания соединений с многочисленными клиентами, как и в варианте на основе нескольких потоков или процессов, может использоваться один поток выполнения, в котором осуществляется асинхронный ввод/вывод. В однопотоковой реализации один поток выполнения берет на себя обязанности одного ведущего и нескольких ведомых потоков. Если готов к работе ведущий сокет, единственный поток сервера вызывает функцию accept для получения нового соединения. А если готов любой другой сокет, поток сервера принимает запрос и передает ответ. В приведенном примере с сервером службы ECHO показано, как обеспечить параллельную обработку в одном потоке, а также проиллюстрированы тонкости программирования. Поскольку функция select перед возвратом управления очищает все дескрипторы, не доступные для чтения или записи, в этом коде набор активных дескрипторов копируется в переменную rfds при каждом проходе по циклу while. 184 Глава 13. Однопотоковые параллельные серверы (TCP)
Материал для дальнейшего изучения Качественная спецификация протокола отличается тем, что она не ограничивает возможности реализации. Например, в однопотоковом сервере, описанном в настоящей главе, осуществлен протокол службы ECHO, который определен в документе [120], а в главах 11 и 12 приведены примеры альтернативных реализаций, созданных на основе той же спецификации протокола. В этих реализациях, соответственно, используются несколько процессов (каждый из которых содержит один поток выполнения) или один многопотоковый процесс. Упражнения 13.1. Проведите эксперимент, который доказывает, что сервер службы ECHO, приведенный в данном примере, обеспечивает параллельное обслуживание соединений. 13.2. Имеет ли смысл использовать реализацию, описанную в данной главе, для создания службы DAYTIME? Объясните ваш ответ. 13.3. Прочитайте оперативную документацию, чтобы узнать, как именно представлены дескрипторы в списке, передаваемом функцией select. Напишите макрокоманды FDJ3ET и FD_CLR. 13.4. Сравните производительность сервера, в котором используется один поток, и сервера, в котором используется несколько процессов на компьютере с несколькими процессорами. При каких обстоятельствах версия с одним потоком будет работать лучше (или одинаково) по сравнению с версией, в которой используется несколько процессов? 13.5. Предположим, что к серверу, который рассматривается в качестве примера в данной главе, одновременно обращается много клиентов (скажем, 100). Объясните, что может наблюдать каждый из этих клиентов. 13.6. Может ли сервер, в котором используется один поток выполнения, когда-либо лишить одного клиента доступа к службе, снова и снова обслуживая запросы другого? Может ли сервер, в котором используется несколько потоков, или сервер, в котором используется несколько однопотоковых процессов, когда-либо отказать в обслуживании какому-либо клиенту? Объясните ваш ответ. Материал для дальнейшего изучения 185
14 Мультипротокольные серверы (TCP, UDP) 14.1. Введение В предыдущей главе было описано, как построить сервер, в котором один поток выполнения осуществляет асинхронный ввод/вывод для обеспечения псевдопараллельного обслуживания нескольких соединений. В этой главе эта тема рассматривается более подробно. В ней показано, как можно обеспечить поддержку нескольких транспортных протоколов с использованием одного потока выполнения. Рассматриваемые принципы иллюстрируются на примере с сервером, предоставляющим доступ к службе DAYTIME и по протоколу UDP, и по протоколу TCP. Хотя сервер, представленный в этом примере, выполняет запросы последовательно, рассматриваемый основной принцип обобщается и распространяется непосредственно на серверы, которые обрабатывают запросы параллельно. 14.2. Чем обусловлено стремление уменьшить количество серверов В большинстве случаев каждый конкретный сервер обрабатывает запросы к одной службе, доступ к которой предоставляется через один транспортный протокол. Например, в компьютерной системе, предоставляющей доступ к службе DAYTIME, часто работают два сервера; один из них обрабатывает запросы, поступающие по протоколу UDP, а другой — запросы, поступающие по протоколу TCP. Основное преимущество использования отдельного сервера для каждого протокола связано с тем, что такими серверами легко управлять. Системный администратор всегда может распорядиться тем, какие протоколы будут поддерживаться компьютером, управляя запуском и остановом соответствующих серверов в процессе работы системы. Основной недостаток использования отдельного сервера для каждого протокола связан с тиражированием серверов. Поскольку доступ ко многим службам может предоставляться либо по протоколу UDP, либо по протоколу TCP, для каждой службы требуется два сервера. Кроме того, поскольку и в серверах UDP, и в серверах TCP используется один и тот же основной алгоритм формирования ответа, оба они содержат одинаковый код обработки запроса и подготовки ответа. Так как обе программы содержат одинаковый код, обеспечивающий предоставление доступа к конкретной службе, сопровождение и отладка программного обеспечения могут стать утомительными. Программист, занимаясь исправлением ошибок или сменяя серверы в соответствии с новыми выпусками системного программного обеспечения, должен следить за тем, чтобы обе серверные программы
оставались одинаковыми. Кроме того, системный администратор должен тщательно координировать эксплуатацию серверов TCP и UDP одной и той же службы для обеспечения того, чтобы они одновременно предоставляли доступ к службе, соответствующей одной и той же версии спецификации данной службы. Еще один недостаток использования отдельных серверов для каждого протокола связан с непроизводительным использованием ресурсов: многочисленные серверные процессы (или потоки) напрасно занимают записи таблицы процессов и другие системные ресурсы. Масштабы этой проблемы становятся очевидными, если учесть, что в стандартах TCP/IP определены десятки служб. 14.3. Проект мультипротокольного сервера Мультипротокольный сервер состоит из одного потока выполнения, в котором используется асинхронный ввод/вывод для обслуживания соединений по протоколу UDP или TCP. В этом сервере первоначально открываются два сокета; в одном применяется транспортный протокол без установления логического соединения (UDP), а в другом — транспортный протокол с установлением логического соединения (TCP). Затем в сервере используются средства асинхронного ввода/вывода для получения информации о готовности к работе одного из сокетов. Если готов к работе сокет TCP, это означает, что от одного из клиентов поступил запрос на установление соединения TCP. Сервер вызывает функцию accept для получения нового соединения, а затем обменивается данными с клиентом через это соединение. Если же готов сокет UDP, это означает, что один из клиентов передал запрос в форме дейтаграммы UDP. В сервере для чтения запроса и регистрации адреса оконечной точки отправителя используется функция г ее vf r от. После формирования ответа сервер отправляет этому клиенту ответе помощью функции sendto. 14.4. Схема организации процессов Схема организации процессов последовательного мультипротокольного сервера приведена на рис. 14.1. Для приема запросов, поступающих по нескольким транспортным протоколам, применяется один поток выполнения, в котором может быть в любое время открыто не более трех сокетов: один для запросов UDP, другой —для запросов на установление соединения TCP, и еще один (временный) сокет — для обслуживания отдельного соединения TCP. В последовательном мультипротокольном сервере может быть открыто в любое время не более трех сокетов. Первоначально в нем открывается один сокет для приема входящих дейтаграмм UDP и второй сокет — для приема входящих запросов на установление соединения по протоколу TCP. После поступления дейтаграммы через сокет UDP сервер формирует ответ и передает его клиенту с использованием того же сокета, а после поступления запроса на установление соединения через сокет TCP сервер вызывает функцию accept для получения нового соединения. Функция accept создает третий сокет, предназначенный для обслуживания соединения, а сервер использует этот новый сокет для обмена данными с клиентом. После завершения сеанса обмена данными сервер закрывает третий сокет и переходит в состояние ожидания активизации других двух сокетов. 188 Глава 14. Мультипротокольные серверы (TCP, UDP)
Прикладной •*— процесс сервера Операционная система Рис. 14.1. Схема организации процессов последовательного мультипротокольного сервера 14.5. Пример мультипротокольного сервера службы DAYTIME Для демонстрации работы мультипротокольного сервера рассмотрим программу daytimed. Она состоит из одного потока выполнения, который предоставляет службу DAYTIME и по протоколу UDP, и по протоколу TCP. /* Файл daytimed.с - главная процедура */ ¦include <sys/types.h> tinclude <sys/socket.h> ¦include <sys/time.h> tinclude <netinet/in.h> ¦include <unistd.h> tinclude <stdio.h> jfinclude <string.h> extern int errno; int daytime(char buf[]); int errexit(const char *format, ...); int passiveTCP(const char *servicer int qlen); int passiveUDP(const char *service); ¦define MAX(x, y) ((x)> (y) ? (x) : (y)) ¦define QLEN 32 ¦define LINELEN 128 /* * Главная процедура - последовательный сервер для службы DAYTIME *.—..—.————...—— — ... ........... . . — ш Сокет для запросов UDP Сервер ] L_J Сокет для запросов на установление соединения TCP ? L_l Сокет для соединения TCP 14.5. Пример мультипротокольного сервера службы DAYTIME 189
*/ int main(int argc, char *argv[]) { char *service = "daytime"; /* Имя службы или номер порта */ char buf[LINELEN+1]; /* Буфер для одной строки текста */ struct sockaddr_in fsin; /* Исходный адрес запроса */ unsigned int alen; /* Длина адреса источника */ int tsock; /* Ведущий сокет TCP */ int usock; /* Сокет UDP */ int nfds; fd_set rfds; /* Дескрипторы, готовые к чтению */ switch (argc) { case 1: break; case 2: service = argv[l]; break; default: errexit("usage: daytiraed [port]\n"); } tsock = passiveTCP(service, QLEN); usock = passiveUDP(service); nfds = MAX(tsock, usock) + 1; /* Длина битовой маски для набора */ /* дескрипторов */ FDJERO(Srfds); while (l) { FD_SET(tsock, fcrfds); FD_SET(usock, &rfds); if (select(nfds, &rfds, (fdjset *H, (fd_set *H, (struct timeval *H) <0) errexit("select error: %s\n", strerror(errno)); if (FD_ISSET(tsock, &rfds)) { int ssock; /* Ведомый сокет TCP */ alen = sizeof(fsin); ssock = accept(tsock, (struct sockaddr *)&fsin, &alen); if (ssock <0) errexit("accept failed: %s\n", strerror(errno)); daytime(buf); (void) write(ssock, buf, strlen(buf)); (void) close(ssock); } if (FD_ISSET(usock, &rfds)) { alen ¦ sizeof(fsin); 190 Глава 14. Мультипротокольные серверы (TCP, UDP)
if (recvfrom(usock, buf, sizeof(buf), 0, (struct sockaddr *)&fsin, fcalen) <0) errexit("recvfrom: %s\n", strerror(errno)); daytime(buf); (void) sendto(usock, buf, strlen(buf), 0, (struct sockaddr *)&fsin, sizeof(fsin)); } } } t • /* —— --- * Процедура daytime - записывает в указанный буфер строку со значением * времени суток * */ int daytime(char buf[]) { char *ctime(); time_t now; (void) time(&now); sprintf(buf, "%s", ctime(&now)); } Программа daytimed принимает необязательный параметр, который позволяет пользователю указать имя службы или номер порта протокола. Если пользователь не задает ни одного параметра, то программа daytimed использует порт службы DAYTIME. После интерпретации параметров в программе daytimed вызываются процедуры passiveTCP и passiveUDP для создания двух пассивных сокетов, предназначенных для работы по протоколам TCP и UDP. Для обоих сокетов используется одна и та же служба, причем, как и в большинстве служб, для обоих применяется один и тот же номер порта протокола. Эти сокеты можно рассматривать как ведущие: сервер поддерживает их в открытом состоянии неопределенно долгое время и все первоначальные попытки контакта со стороны клиента осуществляются через один из них. В вызове процедуры passiveTCP указывается, что система должна ставить в очередь запросы на установление соединения до тех пор, пока их число не станет равным QLEN. После создания ведущих сокетов сервер выполняет подготовку к использованию функции select для перехода в состояние ожидания готовности одного или обоих из этих сокетов к вводу/выводу. Во-первых, в нем переменной nfds присваивается значение индекса битовой маски дескриптора старшего из этих двух сокетов, и битовая маска очищается (переменная rfds), после чего сервер входит в бесконечный цикл. При каждом проходе по циклу в нем используется макрокоманда FDJ5ET для построения битовой маски с битами, установленными для дескрипторов, соответствующих двум ведущим сокетам. Затем вызывается функция select для перехода в состояние ожидания активизации ввода через любой из них. Возврат управления из функции select свидетельствует о том, что готов один или оба из ведущих сокетов. В сервере для проверки сокета TCP, а затем сокета UDP используется макрокоманда FD_ISSET. Сервер должен проверить оба 14.5. Пример мультипротокольного сервера службы DAYTIME 191
сокета, поскольку может случиться так, что дейтаграмма UDP поступила одновременно с запросом на установление соединения TCP, и в этом случае должны быть готовы оба сокета. Если готов сокет TCP, это означает, что какой-то клиент инициализировал запрос на установление соединения. Сервер вызывает функцию accept для установления соединения. Эта функция возвращает дескриптор нового, временного сокета, применяемого только для обмена данными через это новое соединение. Сервер вызывает процедуру daytime для формирования ответа, функцию write для передачи ответа через новое соединение, а затем функцию close для разрыва соединения и освобождения ресурсов. Если готов сокет UDP, это означает, что какой-то клиент прислал дейтаграмму, чтобы запросить ответ от службы DAYTIME. Сервер вызывает функцию recvfrom для чтения входящей дейтаграммы и регистрации адреса оконечной точки клиента. В нем используется процедура daytime для формирования ответа, а затем вызывается функция sendto для передачи ответа клиенту. Поскольку в сервере для всех видов связи используется ведущий сокет UDP, сервер не вызывает функцию close после отправки ответа UDP. 14.6. Понятие разделяемого кода Сервер, рассматриваемый в приведенном примере кода, иллюстрирует весьма важный принцип. Проект мулътипротоколъного сервера позволяет создать единственную процедуру, которая отвечает на запросы к данной службе, и вызывать эту процедуру без учета того, по какому протоколу (UDP или TCP) получены эти запросы. В рассматриваемом примере службы DAYTIME разделяемый код занимает всего несколько строк. Он был помещен в одну процедуру daytime. Однако в большинстве применяемых на практике серверов код, необходимый для формирования ответа, может занимать сотни или тысячи строк и обычно состоит из множества процедур. Размещение всего разделяемого кода в одном месте позволяет упростить сопровождение и обеспечить идентичность службы, предоставляемой по обоим транспортным протоколам. 14.7. Параллельные мультипротокольные серверы Как и в однопротокольном сервере службы DAYTIME, описанном ранее1, в рассматриваемом примере с мультипротокольным сервером DAYTIME используется последовательный метод обработки запросов. Причина использования последовательного решения остается той же, что и в ранее описанном сервере, предоставляющем доступ к службе DAYTIME: последовательный сервер успешно справляется со своей работой, поскольку предоставление услуг службы DAYTIME связано с выполнением минимального объема вычислений при обработке каждого запроса. Последовательная реализация может оказаться неприемлемой для создания других служб, которые требуют большего объема вычислений при обработке каждого запроса. В подобных случаях мультипротокольный проект может быть расширен в целях обеспечения параллельной обработки запросов. В простейшем слу- Код последовательного сервера DAYTIME с установлением логического соединения приведен в главе 10. 192 Глава 14. Мультипротокольные серверы (TCP, UDP)
чае мультипротокольный сервер может создавать новый поток или процесс для одновременного обслуживания каждого из соединений TCP, в то время как обработка запросов UDP будет осуществляться последовательно. Мультипротокольный проект может быть также расширен в целях применения реализации, описанной в главе 13. Такая реализация предусматривает псевдопараллельную обработку запросов, поступающих через несколько соединений TCP или через сокет UDP. 14.8. Резюме Мультипротокольный сервер позволяет проектировщику заключить весь код конкретной службы в одну программу, устранить тиражирование и упростить координацию изменений. Работа мультипротокольного сервера складывается из одного потока выполнения. В потоке открываются ведущие сокеты для работы по протоколам UDP и TCP, и для ожидания перехода одного или обоих из них в состояние готовности используется функция select. Если готов сокет TCP, сервер вызывает функцию accept для получения нового соединения и обрабатывает запросы с использованием нового сокета. Если же готов сокет UDP, сервер принимает запросы и передает ответы. Приведенный в этой главе проект мультипротокольного сервера может быть расширен в целях обеспечения поддержки параллельных соединений TCP. Еще более важно то, что он может быть расширен для обеспечения параллельного выполнения запросов независимо от того, по какому протоколу (TCP или UDP) они поступают. Мультипротокольные серверы позволяют устранить тиражирование кода за счет использования единой процедуры для формирования ответов на запросы к данной службе. Кроме того, мультипротокольный проект расходует меньше системных ресурсов, чем отдельные серверы (например, в нем создается меньше процессов). Материал для дальнейшего изучения Список прикладных протоколов наряду с перечнем портов протоколов UDP и TCP, назначенных каждому из них, приведен в документе [133]. Упражнения 14.1. Доработайте сервер, представленный в примере данной главы, чтобы он мог обрабатывать сразу несколько запросов. Изучите некоторые из наиболее распространенных служб, которые определены для протоколов TCP/IP. Можете ли вы найти примеры, когда в мультипротоколь- ном сервере нельзя использовать для формирования ответов разделяемый код? Объясните ваш ответ. 14.2. В этом примере кода для пользователя предусматривается возможность указать в качестве параметра имя службы или номер порта протокола, и этот параметр используется при создании пассивных сокетов для служб. Известны ли вам примеры служб, в которых для UDP используется номер порта протокола, отличный от того, что используется для TCP? Доработайте код программы, чтобы она позволяла пользователю отдельно указывать номер порта для каждого протокола. 14.3. Сервер, рассматриваемый в данном примере, не дает возможности системному администратору определять то, какие протоколы должны в нем использоваться. Доработайте сервер и предусмотрите параметры, позволяющие администратору указывать, по какому протоколу должна предоставляться служба (по TCP, UDP или по обоим). 14.8. Резюме 193
14.4. Предположим, что принято решение реализовать на сетевом узле средства защиты с использованием схемы авторизации. На узле предусмотрено предоставление каждой серверной программе списка клиентских компьютеров, с которых разрешен доступ к этому серверу, и установлено правило, согласно которому сервер должен отклонять запросы, поступающие с компьютеров, не входящих в этот список. Реализуйте схему авторизации для рассматриваемого примера с мультипротокольным сервером. (Подсказка: тщательно изучите функции сокетов, чтобы понять, как они используются для работы по протоколу TCP.) 194 Глава 14. Мультипротокольные серверы (TCP, UDP)
15 Мультисервисные серверы (TCP, UDP) 15.1. Введение В главе 13 было описано, как построить однопотоковый сервер, в котором используется асинхронный ввод/вывод для обеспечения псевдопараллельного обслуживания нескольких соединений, а в главе 14 показано, как обеспечить на основе мультипротокольного сервера доступ к одной и той же службе с использованием обоих транспортных протоколов, TCP и UDP. В настоящей главе понятия, рассматриваемые в этих двух главах, дополняются и объединяются на примере некоторых проектов последовательных и параллельных серверов, описанных в предыдущих главах. В ней показано, как один сервер может предоставлять доступ к нескольким службам, и этот подход иллюстрируется на примере с сервером, который поддерживает целый ряд служб в одном потоке управления. 15.2. Объединение нескольких серверов в одной программе В большинстве случаев программисты для предоставления доступа к каждой службе разрабатывают отдельный сервер. Иллюстрацией к такому "односервисному" подходу могут служить примеры, рассмотренные в предыдущих главах: каждый из серверов ожидает поступления запросов через общепринятый порт и отвечает на запросы к службе, связанной с этим портом. Поэтому на компьютере обычно работает сервер службы DAYTIME, сервер службы ECHO и т.д. В предыдущей главе было показано, что сервер, в котором используется несколько протоколов, позволяет сэкономить системные ресурсы и упростить сопровождение. Те же преимущества, которые служат стимулом к применению мультипротокольных серверов, лежат в основе стремления объединить средства поддержки нескольких служб в один мультисервисный сервер1. Чтобы оценить объем затрат на создание отдельных серверов для каждой службы, необходимо рассмотреть весь набор стандартизированных служб. В протоколах TCP/IP определен широкий набор мелких служб, предназначенных для проверки, отладки и сопровождения программного обеспечения компьютеров в сети. В предыдущих главах рассматривались такие примеры служб, как DAYTIME, ECHO и TIME, но имеется и много других служб. Если в системе вызвано на выполнение по одному серверу для каждой стандартизированной служ- Недостатком мультисервисного сервера является уменьшение надежности, поскольку при нарушении его работы все поддерживаемые им службы становятся недоступными.
бы, то в ней будут присутствовать десятки серверных процессов, даже несмотря на то, что большинство из них не получат ни одного запроса. Поэтому объединение средств поддержки многих служб в одном серверном процессе позволяет резко уменьшить число одновременно выполняемых процессов . Кроме того, поскольку поддержка многих мелких служб сводится к выполнению простейших вычислений, то основная часть кода сервера будет относиться к реализации всех нюансов приема запросов и передачи ответов. Объединение средств поддержки многих служб в одном сервере позволяет уменьшить суммарный объем кода. 15.3. Проект мультисервисного сервера без установления логического соединения В мультисервисных серверах могут использоваться транспортные протоколы с установлением и без установления логического соединения. На рис. 15.1 показана одна из возможных схем организации процессов мультисервисного сервера без установления логического соединения. В одном потоке выполнения происходит ожидание поступления дейтаграммы через один из нескольких сокетов, где каждый сокет соответствует отдельной службе. Прикладной процесс сервера Операционная щ система Рис. 15,1. Последовательный мультисервисный сервер без установления логического соединения Как показано на рис. 15.1, последовательный мультисервисный сервер без установления логического соединения обычно состоит из одного потока управления, а также кода, необходимого для поддержки предоставляемых им служб. Сервер открывает набор сокетов UDP и привязывает каждый из них к общепринятому порту каждой из предоставляемых служб. В нем используется небольшая таблица для определения соответствия сокетов службам. Для каждого дескрип- * Поскольку в реализациях для операционной системы Linux ограничено максимальное число сокетов, которые могут быть открыты одним процессом, то может оказаться неосуществимой попытка предоставления доступа ко всем службам в одном сервере. Однако если каждый процесс может открыть N сокетов, то применение мультисервисных серверов позволяет уменьшить число требуемых процессов в N раз. Ведущие сокеты (по одному для каждой предоставляемой службы) 196 Глава 15. Мультисервисные серверы (TCP, UDP)
тора сокета а таблице регистрируется адрес процедуры обработки запросов к службе, доступ к которой предоставляется через этот сокет. В сервере используется системный вызов select для перехода в состояние ожидания поступления дейтаграммы через один из этих сокетов. После поступления дейтаграммы сервер вызывает соответствующую процедуру для формирования ответа и его отправки. Поскольку в таблице соответствия зарегистрирована служба, доступ к которой предоставляется через каждый сокет, сервер может легко связать дескриптор сокета с процедурой, поддерживающей данную службу. 15.4. Проект мультисервисного сервера с установлением логического соединения Рассматриваемый мультисервисный сервер с установлением логического соединения также работает по последовательному алгоритму. Вообще, такой сервер выполняет те же задачи, что и ряд последовательных серверов с установлением логического соединения. Точнее, в данном случае один поток выполнения мультисервисного сервера заменяет ведущие потоки целого ряда серверов с установлением логического соединения. На самом верхнем уровне организации работы в мультисервисном сервере для выполнения его функций применяется асинхронный ввод/вывод. Схема организации процессов сервера приведена на рис. 15.2. В любой момент времени в одном потоке управления имеется один открытый сокет для каждой службы, а для обслуживания конкретного соединения открыт лишь один дополнительный сокет. Прикладной процесс сервера Операционная щЩ система Рис. 15.2. Схема организации процессов последовательного мультисервисного сервера с установлением логического соединения Начиная выполнение, мультисервисный сервер создает один сокет для каждой предоставляемой им службы, привязывает каждый сокет к общепринятому порту данной службы, а затем вызывает функцию select для перехода в состояние ожидания входящих запросов на установление соединения через любой из этих сокетов. Если готов один из сокетов, сервер вызывает функцию accept для получения нового 15.4. Проект мультисервисного сервера с установлением логического соединения 197 Ведущие сокеты (по одному для каждой предоставляемой службы) Сокет для каждого отдельного соединения
соединения, выполняя поступивший запрос. Функция accept создает новый сокет для входящего соединения. Сервер использует новый сокет для обмена данными с клиентом, а затем закрывает его. Поэтому кроме одного ведущего сокета для каждой службы, в сервере в любое время открыто не более одного дополнительного сокета. Как и в случае с сервером без установления логического соединения, в данном сервере также ведется таблица соответствия, позволяющая определить способ обработки каждого входящего соединения. Начиная работу, сервер распределяет ведущие сокеты. Для каждого ведущего сокета сервер вносит запись в таблицу соответствия с указанием номера сокета и процедуры, реализующей службу, доступ к которой предоставляется через этот сокет. После распределения ведущего сокета для каждой службы сервер вызывает функцию select для перехода в состояние ожидания запросов на установление соединения. После поступления такого запроса сервер использует таблицу соответствия для определения того, какая из многих внутренних процедур должна быть вызвана для поддержки службы, затребованной клиентом. 15.5. Параллельный мультисервисный сервер с установлением логического соединения Процедура, вызываемая мультисервисным сервером при получении запроса на установление соединения, может сама принимать и обслуживать новое соединение (и в этом случае сервер становится последовательным) или создавать ведомый процесс для обработки этого запроса (в таком случае сервер становится параллельным). По существу, мультисервисный сервер может быть запрограммирован на последовательную поддержку одних служб и параллельную поддержку других; программист не обязан выбирать единый t способ организации всех служб. Как и в описанных выше параллельных серверах, параллельная работа может быть обеспечена с использованием нескольких однопотоковых процессов или одного многопотокового цррцесса. На рис. 15.3 показана схема организации процессов/потоков в мультисервиснрм сервере, в котором применяется параллельная реализация с установлением логического соединения. Как и в последовательной реализации, сразу после завершения взаимодействия с клиентом процедура закрывает используемое ею новое соединение. В случае параллельной реализации ведущий процесс сервера закрывает соединение сразу после создания им ведомого процесса; соединение остается открытым в ведомом процессе. Ведомый процесс работает точно так же, как и ведомый процесс в обычном параллельном сервере с установлением логического соединения: он взаимодействует с клиентом через соединение, выполняя запросы и отправляя ответы. Закончив сеанс взаимодействия, ведомый процесс закрывает сокет, тем самым завершая обмен данными с клиентом, и заканчивает работу. 15.6. Реализация однопотокового мультисервисного сервера Хотя такой проект встречается редко, но возможно также обеспечить выполнение всей работы мультисервисного сервера в одном потоке с использованием точно такой же схемы организации процессов, как и в сервере, описанном в главе 13. Вместо создания ведомого процесса для каждого входящего запроса на установление соединения, сервер добавляет сокет для каждого нового соединения к применяемому им в функции select набору сокетов. Если готов один из ведущих сокетов, сервер вызывает функцию accept; если готов один из ведомых сокетов, сервер вызывает функцию read для получения входящего запроса от клиента, формирует ответ и вызывает функцию write для передачи ответа клиенту. Ве- 198 Глава 15. Мультисервисные серверы (TCP, UDP)
дущий процесс/поток обрабатывает входящие запросы на установление соединения, а ведомый процесс/поток обслуживает каждое соединение. Прикладные процессы ^— (или потоки) сервера Операционная щ система Рис. 15.3. Схема организации процессов /потоков для параллельного мультисервисного сервера с установлением логического соединения 15.7. Вызов отдельных программ из мультисервисного сервера Одним из основных недостатков большинства проектов, описанных выше, является отсутствие модульности: для смены кода поддержки любой отдельно взятой службы требуется перетрансляция всего мультисервисного сервера. Этот недостаток кажется незначительным до тех пор, пока речь не идет о сервере, поддерживающим много служб. Для внесения любого незначительного изменения программист вынужден перетранслировать сервер, остановить существующий и перезапустить его с использованием вновь оттранслированного кода. Если мультисервисный сервер предоставляет доступ ко многим службам, то велика вероятность того, что хотя бы один клиент будет взаимодействовать с ним в любое конкретное время. Поэтому останов сервера может стать для некоторых клиентов источником проблем. Кроме того, чем больше служб предоставляется сервером, тем выше вероятность того, что в какой-то момент потребуется его доработка. Проектировщики часто предпочитают разбить крупный, монолитный, мультисервисный сервер на независимые компоненты с использованием отдельно оттранслированных программ для поддержки каждой службы. Применяемый при этом принцип легче понять при рассмотрении параллельного сервера с установлением логического соединения. Рассмотрим параллельный сервер с установлением логического соединения, который показан на рис. 15.3. Ведущий сервер ожидает поступления запросов на установление соединений через набор ведущих сокетов. После поступления запроса на соединение ведущий сервер вызывает функцию fork для создания ведомого процесса, который будет обслуживать соединение. Сервер должен иметь доступ к коду для всех служб, оттранслированному в целях включения в ведущую программу. На рис. 15.4 показано, как может быть доработан этот проект для разбиения крупного сервера на отдельные части. В сервере используется 15.7. Вызов отдельных программ из мультисервисного сервера 199 (по одному для каждой для отдельных предоставляемой ведомых службы) соединений
функция execve для вызова на выполнение отдельной программы, когда возникает необходимость обслужить каждое соединение. Прикладные ^ процессы серверов ^ Применение функции execve ^ Операционная система Рис. 15.4. Схема организации процессов мулыписервисного сервера с установлением логического соединения Как показано на этом рисунке, в ведущем сервере применяется функция fork для создания нового ведомого процесса при возникновении необходимости обслужить конкретное соединение. Однако в отличие от описанного ранее проекта, в ведомом процессе вызывается функция execve для замены первоначального кода новой программой, которая обеспечивает все взаимодействие с клиентом. Поскольку функция ехесуе выполняет выборку новой программы из файла, то проект, описанный выше, позволяет системному администратору в случае необходимости заменить только один файл без перетрансляции мультисервисного сервера, останова главного сервера или его перезапуска. По существу, применение функции execve позволяет отделить программы поддержки каждой службы от кода ведущего сервера, который устанавливает соединения. Применение в мультисервисном сервере системного вызова execve позволяет отделить код поддержки отдельных служб от кода обслуживания первоначальных запросов клиентов. 15.8. Мультисервисные и мультипротокольные проекты Хотя и принято считать, что мультисервисный сервер должен обязательно быть сервером либо с установлением, либо без установления логического соединения, возможен также мультипротокольный проект. Как описано в главе 14, мультипротокольный проект позволяет обслуживать в одном потоке сервера и сокет UDP, и сокет TCP для одной и той же службы. С другой стороны, мультисервисный сервер может обслуживать сокеты UDP и TCP только для некоторых или одновременно для всех служб, которые он предлагает. Специалисты по сетям для обозначения мультисервисного мультипротоколь- ного сервера часто используют термин суперсервер. В принципе, суперсервер функционирует во многом аналогично обычному мультисервисному серверу. / Применение функции fork /ВедоЛ /ВедоЛ \МЫЙ1/ \МЫЙп/ Ш Ш Ш Ведущие сокеты (по одному для каждой предоставляемой службы) для отдельных ведомых соединений 200 Глава 15. Мультисервисные серверы (TCP, UDP)
Первоначально сервер открывает один или два ведущих сокета для каждой из предоставляемых им служб. Ведущие сокеты для каждой конкретной службы соответствуют транспортному протоколу без установления логического соединения (UDP) или транспортному протоколу с установлением логического соединения (TCP). В сервере используется функция select для перехода в состояние ожидания готовности к работе любого сокета. Если готов сокет UDP, сервер вызывает процедуру, которая считывает следующий запрос (дейтаграмму) из сокета, формирует ответ и передает его клиенту. Если готов сокет TCP, сервер вызывает процедуру, которая получает с помощью функции accept, вызванной с этим сокетом, новое соединение и обслуживает его. Сервер может сам обслуживать соединение, и в этом случае он является последовательным, или создавать новый процесс для обслуживания соединения, и тогда он считается параллельным. 15.9. Пример с мультисервисным сервером Мультисервисный сервер, представленный в файле superd.c, расширяет реализацию сервера, которая описана в главе 14. После инициализации структур данных и открытия сокетов для каждой из предоставляемых служб, главная процедура входит в бесконечный цикл. При каждом проходе по циклу вызывается функция select для перехода в состояние ожидания готовности одного из сокетов. Возврат из функции select происходит после поступления запроса. После возврата управления из функции select сервер перебирает в цикле все возможные дескрипторы сокетов и использует макрокоманду FD_ISSET для проверки готовности дескриптора. Найдя готовый к работе дескриптор сокета, сервер вызывает функцию для обработки содержащегося в сокете запроса. Для этого в сервере вначале используется массив fd2sv для установления соответствия между номером дескриптора и записью в массиве svent. Каждая запись в массиве svent содержит структуру типа service, которая устанавливает соответствие между дескриптором сокета и службой. Поле sv_func содержит адрес функции, предназначенной для поддержки службы. После установления соответствия между дескриптором и записью массива svent программа вызывает выбранную функцию. Для обслуживания сокета UDP сервер вызывает обработчик службы непосредственно, а для обслуживания сокета TCP сервер вызывает обработчик службы косвенно, через процедуру doTCP. Для служб TCP требуется дополнительная процедура, поскольку сокет TCP в сервере с установлением логического соединения соответствует ведущему сокету. Переход в состояние готовности такого сокета означает, что в него поступил запрос на установление соединения. В сервере для обслуживания соединения должен быть создан новый процесс. Поэтому в процедуре doTCP вызывается функция accept для получения нового соединения. В ней затем вызывается функция fork для создания нового ведомого процесса. После закрытия лишних дескрипторов файлов в процедуре doTCP вызывается функция обработчика службы (sv_func). После возврата управления из этой функции поддержки службы ведомый процесс завершается. /* Файл superd.c - главная процедура */ ¦define JJSE_BSD ¦include <sys/types.h> ¦include <sys/param.h> ¦include <sys/socket.h> ¦include <sys/time.h> ¦include <sys/resource.h> 15.9. Пример с мультисервисным сервером 201
¦include <sys/errno*h> ¦include <sys/signal.h> ¦include <sys/wait.h> ¦include <netinet/in.h> ¦include <unistd.h> ¦include <stdlib.h> ¦include <stdio.h> ¦include <string.h> extern int errno; ¦define UDP SERV 0 ¦define TCPJ3ERV 1 ¦define NOSOCK -1 /* Недопустимый дескриптор сокета */ struct service { char *sv_name; char svjiseTCP; int sv_sockj void (*sv func)(int); void TCPechod(int), TCPchargend(int), TCPdaytimed(int), TCPtimed(int); int passiveTCP(const char *service, int qlen); int passiveUDP(const char *service); int errexit(const char *format, ...)? void doTCP(struct service *psv); void reaper(int sig)j struct service svent[] = { { "echo", TCP SERV, NOSOCK, TCPechod }, { "chargen", TCP_SERV, NOSOCK, TCPchargend }, { -daytime", TCP SERV, NOSOCK, TCPdaytimed }, { "time", TCP SERV, NOSOCK, TCPtimed }, { 0, 0, 0, 0 J, >? ¦ifndef MAX ¦define MAX(x, y) ((x)> (y) ? (x) : (y)) ¦endif /* MAX */ ¦define QLEN 32 ¦define LINELEN 128 extern unsigned short portbase; /* из функции passivesock() */ /* . * Главная процедура - основная программа суперсервера 202 Глава 15. Мультисервисные серверы (TCP, UDP)
* - */ int main(int argc, char *argv[]) { struct service *psv, /* Указатель таблицы service */ *fd2sv[N0FILE]; /* Отображение дескриптора файла на */ /* указатель таблицы служб */ int fd, nfds; fd_set afds, rfds; /* Дескрипторы, готовые к чтению */ switch (argc) { case 1: break; case 2: portbase = (unsigned short) atoi(argv[l]); break; default: errexit("usage: superd [portbase]\n"); } nfds = 0; FDJERO(Safds); for (psv = &svent[0]; psv->sv_name; ++psv) { if (psv->sv_useTCP) psv->sv_sock = passiveTCP(psv->sv_name, QLEN); else psv->sv_sock « passiveUDP(psv->sv_name); fd2sv[psv->sv_sock] = psv; nfds - MAX(psv->sv_sock+l, nfds); FD_SET(psv->sv_sock, &afds); } (void) signal(SIGCHLD, reaper); while A) { memcpy(&rfds, &afds, sizeof(rfds)); if (select(nfds, &rfds, (fd_set *H, (fd_set *H, (struct timeval *H) <0) { if (errno == EINTR) continue; errexit("select error: %s\nH, strerror(errno)); } for (fd=0; fd<nfds; ++fd) if (FDJSSET(fd, &rfds)) { psv = fd2sv[fd]; if (psv->sv_useTCP) doTCP(psv); else psv->sv_func(psv->sv_sock); } } } 15.9. Пример с мультисервисным сервером 203
/* * Процедура doTCP - выполняет обработку запросов к службе TCP • .-_ .. - -- ... . ...................... .. ..... ...... */ void doTCP(struct service *psv) { struct sockaddr_in fsin; /* Исходный адрес запроса */ unsigned int alen; /* Длина адреса источника */ int fd, ssock; alen = sizeof(fsin); ssock ¦ accept(psv->sv_sock, (struct sockaddr *)&fsin, &alen); if (ssock <0) errexit("accept: %s\n", strerror(errno)); switch (fork()) { case 0: break; case -1: errexit("fork: %s\n", strerror(errno)); default: (void) close(ssock); return; /* Родительский процесс */ } /* Дочерний процесс */ for (fd « NOFILE; fd>* 0; —fd) if (fd 1= ssock) (void) close(fd); psv->sv func(ssock); exit@O } /* * Процедура reaper - убирает записи дочерних процессов-зомби из системных * таблиц */ void reaper(int sig) { int status; while (wait3(&status, WNOHANG, (struct rusage *H)>= 0) /* Пустое тело цикла */; } Суперсервер, приведенный в данном примере, предоставляет доступ к четырем службам: ECHO, CHARGEN, DAYTIME и TIME. Все службы, кроме CHARGEN, были описаны в качестве примеров в предыдущих главах. Служба CHARGEN используется программистами для проверки клиентского программного обеспечения. Сразу после установления клиентом соединения с сервером CHARGEN сервер формирует бесконечную последовательность символов и передает их клиенту. 204 Глава 15. Мультисервисные серверы (TCP, UDP)
Код функций, предназначенных для поддержки каждой из отдельных служб, приведен в файле sv_funcs.c. /* Файл svjEuncs - процедуры TCPechod, TCPchargend, TCPdaytimed, TCPtimed */ ¦include <sys/types.h> ¦include <unistd.h> ¦include <stdio.h> ¦include <time.h> ¦include <string.h> ¦define BUFFERSIZE 4096 /* Максимальный размер буфера чтения */ extern int errno; void TCPechod(int), TCPchargend(int), TCPdaytimed(int), TCPtimed(int); int errexit(const char *format, ...); /* . * Процедура TCPecho - предоставляет доступ к службе ECHO по протоколу TCP * через указанный сокет *—.... .......... ....... ... . . ..... . ... */ void TCPechod(int fd) { char buf[BUFFERSIZE]j int cc; while (cc = read(fd, buf, sizeof buf)) { if (cc <0) errexit("echo read: %s\n", strerror(errno)); if (write(fd, buf, cc) <0) errexit("echo write: %в\пи, strerror(errno)); } } ¦define LINELEN 72 /* * Процедура TCPchargend - предоставляет доступ к службе CHARGEN по протоколу * TCP через указанный сокет *....................... —................_...—.—.........—......... */ void TCPchargend(int fd) { char с, buf[LINELEN+2]; /* Вывести LINELEN символов и символы \r\n */ с * ' '; buf[LINELEN] = '\r'j buf[LINELEN+l] * '\n'j while A) { 15.9. Пример с мультисервисным сервером 205
int i; for (i=0? KLINELEN; ++i) { buf[i] = C++; if (c> '"') с = ' '; } if (write(fd, buf, LINELEN+2) <0) break; } } /* . * Процедура TCPdaytimed - предоставляет доступ к службе DAYTIME по протоколу * TCP через указанный сокет *__ . ----- -- - -------- -- .... - -- */ void TCPdaytimed(int fd) { char buf[LINELEN], *ctime(); time_t now; (void) time(&nov); sprintf(buf, "Is", ctime(Snow)); (void) write(fd, buf, strlen(buf)); >¦ ¦define UNIXEPOCH 2208988800UL /* Время наступления эпохи UNIX: число */ /* секунд с начала эпохи Internet */ . /* —- * Процедура TCPtimed - предоставляет доступ к службе TIME по протоколу TCP * через указанный сокет *. ... .. . .. . . */ void TCPtimed(int fd) { time_t now; (void) time(finow); now = htonl((unsigned long)(now + UNIXEPOCH)); (void) write(fd, (char *)&now, sizeof(now)); } Код большинства отдельных функций должен быть уже знаком читателю; он был взят из примеров с серверами, которые рассматривались в предыдущих главах. Код поддержки службы CHARGEN приведен в процедуре TCPchargend; он является несложным. Процедура состоит из цикла, в котором повторно формируется буфер, заполненный символами ASCII, а затем для передачи содержимого буфера клиенту вызывается функция write. 206 Глава 15. Мультисервисные серверы (TCP, UDP)
15.10. Статическая и динамическая конфигурация сервера На практике во многих операционных системах предусмотрен механизм суперсервера, в который системные администраторы могут встраивать дополнительные службы. В целях упрощения эксплуатации суперсерверы часто являются настраиваемыми: набор служб, поддерживаемых суперсервером, может быть изменен без перетрансляции исходного кода. Возможны конфигурации двух типов: статическая и динамическая. Статическая конфигурация применяется в тот момент, когда суперсервер вызывается на выполнение. Как правило, информация о конфигурации помещается в файле, считываемом суперсервером во время запуска на выполнение. В файле конфигурации определен набор служб, предоставляемый суперсервером, а также перечень выполняемых программ, которые должны использоваться для каждой службы. Для изменения состава предоставляемых служб системному администратору достаточно просто откорректировать файл конфигурации и перезапустить суперсервер. Динамическая конфигурация применяется во время работы суперсервера. Как и статически настраиваемый, динамически настраиваемый суперсервер считывает файл конфигурации во время запуска на выполнение. Файл конфигурации определяет первоначальный набор служб, поддерживаемых суперсервером. Однако в отличие от статически настраиваемого, динамически настраиваемый суперсервер позволяет переопределить набор предоставляемых им служб без перезапуска. Для изменения состава службы системный администратор вносит изменения в файл конфигурации, а затем передает суперсерверу указание, что требуется реконфигурация. Суперсервер проверяет файл конфигурации и соответствующим образом меняет свое поведение. Как может администратор передать суперсерверу информацию о том, что требуется реконфигурация? Ответ зависит от операционной системы. В системе Linux для межпроцессной связи используется механизм сигналов. Администратор передает суперсерверу определенный сигнал; суперсервер должен перехватывать этот сигнал и интерпретировать факт его получения как требование выполнить реконфигурацию. В операционной системе, не имеющей механизма межпроцессной связи, для динамической реконфигурации используется обычная связь: для обмена информацией с суперсервером администратор использует протокол TCP/IP. Для того чтобы такая связь была возможной, суперсервер запрограммирован на открытие отдельного сокета, используемого для управления суперсервером. При возникновении необходимости реконфигурации администратор вступает во взаимодействие с суперсервером через управляющее соединение. После того как администратор передает суперсерверу команду динамически изменить конфигурацию, суперсервер считывает файл конфигурации и изменяет состав предоставляемых им служб. Если файл конфигурации содержит одну или несколько служб, отсутствующих в предыдущей конфигурации, суперсервер открывает сокеты для приема запросов к новым службам. Если же из файла конфигурации удалены одна или несколько служб, то суперсервер закрывает сокеты, соответствующие службам, которые он больше не поддерживает. Безусловно, хорошо спроектированный суперсервер выполняет реконфигурацию корректно: хотя он и прекращает прием новых запросов к службам, которые больше не поддерживаются, он не разрывает аварийно ранее установленные соединения. Поэтому происходит полный отказ в приеме нового запроса от клиента, но существующий запрос выполняется до конца. Обеспечение возможности динамической реконфигурации суперсервера способствует значительному повышению удобства в эксплуатации. Выполняемая программа, обеспечивающая поддержку определенной службы, может быть заменена без смены самого суперсервера. Кроме того, поскольку набор служб, предоставляемых суперсервером, может быть изменен без перетрансляции кода или перезапуска сущест- 15.10. Статическая и динамическая конфигурация сервера 207
вующего суперсервера, программист может проверять новые службы, не нарушая работы производственных служб. Еще более важно то, что реконфигурация не требует внесения изменений в исходный код, поэтому задача настройки суперсервера может быть возложена на служащих, не имеющих подготовки в области программирования. Суперсервер, который позволяет изменять свою конфигурацию динамически, является удобным в эксплуатации, поскольку набор предоставляемых им служб можно изменить без перетрансляции серверной программы или перезапуска суперсервера. 15.11. Суперсервер inetd В поставку большинства систем UNIX, включая Linux, входит суперсервер, который поддерживает широкий набор служб. Вероятно, наибольшую известность получил суперсервер UNIX под названием inetd; версию inetd включают в свои системы большинство поставщиков. Первоначально стимулом к созданию суперсервера inetd явилось стремление создать эффективный механизм, способный обеспечить поддержку множества служб без использования слишком большого объема системных ресурсов. В частности, хотя такие службы TCP/IP, как ECHO и CHARGEN, необходимы для проверки или отладки, они редко применяются в производственной системе. Развертывание сервера для каждой из таких служб требует затрат системных ресурсов (например, резервирования записей в таблице процессов и места в области подкачки). Кроме того, отдельные процессы конкурируют за доступ к памяти, если они работают параллельно. Поэтому объединение серверов в один суперсервер позволяет уменьшить издержки без потери функциональных возможностей. Суперсервер inetd является динамически настраиваемым3; информация о его конфигурации хранится в виде текста ASCII в файле /etc/inetd.conf. Каждая запись в этом файле может включать от шести и более полей, как показано в табл. 15.1. Первые шесть полей являются обязательными и не должны содержать пробельных символов; последние слова в строке представляют собой параметры. Таблица 15.1. Поля записи в файле конфигурации inetd Поле Назначение service name Имя предоставляемой службы (имя должно присутствовать в базе данных служб системы) socket type Тип используемого сокета (должен представлять собой допустимый тип сокета, такой как stream или dgram) protocol Имя протокола, используемого со службой (должен представлять собой допустимый протокол, такой как tcp или udp) wait status Значение wait, которое указывает, что суперсервер inetd должен ждать завершения обработки программой поддержки службы одного запроса, и только после этого приступать к обработке другого, или nowait, которое разрешающего параллельную обработку userid Имя учетной записи пользователя, с правами которого должна работать про- грамма поддержки службы. Специальная учетная запись root предоставляет не- Суперсервер inetd изменяет свою конфигурацию при получении каждого сигнала SIGHUP (т.е. сигнала со значением 1). 208 Глава 15. Мультисервисные серверы (TCP, UDP)
ограниченные привилегии Окончание табл. 15.1 Поле Назначение server Имя программы поддержки службы, которая должна быть вызвана на выполне- program ние, или строка internal, для вызова на выполнение кода поддержки службы, оттранслированного в составе программы суперсервера inetd arguments От нуля и более параметров, передаваемых программе поддержки службы, которая будет вызвана на выполнение суперсервером inetd Сразу после запуска или реконфигурации суперсервер inetd должен создать ведущий сокет для каждой новой службы, указанной в файле конфигурации. Для этого сервер inetd интерпретирует файл конфигурации и извлекает из него значения отдельных полей, Поле с обозначением типа сокета используется для определения того, какой тип сокета (stream или dgram) должен применяться для ведущего сокета. Сервер inetd должен также выполнить привязку к сокету локального порта протокола. Чтобы определить номер порта протокола, сервер inetd извлекает из файла поля с именем службы и протокола и использует их для передачи запроса в базу данных служб операционной системы. База данных возвращает номер порта протокола, предназначенного для использования с этой службой; если база данных служб не содержит записи, соответствующей комбинации значений полей с именем службы и протокола, суперсервер inetd не может поддерживать такую службу. После создания ведущего сокета суперсервер inetd регистрирует остальную информацию из файла конфигурации и ожидает поступления запроса из ведущего сокета. Если какой-то клиент обращается к одной из указанных служб, суперсервер inetd использует зарегистрированную информацию для определения своих дальнейших действий. Например, поле wait status с обозначением режима ожидания определяет, должен ли суперсервер inetd параллельно эксплуатировать сразу несколько копий программы поддержки службы. Если в файле конфигурации в этом поле указано значение nowait, то суперсервер inetd создает новый процесс для каждого поступающего запроса и обеспечивает параллельное выполнение таких процессов. Поскольку суперсервер inetd создает с помощью функции fork дочерний процесс, выполняющий программу службы, при получении каждого запроса создается новый процесс. Процесс inetd, который всегда находится в состоянии готовности, продолжает ожидать поступления запросов через ведущие сокеты. Вообще, значение wait поля wait status показывает, что суперсервер inetd должен обрабатывать запросы к данной службе последовательно (т.е. программа службы должна закончить обработку одного запроса, прежде чем суперсервер inetd запустит следующий процесс для выполнения этой программы). Однако отметим, что при поступлении первого запроса к службе, для которой указан режим ожидания wait, суперсервер inetd создает путем ветвления отдельный процесс для эксплуатации программы данного сервера. Чтобы понять, с чем это связано, необходимо отметить, что процесс inetd не может блокироваться, ожидая окончания работы какой-то службы, поскольку для других служб может потребоваться параллельное выполнение. Чтобы предотвратить запуск нескольких процессов с режимом ожидания wait, в суперсервере inetd просто применяется следующий принцип: не принимать дальнейшие запросы к этой службе до тех пор, пока ее сервер не закончит свою работу; после запуска процесса для конкретной службы суперсервер inetd использует значение поля wait status для определения способа 15.11. Суперсервер inetd 209
дальнейших действий. Если в этом поле указано значение wait, суперсервер inetd удаляет ведущий сокет этой службы из набора сокетов, через которые он принимает запросы на установление соединения. После завершения процесса, поддерживающего данную службу, суперсервер inetd снова добавляет сокет к набору активных сокетов и переходит в состояние ожидания запроса к этой службе. Хотя поле wait status подчеркивает концептуальное различие между последовательным и параллельным выполнением, часто целесообразно выбрать для него значение wait по практическим соображениям. В частности, в службах UDP значение wait используется для служб, которые предусматривают обмен между клиентом и сервером многочисленными дейтаграммами. Такое значение поля wait status исключает возможность использования суперсервером inetd этого сокета до тех пор, пока программа службы, не закончит свою работу. Таким образом, клиент может передавать свои дейтаграммы программе службы без посторонних помех. Как только программа службы завершит свою работу, суперсервер inetd снова приступает к использованию этого сокета. При любом из этих двух указанных режимов ожидания суперсервер inetd использует поле server program в файле конфигурации для определения того, какая программа службы должна быть вызвана на выполнение. Если в этом поле содержится значение internal, суперсервер inetd вызывает для поддержки этой службы внутреннюю процедуру4. В ином случае, суперсервер inetd воспринимает эту строку как имя файла, который должен быть вызван на выполнение. При вызове программы сервера суперсервер inetd передает ей содержимое поля параметров arguments. 15.12. Пример с суперсервером inetd Рассмотрим простую программу, которая позволит пояснить описание конфигурации суперсервера inetd и привести некоторые дополнительные сведения. Предположим, что программист хочет отладить новый сервер для службы DAYTIME. Этот новый сервер должен обеспечить дополнение конфигурации суперсервера inetd. Во- первых, новой службе назначается временное имя, также на время выбирается номер порта протокола и информация добавляется к базе данных системных служб. Например, если программист выберет имя timetest и номер порта протокола 10250, то к файлу /etc/services может быть добавлена следующая запись: timetest 10250/tcp Кроме того, необходимо написать серверную программу. Поскольку задачу создания необходимого сокета и приема входящих запросов на установление соединения выполняет суперсервер inetd, серверная программа не должна содержать кода выполнения таких действий; в сервере достаточно обеспечить обмен данными через одно соединение. Например, файл inetd_daytimed.c содержит код* сервера DAYTIME, который может использоваться с суперсервером inetd: /* Файл inetd_daytimed.с - главная процедура */ linclude <sys/types.h> iinclude <unistd.h> Код нескольких простейших служб встроен в суперсервер inetd для повышения эффективности. 210 Глава 15. Мультисервисные серверы (TCP, UDP)
¦include <stdlib.h> ¦include <string.h> /* * Главная процедура - клиент inetd для службы DAYTIME * _„ _ */ int main(int argc, char *argv[]) { char *pts; time_t now; char *ctime(); (void) time(&now); pts = ctime(&now); (void) write@, pts, strlen(pts))? exit(O); } Как показывает этот пример, для создания простого сервера с установлением логического соединения, вызываемого суперсервером inetd, достаточно предусмотреть лишь небольшой объем кода. После приема входящего запроса на установление соединения суперсервер inetd передает полученное соединение в дескриптор файла 0 перед вызовом сервера на выполнение. Таким образом, в сервере для обмена данными с клиентом всегда используется дескриптор 4). Кроме того, в сервере не содержится код, позволяющий выбрать последовательный или параллельный режим выполнения, поскольку все эти действия выполняет суперсервер inetd. После трансляции кода сервера и размещения полученной выполняемой программы в файле можно изменить конфигурацию суперсервера inetd и включить в нее ссылку на новый сервер. Например, если оттранслированная версия приведенной выше программы находится в файле /pub/inetd_daytimed, то в файл конфигурации /etc/inetd.conf можно ввести следующую запись: timetest stream tcp nowait root /pub/inetd_daytimed inetd_daytimed В этой записи приведено имя службы timetest и указано то, что для нее требуется потоковый сокет stream и протокол tcp. Сервер работает параллельно (nowait), причем от имени пользователя root. И наконец, выполняемая версия сервера находится в файле /pub/inetd^daytimed, а единственным параметром командной строки, передаваемым серверу, является его имя, inetd_daytimed. 15.13. Перечень разновидностей серверов В настоящей главе был представлен целый ряд реализаций мультисервис- ных и мультипротокольных серверов. Сводные данные об этих реализациях приведены в табл. 15.2. Из всех разновидностей мультисервисных проектов наиболее широкое распространение получили суперсерверы (и серверы, основанные на использовании транспортного протокола с установлением логического соединения). /* Указатель на строку со значением времени */ /* Текущее время */ 15.13. Перечень разновидностей серверов 211
Таблица 15.2. Широко распространенные разновидности серверов, рассматриваемые в этой главе Тип Описание Последовательный Применяется редко. Односервисный мультипротокольный. Мультисервисный однопротокольный Параллельный, (обычно) с одним потоком вы- Применяется очень широко, полнения Односервисный мультипротокольный. Мультисервисный однопротокольный. Мультисервисный мультипротокольный Параллельный, с несколькими процессами или Обычно мультисервисный однопротокольный потоками Параллельный, с вызовом на выполнение от- Известен под названием суперсервера. дельных программ Обычно мультисервисный мультипротокольный, имеющий файл конфигурации 15.14. Резюме При проектировании сервера программист может выбирать любую из множества возможных версий. Хотя большинство серверов предоставляет доступ только к одной службе, программист может выбрать мультисервисную реализацию для сокращения числа серверов, которые должны быть вызваны на выполнение. В большинстве мультисервисных серверов используется один транспортный протокол. Однако для объединения нескольких служб с установлением и без установления логического соединения в одном сервере может применяться несколько транспортных протоколов. И наконец, программист может выбрать для реализации параллельный мультисервисный сервер с параллельными потоками или процессами или с одним потоком выполнения, в котором используется асинхронный ввод/вывод для обеспечения псевдопараллельной обработки. Пример сервера, представленный в данной главе, показывает, как может применяться асинхронный ввод/вывод в мультисервисном сервере для замены целого ряда ведущих серверов. В рассматриваемом сервере вызывается примитив операционной системы select для перехода в состояние ожидания активизации любого из ведущих сокетов. Суперсерверы могут быть настраиваемыми статически или динамически. Статическое определение конфигурации происходит при вызове суперсервера на выполнение; динамическое определение конфигурации происходит во время работы суперсервера. Динамическая смена конфигурации (реконфигурация) позволяет системному администратору изменять набор предоставляемых служб без перетрансляции или перезапуска суперсервера. Суперсервер inetd операционной системы Linux обеспечивает динамическую реконфигурацию. Материал для дальнейшего изучения Суперсервер inetd описан в разделе 8 руководства программиста Linux Programmers Manual. В этом документе описана также синтаксическая структура записей /etc/inetd.conf — файла конфигурации inetd. 212 Глава 15. Мультисервисные серверы (TCP, UDP)
Упражнения 15.1. Если параллельный мультисервисный сервер с установлением логического соединения поддерживает всего К служб, то какое максимальное число сокетов должно в нем использоваться? 15.2. Какое число сокетов может быть создано отдельным процессом в вашей локальной компьютерной системе? 15.3. Рассмотрим однопотоковую реализацию мультисервисного сервера. Напишите алгоритм, который показывает, как сервер обслуживает соединения. 15.4. Введите поддержку службы UDP в программу мультисервисного сервера, представленную в качестве примера в этой главе. 15.5. Прочитайте документ RFC 1288 для получения информации о службе FINGER. Введите поддержку службы FINGER в программу мультисервисного сервера, представленную в качестве примера в этой главе. 15.6. Спроектируйте суперсервер, который обеспечивает добавление новых служб без его перетрансляции или перезапуска процессов. 15.7. Для каждого из проектов последовательных и параллельных мульти- сервисных серверов, описанных в данной главе, напишите формулу для определения максимального числа сокетов, распределяемых сервером. Представьте свой ответ как функцию числа предоставляемых служб и числа параллельно обрабатываемых запросов. 15.8. В чем состоит основной недостаток суперсервера, который выполняет ветвление процесса с помощью функции fork для обслуживания каждого соединения, а затем с помощью функции execve вызывает на выполнение программу, поддерживающую данную службу? 15.9. Рассмотрите файл конфигурации суперсервера inetd на любом компьютере Linux, чтобы узнать, какие службы он предоставляет. 15.10. Прочитайте справочное руководство с описанием программы inetd и ее файла конфигурации. Может ли суперсервер inetd поддерживать программы RPC? 15.11. Прочитайте справочное руководство с описанием файла конфигурации программы inetd. Объясните, что означает строка %А в поле параметров. В каких случаях она используется? Упражнения 213
16 Единообразное и эффективное управление параллельной работой сервера 16.1. Введение В начальных главах были представлены конкретные проекты серверов и показано, как в каждом проекте используется последовательная или параллельная обработка. В предыдущей главе описано, как можно объединить некоторые из этих проектов для создания мультисервисного сервера. В настоящей главе параллельные серверы рассматриваются в более широком контексте. В ней исследуются проблемы, связанные с проектированием серверов, и описано несколько методов управления параллельной работой, которые могут применяться во многих описанных выше проектах. Эти методы способствуют расширению области применения проекта и позволяют разработчику оптимизировать производительность сервера. Хотя два представленных подхода могут на первый взгляд показаться противоположными друг другу, каждый из них позволяет повысить производительность сервера в определенных обстоятельствах. Кроме того, в данной главе показано, что оба эти метода основаны на одном и том же принципе. 16.2. Выбор между последовательным и параллельным проектами Проекты серверов, описанные ранее, подразделяются на две категории: обрабатывающие запросы последовательно и выполняющие обработку запросов параллельно. Как показано в предыдущих главах, разработчик должен сделать обоснованный выбор между этими двумя основными подходами, прежде чем приступить к созданию сервера. Выбор между последовательной и параллельной реализациями является самым важным, поскольку от него зависит вся структура программы, время отклика и способность сервера обрабатывать несколько запросов. Если разработчик примет неправильное решение на раннем этапе процесса проектирования, то стоимость внесения исправлений может быть весьма велика, поскольку ему придется переделать значительную часть программы. Как узнать, будет ли обеспечиваться параллельная работа? Как выбрать оптимальный проект сервера? А еще важнее то, как оценить предполагаемую нагрузку или время обслуживания? На эти вопросы ответить сложно, поскольку
сети быстро развиваются; опыт показывает, что сети растут быстро и непредсказуемо. Как только пользователи узнают о какой-то услуге, интенсивность доступа к ней стремительно увеличивается. По мере увеличения числа пользователей растет потребность в отдельных серверах. Наряду с этим появление новых технологий и программных продуктов способствует непрерывному улучшению связи и повышению скорости обработки. Однако развитие средств передачи и обработки данных не всегда происходит с одинаковой скоростью. Трудно себе представить, как можно сегодня, когда все так быстро меняется, принять такое важное решение по выбору основного подхода. Это решение обычно основано на опыте и интуиции: разработчик строит наилучший возможный прогноз на основе изучения новейших тенденций. По сути, выбор оптимального проекта осуществляется на основе изучения последних достижений и формирования прогноза на ближайшее будущее. Безусловно, разработчики могут сделать свой выбор только с определенной степенью достоверности; поскольку технологии развиваются, а потребности пользователей изменяются, перед разработчиком может возникнуть необходимость пересмотреть свое решение или даже выбрать другой подход. Выбор между последовательным и параллельным проектами сервера может оказаться сложным, поскольку потребности пользователей, возможности связи и обработки быстро меняются. Большинство разработчиков, выбирая тот или иной подход, строят прогноз на основе изучения новейших тенденций. 16.3. Степень распараллеливания Рассмотрим одну из характеристик реализации параллельного сервера: допустимую степень распараллеливания. Как было указано выше, степень распараллеливания работы сервера характеризуется как общее число потоков выполнения, которые могут работать в сервере в любое указанное время1. Степень распараллеливания меняется со временем, по мере того, как сервер создает потоки для обработки входящих запросов на установление соединения или ведомый поток заканчивает обслуживание соединения и завершает свою работу. Программистов и системных администраторов обычно не интересует, какова степень распараллеливания в любой момент времени, но для них важно то, какая максимальная степень распараллеливания может быть достигнута сервером в процессе его функционирования. Лишь немногие из представленных до сих пор проектов предусматривают определение максимальной степени распараллеливания сервера. Большинство проектов допускает создание ведущим процессом сервера любого числа ведомых параллельных процессов для обработки всех входящих запросов. Обычно в параллельном сервере с установлением логического соединения создается по одному ведомому процессу (или потоку) для каждого запроса на установление соединения, полученного от клиента. Безусловно, на практике сервер не может поддерживать произвольное число соединений. Б каждой реализации протокола TCP установлено ограничение на число возможных активных соединений, а каждая операционная система накладывает ограничение на объем доступных ресурсов, таких как число потоков (система должна ограничивать либо число потоков, доступных для каждого пользователя, либо общее число доступ- Напомним, что параллельные потоки могут входить в состав одного и того же процесса (так называемая многопотоковая реализация) или каждый поток может быть связан с отдельным процессом (так называемая однопотоковая реализация). Максимальная степень распараллеливания может регламентироваться и в том, и в другом случае. 216 Глава 16. Единообразное и эффективное управление...
ных потоков). После достижения сервером одного из этих пределов система будет отклонять его требования на выделение дополнительных ресурсов. В целях расширения области применения сервера многие программисты стараются не устанавливать постоянный максимальный предел степени распараллеливания в своей программе. Бели в коде сервера не установлена заранее определенная максимальная степень распараллеливания, то одна и та же реализация может применяться при самых разных обстоятельствах (например, и в тех случаях, если не требуется значительное распараллеливание, и в тех случаях, если количество клиентов очень велико). Программисту не приходится корректировать код или перетранслировать свою программу при перемещении сервера из одной прикладной среды в другую. Однако серверы, в которых не заданы ограничения на степень распараллеливания, могут создавать предпосылки аварии в тех случаях, если нагрузка слишком велика. Дело в том, что степень распараллеливания может возрасти настолько, что операционная система серверного компьютера фактически не сможет функционировать. 16.4. Распараллеливание с учетом потребностей В целях расширения области применения в большинстве проектов параллельных серверов, представленных в предыдущих главах, для активизации операции повышения степени распараллеливания используются входящие запросы на установление соединения. Мы называем такие схемы учитывающими потребности и при описании их работы указываем, что их степень распараллеливания возрастает в зависимости от потребностей. Серверы, степень распараллеливания которых возрастает с учетом потребностей, могут оказаться наиболее эффективными, поскольку они не требуют таких системных ресурсов, как потоки или буферы, до тех пор, пока в них не возникает необходимость. Поэтому серверы, управляемые с учетом потребностей, не захватывают ресурсы без необходимости. Кроме того, серверы, управляемые с учетом потребностей, обеспечивают достаточно низкое наблюдаемое время отклика, поскольку могут обрабатывать сразу несколько запросов, не ожидая завершения обработки одного запроса перед переходом к следующему. 16.5. Издержки распараллеливания Хотя в целом стремление обеспечить распараллеливание с учетом потребностей можно только приветствовать, варианты программ, представленные в предыдущих главах, не всегда позволяют добиться оптимальных результатов. Чтобы понять, с чем это связано, необходимо изучить все тонкости создания и планирования потоков или процессов, а также подробно рассмотреть работу сервера. Основной вопрос состоит в том, как в данном случае оценивать затраты и полученные результаты. В частности, необходимо определить преимущества и недостатки распараллеливания. 16.6. Издержки и задержки Во всех проектах серверов, рассматриваемых в предыдущих главах, мерой потребности считалось число входящих запросов на установление соединения, и эти запросы служили для оптимизации операции повышения степени распараллеливания. Ведущий^ сервер ожидал поступления запроса и создавал для его обработки новый ведомый процесс или поток сразу после получения запроса. Поэтому степень распараллеливания в любой момент времени отражала число, полученных сервером запросов, обработка которых не была закончена. 16.4. Распараллеливание с учетом потребностей 217
При всей внешней простоте серверной программы, управляемой с учетом потребностей, создание ведомого потока или процесса для обработки каждого нового запроса может оказаться весьма дорогостоящим. Вне зависимости от того, используется ли в сервере транспортный протокол с установлением или без установления логического соединения, операционная система должна информировать ведущий процесс о том, что поступило сообщение или запрос на установление соединения. Затем ведущий процесс должен потребовать от операционной системы создания ведомого процесса. На получение запроса из сети и создание ведомого процесса может потребоваться немало времени. Создание процесса или потока не только задерживает обработку запроса, но и потребляет системные ресурсы. Поэтому в обычном однопроцессорном компьютере сервер будет простаивать в течение всего того времени, пока операционная система создает новый ведомый процесс и переключает контекст. 16.7. Замедления, вызванные малыми задержками Можно ли проигнорировать небольшие задержки, возникающие при создании новых процессов? Как показано на рис. 16.1, если запрос не требует продолжительной обработки, то значимость таких задержек возрастает. Обработка запроса 1 Обработка запроса 2 Параллельный Создание ведомого процесса 1 Создание ведомого процесса 2 2с 2с+р (а) Последовательный - Обработка запроса 1 Обработка запроса 2 2р (б) Рис. 16.1. Затраты времени на обработку двух запросов в параллельном сервере (а) и в последовательном сервере (б) На этом рисунке приведен пример, в котором затраты времени на обработку запроса меньше, чем затраты времени на создание нового процесса. Обозначим как р время обработки, а буквой с обозначим затраты времени на создание процесса. Предположим, что в момент времени 0 одновременно поступают два запроса. В параллельном сервере обработка первого запроса будет выполнена через е*р единиц времени, а второй запрос будет выполнен через 2сМ-р единиц времени. Поэтому в этом сервере затраты времени на обработку одного запроса составляют в среднем Зс/2+р единиц времени. В последовательном сервере обработка первого запроса закончится через р, а второго — через 2р единиц времени; таким образом, средние затраты времени на обработку одного запроса составляют Зр/2 единиц времени. Итак, проект последовательного сервера характеризуется более низкой средней задержкой по сравнению с параллельной версией, поскольку для обработки в нем запроса требуется меньше времени, по сравнению с затратами времени на создание процесса. Небольшая дополнительная задержка может показаться незначительной при рассмотрении лишь нескольких запросов. Однако такая задержка может стать существенной, если рассматривается непрерывная работа сервера под интенсивной нагрузкой. Если большое число запросов поступает примерно в одно и то же время, им приходится ждать, пока сервер не создаст процессы для их 218 Глава 16. Единообразное и эффективное управление...
обработки. Если дополнительные запросы поступают быстрее, чем сервер может их обработать, задержки увеличиваются. В ближайшей перспективе, небольшие задержки в сервере влияют на наблюдаемое время отклика, но не на общую производительность. Если пакеты запросов поступают одновременно или почти одновременно, то программное обеспечение протокола, входящее в состав операционной системы, помещает запросы в очередь до того момента, пока сервер не сможет их извлечь и обработать. Например, если в сервере используется транспортный протокол с установлением логического соединения, то программное обеспечение TCP ставит в очередь запросы на установление соединения. Если же в сервере используется транспортный протокол без установления логического соединения, то программное обеспечение UDP ставит в очередь входящие дейтаграммы. В долгосрочной перспективе, чрезмерные задержки могут вызвать потерю запросов. Чтобы понять, как это происходит, представим себе сервер, которому требуется с единиц времени для создания ведомого процесса, но только р единиц времени (р<с) для обработки запроса. Параллельная реализация сервера позволяет обрабатывать в среднем 1/с запросов за единицу времени, а последовательная версия — 1/р запросов. Проблема возникает, если скорость поступления запросов превышает 1/с, но остается меньше 1/р. В этом случае последовательная реализация может выдержать нагрузку, а параллельная реализация требует слишком много времени на создание ведомых процессов. В параллельной версии очереди в программном обеспечении протокола в конечном итоге заполняются, и программное обеспечение начинает отбрасывать следующие запросы. На практике лишь немногие серверы действуют почти на пределе максимальной производительности. Более того, лишь немногие разработчики используют параллельные серверы, если стоимость создания ведомых процессов превышает стоимость обработки. Поэтому во многих приложениях не возникают задержки при обработке запросов и не происходит потеря запросов. Однако в серверах, предназначенных для обеспечения оптимального времени отклика при интенсивной нагрузке, необходимо рассмотреть возможность применения других вариантов по отношению к распараллеливанию с учетом потребностей. 16.8. Предварительное создание ведомых потоков или процессов В тех случаях, когда затраты времени на создание процесса являются значительными, может применяться простой метод уменьшения задержки, ограничения максимальной степени распараллеливания и обеспечения высокой производительности параллельных серверов. В основе этого метода лежит предварительное создание параллельных процессов или потоков, что позволяет исключить издержки их создания в процессе работы. После их создания потоки продолжают функционировать (т.е. не завершают свою работу). При использовании метода предварительного создания разработчик предусматривает создание в программе ведущего сервера N ведомых процессов или потоков с началом ее выполнения. Каждый ведомый процесс использует средства, доступные в операционной системе, для перехода в режим ожидания поступления запроса. После поступления запроса один из ожидающих ведомых процессов начинает выполнение и обрабатывает этот запрос. После завершения обработки запроса ведомый процесс не заканчивает свою работу, а возвращается к выполнению кода, предусматривающего ожидание очередного запроса. Основное преимущество предварительного создания обусловлено снижением издержек операционной системы. Поскольку серверу не приходится ждать соз- 16.8. Предварительное создание ведомых потоков или процессов 219
дания ведомого процесса после поступления запроса, он обрабатывает запросы быстрее. Этот метод приобретает особое значение, если на обработку запроса требуется выполнение более продолжительных операций ввода/вывода по сравнению с вычислениями. Метод предварительного создания позволяет операционной системе сервера переключить процессор на другой ведомый процесс и приступить к обработке следующего запроса, в то время как предыдущий запрос ожидает завершения связанных с ним операций ввода/вывода. При использовании метода предварительного создания сервер создает одновременно работающие ведомые процессы во время своего запуска на выполнение. Метод предварительного создания позволяет уменьшить задержку, вносимую сервером, поскольку исключает затраты на создание процесса при поступлении каждого запроса и позволяет совмещать обработку одного запроса с операциями ввода/вывода, связанными с выполнением другого запроса. Хотя применение постоянных ведомых процессов позволяет уменьшить стоимость создания процессов, такой метод имеет свои недостатки: программисты обязаны соблюдать крайнюю осторожность при использовании ресурсов. Чтобы понять, с чем это связано, рассмотрим постоянный ведомый процесс, в котором предусмотрено распределение небольшого объема памяти при поступлении каждого запроса, но после завершения обработки запроса освобождение памяти не происходит из-за ошибки в программе. Хотя такая проблема может проявиться не сразу, со временем данный ведомый процесс может исчерпать все адресное пространство. 16.8.1. Предварительное создание ведомых процессов и потоков в системе Linux Такие операционные системы, как Linux, в которых предусмотрена поддержка потоков, упрощают взаимодействие между ведущим потоком и предварительно созданными ведомыми потоками, поскольку ведущий поток и ведомые потоки могут совместно использовать память. Даже если ведомые компоненты функционируют как отдельные процессы, не имеющие общей памяти с ведущим процессом, предварительное создание процессов остается осуществимым. Координация работы между процессами выполняется через разделяемые сокеты. После вызова в процессе функции fork вновь созданный дочерний процесс получает копию всех открытых дескрипторов, включая те из них, что соответствуют сокетам. Для обеспечения возможности совместного использования сокета ведущий процесс открывает необходимый сокет перед предварительным созданием ведомых процессов. В частности, ведущий процесс сервера после запуска открывает сокет для общепринятого порта, через который будут поступать запросы. Затем ведущий процесс вызывает функцию fork для создания необходимого числа ведомых процессов. Поскольку каждый процесс получает все копии дескрипторов сокетов от родительского, все ведомые процессы имеют доступ к сокету общепринятого порта. В следующих разделах приведены подробные сведения об использовании метода предварительного создания в серверах с установлением и без установления логического соединения. 16.8.2. Применение метода предварительного создания в сервере с установлением логического соединения Если в параллельном сервере для связи используется протокол TCP, то степень распараллеливания зависит от числа активных соединений. Каждый входящий запрос на установление соединения должен быть обработан независимым 220 Глава 16. Единообразное и эффективное управление...
процессом. К счастью, в системе Linux предусмотрена возможность применения мьютекса для координации работы нескольких процессов, одновременно пытающихся получить доступ к соединению в одном и том же сокете. Каждый ведомый процесс вызывает функцию accept, которая блокируется в ожидании поступления входящего запроса на установление соединения через общепринятый порт. После поступления запроса на установление соединения операционная система разблокирует один и только один из ведомых процессов. В отдельном ведомом процессе после возврата управления функция accept предоставляет новый дескриптор файла для входящего соединения. Ведомый процесс обслуживает соединение, закрывает новый сокет, а затем вызывает функцию accept для перехода в состояние ожидания следующего запроса. Схема организации процессов показана на рис. 16.2. В этом примере показано три предварительно созданных ведомых процессах, но лишь один из них фактически обслуживает соединение. Ведущий процесс открывает сокет для общепринятого порта, но не использует его. Предварительно щ распределенные ведомые процессы Операционная ^ система Рис. 16.2. Схема организации процессов в параллельном сервере с установлением логического соединения и предварительным созданием ведомых процессов Как показано на этом рисунке, все ведомые процессы наследуют права доступа к сокету для общепринятого порта. Каждый отдельный ведомый процесс получает новый сокет для отдельного соединения после возврата управления из функции accept. Хотя ведущий процесс создает сокет, соответствующий общепринятому порту, он не использует сокет для выполнения каких-либо дальнейших действий. Пунктирная линия на этом рисунке обозначает, где заканчивается использование сокета ведущим процессом и начинается его использование ведомыми процессами. Хотя рис. 16.2 показывает, что ведущий процесс функционирует одновременно с ведомыми, он также подчеркивает, что разница между ведущим и ведомым процессами при такой организации работы становится более размытой. По существу, ведущий процесс после предварительного создания ведомых процессов больше не выполняет никаких функций. Поэтому ведущий процесс может просто завершить свою ра- Сокет для запросов на установление соединения Сокеты для отдельных соединений (созданные функцией accept) 16.8. Предварительное создание ведомых потоков или процессов 221
боту после запуска всех ведомых процессов2. В остроумной программе можно было бы даже предусмотреть создание ведущим процессом всех ведомых процессов, кроме последнего. Затем ведущий процесс может стать последним ведомым процессом и сэкономить тем самым затраты на создание еще одного процесса. В системе Linux для выполнения такого действия требуется простейший код. 16.8.3. Мьютекс, блокировка файла и параллельные вызовы функции accept Предварительное создание ведомых процессов особенно просто осуществляется в системе Linux, поскольку эта операционная система различает между собой параллельные процессы, вызывающие функцию accept с конкретным сокетом. Бще более важно то, что система Linux эффективно обрабатывает параллельные вызовы. Поэтому каждый из предварительно созданных ведомых процессов наследует дескриптор ведущего сокета, и каждый ведомый процесс вызывает функцию accept. Система оставляет все ведомые процессы в заблокированном состоянии до тех пор, пока не поступит запрос на установление соединения. В этот момент система разблокирует один и только один ведомый процесс. К сожалению, не во всех версиях UNIX предусмотрена такая же организация работы, как в Linux. В некоторых версиях UNIX не допускается параллельный вызов функции accept: после того как один процесс вызовет функцию accept с некоторым сокетом, все последующие вызовы возвращают сообщения об ошибке. Другие версии UNIX допускают параллельные вызовы, но обрабатывают их неэффективно. В частности, при поступлении нового запроса на установление соединения эти версии операционной системы предусматривают разблокирование сразу всех процессов, в которых была вызвана функция accept. Новое соединение получает тот процесс, который был активизирован первым; после их активизации все остальные процессы обнаруживают, что отсутствует доступное соединение, и возвращаются в заблокированное состояние. Поэтому если заблокировано К процессов, то активизация JC-1 из них приводит к непроизводительным затратам ресурсов. Каждый активизированный процесс требует переключения контекста; на это расходуется процессорное время, после чего процесс обнаруживает, что запрос на установление соединения был уже принят, и возвращается в заблокированное состояние. Может ли метод предварительного создания использоваться в операционной системе, которая не обеспечивает эффективного параллельного выполнения функции accept? Ответ на этот вопрос является положительным. В этом случае вместо, применения параллельных вызовов функции accept, в программе необходимо использовать разделяемый мьютекс или блокировку файла (например, с применением функции flock) для обеспечения того, чтобы только один ведомый процесс одновременно вызывал функцию accept. Например, при использовании мьютекса каждый ведомый процесс вызывает функцию pthreadjnutex_lock перед вызовом функции accept, а после ее вызова — функцию pthreadjnutexjmlock. В любой момент все ведомые процессы, кроме одного, блокируются в вызове функции pthread_mutex_lock, а один ведомый процесс продолжает выполняться и переходит к вызову функции accept. После возврата управления из функции accept выполняющийся ведомый процесс вызывает функцию pthreadjnutexjmlock, позволяя продолжить работу одному и только одному 2 На практике ведущий процесс может использоваться для уничтожения процессов-зомби, если в нем выполняется цикл для повторного вызова системной функции wait3. Бели же ведущий процесс завершит свою работу, то родительским для ведомых процессов становится системный процесс init. 222 Глава 16. Единообразное и эффективное управление...
ведомому процессу, в результате чего в любое время функцию accept вызывает только один процесс, а системные ресурсы используются эффективно. 16.8.4. Применение метода предварительного создания в сервере без установления логического соединения Если в параллельном сервере используется транспортный протокол без установления логического соединения, то степень распараллеливания зависит от числа поступающих запросов. Каждый входящий запрос поступает в отдельной дейтаграмме UDP, и ему должен быть назначен отдельный поток выполнения. В проектах параллельных серверов без установления логического соединения обычно предусматривается создание ведущим сервером отдельного ведомого процесса при поступлении запроса. Система Linux позволяет осуществлять в сервере без установления логического соединения предварительное создание ведомых процессов с использованием такого же метода предварительного создания, который применяется в серверах с установлением логического соединения. Схема организации процессов приведена на рис. 16.3. На этом рисунке показано три ведомых процесса, выполняющих чтение из сокета для общепринятого порта. Каждый входящий запрос передается только одному ведомому процессу. Предварительно "^— распределенные ведомые процессы Операционная "^ система Рис. 16.3. Схема организации процессов параллельного сервера без установления логического соединения, в котором предварительно создано несколько ведомых процессов Как показано на этом рисунке, каждый ведомый процесс наследует сокет общепринятого порта. Поскольку связь происходит без установления логического соединения, все ведомые процессы могут использовать один и тот же сокет не только для приема входящих запросов, но и для передачи ответов. В ведомом процессе для получения адреса отправителя, а также дейтаграммы от него вызывается функция recvf rom; для передачи ответа вызывается функция sendto. Как и в сервере с установлением логического соединения, в котором применяется метод предварительного создания, ведущий процесс в сервере без установления логического соединения после открытия сокета для общепринятого порта и предварительного создания ведомых процессов не выполняет почти никаких функций. Поэтому программист может либо завершить этот процесс, либо 16.8. Предварительное создание ведомых потоков или процессов 223 Сокет для общепринятого порта
преобразовать его в последний ведомый процесс для уменьшения издержек, связанных с созданием последнего процесса. 16.8.5. Метод предварительного создания, неравномерный трафик и система NFS Как показывает опыт, в большинстве реализаций протокола UDP не предусмотрено создание больших очередей для входящих дейтаграмм, поэтому большие пакеты входящих запросов могут легко переполнить очередь. Программное обеспечение UDP просто отбрасывает дейтаграммы, которые поступают после переполнения очереди получателя, поэтому неравномерный трафик может стать причиной потери дейтаграмм. Решение проблемы переполнения очереди может стать особенно сложным, поскольку программное обеспечение UDP часто входит в состав самой операционной системы. Поэтому прикладные программисты не могут вносить в него изменения. Однако они могут предусмотреть предварительное создание ведомых процессов. Для устранения потерь дейтаграмм обычно достаточно применить метод предварительного создания. Метод предварительного создания используется для предотвращения потери дейтаграмм во многих версиях системы NFS. Изучение успешно работающих систем NFS показывает, что в них предусмотрен целый ряд предварительно созданных серверных процессов, выполняющих чтение из одного и того же сокета UDP. По существу, именно в зависимости от того, используется ли в конкретной реализации NFS метод предварительного создания, можно судить, является ли данная реализация работоспособной. 16.8.6. Применение метода предварительного создания в многопроцессорной системе Метод предварительного создания в многопроцессорной системе имеет особое значение. Он позволяет разработчику согласовать степень распараллеливания в сервере с возможностями аппаратных средств. Если на компьютере установлено К процессоров, то разработчик может предусмотреть предварительное создание К ведомых процессов. Поскольку многопроцессорные операционные системы назначают каждому ведомому процессу отдельный процессор, метод предварительного создания обеспечивает согласование степени распараллеливания с возможностями аппаратного обеспечения. После поступления запроса операционная система передает его одному из предварительно созданных ведомых процессов и назначает этому процессу один из процессоров. Поскольку ведомый процесс уже был предварительно создан, то для его запуска не требуется много времени. Поэтому операционная система может быстро передавать запросы на выполнение. При поступлении пакета запросов каждый процессор обрабатывает один запрос, что приводит к достижению максимально возможного быстродействия. 16.9. Отсроченное создание ведомых процессов Хотя метод предварительного создания способствует повышению эффективности, он не решает всех проблем. Любопытно то, что при некоторых обстоятельствах эффективность работы можно повысить с использованием противоположного подхода, а именно метода отсроченного создания ведомых процессов. Чтобы понять, почему может возникнуть необходимость отложить создание ведомых процессов, следует вспомнить, что создание ведомого процесса требует времени и ресурсов, и поэтому может быть оправдано только в том случае, если приведет к 224 Глава 16. Единообразное и эффективное управление...
некоторому повышению производительности системы или к уменьшению задержки. Создание ведомого процесса не только требует времени, но и порождает дополнительную нагрузку на те компоненты операционной системы, которые должны управлять еще одним процессом или потоком. Кроме того, предварительное создание нескольких ведомых процессов, предпринимающих попытки получения входящих запросов, может увеличить нагрузку сетевого программного обеспечения. Как уже было сказано выше, распараллеливание позволяет уменьшить задержку только в том случае, если затраты на создание ведомого процесса меньше по сравнению с затратами на обработку запроса. Если стоимость обработки запроса ниже, то последовательное решение является более целесообразным. Однако не всегда есть возможность определить, какие из этих затрат будут меньше, поскольку затраты часто зависят от самого запроса (например, от содержания запроса зависит продолжительность поиска в базе данных). Кроме того, необходимо учитывать вероятность обнаружения ошибки в запросе. Чтобы понять, с чем это связано, рассмотрим, как работает основная часть серверного программного обеспечения. Сразу после поступления запроса серверная программа проверяет сообщение для определения того, содержат ли все его поля допустимые значения, и имеет ли клиент право присылать запрос. Проверка может потребовать лишь несколько микросекунд или может повлечь за собой дополнительный обмен данными по сети, который займет значительно больше времени. С одной стороны, если сервер обнаружит ошибку в сообщении, он быстро отклонит этот запрос, в результате чего общие затраты времени на обработку сообщений будут настолько малы, что ими вполне можно пренебречь. С другой стороны, если сервер получит допустимый запрос, то на его обработку может уже потребоваться значительное время. В тех случаях, если время обработки невелико, параллельная обработка не оправдана; последовательный сервер характеризуется меньшей задержкой и более высокой производительностью. Как могут разработчики оптимизировать задержку и производительность, не зная, при каких обстоятельствах будет оправдана параллельная обработка? Ответом может служить применение метода отсроченного создания ведомых процессов. В основе этого метода лежит простой принцип: вместо выбора последовательного или параллельного проекта необходимо предусмотреть в сервере измерение стоимости обработки и принятие решения о переходе к последовательной обработке или параллельной обработке в зависимости от полученных результатов. Переход от одного режима к другому происходит динамически, поскольку ситуация изменяется от одного запроса к другому. Для реализации динамического метода отсроченного создания ведомых процессов в серверах обычно предусматривается оценка стоимости обработки путем измерения затрат времени. Ведущий сервер получает запрос, устанавливает таймер и приступает к последовательной обработке запроса. Если сервер завершает обработку запроса до истечения установки таймера, то сервер сбрасывает таймер. Если установка таймера истекает до того, как сервер заканчивает обработку запроса, сервер создает ведомый процесс и позволяет ему продолжить обработку запроса. При использовании метода отсроченного создания ведомых процессов сервер вначале приступает к последовательной обработке каждого запроса и создает параллельный ведомый процесс для обработки конкретного запроса, только если его обработка требует значительных затрат времени. Поскольку создание ведомого процесса откладывается, это позволяет ведущему процессу проверять ошибки и обрабатывать короткие запросы, прежде чем появится необходимость создать ведомый процесс или переключить контекст. 16.9. Отсроченное создание ведомых процессов 225
В системе Linux метод отсроченного создания ведомых процессов реализуется очень просто. Поскольку в этой системе предусмотрен сигнальный механизм, ведущий поток может установить таймер и предусмотреть выполнение определенной процедуры по истечении установки таймера. Поскольку функция fork системы Linux позволяет вновь созданному процессу унаследовать открытые соке- ты, а также копию выполняемой программы и данных от родительского процесса, то ведущий процесс может создать ведомый процесс, который продолжит обработку именно с той точки программы, в какой находился ведущий процесс в момент истечения установки таймера. 16.10. Единая основа обоих методов На первый взгляд может показаться, что методы предварительного создания и отсроченного создания ведомых процессов не имеют ничего общего. По сути, они кажутся полностью противоположными. Однако эти два метода имеют между собой большое сходство, поскольку оба вытекают из одного концептуального принципа: одна из возможностей повышения производительности некоторых параллельных серверов состоит в ослаблении причинно-следственной связи между поступлением запроса и созданием ведомого процесса. Метод предварительного создания предусматривает повышение степени распараллеливания сервера до поступления запросов, а метод отсроченного создания увеличивает степень распараллеливания сервера после их поступления. Этот принцип можно обобщить следующим образом. Методы предварительного создания и отсроченного создания вытекают из одного принципа: ослабляя непосредственную зависимость степени распараллеливания сервера от числа активных в настоящее время запросов, можно расширить область применения сервера и повысить его эффективность. 16.11. Объединение методов Методы отсроченного создания и предварительного создания ведомых процессов могут применяться в сочетании друг с другом. Сервер может приступить к работе без предварительно созданных ведомых процессов и действовать по методу отсроченного создания. Он ожидает поступления запроса и создает ведомый процесс, только если обработка занимает много времени (т.е. если истекает установка таймера). Однако после его создания ведомый процесс не должен немедленно завершать работу; может быть предусмотрено его дальнейшее функционирование. После обработки одного запроса ведомый процесс может ждать поступления следующих входящих запросов. При организации работы такой комбинированной системы основные сложности связаны с необходимостью управлять степенью распараллеливания. Дело в том, что легко определить момент, когда возникает необходимость в создании дополнительных ведомых процессов, но гораздо сложнее определить, когда какой-то ведомый процесс должен завершить свою работу, а не ждать поступления следующих запросов. Одно из возможных решений состоит в том, чтобы ведущий процесс устанавливал максимальный коэффициент тиражирования ведомого процесса М при его создании. Ведомый процесс может создавать вплоть до М дополнительных ведомых процессов, но каждому из них разрешено создавать нуль дополнительных процессов. Таким образом, система начинает работу с вызова на выполнение только одного ведущего потока, но в конечном счете достигает определенного максимального уровня распараллеливания. Еще один метод управления степенью распараллеливания может преду - 226 Глава 16. Единообразное и эффективное управление...
сматривать завершение работы ведомого процесса по истечении определенного периода простоя. Ведомый процесс запускает таймер, переходя в состояние ожидания следующего запроса. Если установка таймера истекает до поступления запроса, ведомый процесс завершает свою работу. Если ведомые компоненты сервера реализованы как потоки одного процесса, они для координации своих действий могут использовать средства разделяемой памяти. В разделяемой памяти может храниться переменная, в которой регистрируется степень распараллеливания любого экземпляра сервера, и значение этой переменной может использоваться ведомым потоком для определения того, должен ли он продолжить свое функционирование или завершить работу после обработки запроса3. В системах, позволяющих приложению определить число запросов, поставленных в очередь в сокете, ведомый процесс может также использовать данные о длине очереди для принятия решения о том, достигнута ли максимально допустимая степень распараллеливания. 16.12. Резюме Для повышения производительности параллельного сервера могут применяться два основных метода: предварительное создание и отсроченное создание ведомых процессов. Метод предварительного создания позволяет оптимизировать задержку, поскольку предусматривает создание ведомых процессов еще до того, как в них возникает необходимость. Ведущий сервер открывает сокет для применяемого им общепринятого порта, а затем выполняет предварительное создание всех необходимых ведомых процессов или потоков. Поскольку ведомые процессы наследуют этот сокет, каждый из них может ожидать поступления запросов. Операционная система передает каждый входящий запрос одному из ведомых процессов. Метод предварительного создания имеет большое значение для параллельных серверов без установления логического соединения, поскольку в них затраты времени на обработку запроса обычно невелики, поэтому издержки создания потока или процесса по сравнению с ними являются весьма значительными. Метод предварительного создания позволяет также разрабатывать эффективные проекты параллельных серверов без установления логического соединения для мультипроцессорных систем. В методе отсроченного создания используется подход, предусматривающий создание ведомого процесса только в случае необходимости. Ведущий сервер приступает к последовательной обработке каждого запроса, но с этого момента устанавливает таймер. Он создает параллельный ведомый процесс для обработки запроса, если установка таймера истекает до завершения работы ведущим процессом. Метод, отсроченного создания успешно применяется в тех случаях, если продолжительность обработки зависит от запроса или сервер должен проверять правильность запроса (в частности, проверять права доступа клиента). Метод отсроченного создания позволяет исключить издержки создания ведомого процесса при обработке коротких запросов или запросов, которые содержат ошибки. Хотя эти методы оптимизации работы сервера могут показаться взаимно противоположными, оба они вытекают из одного основного принципа: ослабление жесткой причинно-следственной связи между степенью распараллеливания работы сервера и числом запросов, ожидающих обработки, способствует повышению производительности сервера. Безусловно, для предотвращения возможности внести изменения в это значение одновременно двумя ведомыми потоками должен использоваться мьютекс. 16.12. Резюме 227
Материал для дальнейшего изучения Сетевая файловая система (NFS — Network File System) описана в главах 24 и 25. Метод предварительного создания используется во многих реализациях NFS в целях предотвращения потери запросов. Упражнения 16.1. Доработайте один из серверов, представленных в качестве примера в предыдущих главах таким образом, чтобы в нем использовался метод предварительного создания. Насколько изменится в данном случае производительность сервера? 16.2. Доработайте один из серверов, представленных в качестве примера в предыдущих главах, таким образом, чтобы в нем использовался метод отсроченного создания. Насколько изменится в данном случае производительность сервера? 16.3. Проверьте на многопроцессорном компьютере сервер без установления логического соединения, в котором используется метод предварительного создания. Обязательно примените для проверки такие клиенты, которые передают пакеты запросов. Насколько зависит допустимая степень распараллеливания от числа процессоров? Если между ними нет зависимости, объясните, с чем это связано. 16.4. Разработайте алгоритм сервера, в котором применяется сочетание методов отсроченного создания и предварительного создания. Как можно ограничить максимальную степень распараллеливания? 16.5. Исходя из условий предыдущего упражнения, ответьте, как можно использовать для управления степенью распараллеливания средства передачи сообщений, если они предусмотрены в операционной системе? 16.6. Какие преимущества могут быть достигнуты путем объединения методов, описанных в настоящей главе, в сервере, использующем один поток выполнения для обеспечения псевдопараллельной обработки? 16.7. Как можно применить методы, описанные в этой главе, для создания мультисервисного сервера? 16.8. Постройте и измерьте характеристики трех версий сервера с предварительным созданием ведомых процессов: (а) позволяющей ведомым процессам параллельно вызывать функцию accept, (б) предусматривающей использование разделяемого мьютекса для обеспечения того, чтобы только один ведомый процесс одновременно вызывал на выполнение функцию accept, и (в) предусматривающей применение функции flock. Какая из этих версий работает быстрее? Изменяются ли характеристики сервера в зависимости от числа ведомых процессов? 228 Глава 16. Единообразное и эффективное управление...
17 Распараллеливание работы клиентских программ 17.1. Введение В предыдущих главах показано, как обеспечить параллельную обработку запросов серверами. В настоящей главе затрагиваются вопросы параллельной организации работы клиентского программного обеспечения. В ней показаны преимущества использования средств распараллеливания работы в клиентских программах, а также рассматривается, как работает параллельный клиент. И наконец, в ней приведен пример клиента, который иллюстрирует параллельную организацию работы. 17.2. Преимущества параллельной работы Параллельная организация работы применяется в серверах по двум основным причинам: ¦ она позволяет сократить наблюдаемое время отклика (и поэтому повысить общую производительность сервера); ¦ она дает возможность устранить предпосылки возникновения тупиковых ситуаций. Кроме того, параллельная реализация позволяет легко создавать мультипро- токольные или мультисервисные серверы. И наконец, параллельные версии программ, в которых используются многочисленные процессы, имеют исключительно широкую область применения, поскольку способны работать на самых разных аппаратных платформах. Серверы функционируют вполне успешно на однопроцессорном компьютере, а после установки на многопроцессорном компьютере позволяют воспользоваться дополнительными вычислительными средствами без внесения изменений в код, поэтому действуют еще более эффективно. Может показаться, что параллельная организация работы клиентских программ не способствует повышению ее эффективности в основном потому, что клиенты обычно выполняют одновременно только одно действие. После передачи запроса на сервер клиент не может продолжать работу до тех пор, пока не получит ответ. Кроме того, проблемы эффективности работы клиентских программ и возникновения в них тупиковых ситуаций не являются столь серьезными, как, например, проблема тупиковой ситуации в сервере, поскольку при замедлении работы или нарушении функционирования одной клиентской программы другие продолжают успешно работать. Однако вопреки очевидному, параллельная организация работы клиентских программ также способствует улучшению их характеристик. Во-первых, параллельные реализации могут оказаться проще для программирования, поскольку в них функциональные средства распределены по концептуально отдельным ком-
понентам. Во-вторых, параллельные реализации могут оказаться проще в сопровождении и доработке в связи с тем, что в результате их применения код становится модульным. В-третьих, параллельные клиенты могут обращаться сразу к нескольким серверам одновременно, либо для сравнения их времени отклика, либо для объединения результатов, полученных от серверов. В-четвертых, параллельная организация работы позволяет пользователю изменять параметры клиента, запрашивать информацию о его состоянии или динамически управлять обработкой. Настоящая глава в основном посвящена применению параллельных клиентов для одновременного взаимодействия с несколькими серверами. Основное преимущество использования параллельной организации клиентов связано с обеспечением возможности асинхронного выполнения различных действий. Это позволяет клиентской программе выполнять одновременно несколько задач, не налагая строгих ограничений на порядок их выполнения. 17.3. Необходимость в использовании средств управления Одно из возможных направлений использования асинхронной обработки связано с необходимостью отделить функции управления от обычной обработки. Например, предположим, что клиентская программа используется для выполнения запроса в большой базе данных с демографической информацией. Допустим, что пользователь выдает примерно такой запрос: Find all people who live on Elm Street. (Найти всех людей, живущих на улице Вязов.) Если база данных содержит информацию об одном городе, то ответ может включать менее 1000 фамилий. Однако если база данных содержит информацию обо всех жителях США, ответ может включать сотни тысяч фамилий. Кроме того, если рассматриваемая система баз данных состоит из многих серверов, распределенных по обширному географическому региону, то на поиск информации может потребоваться весьма продолжительное время. Этот пример с базой данных иллюстрирует важную особенность многих сеансов взаимодействия типа клиент/сервер: пользователь, вызывающий клиентскую программу, может плохо представлять себе или даже вообще не знать, сколько времени потребуется для получения ответа или какой объем информации будет передан в ответ на его запрос. Основная часть клиентского программного обеспечения просто предусматривает переход в состояние ожидания до поступления ответа. Безусловно, в таком случае при нарушении в работе сервера возникает тупиковая ситуация и клиент блокируется, выполняя попытки чтения ответа, который никогда не поступит. К сожалению, пользователь не имеет сведений о том, действительно ли возникла тупиковая ситуация или просто обработка его запроса проходит очень медленно, поскольку сетевые задержки высоки или сервер перегружен. Кроме того, пользователь не имеет сведений о том, получены ли вообще клиентом какие-либо сообщения от сервера. Если пользователь потеряет терпение или решит, что получение ответа на тот конкретный запрос, который он отправил, требует слишком много времени, он имеет только одну возможность: аварийно прервать работу клиентской программы и повторить свою попытку позднее. В таких ситуациях может помочь параллельная организация работы, поскольку правильно спроектированная параллельная клиентская программа может дать возможность пользователю продолжать обмениваться с ней данными во время ожидания ответа. Пользователь может узнать, получены ли какие-либо предварительные данные, иначе сформулировать свой запрос или корректно разорвать связь с сервером. В качестве примера снова рассмотрим гипотетический клиент базы данных, описанный выше. Параллельная реализация позволяет читать и обрабатывать 230 Глава 17. Распараллеливание работы клиентских программ
данные, введенные пользователем с помощью клавиатуры или мыши, одновременно с поиском в базе данных. Поэтому пользователь может вызвать меню и выбрать команду типа Status (состояние работы) для определения того, было ли успешно открыто клиентом соединение с сервером и отправлен ли запрос. Затем пользователь может щелкнуть на элементе меню Abort, чтобы разорвать связь, или, например, выбрать команду New Server, чтобы дать указание клиенту прекратить текущий сеанс связи и попытаться обратиться к другому серверу. Отделение в клиенте средств управления от обычных средств обработки позволяет пользователю взаимодействовать с клиентом и в том случае, если входные данные для клиента поступают из файла. Таким образом, даже после того, как пользователь запускает в клиентской программе обработку большого входного файла, он может продолжать взаимодействовать с работающей клиентской программой и узнавать о том, как проходит обработка. Аналогичным образом, параллельный клиент может помещать ответы сервера в выходной файл, продолжая вместе с тем взаимодействовать с пользователем. 17.4. Одновременное взаимодействие с несколькими серверами Параллельная организация работы позволяет использовать одну клиентскую программу для вступления во взаимодействие одновременно с несколькими серверами и передачи сообщения пользователю сразу после получения ответа от любого из них. Например, параллельный клиент службы TIME может одновременно обратиться к нескольким серверам, а затем либо принять первый поступивший ответ, либо вычислить среднее значение по нескольким ответам. Рассмотрим клиентскую программу, в которой используется служба ECHO для измерения пропускной способности участка сети от локального компьютера до указанного места назначения. Предположим, что клиент устанавливает соединение TCP с сервером службы ECHO, передает большой объем данных, читает полученный эхо-повтор этих данных, вычисляет общую продолжительность времени выполнения этой задачи, а затем сообщает значение пропускной способности в байтах в секунду. Пользователь может вызвать на выполнение такую клиентскую программу для определения текущей пропускной способности сети. А теперь рассмотрим, как может параллельная организация работы улучшить клиентскую программу, в которой используется протокол ECHO для измерения пропускной способности. Вместо измерения характеристик только одного соединения, параллельный клиент может одновременно обратиться по нескольким адресам назначения. Он может параллельно выполнять обмен данными с любым из своих серверов. Такой клиент выполняет все измерения параллельно, поэтому справляется со своей задачей быстрее, чем непараллельный клиент. Кроме того, поскольку все измерения выполняются одновременно, на состоянии всех соединений равным образом отражается текущая нагрузка процессора локального компьютера и локальной сети. 17.5. Принципы разработки параллельных клиентов Как и в параллельных серверах, в большинстве реализаций параллельных клиентов применяется один из двух основных подходов: ¦ работа клиента осуществляется с использованием двух или нескольких потоков управления, в каждом из которых выполняется одна функция; ¦ клиент состоит из одного потока, в котором для асинхронной обработки множества событий ввода и вывода используется функция select. 17.4. Одновременное взаимодействие с несколькими серверами 231
В таких системах, как Linux, обеспечивающих совместный доступ нескольких потоков одного процесса к разделяемой памяти, многопотоковые реализации применяются вполне успешно. На рис. 17.1 показано, как может использоваться несколько потоков в таких операционных системах для поддержки прикладного протокола с установлением логического соединения. Один поток обрабатывает данные, введенные пользователем, и передает запросы на сервер, а другой поток получает ответы и обрабатывает выходные данные. ( Управле-] V ние J [ 1 1_1 Дескриптор управления ( Ввод ) ( l_l l_J Ввод Сокет TCP Вывод ) Т 1_1 Вывод Клиентские прикладные процессы Операционная система Рис. 17.1. Одна из возможных схем организации процессов в клиентской программе с установлением логического соединения, в которой используются несколько потоков для обеспечения параллельной обработки Как показано на рис. 17.1, применение нескольких потоков позволяет разделить в клиентской программе обработку входной и выходной информации. На этом рисунке показано, как потоки взаимодействуют с дескрипторами файлов и дескриптором сокета. Входной поток читает данные со стандартного устройства вывода, формирует запросы и передает их на сервер через соединения TCP, в то время как отдельный выходной поток получает ответы от сервера и выводит их на стандартное устройство вывода. Между тем, поток управления принимает команды от пользователя, управляющего обработкой. 17.6. Однопотоковые реализации В тех случаях, если система не поддерживает разделяемую память или создание дополнительных потоков считается нежелательным, параллельная организация работы клиента может быть осуществлена с помощью однопотокового алгоритма, аналогичного алгоритму 8.51 и показанного на примерах в главах 13-15. Такая схема организации процессов приведена на рис. 17.2. В клиентской программе для одновременной поддержки нескольких соединений используется функция select. В однопотоковом клиенте, как и в однопотоковом сервере, используется асинхронный ввод/вывод. Клиент создает дескрипторы сокетов для соединений TCP с несколькими серверами. В нем может также дополнительно контролироваться Описание алгоритма 8.5 приведено на стр. 139. 232 Глава 17. Распараллеливание работы клиентских программ
один или несколько дескрипторов файлов, через которые поступают данные, введенные с помощью клавиатуры или мыши. Тело клиентской программы состоит из цикла, в котором вызывается функция select для перехода к ожиданию состояния готовности одного из дескрипторов. Если готов дескриптор ввода, клиент считывает введенные данные и либо сохраняет их для дальнейшего использования, либо немедленно на них реагирует. Если же готов для вывода сокет TCP, клиент формирует и передает запрос через соединение TCP. А если этот сокет готов для ввода, клиент считывает и обрабатывает ответ, полученный с сервера. Клиентские щ прикладные процессы _ Операционная ^~~ система Рис. 17.2. Схема организации процессов, применяемая для обеспечения псевдопараллельной обработки в однопото- ковом клиенте с установлением логического соединения Безусловно, однопотоковый параллельный клиент во многом характеризуется такими же преимуществами и недостатками, как и однопотоковая версия сервера. Клиент успешно читает входные данные или ответы сервера независимо от того, с какой скоростью они вырабатываются. Локальная обработка продолжается, даже если сервер на короткое время замедляет свою работу, поэтому клиент продолжает читать и выполнять команды управления, даже если сервер больше не в состоянии отвечать на запросы. Однопотоковый клиент может оказаться в тупиковой ситуации, если в нем произойдет вызов системной функции, которая заблокируется. Поэтому в процессе разработки клиентской программы необходимо следить за тем, чтобы в ней не происходила блокировка на неопределенно долгое время в ожидании события, которое никогда не произойдет. Безусловно, иногда разработчики такую возможность развития событий игнорируют и возлагают задачу определения тупиковой ситуации на пользователя. Поэтому важно понимать все эти тонкости и уметь принимать продуманные решения по устранению каждой из возможных тупиковых ситуаций. 17.7. Пример параллельного клиента, в котором используется служба ECHO Проиллюстрируем описанные выше понятия на примере клиентской программы, в которой обеспечивается параллельная организация работы с использованием одного потока. В приведенном ниже примере параллельной клиентской программы, ко- 17.7. Пример параллельного клиента, в котором используется служба ECHO 233
торая находится в файле TCPtecho.c, для одновременного выполнения нескольких измерений пропускной способности сети используется служба ECHO (см. главу 7). /* Файл TCPtecho.c - главная процедура, процедуры TCPtecho, reader, writer, mstime */ linclude <sys/types.h> ¦include <sys/param.h> ¦include <sys/ioctl.h> ¦include <sys/time.h> ¦include <sys/socket.h> ¦include <unistd.h> ¦include <stdlib.h> ¦include <string.h> ¦include <stdio.h> extern int errno; int TCPtecho(fd_set *pafds, int nfds, int ccount, int hcount); int reader (int fd, fd__set *pfdset); int writer(int fd, fd_set *pfdset); int errexit(const char *format, ...); int connectTCP(const char *host, const char *service); long mstime(unsigned long *); ¦define BUFSIZE 4096 /* Размер буфера записи */ ¦define CCOUNT 64*1024 /* Число символов, установленное по умолчанию */ ¦define USAGE "usage: TCPtecho [ -с count ] hostl host2...\n" char *hname[NOFILE]; /* Таблица соответствия дескрипторов файлов */ /* и имен хостов */ int rc[NOFILE], wc[NOFILE]; /* Число символов, предназначенных */ /* для чтения/записи */ char buf[BUFSIZE]? /* Буфер данных для чтения/записи */ /* - - * Главная процедура - параллельный клиент TCP для измерения времени отклика * службы ECHO *_ _ . . . */ int main(int argc, char *argv[]) { int ccount = CCOUNT; int i, hcount, maxfd, fd; int one =1; fd_set afds; hcount =0; maxfd = -1; * for A=1; i<argc; ++i) { 234 Глава 17. Распараллеливание работы клиентских программ
if (strcmp(argv[i], "-с") « 0) { if (++i <argc && (ccount = atoi(argv[i]))) continue; errexit(USAGE); } /* В ином случае это - хост */ fd = connectTCP(argv[i], "echo"); if (ioctl(fd, FIONBIO, (char *)&one)) errexit("can't mark socket nonblocking: %s\n", strerror(errno)); . if (fd> maxfd) maxfd = fd; hname[fd] =¦ argv[i]; ++hcount; FD_SET(fd, Safds); } TCPtecho(Safds, maxfd+1, ccount, hcount); exit@); } /* — * Процедура TCPtecho - измеряет время выполнения запросов к службе ECHO, * передаваемых по протоколу TCP на несколько хостов *. */ int TCPtecho(fd_set *pafds, int nfds, int ccount, int hcount) { fd_set rfds, wfds; /* Наборы дескрипторов для чтения/записи */ fd_set rcfds, wcfds; /* Наборы дескрипторов для чтения/записи */ /* (копия) */ int fd, i; for (i=0; i<BUFSIZE; ++i) /* Эхо-повтор данных */ buf[i] = 'D'; memcpy(&rcfds, pafds, sizeof(rcfds)); memcpy(&wcfds, pafds, sizeof(wcfds)); for (fd=0; fd<nfds; ++fd) rc[fd] = wc[fd] = ccount; (void) mstimef(unsigned long *H); /* Установить начало эпохи */ while (hcount) { memcpy(&rfds, Srcfds, sizeof(rfds)); memcpy(&wfds, &wcfds, sizeof(wfds)); if (select(nfds, Srfds, &wfds, (fd_set *H, (struct timeval *H) <0) errexit("select failed: %s\n", strerror(errno)); for (fd=0; fd<nfds; ++?d) { if (FD_ISSET(fd, Srfds)) 17.7. Пример параллельного клиента, в котором используется служба ECHO
if (reader(fd, &rcfds) == 0) hcount--; if (FD_ISSET(fd, &wfds)) writer(fd, &wcfds); } } } /* — * Процедура reader - выполняет операции чтения по протоколу ECHO * . . . */ int reader(int fd, fd_set *pfdset) { unsigned long now? int cc; cc = read(fd, buf, sizeof(buf)); if (cc <0) errexit("read: %s\n", strerror(errno)); if (cc == 0) errexit(Hread: premature end of file\n"); rc[fd] -= cc; if (rc[fd]> return 1; (void) mstime(&now); printff'ls: %d ms\nM, hname[fd], now); (void) close(fd); FD_CLR(fd, pfdset); return 0; } /* * Процедура writer - выполняет операции записи по протоколу ECHO * . */ int writer(int fd, fd_set *pfdset) { int cc; cc = write(fd, buf, MIN((int)sizeof(buf), wc[fd])); if (cc <0) errexit("read: %s\n", strerror(errno)); wc[fd] -= cc; if (wc[fd] == 0) { (void) shutdown(fd, 1); FD_CLR(fd, pfdset); } /* 236 Глава 17. Распараллеливание работы клиентских
* Процедура mstime - сообщает число истекших миллисекунд * . « */ long mstime(unsigned long *pms) { static struct timeval epoch; struct timeval now; if (gettimeofday(&now, (struct timezone *H)) errexit("gettimeofday: %s\n", strerror(errno)); if (!pms) { epoch = now; return 0; } *pms = (now.tv_sec - epoch.tv_sec) * 1000; *pms += (now.tvjisec - epoch.tvjisec + 500)/ 1000; return *pms; } 17.8. Описание параллельной клиентской программы Программа TCPtecho принимает в качестве параметров имена нескольких компьютеров. Для каждого компьютера она открывает соединение TCP с сервером ECHO на этом компьютере, посылает ccount символов (байтов) через соединение, считывает байты, полученные от каждого сервера, и выводит на экран значение общей продолжительности времени, которое потребовалось для выполнения этой задачи. Поэтому программа TCPtecho может применяться для одновременного выполнения нескольких измерений пропускной способности сети. Работа программы TCPtecho начинается с установки применяемого по умолчанию начального значения переменной CCOUNT, которая содержит число передаваемых символов. Затем программа интерпретирует свои параметры для определения того, была ли введена пользователем опция -с. В случае положительного ответа программа TCPtecho преобразует заданное с помощью этой опции количество символов в целое число и записывает его в переменную ccount, перекрывая значение, применяемое по умолчанию. В программе TCPtecho предполагается, что все параметры, отличные от заданного с помощью опции -с, указывают имя компьютера. Для каждого из таких параметров программа вызывает процедуру connectTCP для установления соединения TCP с сервером ECHO на указанном компьютере. Программа TCPtecho регистрирует имя компьютера в массиве hname и вызывает макрокоманду FDJ3ET для установки в маске дескрипторов файлов бита, соответствующего данному сокету. Программа также записывает максимальный номер дескриптора в переменную maxf d (которая нужна для вызова функции select). После установления соединения TCP с каждым компьютером, указанным в качестве параметров, главная процедура программы TCPtecho вызывает процедуру TCPtecho для передачи и приема данных. Эта процедура обслуживает все соединения одновременно, заполняет буфер buf данными, которые должны быть переданы (повторяющиеся буквы D), а затем вызывает функцию select для перехода в состояние ожидания готовности любого соединения TCP к вводу или вы- 17.8. Описание параллельной клиентской программы 237
воду. После возврата управления из функции select процедура TCPtecho проверяет в цикле все дескрипторы и определяет, какой из них готов к работе. Если какое-то соединение готово для вывода, процедура TCPtecho вызывает процедуру writer для передачи такого объема данных из буфера, какой может быть принят протоколом TCP в одном вызове функции write. Если процедура writer обнаружит, что передан весь буфер, она вызывает функцию shutdown, чтобы закрыть дескриптор для вывода, и удаляет этот дескриптор из набора дескрипторов вывода, используемого функцией select. Если какой-либо дескриптор готов для ввода, процедура TCPtecho вызывает процедуру reader, которая принимает такой объем данных из соединения, какой может быть доставлен программным обеспечением протокола TCP, и помещает их в буфер. Процедура reader считывает данные в буфер и уменьшает число оставшихся символов в соответствии с объемом полученных данных. Если число оставшихся символов достигает нуля (т.е. клиент получил такое же число символов, какое было им отправлено), процедура reader определяет, сколько времени прошло с момента начала передачи данных, выводит сообщение и закрывает соединение. Она также удаляет дескриптор из набора дескрипторов ввода, используемого функцией select. Поэтому сообщение с указанием общего количества времени, которое потребовалось для эхо-повтора данных, появляется на устройстве вывода после закрытия каждого соединения. После выполнения в соединении одной операции ввода или вывода каждая из процедур reader и writer выполняет возврат, цикл в процедуре TCPtecho повторяется и снова вызывается функция select. Процедура reader возвращает значение О, если обнаружен признак конца файла и закрыто соединение, и значение 1 — в ином случае. В процедуре TCPtecho используется код возврата процедуры reader для определения того, следует ли уменьшить на единицу число активных соединений. После того как число активных соединений достигает нуля, цикл в процедуре TCPtecho завершается, эта процедура возвращает управление главной процедуре и работа клиентской программы прекращается. В листинге 17.1 показан пример вывода, полученного в результате трех отдельных вызовов на выполнение программы TCPtecho. Результаты первого вызова показывают, что программе TCPtecho потребовалось только 311 миллисекунд для передачи данных на сервер ECHO, установленный на локальном компьютере. В командной строке был указан один параметр localhost. Поскольку при втором вызове использовалось три параметра (ector, arthur и merlin), в нем программа TCPtecho применялась для обмена данными со всеми тремя компьютерами одновременно. В третьем вызове было предусмотрено измерение количества времени, необходимого для достижения компьютера sage, но в командной строке было указано, что программа TCPtecho должна отправить только 1000 символов вместо значения, применяемого по умолчанию F4 Кбайт). Для получения ответа от компьютера требовалось больше времени, если он находился дальше от клиента или имел более медленный процессор. Листинг 17.1. Пример вывода, полученного в результате трех отдельных вызовов на выполнение программы TCPtecho для доступа к компьютерам Университета Пердью % TCPtecho localhost localhost: 311 ms % TCPtecho ector arthur merlin arthur: 601 ms merlin: 4921 ms ector: 11791 ms 238 Глава 17. Распараллеливание работы клиентских программ
% TCPtecho -с 1000 sage sage: 80 ms 17.9. Оценка результатов применения параллельной организации работы в приведенном выше примере кода Параллельная реализация программы TCPtecho улучшает ее работу в двух отношениях. Во-первых, параллельная реализация позволяет получить более точные результаты измерения количества времени, которое потребовалось для передачи данных в каждом соединении, поскольку пропускная способность всех соединений измерялась в течение одного и того же интервала времени. Поэтому при появлении затора в сети он равным образом воздействует на все соединения. Во-вторых, параллельная реализация повышает привлекательность программы TCPtecho для пользователя. Чтобы понять, с чем это связано, снова рассмотрим результаты, полученные при второй попытке. Для получения ответа от компьютера arthur потребовалось чуть больше половины секунды, сообщение от компьютера merlin было получено примерно через пять секунд, а последнее сообщение, от компьютера ector, появилось примерно через двенадцать секунд. Если бы пользователю пришлось ждать последовательного выполнения всех проверок, то общее время выполнения программы заняло бы примерно восемнадцать секунд. Измерение характеристик компьютеров, расположенных на большем удалении в Internet, в отдельных случаях потребует значительно более продолжительного ожидания, поэтому параллельная версия позволяет получить результаты гораздо быстрее. Во многих случаях применение последовательной реализации клиента для выполнения N измерений характеристик связи может потребовать примерно в N раз больше времени по сравнению с параллельной версией. 17.10. Резюме Параллельная организация работы предоставляет мощное инструментальное средство, которое может использоваться не только в серверах, но и в клиентах. Параллельные версии клиентов могут обеспечить более быстрое время отклика, а также исключить проблемы тупиковых ситуаций. И наконец, распараллеливание работы позволяет отделить функции управления и обработки информации о состоянии от обычного ввода и вывода. В настоящей главе рассматривался пример клиента с установлением логического соединения, в котором измеряется время, необходимое для доступа к серверам службы ECHO на одном или нескольких компьютерах. Поскольку этот клиент обеспечивает параллельную поддержку нескольких соединений, он позволяет исключить из рассмотрения различия в пропускной способности, вызванные изменением состояния сети (например, в результате затора), поскольку все измерения выполняются в течение одного и того же интервала времени. Параллельная реализация привлекательна и для пользователей, поскольку в ней все измерения совмещены, т.е. пользователь не должен выполнять их последовательно. Упражнения 17.1. Обратите внимание, что в приведенном примере клиентской программы проверка готовности дескрипторов файлов происходит последовательно. Если несколько дескрипторов одновременно переходят в состояние готовности, клиент вначале обрабатывает дескрипторы с меньшими номерами, а затем проходит в цикле по остальным. После 17.9. Оценка результатов применения параллельной организации работы ... 239
обработки всех готовых дескрипторов он снова вызывает функцию select для перехода в состояние ожидания готовности другие дескрипторов. Рассмотрим, сколько времени проходит между обработкой готового дескриптора и вызовом функции select. После обработки дескрипторов с более высокими номерами проходит меньше времени, чем после обработки дескрипторов с меньшими номерами. Может ли это различие привести к нарушению работы некоторых потоков из-за нехватки ресурсов? Объясните ваш ответ. 17.2. Доработайте представленную в качестве примера клиентскую программу для предотвращения неравноправного подхода к обработке дескрипторов, показанного в предыдущем упражнении. 17.3. Для каждого из проектов последовательных и параллельных клиентов, описанных в этой главе, напишите выражение, которое определяет максимальное число используемых сокетов. 240 Глава 17. Распараллеливание работы клиентских программ
18 Туннелирование на транспортном и прикладном уровнях 18.1. Введение В предыдущих главах было описано проектирование клиентского и серверного программного обеспечения, применяемого в тех случаях, если все взаимодействующие компьютеры связаны с помощью объединенной сети TCP/IP. Во многих представленных проектах предполагается, что клиенты и серверы работают на достаточно мощных компьютерах, операционная система которых обеспечивает параллельное выполнение процессов, а также предоставляет полную поддержку протоколов TCP/IP. С этой главы начинается рассмотрение методов, применяемых системными администраторами и программистами при наличии иных сетевых топологий. В частности, в ней описаны методы, позволяющие использовать в компьютерах высокоуровневые службы для доставки трафика IP, и проекты, в которых протокол IP используется для доставки трафика, предназначенного для других систем протоколов. 18.2. Мультипротокольная среда Было бы просто замечательно, если протоколы TCP/IP требовались бы только для создания клиентского и серверного программного обеспечения для компьютеров, которые подключаются непосредственно к объединенной сети TCP/IP и предоставляют полную поддержку ее протоколов. Однако в действительности не все компьютеры предоставляют полную поддержку TCP/IP и не на всех предприятиях для связи между компьютерами используются исключительно только эти протоколы. Например, на предприятии могут применяться встроенные компьютерные системы, мощность которых не позволяет эксплуатировать серверное программное обеспечение, или могут эксплуатироваться группы компьютеров, подключенные к сетям, в которых используются более старые протоколы, такие как SNA или Х.25. Как правило, на большинстве предприятий сети развивались на протяжении продолжительного времени, по мере того, как к ним добавлялись новые сети для обеспечения взаимодействия существующих групп компьютеров. Обычно сетевые администраторы выбирают аппаратную технологию и набор протоколов для каждой группы компьютеров независимо от других компьютеров. Выбирая сетевые средства, они учитывают такие факторы, как стоимость, расстояние, желаемое быстродействие и наличие поставщика. Если сети были развернуты на предприятии еще до появления протоколов TCP/IP, это предприятие уже могло выбрать набор
протоколов конкретного поставщика. В результате развития сетей на большинстве крупных предприятий имеется несколько групп компьютеров и в каждой из этих групп применяется собственный набор протоколов. На многих крупных предприятиях сети развивались постепенно в течение многих лет, в них внедрялись собственные сетевые системы разных поставщиков, не всегда совместимые с протоколами TCP/IP, поэтому на этих предприятиях часто имеются группы компьютеров, в которых для обмена данными используются другие системы протоколов. Кроме того, в целях сокращения расходов предприятия часто продолжают применять устаревшие сетевые системы до тех пор, пока не появятся реальные предпосылки перехода на новые технологии. Например, на рис. 18.1 показано предприятие, на двух узлах которого используются три сети. На каждом узле установлена отдельная локальная сеть Ethernet. Хосты этих двух узлов соединяет одна распределенная сеть, в которой используются протоколы Х.251. Как показано на этом рисунке, к каждой сети подключен ряд компьютеров. Во всех компьютерах, подключенных к распределенной сети, используются протоколы Х.25, а во всех компьютерах, подключенных к локальной сети — протоколы TCP/IP. Хосты TCP/IP Хосты TCP/IP Ethernet Хосты Х.25 Ethernet о Хосты Х.25 Рис. 18.1. Пример применения трех сетей на предприятии Основной недостаток применения нескольких сетевых систем связан с увеличением численности обслуживающего персонала и ограниченной способностью к взаимодействию. В хостах, подключенных к распределенной сети Х.25, приходится использовать протоколы Х.25 вместо протоколов TCP/IP. Поэтому если клиент и сервер работают на хостах, подключенных к сети Х.25 (см. рис. 18.1), то в них для связи приходится использовать виртуальные каналы Х.25, тогда как в клиентах и серверах, работающих в сети Ethernet, используются виртуальные каналы TCP. 18.3. Совместное применение различных сетевых технологий Обычно объединенная сеть TCP/IP состоит из ряда хостов в физических сетях, соединенных с помощью маршрутизаторов (шлюзов IP). Во всех хостах и маршрутизаторах объединенной сети должны применяться протоколы TCP/IP. Сеть Х.25 функционирует по принципу установления логического соединения, аналогично сети Frame Relay или ATM. 242 Глава 18. Туннелирование на транспортном и прикладном уровнях
Аналогичным образом, сеть, в которой эксплуатируются какие-либо другие протоколы, состоит из физических каналов и компьютеров, в которых используются исключительно только эти протоколы. Поскольку служба транспортного уровня позволяет доставлять пакеты из одной точки в другую так же просто, как и аппаратное обеспечение коммутации пакетов, то должна существовать возможность заменить одно физическое звено в системе коммутации пакетов любой службой коммутации транспортного уровня. Уже создано много объединенных сетей, в которых вместо физических сетей используются службы коммутации транспортного уровня. Например, снова рассмотрим сети, показанные на рис. 18.1. Предположим, что на предприятии было решено связать между собой две сети Ethernet, чтобы создать одну объединенную сеть TCP/IP, позволяющую обмениваться данными хостам, подключенным к сетям Ethernet. Само собой напрашивается решение: установить между этими сетями два маршрутизатора. Однако если две сети Ethernet находятся друг от друга на большом расстоянии, то стоимость дополнительной выделенной арендованной линии, необходимой для соединения этих двух сетей, может оказаться чрезмерно высокой. Оправдать дополнительные затраты может быть особенно сложно, поскольку на предприятии уже есть сеть X.25, соединяющая ее два узла. На рис. 18.2 показано, как можно использовать на предприятии, схема сети которого приведена на рис. 18.1, существующие средства связи сети Х.25 для создания соединений объединенной сети TCP/IP между ее двумя узлами. Хосты TCP/IP Хосты TCP/IP Ethernet 1 Маршрутизатор Хосты Х.25 Рис. 18.2. IP-маршрутизаторы, в которых используется служба транспортного уровня протоколов Х.25 вместо физической сети Предприятие устанавливает на каждом узле новый маршрутизатор. Каждый из этих новых маршрутизаторов подключается к сети Х.25 и к локальной сети Ethernet данного узла. После начальной загрузки маршрутизаторов в каждом из них используются протоколы Х.25 для формирования обычного виртуального канала транспортного уровня с другим маршрутизатором через распределенную сеть Х.25. Таблица маршрутизации в каждом из маршрутизаторов настраивается таким образом, чтобы весь трафик, отличный от локального, перенаправлялся через канал Х.25. В маршрутизаторах для обмена дейтаграммами IP друг с другом используются протоколы Х.25. Что касается маршрутизаторов, сеть Х.25 просто предоставляет канал, по которому могут передаваться дейтаграммы, а если речь идет о сети Х.25, то программное обеспечение протокола IP на обоих маршрутизаторах ничем не отличается от прикладного программного обеспечения других хостов. 18.3. Совместное применение различных сетевых технологий 243 Ethernet 2 Маршрутизатор Хосты Х.25
Программное обеспечение службы Х.25 не имеет информации о том, что данные, отправляемые по виртуальному каналу, состоят из дейтаграмм IP. После установки этих двух маршрутизаторов пользователь любого хоста может вызвать на выполнение стандартное клиентское программное обеспечение TCP/IP, которое вступает во взаимодействие с сервером на любом другом хосте. Соединения между клиентом и сервером могут устанавливаться в пределах одной сети Ethernet или проходить через сеть Х.25. Ни пользователь, ни приложение типа клиент/сервер не должны учитывать, что дейтаграммы могут пройти через сеть Х.25, если они передаются из сети Ethernet одного узла в сеть Ethernet другого. Обе сети Ethernet просто составляют часть одной объединенной сети TCP/IP. Кроме того, в программное обеспечение хостов, использующих протоколы Х.25 в распределенной сети, не нужно вносить какие-либо изменения. Они могут по-прежнему обмениваться данными, не испытывая помех со стороны трафика TCP/IP, поскольку виртуальные каналы, которые в них используются, остаются независимыми от новых соединений между маршрутизаторами. 18.4. Динамическое создание каналов В примере сетевой топологии, приведенном на рис. 18.2, для передачи трафика объединенной сети TCP/IP достаточно иметь только один виртуальный канал через сеть Х.25, поскольку предприятие включило в объединенную сеть только два узла. Если же предприятие будет подключать дополнительные узлы, то для расширения рассматриваемой сетевой топологии достаточно установить маршрутизатор на каждом новом узле и создать дополнительные каналы через сеть Х.25 для подключения каждого нового маршрутизатора к маршрутизаторам существующих узлов. Однако статическая схема создания каналов, описанная выше, не может быть распространена на произвольное число узлов, поскольку большинство сетей Х.25 ограничивают число каналов, которые могут быть подключены к одному компьютеру одновременно. Как правило, аппаратное обеспечение устанавливает предел числа виртуальных каналов, создаваемых на одном компьютере, равный 16 или 32. Если на предприятии имеется N узлов, то для соединения всех этих узлов друг с другом предприятию необходимо (N * (N-l))/2 каналов. Поэтому в маршрутизаторах необходимо установить 15 соединений для поддержки 6 узлов, а если число узлов на предприятии достигает 9, то число соединений превышает 32. Безусловно, существует возможность установить дополнительные маршрутизаторы с тем, чтобы на каждом отдельном маршрутизаторе не приходилось создавать каналы для связи во всех направлениях. Однако в целях ограничения расходов в большинстве узлов, использующих протоколы Х.25 для транспортировки дейтаграмм, применяется иной подход: в них каналы создаются по требованию и закрываются в том случае, если они больше не используются. После получения дейтаграммы маршрутизатор ищет в своих таблицах адрес ее назначения для определения маршрута дальнейшего следования дейтаграммы. В процессе поиска в таблице маршрутизации определяется адрес назначения следующего участка маршрута, т.е. адрес следующего маршрутизатора, на который должна быть отправлена эта дейтаграмма. Если в адресе назначения следующего участка маршрута задан удаленный узел, то маршрутизатор просматривает таблицу активных виртуальных каналов Х.25. Если существует канал, ведущий к следующему участку маршрута, маршрутизатор перенаправляет дейтаграмму через этот канал. Если же такого канала не существует, маршрутизатор динамически открывает новый канал, ведущий к требуемому месту назначения. Если при возникновении необходимости открыть новый канал маршрутизатор не имеет неиспользуемых каналов, следует закрыть один из существующих каналов, чтобы освободить ресурсы. В этом случае возникает проблема выбора канала, кото- 244 Глава 18. Туннелирование на транспортном и прикладном уровнях
рый может быть закрыт. Обычно в маршрутизаторе для этого используется такое же правило, как и в системе подкачки страниц по требованию: маршрутизатор закрывает канал с учетом того, давно ли он открыт. После передачи дейтаграммы по новому каналу маршрутизатор оставляет его открытым. Часто исходящие дейтаграммы содержат запрос, на который получатель должен передать ответ, поэтому поддержание канала в открытом состоянии позволяет уменьшить задержки и затраты. Динамически открывая и закрывая виртуальные каналы, маршрутизатор может ограничить число необходимых ему соединений без потери способности взаимодействовать со всеми узлами. В маршрутизаторе достаточно иметь открытым по одному каналу для каждого узла, с которым он в настоящее время обменивается данными. 18.5. Инкапсуляция и туннелирование Инкапсуляцией называется процесс размещения дейтаграммы IP в сетевом пакете или фрейме для его дальнейшей передачи по базовой сети. Сам процесс инкапсуляции зависит от того, как используется аппаратное обеспечение коммутации пакетов сетевым интерфейсом. Например, два хоста, обменивающиеся данными по сети Ethernet с помощью протокола IP, инкапсулируют для передачи каждую дейтаграмму в один пакет Ethernet. В стандарте инкапсуляции для протоколов TCP/IP указано, что дейтаграмма IP занимает область данных пакета Ethernet и что в поле типа такого пакета должно быть задано значение, которое обозначает протокол IP. В отличие от этого, туннелированием называется процесс использования высокоуровневой транспортной сетевой службы для передачи пакетов или сообщений другой службы. Пример, приведенный на рис. 18.2, показывает, что маршрутизаторы могут применять туннели через службу Х.25 для передачи друг Другу дейтаграмм IP. Основное различие между туннелированием и инкапсуляцией связано с тем, передаются ли дейтаграммы IP в аппаратных пакетах или для их доставки используется высокоуровневая транспортная служба2. Программное обеспечение IP предусматривает инкапсуляцию каждой дейтаграммы в пакете, если в нем непосредственно используются аппаратные средства. Это программное обеспечение создает туннель, если в нем применяется высокоуровневая транспортная служба доставки для передачи дейтаграмм из одной точки сети в другую. 18.6. Туннелирование через объединенную сеть IP Сразу после разработки спецификаций протоколов TCP/IP были проведены эксперименты для определения того, можно ли создать программные туннели IP через существующие сети (такие как Х.25) для доставки дейтаграмм. Цель таких экспериментов очевидна: во многих организациях к тому времени уже были развернуты сети. Однако со временем эта тенденция изменилась на противоположную. Теперь туннелирование в основном применяется в связи с тем, что поставщики используют протокол IP для доставки пакетов, не относящихся к протоколам TCP/IP. Чтобы понять, почему туннелирование стало применяться для других целей, необходимо рассмотреть, какие изменения произошли в организации сетей. По мере дальнейшего распространения протоколов TCP/IP многие предприятия ста- Различие между туннелированием и инкапсуляцией может стать менее ясным при использовании таких технологий, как ATM и Frame Relay, которые предусматривают двухточечную связь. Мы считаем, то в таких сетях происходит инкапсуляция дейтаграмм IP, поскольку в основе их работы лежат аппаратные средства с установлением логического соединения, а не транспортные протоколы. 18.5. Инкапсуляция и туннелирование 245
ли рассматривать объединенные сети TCP/IP как универсальный механизм доставки пакетов. Сегодня можно утверждать, что протокол IP предоставляет самые широкие возможности связи между компьютерами на большинстве предприятий. Чтобы показать, как универсальность протокола IP влияет на применение других протоколов, предположим, что нужно обеспечить взаимодействие двух компьютеров на предприятии с использованием протокола конкретного поставщика. Вместо установки дополнительных физических сетевых соединений между этими двумя компьютерами, сетевой администратор может рассматривать объединенную сеть IP предприятия как общую сеть и предусмотреть в сетевом программном обеспечении этих двух компьютеров обмен данными путем передачи и приема дейтаграмм IP. В настоящее время на коммерческой основе поставляется разнообразное программное обеспечение, в котором протокол IP используется для доставки трафика многих других протоколов высокого уровня. 18.7. Туннелирование на прикладном уровне между клиентами и серверами Хотя туннелирование в целом представляет собой использование одного набора протоколов транспортного уровня для доставки трафика другого протокола, этот принцип можно распространить на взаимодействие между клиентом и сервером. В частности, для создания канала связи между клиентом и сервером может применяться туннелирование на прикладном уровне. Чтобы понять принципы работы туннелирования на прикладном уровне, рассмотрим два компьютера, подключенных к сети Х.25. Предположим, что на одном из них необходимо эксплуатировать клиентское приложение UDP, а на другом — серверное приложение UDP. Часто возможность внесения изменений в программное обеспечение операционной системы отсутствует, поскольку оно допускает лишь создание прикладных программ. Кроме того, если операционные системы этих двух компьютеров не поддерживают протоколы TCP/IP и туннелирование на транспортном уровне, то применение протокола UDP или обеспечение туннелирования дейтаграмм IP через сеть Х.25 становится затруднительным или даже невозможным. В таких случаях для обеспечения возможности взаимодействия клиентов и серверов через сеть Х.25 может применяться туннелирование на прикладном уровне. Для этого необходимо разработать библиотеку процедур, которая эмулирует интерфейс сокетов. Такая эмуляционная библиотека должна предоставить возможность создавать в приложениях активные или пассивные сокеты UDP, а также передавать или принимать дейтаграммы UDP. Процедуры в библиотеке эмуляции сокетов преобразовывают вызовы стандартных процедур сокетов (например, socket, send и recv) в операции, предусматривающие распределение и манипуляцию с локальными структурами данных и передачу сообщений по сети Х.25. После вызова в клиентской программе функции socket для создания сокета, библиотека процедур сокетов создает соединение Х.25 с сервером. При вызове в клиентской или серверной программе функции send для передачи сообщения, библиотечная процедура send передает дейтаграмму UDP через соединение Х.25. После создания библиотеки эмуляции сокетов программист может оттранслировать любую клиентскую или серверную программу UDP, связать эту программу с библиотекой эмуляции, а затем вызвать полученную программу на выполнение. На рис. 18.3 показана применяемая при этом структура программного обеспечения. Библиотека эмуляции сокетов позволяет клиенту и серверу обмениваться дейтаграммами UDP с помощью транспортной службы, отличной от TCP/IP. 246 Глава 18. Туннелирование на транспортном и прикладном уровнях
Клиентское приложение UDP Библиотека эмуляции сокетов Операционная система с поддержкой Х.25 Клиентское приложение UDP Библиотека эмуляции сокетов Операционная система с поддержкой Х.25 Рис. 18.3. Концептуальная организация программного обеспечения в клиенте и сервере, в которых используется туннелирование на прикладном уровне через сеть Х.25 18.8. Туннелирование, инкапсуляция и коммутируемые телефонные линии Как известно, для обеспечения связи двух компьютеров с помощью коммутируемых телефонных систем применяются модемы. Разработан ряд протоколов передачи трафика IP по коммутируемому каналу, включая SLIP (Serial Line Internet Protocol — Межсетевой протокол для последовательного канала) и РРР (Point-to-Point Protocol — Протокол двухточечного соединения). Следует ли рассматривать передачу трафика IP по коммутируемому соединению как своего рода туннелирование или инкапсуляцию? Безусловно, применение коммутируемых каналов имеет много общего с туннелированием, которое описано в данной главе. Телефонная система может рассматриваться как транспортная система, по которой туннелируются дейтаграммы IP. Кроме того, коммутируемыми соединениями можно управлять во многом так же, как каналами Х.25. Однако большинство специалистов считает, что коммутируемую телефонную систему не следует рассматривать как транспортную. В действительности, она должна рассматриваться как физическая сеть с установлением логического соединения. Поэтому такие протоколы, как SUP и РРР, определяют своего рода инкапсуляцию; в каждом из них регламентирован формат фреймов канального уровня, который определяет, как должны инкапсулироваться дейтаграммы для передачи. Аналогично выделенной последовательной линии, телефонная система может применяться для соединения маршрутизатора одного узла с маршрутизатором другого узла. В следующей главе описано, как могут быть расширены протоколы SLIP и РРР для поддержки коммутируемых соединений в разнородной среде адресования. 18.8. Туннелирование, инкапсуляция и коммутируемые телефонные линии 247
18.9. Резюме Туннелирование представляет собой передачу пакетов между компьютерами с использованием системы доставки пакетов транспортного уровня вместо их передачи непосредственно по физическим сетям. На первых порах исследования по проблеме туннелирования IP через существующие сетевые системы проводились предприятиями, в которых уже были установлены распределенные сети. Эти предприятия стремились избежать издержек, связанных с созданием новых физических соединений для трафика IP. Были разработаны способы, позволяющие использовать дейтаграммы IP для передачи пакетов в существующих сетях без внесения в них изменений. Программное обеспечение IP рассматривает такую транспортную службу как единое аппаратное звено; в транспортной службе трафик IP не отличается от трафика, передаваемого любым другим приложением. Программное обеспечение IP легло в основу создания системы доставки информации, обеспечивающей наиболее широкие возможности взаимодействия с существующими системами. В связи с этим в настоящее время исследования в области туннелирования сосредотачиваются на поиске способов использования программного обеспечения IP в качестве системы доставки пакетов, позволяющей передавать пакеты других сетевых протоколов. Многие поставщики объявили о создании программного обеспечения, которое дает возможность обеспечить связь разработанных ими сетевых систем по базовой объединенной сети IP. Принцип туннелирования может быть реализован на уровне прикладного программного обеспечения. Для этого необходимо разработать библиотеку, в которой эмулируется интерфейс сокетов, но для доставки сообщений используется транспортная служба, отличная от TCP/IP. В частности, можно легко разработать библиотеку эмуляции сокетов, которая позволяет обеспечить взаимодействие между клиентами и серверами с помощью протокола UDP, даже если соединение между клиентским и серверным компьютерами обеспечивается с помощью такой системы коммутации транспортного уровня, как сеть Х.25. Для туннелирования могут использоваться не только постоянные соединения, но и коммутируемые телефонные соединения. Для обеспечения взаимодействия двух маршрутизаторов по коммутируемой телефонной линии в них необходимо установить модемы коммутируемой линии передачи и согласовать протокол канального уровня. Материал для дальнейшего изучения В статье [35] описано, как обеспечить туннелирование трафика IP через сеть Х.25, в частности, как организовать управление виртуальными каналами Х.25 при наличии постоянных ограничений на число одновременных соединений, налагаемых аппаратными средствами. В документе [86] приведены технические требования к применяемым для этого форматам. Упражнения 18.1. Прочитайте документ RFC 877. Каким образом в маршрутизаторе, применяемом для туннелирования через сеть Х.25, IP-адрес назначения преобразуется в эквивалентный адрес Х.25? 18.2. Во многих службах транспортного уровня реализована собственная схема повторной передачи для обеспечения надежной доставки. Что может произойти, если повторную передачу сообщений будут выполнять и протокол TCP, и протоколы базовой сети? 248 Глава 18. Туннелирование на транспортном и прикладном уровнях
18.3. Как было указано выше, в маршрутизаторе для обеспечения туннелиро- вания через сеть с установлением логического соединения используется динамическое распределение виртуальных каналов, а для закрытия существующего соединения и освобождения ресурсов обычно применяется алгоритм, учитывающий, давно ли открыто это соединение (LRU — Least Recently Used). Объясните, что произойдет в маршрутизаторе, если его интерфейс позволяет поддерживать К параллельных соединений, а в нем будет предпринята попытка обеспечить одновременный обмен данными с другими узлами, число которых составляет JC+1. 18.4. Разработайте библиотеку эмуляции сокетов, которая позволяет обеспечить обмен дейтаграммами UDP между клиентским и серверным приложениями с использованием протокола транспортного уровня, отличного от TCP/IP. Проведите проверку этой библиотеки, обеспечив взаимодействие клиента ECHO UDP с сервером ECHO UDP. ажнения 249
19 Шлюзы прикладного уровня 19.1. Введение В предыдущей главе рассматривался метод туннелирования, позволяющий использовать в одном наборе протоколов службу доставки транспортного уровня другого набора протоколов вместо физической сети. С точки зрения прикладного программиста, туннелирование позволяет обеспечить взаимодействие клиента и сервера с применением одного протокола, даже если сеть, лежащая между ними, работает по другому протоколу. В настоящей главе продолжается рассмотрение методов, используемых для обеспечения взаимодействия клиентов и серверов в разнородной среде. В ней показано, что в качестве посредника между двумя разными службами может действовать определенное приложение, и описано, как применение таких посредников позволяет расширить перечень доступных служб. 19.2. Клиенты и серверы в среде с ограниченными возможностями 19.2.1. Недостатки ограниченного доступа Архитектура типа клиент/сервер, описанная в предыдущих главах, основана на предположении, что компьютеры подключаются к единой однородной объединенной сети, а программное обеспечение транспортного протокола предоставляет необходимые средства взаимодействия по сети. Однако такой непосредственный, единообразный доступ не всегда возможен. Даже если базовые сетевые средства обеспечивают возможность взаимодействия любых компьютеров, на них могут распространяться ограничения доступа, связанные с экономическими и организационными причинами. Например, как было указано в главе 18, на многих предприятиях сети развивались постепенно, поэтому в отдельных подразделениях могло быть выбрано разное программное обеспечение протокола в соответствии с требованиями конкретных приложений. Еще более важно то, что программное обеспечение клиента или сервера иногда входит в состав других приложений, поэтому программные продукты, необходимые для выполнения конкретной задачи, могут включать другие клиентские или серверные службы. Для программистов, работающих в разнородной среде, задача создания программного обеспечения усложняется, поскольку им приходится сталкиваться с несовместимыми системами. Если на предприятии не применяется туннелирование, то невозможно обеспечить единообразное взаимодействие между любыми компьютерами на транспортном уровне. Поэтому при таких условиях исключена возможность
использовать транспортный протокол для обеспечения взаимодействия между произвольными парами компьютеров, а также создавать клиентское или серверное программное обеспечение, которое могло бы работать на любых компьютерах. Иногда предпринимаются попытки решить проблему разнородной среды путем создания и сопровождения отдельной версии каждой программы для каждого типа компьютера или каждого типа сетевой системы. Например, если на предприятии установлены сети трех типов, то может быть принято решение разработать и сопровождать три отдельные системы электронной почты. 19.2.2. Компьютеры с ограниченными функциональными возможностями При разработке программного обеспечения приходится не только решать проблему наличия нескольких сетей, но проводить разработку для малых, встроенных вычислительных систем, имеющих ограниченную память, низкое быстродействие процессора и неполное программное обеспечение протокола. Хотя подобные устройства должны выполнять все сетевые функции обычного компьютера, они во многих случаях не позволяют реализовать алгоритмы параллельного сервера, описанные в главе 8, или алгоритмы параллельного клиента, которые рассматриваются в главе 17. 19.2.3. Ограничения, налагаемые на взаимодействие между компьютерами по требованиям защиты На многих предприятиях приняты правила защиты, которые также ограничивают возможность взаимодействия между клиентами и серверами. На некоторых предприятиях компьютеры распределены по защищенным и незащищенным подсетям. Для предотвращения нарушения защиты при взаимодействии клиентских и серверных программ сетевой администратор устанавливает правила взаимодействия отдельных хостов в сети. В частности, администратор может обеспечить возможность взаимодействия между собой компьютеров, относящихся к защищенной подсети, но исключить возможность инициализации ими контакта с серверами, относящимися к незащищенной подсети, или приема запросов от клиентов в незащищенной подсети. Хотя такие правила предотвращают нарушение защиты, они могут усложнить разработку приложений, действующих по принципу взаимодействия клиент/сервер. Например, при таких условиях компьютеры одной подсети не могут непосредственно обращаться к службам, предоставляемым компьютерами другой подсети. 19.3. Применение прикладных шлюзов Для преодоления всех несовместимостей, нарушающих взаимодействие между клиентами и серверами в ограниченной среде, мож:ет применяться единый универсальный метод, заключающийся в том, что на промежуточных компьютерах устанавливаются дополнительные прикладные программы, которые ретранслируют информацию, передаваемую между клиентом и сервером. Промежуточная программа, предоставляющая такую службу, называется прикладным шлюзом1. Если для эксплуатации одной конкретной программы прикладного шлюза выделен промежуточный компьютер, иногда именно его и называют шлюзом. Например, почтовым шлюзом называют компьютер, выделенный для эксплуатации На практике применение термина "шлюз" может привести к путанице, поскольку IP- шлюзами первоначально назывались IP-маршрутизаторы, поэтому при совместном употреблении этих терминов необходимо указывать на различие между ними. 252 Глава 19. Шлюзы прикладного уровня
программы, передающей электронную почту из одного почтового домена в другой. Формально, термин прикладной шлюз относится к программе, выполняющей эту функцию, но этим термином часто обозначают и компьютер, на котором работает прикладной шлюз. На рис. 19.1 показан типичный пример использования прикладного шлюза в качестве посредника мея^ду двумя системами электронной почты. Прикладной шлюз воспринимает синтаксис и семантику обеих почтовых систем и преобразует сообщения в нужный формат, передавая их из одной системы в другую. Интерфейс к почтовой системе SMTP Операционная система с поддержкой протоколов TCP/IP и Х.400 Интерфейс к почтовой системе Х.400 Хост с почтовой службой SMTP Хост с почтовой службой Х.400 Рис. 19.1. Прикладная программа, применяемая для передачи электронной почты между двумя несовместимыми почтовыми доменами На рис. 19.1 показано предприятие, имеющее доступ к двум почтовым системам: основанной на стандарте SMTP (Simple Mail Transfer Protocol — Простой протокол электронной почты), который используется в составе протоколов TCP/IP сети Internet, и функционирующей в соответствии со стандартом Х.400. В каждой из этих почтовых систем используется собственный синтаксис почтовых сообщений и применяются разные протоколы передачи, но фактически обе эти системы предоставляют одинаковые услуги. Каждая система позволяет пользователю составить и отправить исходящее сообщение или получить и прочитать входящее сообщение. Однако непосредственное взаимодействие между этими двумя системами исключено, поскольку в них применяется разный синтаксис адресов источника и назначения, а также собственный протокол передачи почты. Для обеспечения возможности обмениваться сообщениями электронной почты для пользователей систем обоих типов на предприятии установлена прикладная программа, которая действует как почтовый шлюз. В данном примере программа почтового шлюза работает на компьютере, имеющем доступ к обеим почтовым системам. Почтовый шлюз должен быть тщательно спроектирован, таким образом, чтобы он мог взаимодействовать с любым хостом на предприятии. Он должен быть способен передавать сообщения с использованием любой из двух почтовых систем, а также иметь логические соединения с обеими сетями. 19.3. Применение прикладных шлюзов 253
19.4. Обеспечение взаимодействия компьютеров с помощью почтового шлюза На предприятии, показанном на рис. 19.1, достаточно было установить единственную программу почтового шлюза, чтобы решить задачу обеспечения взаимодействия двух разных систем электронной почты. Как и во время работы с любой другой системой электронной почты, каждый хост на предприятии проверяет адрес назначения исходящей почты и выбирает следующий компьютер на пути к месту назначения. Если исходящая почта предназначена для компьютера в той же сети, где находится компьютер-отправитель, то для доставки сообщения используется система электронной почты самой сети. Однако если хост обнаруживает, что исходящая почта предназначена для компьютера, находящегося в другом почтовом домене, он не может сам доставить сообщение, поэтому передает его в программу почтового шлюза. К почтовому шлюзу может непосредственно обратиться любой хост, поскольку шлюз работает на компьютере, подключенном к обоим почтовым доменам, и способен обмениваться сообщениями с использованием любого из двух протоколов передачи почты. После поступления письма в почтовый шлюз оно должно быть снова перенаправлено. Почтовый шлюз проверяет адрес назначения письма для определения дальнейших действий. В процессе принятия такого решения почтовый шлюз может также обратиться к базе данных адресов назначения. Получив информацию о том, где находится намеченный получатель и через какую почтовую систему должно быть доставлено сообщение, почтовый шлюз выбирает соответствующий протокол передачи почты. В почтовом шлюзе может возникнуть необходимость переформатировать почтовое сообщение или откорректировать его заголовок перед перенаправлением из одного домена в другой. В частности, почтовый шлюз обычно изменяет содержимое поля адреса ответа в заголовке письма, чтобы почтовый интерфейс получателя мог правильно построить адрес для ответа. Корректировка обратного адреса может оказаться простой (например, добавление суффикса с обозначением сети отправителя) или более сложной (например, добавление информации с обозначением почтового шлюза как промежуточного компьютера, через который можно связаться с отправителем письма). 19.5. Реализация почтового шлюза С теоретической точки зрения, для реализации почтового шлюза достаточно одного потока управления. Однако в большинстве практически применяемых программ функциональные средства такого шлюза распределены по двум потокам, которые могут функционировать даже в составе отдельных процессов. Один поток обрабатывает входящие почтовые сообщения, в то время как другой управляет исходящей почтой. Поток, обрабатывающий входящую почту, не отправляет сообщения. Он определяет обратный адрес, перенаправляет почту к месту ее назначения, а затем помещает исходящее сообщение в очередь для последующей передачи. Поток, обрабатывающий исходящие сообщения, не может сам принять входящие сообщения. Вместо этого он периодически просматривает выходную очередь. Для каждого сообщения, обнаруженного в выходной очереди, поток вывода создает сетевое соединение с получателем и передает сообщение. При отсутствии возможности создать соединение с получателем (например, в связи с аварией на компьютере получателя) поток вывода оставляет сообщение в выходной очереди и переходит к обработке следующего сообщения в очереди. В последствии при следующем просмотре очереди поток вывода снова предприни- 254 Глава 19. Шлюзы прикладного уровня
мает попытку установить связь с получателем и передать ему сообщение. Если это сообщение остается в выходной очереди в течение продолжительного времени (например, трое суток), поток вывода сообщает об ошибке доставки пользователю, который передал первоначальное сообщение. Разделение почтового шлюза и компонентов ввода и вывода позволяет обеспечить независимое функционирование каждого компонента. Поток вывода предпринимает попытку выполнить соединение, проверяет результаты этой попытки, а затем переходит к обработке следующего сообщения, не координируя свою работу с потоком ввода. Если попытка соединения оказалась успешной, поток вывода может передать сообщение любой длины. Ему не нужно прерывать передачу для приема входящих сообщений, поскольку выполнение этой операции обеспечивает поток ввода. Между тем, поток ввода продолжает принимать входящие сообщения, перенаправлять их и записывать в очередь для дальнейшей передачи. Поскольку оба компонента шлюза функционируют независимо друг от друга, то при передаче длинных выходных сообщений не нарушается обработка входных сообщений, а прием длинных входных сообщений не препятствует обработке выходных сообщений. 19.6. Сравнение прикладных шлюзов и туннелей Как было показано в предыдущей главе, для обеспечения взаимодействия между компьютерами в разнородной среде может применяться туннелирование. Иногда бывает сложно сделать выбор между туннелями и прикладными шлюзами, поскольку ни один из этих методов не решает всех проблем, и каждый метод обнаруживает определенные преимущества и недостатки в разных ситуациях. Основное преимущество использования прикладного шлюза вместо туннеля связано с тем, что для создания прикладных шлюзов не требуется вносить изменения в операционную систему компьютера. Во многих случаях для программистов исключена возможность внесения изменений в операционную систему потому, что у них нет доступа к исходному коду или отсутствует необходимый для этого опыт. Прикладной шлюз может быть создан с использованием обычных инструментальных средств программирования; для этого не требуется вносить какие-либо изменения в программное обеспечение базового протокола. Кроме того, после установки прикладного шлюза на сетевом узле могут использоваться обычные клиентские и серверные программы. Применение прикладных шлюзов имеет еще одно преимущество: оно позволяет обеспечить дальнейшее функционирование всех сетевых систем без каких- либо осложнений. Системным администраторам не нужно осваивать новые сетевые технологии или вносить изменения в какие-либо физические сетевые соединения. Пользователям также не нужно изучать новые интерфейсы применяемых ими служб; каждый пользователь продолжает работать с существующим клиентским программным обеспечением тех сетей, которыми он привык пользоваться. Прикладные шлюзы имеют также определенные недостатки. Метод с применением прикладного шлюза требует разработки отдельной программы прикладного шлюза для каждой службы. Почтовый шлюз обеспечивает взаимодействие почтовых служб Двух отдельных систем, но не предоставляет возможности дистанционного доступа к файлам или дистанционной регистрации. При развёртывании в сетевых системах предприятия каждой новой службы приходится создавать новый прикладной шлюз, обеспечивающий взаимодействие компонентов новой службы, относящихся к разным сетям. Для прикладных шлюзов могут также потребоваться дополнительные аппаратные ресурсы. Для их установки на предприятии может возникнуть необходимость приобретения новых компьютеров или установки дополнительных сетевых интерфейсов на существующих компьютерах. А для установки новых сетевых интерфей- 19.6. Сравнение прикладных шлюзов и туннелей 255
сов может потребоваться дополнительное аппаратное и программное обеспечение. Поскольку преобразование, необходимое для перенаправления сообщения или данных, может оказаться сложным, то для развертывания прикладных шлюзов требуются компьютеры с мощными процессорами или с большим объемом памяти. Поэтому перед предприятием может возникнуть необходимость приобрести дополнительные компьютеры или модернизировать существующие, чтобы они могли выдерживать нагрузку, связанную с установкой новых служб. В связи с увеличением затрат процессорного времени появляется дополнительная задержка обработки, которая складывается с существующей задержкой, возникающей в процессе обмена данными между клиентом и сервером. Если эта задержка становится значительной, клиентская программа может зарегистрировать тайм-аут при передаче сообщения и отправить его повторно. В отличие от метода, предусматривающего использование прикладных шлюзов, метод с использованием туннелей не требует внесения каких-либо изменений при появлении новых служб. После его установки туннель транспортного уровня становится частью базовой сетевой структуры. Поскольку приложения работают без учета наличия туннеля, они могут использоваться для предоставления доступа к любой сетевой службе. Кроме того, туннелирование обеспечивает единообразие, поскольку благодаря применению этого метода для взаимодействия между любыми компьютерами на предприятии может использоваться единственный транспортный протокол. Метод с применением туннелей характеризуется также некоторыми недостатками по сравнению с методом, предусматривающим использование прикладных шлюзов. Для установки туннеля транспортного уровня, предоставляющего полный набор функциональных возможностей, на сетевом узле возникает необходимость внести изменения в операционную систему шлюза, соединяющего две сетевые системы. Любопытно отметить, что на предприятии иногда возникает также необходимость внести изменения в программное обеспечение хостов, в которых применяется туннель. Для лучшего понимания того, с чем связана необходимость внесения таких изменений, предположим, что сеть Х.25 настроена для использования в качестве туннеля для трафика IP. Рассмотрим хост, подключенный к сети. Прежде чем в каком-либо приложении появится возможность применить туннель IP, оно должно получить доступ к программному обеспечению протоколов TCP/IP, а программное обеспечение IP должно иметь информацию о том, как туннелировать дейтаграммы через сеть Х.25. Поэтому операционная система хоста должна предоставлять прикладным программам доступ к интерфейсу IP (т.е. к интерфейсу уровня сокетов) и перенаправлять трафик в туннель. Если в операционной системе отсутствует программное обеспечение протоколов TCP/IP, оно должно быть установлено дополнительно, а если существующее программное обеспечение протокола IP не имеет информации о том, как перенаправлять сообщения в туннель, оно должно быть соответствующим образом доработано. Туннелирование может также оказать существенное влияние на работу пользователей. Дело в том, что на предприятиях туннелирование используется как способ предоставления, единообразного доступа к транспортным службам в разнотипных сетях. После установки туннеля на всех хостах предприятия начинает использоваться единый транспортный протокол для обмена данными между клиентами и серверами. Например, если на предприятии принято решение применять протокол IP и установлен туннель через сеть Х.25 для обеспечения взаимодействия между компьютерами разных сетей, то появляется возможность использовать протоколы TCP/IP для транспортных соединений во всех компьютерах предприятия. Из этого следует вывод, что все компьютеры теперь могут поддерживать сеансы взаимодействия между клиентом и сервером с применением протоколов TCP/IP. 256 Глава 19. Шлюзы прикладного уровня
К сожалению, изменение базовых сетевых протоколов обычно влечет за собой необходимость внесения изменений в клиентское программное обеспечение, с которым работают пользователи. Большинство предприятий приобретает коммерческое клиентское программное обеспечение для таких стандартных приложений, как электронная почта. Поэтому предприятие должно использовать то программное обеспечение, которое подходит для данного транспортного протокола. После перехода на универсальный транспортный протокол приходится приобретать новое клиентское программное обеспечение, в котором применяется новый набор протоколов. С точки зрения пользователя, переход на новое программное обеспечение связан с изучением нового интерфейса. Если новый интерфейс не предоставляет всех функциональных возможностей существующего интерфейса, пользователи будут разочарованы. Таким образом, чтобы исключить необходимость внесения изменений в пользовательскую среду, многие предприятия выбирают решение, предусматривающее использование прикладного шлюза. Проект прикладного шлюза должен быть тщательно продуман таким образом, чтобы после внедрения готовой программы не потребовалось вносить изменения в пользовательские интерфейсы какой-либо сети. Например, после установки прикладного шлюза для электронной почты пользователи должны иметь возможность использовать старое клиентское программное обеспечение для передачи и приема почты. В почтовой системе может применяться синтаксис адреса назначения для проведения различия между сообщениями, отправленными адресатом, который относится к локальной сети, и сообщениями, отправленными в другие сети. Такое решение позволит пользователям применять прежние адреса для той переписки, которую они вели ранее, и изучить новые адреса только для новой корреспонденции. 19.7. Прикладные шлюзы и ограничения по обмену данными, существующие в сети Internet Прикладные шлюзы позволяют расширить перечень служб, доступ к которым может быть получен с компьютеров, имеющих ограниченные возможности по установлению соединений. Например, рассмотрим работу провайдера Internet, предоставляющего своим заказчикам коммутируемый доступ к Internet. Допустим, что некоторый заказчик не имеет постоянного соединения с Internet. Вместо этого компьютер заказчика переходит в оперативный режим только на время установления заказчиком коммутируемого соединения. Аналогичным образом, рассмотрим ситуацию, когда пользователи корпоративной сети имеют переносные компьютеры, которые они отключают и каждый вечер уносят домой. На компьютере с ограниченными возможностями связи может эксплуатироваться клиентское программное обеспечение, поскольку существует возможность вызвать это программное обеспечение на выполнение после того, как установлена связь. Например, рассмотрим работу электронной почты. Пользователь может составить сообщение и передать его на хранение в память компьютера до тех пор, пока клиентская программа не сможет отправить копию сообщения по назначению. Не имеет смысла эксплуатировать компьютер с ограниченными возможностями связи как сервер электронной почты, поскольку после отключения компьютера от сети сервер становится недоступным. Прикладные шлюзы являются приемлемым решением в тех ситуациях, когда на хосте нельзя эксплуатировать сервер. На предприятии создается прикладной шлюз, который выполняет две задачи. Во-первых, шлюз работает как сервер, который принимает входящую информацию от имени каждого из хостов. Во- вторых, шлюз позволяет хосту получить доступ к поступившей для него инфор- 19.7. Прикладные шлюзы и ограничения по обмену данными... 257
мации. На рис. 19.2 показана схема сети, в которой для предоставления пользователям переносных компьютеров доступа к электронной почте применяется шлюз. Шлюз постоянно готов принимать электронную почту. К остальной части объединенной сети IP- маршрутизатор ЬЪ 5 5^~ Переносные компьютеры, на которых не функционируют почтовые серверы Рис. 192. Сеть, к которой подключаются переносные компьютеры, и прикладной шлюз, применяемый пользователями этих компьютеров для доступа к электронной почте Как показано на этом рисунке, на предприятии должен быть хотя бы один компьютер, постоянно подключенный к сети. На нем должно работать программное обеспечение, принимающее входящие сообщения электронной почты. Сервер может быть реализован как часть прикладного шлюза; может также применяться стандартный сервер. В любом случае сервер должен постоянно быть готовым к приему входящей электронной почты. После поступления сообщения сервер записывает его в файл на диске. Такие файлы часто называют файлами почтовых ящиков или просто почтовыми ящиками. В системе может быть предусмотрено применение одного файла почтового ящика для каждого пользователя или размещение каждого сообщения в отдельном файле. Обычно в тех версиях, где предусмотрено использование отдельного файла для каждого сообщения, эти файлы сгруппированы по каталогам, соответствующим конкретным пользователям. Кроме стандартного почтового сервера, этот компьютер должен также предоставлять службу прикладного шлюза, которая позволяет пользователям обращаться к своим почтовым ящикам. Для чтения почты пользователь переносного компьютера вызывает на выполнение клиентское программное обеспечение, которое подключается к прикладному шлюзу. После установления его подлинности пользователь выдает команды, под управлением которых шлюз выбирает сообщения из почтового ящика пользователя и передает их на клиентский компьютер. Кроме того, пользователь указывает, должен ли прикладной шлюз хранить или удалить конкретные сообщения. Для передачи электронной почты особенно важна одна из основных характеристик прикладных шлюзов — их способность поддерживать разнотипные протоколы. При передаче сообщения хостом, подключенным к Internet, используется протокол SMTP. Однако когда пользователь переносного компьютера обращается к своему почтовому ящику, то в соответствующем программном обеспечении используется не протокол SMTP, а такой протокол доступа, как POP (Post Office Protocol — Почтовый протокол). Еще более важно то, что прикладной шлюз позволяет преобразовать формат, используемый для почтовых сообщений, что дает возможность применять в переносных компьютерах иное представление данных по сравнению со всеми другими компьютерами в Internet. Мощный компьютер, на котором работает прикладной шлюз Операционная система 258 Глава 19. Шлюзы прикладного уровня
19.8. Применение прикладных шлюзов для защиты информации На многих предприятиях прикладные шлюзы применяются для решения проблемы защиты. Например, предположим, что на каком-то предприятии необходимо ограничить использование сеансов дистанционного доступа. Допустим, что на этом предприятии принято правило, согласно которому применение таких сеансов разрешено только для определенной категории сотрудников. На рис. 19.3 показано, как может использоваться прикладной шлюз для реализации этого правила защиты. Пользователь должен подключаться к удаленным компьютерам через прикладной шлюз, который обеспечивает проверку прав доступа. А IP-маршрутизатор Т пропускает пакеты только тех сеансов удаленного доступа, в которых участвует компьютер С IP- маршрутизатор 55 5 ЪЪ Обычные хосты могут передавать пакеты сеансов удаленного доступа только через прикладной шлюз, который управляет доступом Рис. 19.3. Прикладной шлюз, который служит для реализации правила защиты, регламентирующего использование сеансов дистанционного доступа Как показано на этом рисунке, для блокировки всех дейтаграмм, содержащих запросы на установление сеанса дистанционного доступа, кроме тех, что исходят из хоста, на котором работает прикладной шлюз, используется обычный фильтр IP-маршрутизатора (называемый также брандмауэром). Для открытия сеанса дистанционного доступа пользователь любого хоста на предприятии вызывает на выполнение клиентскую программу, которая вначале подключается к прикладному шлюзу. После получения пользователем разрешения на открытие сеанса, прикладной шлюз подключает его к требуемому компьютеру назначения. 19.9. Прикладные шлюзы и проблема дополнительного участка маршрута Проблема дополнительного участка маршрута относится к той ситуации, когда дейтаграммы дважды проходят по одной и той же сети на пути к конечному месту назначения. Эта проблема обычно вызвана неправильной настройкой таблиц маршрутизации. Установка прикладного шлюза в существующей сети также может породить проблему появления дополнительного участка маршрута. Чтобы" понять, с чем это связано, рассмотрим сетевую топологию, показанную на рис. 19.4а. На этом рисунке изображен путь, который должен быть пройден сообщением от хоста к удаленному серверу, если хост поддерживает тот же транспортный протокол, что и сервер. А те- Компьютер С, на котором работает прикладной шлюз сетевого узла, обеспечивающий управление доступом Операционная система 19.8. Применение прикладных шлюзов для защиты информации 259
перь предположим, что пользователь этого хоста решил обратиться к службе, доступной только по протоколу, отличному от используемого в данном хосте. В этом случае можно обеспечить способность к взаимодействию, введя прикладной шлюз, как показано на рис. 19.46. Прикладной шлюз принимает запросы с использованием одного набора протоколов и передает их на удаленный сервер с использованием другого. К сожалению, при этом каждое сообщение проходит по сети дважды. Этот рисунок полностью отражает реальность, поскольку сетевые администраторы часто физически устанавливают новый компьютер для каждой программы прикладного шлюза, стремясь избежать перегрузки существующих компьютеров. Но появление нового шлюза приводит к тому, что каждое сообщение проходит по сети дважды. К остальной части объединенной сети IP- маршрутизатор t i i i i~t (a) К остальной части 1 объединенной сети С 1 IP- маршрутизатор !] i_ ] ± Компьютер с прикладным шлюзом _j ] t_ i h (б) Рис. 19.4. (а) Ряд хостов и один шлюз; темная стрелка показывает путь, который проходит сообщение от хоста к удаленному серверу, (б) Путь сообщения после установки прикладного шлюза После его установки прикладной шлюз используется клиентскими программами, работающими на хостах сети, для доступа к предоставляемой им службе. Клиент передает свой запрос на прикладной шлюз с использованием одного протокола, после чего шлюз перенаправляет запрос на удаленный сервер с использованием другого протокола. После возврата сервером ответа прикладной шлюз передает ответ клиенту. Создается впечатление, что система работает хорошо. И действительно, не приходится менять существующее программное обеспечение протокола на хостах, а после установки прикладного шлюза появляется возможность обеспечить доступ к желаемой службе из клиентской программы, работающей на любом хосте. 260 Глава 19. Шлюзы прикладного уровня
К сожалению, при более внимательном изучении базовой сети обнаруживается, что конфигурация, показанная на рис. 19.46, не обеспечивает рационального использования сетевых ресурсов. Она создает проблему дополнительного участка маршрута. Каждый запрос должен пройти по локальной сети дважды. Во- первых, запрос проходит от первоначального хоста к компьютеру прикладного шлюза и, во-вторых, передается из компьютера прикладного шлюза в конечное место назначения — на сервер. Если сервер входит в состав объединенной сети TCP/IP, находящейся за пределами IP-маршрутизатора, еще одна передача происходит, когда сообщение проходит от компьютера прикладного шлюза к IP- маршрутизатору. Если же сервер находится в локальной сети, то вторая передача выполняется, когда сообщение передается из компьютера прикладного шлюза на компьютер, где работает сервер. Если служба не предусматривает использование слишком большого объема сетевого трафика, то издержки, связанные с появлением дополнительного участка маршрута, могут оказаться несущественными. И действительно, некоторые поставщики создают программные продукты, в которых используется топология, показанная на рис. 19.46. Но если сеть в значительной степени загружена или в службе используется большой объем сетевого трафика, то в связи с издержками дополнительного участка маршрута такое решение может оказаться слишком дорогостоящим. Поэтому проектировщики должны тщательно рассчитывать ожидаемую нагрузку, прежде чем принять на вооружение метод с использованием прикладного шлюза. 19.10. Пример с прикладным шлюзом Прикладной шлюз позволяет расширить перечень предоставляемых служб путем обеспечения доступа для тех клиентских компьютеров, на которых установлены не все протоколы. Например, предположим, что пользователь работает на хосте, имеющем доступ к электронной почте, но не позволяющим работать с такими протоколами передачи файлов, как FTP. Подобные ограничения могут быть обоснованы экономически (например, учтена высокая стоимость программного обеспечения FTP), они также могут зависеть от состояния дел на рынке программного обеспечения (например, никто не продает клиентское программное обеспечение FTP для рассматриваемого компьютера) или могут определяться соображениями защиты (например, на сетевом хосте решено снизить риск нарушения защиты путем запрета высокоскоростной передачи данных). Предположим, что пользователям компьютера с ограниченными возможностями нужен доступ к документам RFC (Request For Comments — Запрос на комментарии). Метод с использованием прикладного шлюза позволяет решить проблему доступа, поскольку с его помощью предприятие может состыковать службы электронной почты и FTP, управляя доступом и обеспечивая проверку полномочий пользователей. Для предоставления возможности передать документ RFC по электронной почте прикладной шлюз должен быть подключен к обеим службам. А пользователь для работы с таким шлюзом должен отправить на него сообщение электронной почты, в котором указан требуемый документ RFC. Прикладной шлюз проверяет, разрешено ли пользователю получение документов RFC, устанавливает соединение FTP, получает копию документа RFC и передает его пользователю в сообщении электронной почты. Например, предположим, что прикладной шлюз находится по адресу электронной почты rfc на компьютере somewhere. com. Допустим также, что прикладной шлюз принимает почтовые сообщения, в которых номер RFC указан в строке темы, использует FTP для выборки соответствующего документа RFC и передает полученные результаты отправителю письма. 19.10. Пример с прикладным шлюзом 261
Для использования такого шлюза пользователь должен создать почтовое сообщение, направленное по адресу rfcgsomewhere.com: То: rfc§somewhere.com From: user@elsewhere.edu Subject: 791 Тело сообщения (возможно, пустое) Это почтовое сообщение содержит запрос на получение документа RFC 791, передаваемый прикладному шлюзу. Поскольку шлюз рассматривает только номер в строке Subject:, содержимое тела сообщения не играет роли; оно может быть пустым. 19.11. Реализация прикладного шлюза » Для реализации примера прикладного шлюза, описанного выше, программисту нужны следующие аппаратные и программные средства: компьютер, который имеет доступ к электронной почте и серверу FTP, программа, которая будет действовать как прикладной шлюз, и механизм, передающий в программу шлюза каждое входящее сообщение электронной почты, направленное по определенному адресу назначения. Большинство систем UNIX, включая Linux, позволяет системному администратору легко подготовить необходимые компоненты. Администратор может создать специальный адрес назначения электронной почты rfc, добавив его к файлу псевдонимов электронной почты, или создать учетную запись для фиктивного пользователя rfc и обеспечить перенаправление всей почты для этого пользователя в программу. Почти все системы UNIX позволяют перенаправлять электронную почту в программу любому отдельно взятому пользователю, поэтому построить и отладить прикладной шлюз электронной почты может даже непривилегированный пользователь. Кроме прочих задач, прикладной шлюз должен осуществить выборку документа RFC. В данном примере реализации для выборки документа RFC используется отдельная программа. Эта программа, показанная ниже, представляет собой сценарий командного интерпретатора: t! /bin/sh t # Сценарий rfc: выборка документа RFC по номеру I t Выполняемые действия: проверить наличие в локальном кэше I копии затребованного документа и при ее t отсутствии получить копию из архива с t использованием протокола FTP; записать # копию в локальный кэш после ее # получения из архива # # Организация работы: вести локальный кэш в каталоге t /usr/tmp/RFC; использовать встроенные t команды для вызова программы ftp. # Исходить из предположения, что файлы с t расширениями Л являются упакованными и I вызывать программу zcat для их распаковки * SERVER=ftp.isi.edu PATH=/bin:/usr/bin 262 Глава 19. Шлюзы прикладного уровня
CACHE=/usr/tmp/RFC USER=anonymous PASS=guest umask 022 if test ! -d $CACHE then mkdir $CACHE chmod 777 $CACHE fi for i do if test $i = "index" then i="-index" fi if test ! -r $CACHE/$i -a ! -r $CACHE/$i".Z" -o $i = "-index" then trap "rm -f $CACHE/$i; echo int - $CACHE/$i removedjexit 1" 1 2 3 13 15 24 25 ftp -n ${SERVER}>/dev/null «! user $USER $PASS binary get rfc/rfc$i.txt $CACHE/$i quit ! trap 1 2 3 13 15 24 25 fi if test -r $CACHE/$i then cat $CACHE/$i elif test -r $CACHE/$i".Z" then zcat $CACHE/$i else echo Could not retrieve RFC $i fi done chmod 666 $CACHE/-index>/dev/null 2>&1 Сценарий rfс принимает параметр, который либо задает номер RFC, либо содержит слово index (для обозначения списка документов RFC). Он создает необходимые команды протокола FTP, а затем вызывает программу ftp. Команды служат для программы ftp указанием, что нужно открыть соединение с узлом сетевого информационного центра Network Information Center и выбрать копию указанного документа RFC. Сценарий rfc сложнее по сравнению с минимально необходимым кодом, поскольку содержит средства оптимизации. Этот код предусматривает кэширование копии каждого полученного документа RFC" в каталоге /usr/tmp/RFC. Перед выборкой документа RFC сценарий rfc проверяет, не находится ли в данный момент в этом кэше его копия. Если в кэше имеется копия, то сценарий осуществляет ее выборку, исключая тем самым ненужный сетевой трафик. 19.11. Реализация прикладного шлюза 263
19.12. Код прикладного шлюза После установки сценария rfс в системе построение прикладного шлюза для доступа к документам RFC значительно упрощается. Необходимый для этого код находится в файле rfcd. Как и программа rfc, приведенная выше, программа rfcd представляет собой сценарий командного интерпретатора. В ней для выборки документа RFC используется программа rfc, а для передачи результатов запросившему их пользователю применяется обычная программа электронной почты (/bin/mail). #!/bin/sh t # Сценарий rfcd: прикладной шлюз между клиентом электронной почты # и сервером FTP I I Выполняемые действия: получить сообщение электронной почты, # извлечь номер RFC из строки темы, # получить копию RFC и отправить ее # пользователю, приславшему запрос по # электронной почте # Организация работы: использовать сценарий awk для # интерпретации заголовка сообщения, # извлечения информации и формирования I команды; направлять команду для # выполнения непосредственно в командный f интерпретатор PATH=/bin:/usr/bin RFCPROG=/usr/local/bin/rfc awk " BEGIN { fnd = 0 } /AFrom:/ { retaddr = substr(\$0, 6) ; next } /Subject:/{ rfcnum - substr(\$0, 9) ; fnd++ ; next } Г *\$/ { if (fnd==0 || rfcnum!~/A[0-9 ]*$/) exit cmd = \M$RFCPR0G \"rfcnum cmd = cmd \" | /bin/mail -s 'RFC V'rfcnumV' 'V cmd = cmd \" 'V'retaddrV '\M print cmd ; exit }" | /bin/sh Хотя код этого сценария может показаться сложным для пользователей, не знакомых со сценариями командного интерпретатора и утилитой awk, должно быть очевидно, что для создания сценария, выполняющего такие сложные действия, потребовался лишь небольшой объем программного кода. По сути, сценарий rfcd считывает почтовое сообщение и извлекает из него содержимое строк From: и Subject:; В нем для выполнения основной работы используется программа awk, которая просматривает каждую строку входящего почтового сообщения. В этой программе принято предположение, что строка Subject: содержит номер RFC, а строка From: содержит адрес электронной почты отправителя. Адрес отправителя становится адресом назначения при формировании ответа сценарием rfcd. 264 Глава 19. Шлюзы прикладного уровня
После обнаружения пустой строки программа awk, входящая в состав сценария rfcd, определяет, что достигнут конец почтового заголовка. Она прекращает обработку, формирует и выводит программу командного интерпретатора. Поскольку сценарий rfcd перенаправляет вывод программы awk непосредственно в командный интерпретатор (sh), сформированная программа выполняется немедленно. Если входящее сообщение содержит в своем заголовке строку Subject:, имеющую правильный формат, то сформированная программа вызывает сценарий /usr/local/bin/rfc для получения указанного документа RFC и программу /bin/mail для передачи документа пользователю, приславшему запрос. Если же почтовое сообщение не содержит строки темы или в этой строке находится нечто иное, чем цифры или пробелы, то программа awk завершает работу без выработки какого-либо кода, В случае преждевременного завершения работы программы awk сценарий rfcd не предпринимает попыток получить документ RFC или даже отправить какую-либо почту (т.е. в этой версии не предусматривается передача пользователю сообщений об ошибках). 19.13. Пример обмена сообщениями с помощью шлюза Поясним этапы работы шлюза на примере. Предположим, что пользователь comer решил запросить документ RFC 1094. Он передает по адресу прикладного шлюза rfc8somewhere.com письмо с указанием в строке Subject: номера 1094. Сообщение содержит следующее: То: гfc§somewhere.com From: comer Subject: 1094 Тело сообщения (возможно, пустое) Почтовая система хоста somewhere.com настроена на передачу входящего сообщения в сценарий rfcd. После вызова сценария rfcd на выполнение сценарий awk извлекает адрес отправителя comer из строки From: почтового сообщения и сохраняет его в переменной retaddr. Затем сценарий извлекает номер 1094 из строки Subject: и сохраняет его в переменной rf cnum. После обнаружения пустой строки, отделяющей заголовок от тела сообщения, сценарий rfcd вырабатывает следующую командную строку: /usr/local/bin/rfc 1094 | /bin/mail -s 'RFC 1094' 'comer' Пользователи Linux могут обнаружить, что эта строка является допустимым вводом для командного интерпретатора bash. В ней указано, что командный интерпретатор должен выполнить программу из файла /usr/local/bin/rfc с параметром 1094, а затем перенаправить ее вывод в программу /bin/mail по каналу. Программа mail принимает три параметра: -s, RFC 1094 и comer. Первые два параметра указывают, что тема сообщения состоит из строки RFC 1094, а третий параметр определяет, что письмо должно быть отправлено по адресу comer. Тело сообщения состоит из документа RFC, полученного от сценария rf с. Поскольку сценарий rfcd передает вывод программы awk непосредственно в командный интерпретатор, последний выполняет командную строку, показанную выше. Командный интерпретатор вызывает на выполнение программу rfc, a затем передает результаты по почте пользователю comer. 19.13. Пример обмена сообщениями с помощью шлюза 265
19.14. Применение сценария rfcd в сочетании с файлом .forward или программой slocal Выполнить проверку сценария rfcd может любой пользователь с применением своей учетной записи электронной почты. Для этого пользователь должен создать файл .forward в своем начальном каталоге. При получении каждого сообщения почтовая система извлекает имя получателя из поля То: и проверяет наличие файла .forward в начальном каталоге пользователя. Обнаружив такой файл, почтовая система считывает его и выполняет содержащуюся в нем команду. Например, предположим, что пользователю нужно обеспечить передачу с помощью обработчика почты каждого из адресованных ему входящих сообщений в программу с полным именем пути /usr/XXX/Q. Пользователь помещает в файл . forward одну строку: | M/usr/XXX/QH Почтовая система интерпретирует эту строку, как требование вызвать программу /usr/XXX/Q, передав на ее стандартный вход полученное почтовое сообщение. Таким образом, для вызова прикладного шлюза электронной почты, описанного выше, пользователь должен поместить сценарий rfcd в файл, отметить этот файл как предназначенный для выполнения и ввести в файл .forward запись, указывающую на файл сценария. Любопытно отметить, что пользователи системы Linux могут не разрабатывать почтовый шлюз с самого начала, а воспользоваться программой slocal, которая выполняет основную часть работы. Пользователь указывает с помощью файла .forward, что каждое входящее сообщение электронной почты должно быть передано программе slocal. Программа slocal обращается к файлу спецификации .maildelivery для определения способа дальнейшей обработки сообщения. Пользователь может предусмотреть, чтобы некоторые входящие сообщения поступали в почтовый ящик, а другие передавались в прикладной шлюз. 19.15. Универсальный прикладной шлюз Для использования с коммутируемыми телефонными линиями был разработан интересный прикладной шлюз под названием slirp. Этот шлюз отличается тем, что обеспечивает доступ к нескольким службам. В частности, шлюз slirp был разработан для обеспечения возможности выполнения на домашнем компьютере любой прикладной программы для доступа к произвольному приложению в сети Internet по протоколам TCP/IP. Чтобы оценить важность проекта slirp, необходимо знать, что он решает другую проблему по сравнению с протоколами SLIP или РРР. Программа slirp не определяет протокол инкапсуляции, предназначенный для работы по последовательной линии, а решает важную проблему IP-адресации. Эта проблема возникает, когда пользователь домашнего компьютера желает получить коммутируемый доступ к Internet. В традиционной модели IP-адресации каждому компьютеру должен быть присвоен уникальный IP-адрес. К сожалению, коммутируемый доступ усложняет проверку того, кому принадлежит данный конкретный IP-адрес, и поэтому контроль соблюдения ограничений доступа с помощью такого адреса является затруднительным. Вот почему исходя из соображений защиты многие компании не присваивают постоянные IP-адреса удаленным компьютерам и ограничивают использование обычных терминальных сеансов в режиме коммутируемого доступа. 266 Глава 19. Шлюзы прикладного уровня
Основным недостатком ограничения коммутируемого доступа является сужение функциональных возможностей: хотя терминальный сеанс в режиме коммутируемого доступа может выполняться с применением таких символьных протоколов, как FTP и TELNET, пользователь не может обращаться к службам типа World Wide Web, которые требуют наличия у клиентов полного доступа по протоколу IP. Программа slirp позволяет преодолеть это ограничение с помощью механизма, предоставляющего доступ по протоколу IP через коммутируемые телефонные линии без присвоения IP-адреса каждому компьютеру. 19.16. Принципы работы программы slirp Программа slirp работает как прикладной шлюз на компьютере, имеющем и доступ к Internet, и модем для коммутируемого доступа по телефонной линии. В процессе работы с программой slirp необходимо вначале открыть обычный терминальный сеанс. Это означает, что пользователь домашнего компьютера вначале дозванивается на компьютер, предположим G, на котором работает шлюз slirp, и открывает терминальный сеанс, отправив идентификатор регистрационной учетной записи и пароль. После открытия терминального сеанса работы с компьютером G пользователь быстро выполняет один за другим два действия. Во-первых, он вызывает программу slirp на компьютере G. Во-вторых, он закрывает терминальный сеанс на домашнем компьютере и разрешает программному обеспечению протокола РРР2 использовать установленное соединение. В действительности, существует программное обеспечение, позволяющее автоматизировать действия по дозвону, запуску программы slirp на компьютере сервера, а затем запуску программного обеспечения РРР на локальном компьютере. В процессе работы по коммутируемому соединению программное обеспечение РРР рассматривает его как последовательную линию. Средства 1Р-маршрутизации домашнего компьютера используют коммутируемое соединение как применяемый по умолчанию маршрут для всего трафика. Программное обеспечение РРР в одном конце соединения инкапсулирует дейтаграммы во фреймы РРР для передачи, а на другом конце соединения программное обеспечение этого протокола извлекает эти дейтаграммы из фреймов. Поскольку программа slirp совместима со средствами инкапсуляции протокола РРР, она позволяет передавать или принимать дейтаграммы по коммутируемому соединению. Кроме того, программа slirp содержит весь код, необходимый для обработки входящих сегментов TCP, а также дейтаграмм IP. Например, эта программа может формировать или проверять контрольную сумму IP, a также посылать подтверждения в ответ на получение сегмента TCP. Хотя программа slirp является просто прикладной программой, она содержит программное обеспечение для всего стека протоколов TCP/IP. 19.17. Поддержка соединений программой slirp Поскольку slirp — это прикладная программа, содержащийся в ней код поддержки протоколов TCP/IP не взаимодействует непосредственно с программным обеспечением протоколов TCP/IP в операционной системе компьютера, на котором установлен этот шлюз. Вместо этого в программе slirp код поддержки протоколов TCP/IP используется только для интерпретации дейтаграмм, поступаю- Хотя в этом примере упоминается протокол РРР, есть и такие версии программы slirp, которые позволяют использовать протокол SLIP. 19.16. Принципы работы программы slirp 267
щих по коммутируемому соединению; для взаимодействия с Internet в этой программе применяется интерфейс сокетов. Программа slirp устанавливает соответствие между событиями, происходящими в коммутируемом соединении и в Internet. Например, предположим, что пользователь домашнего компьютера установил соединение TCP с адресом назначения D. По мере поступления сегментов TCP из домашнего компьютера программа slirp формирует на них ответ. Это означает, что при получении от домашнего компьютера сегмента SYN программа slirp отвечает так, как если бы ответ прислал компьютер D (т.е. slirp исполняет роль компьютера D). Между тем, программа slirp создает сокет и подключается к компьютеру D. Аналогичным образом, программа slirp выполняет переброску данных между домашним компьютером и сетью Internet. Например, если данные поступают через соединение TCP из домашнего компьютера, программа slirp получает эти данные, возвращает подтверждение TCP, а затем использует интерфейс сокетов для передачи данных в место назначения. 19.18. IP-адресация и slirp В чем состоят отличия программы slirp от обычных программ, использующих протокол РРР? Ответ заключается в том, что в этой программе применяется иная IP-адресация, как показано на рис. 19.5. При взаимодействии с хостом, находящимся в Internet, программа slirp использует интерфейс сокетов, а поэтому действует как и любое другое приложение, работающее на компьютере шлюза. В частности, во всех сеансах связи, проводимых программой slirp через объединенную сеть, используется IP-адрес, присвоенный компьютеру шлюза, поэтому на удаленном конце каждого соединения невозможно определить, проводится ли связь с помощью программы slirp или любого другого приложения на компьютере шлюза. Хотя на домашнем компьютере может использоваться произвольный IP-адрес, в программе slirp при взаимодействии с хостом Internet по протоколам TCP/IP применяется действительный адрес. Таким образом, в программе slirp, обменивающейся данными через Internet, используется стандартная адресация, тогда как адресация, применяемая этой программой в коммутируемом соединении, является нестандартной. Поскольку программа slirp перехватывает все дейтаграммы, поступающие от домашнего компьютера, IP-адрес, применяемый на этом компьютере, не обязательно должен быть действительным, т.е. может использоваться любой адрес, но при условии, что он не применяется где-то в другом месте3. Таким образом, пользователи slirp часто выбирают IP-адрес типа 10.0.0.1, который не присваивается какому- либо иному хосту в Internet, а также является удобным для запоминания. 19.19. Резюме Туннелирование позволяет использовать в одной системе протоколов в качестве транспортного средства другой протокол высокого уровня, но имеет ограниченную область применения, поскольку для его использования разработчик должен иметь доступ к коду операционной системы. В отличие от туннелирования, метод согласования разных протоколов, основанный на применении прикладного шлюза, позволяет прикладным программистам организовывать взаимодействие разнотипных систем без внесения изменений в операционную систему. Домашний компьютер, в котором используется действительный адрес Internet, не сможет взаимодействовать с компьютером, которому фактически принадлежит этот адрес. 268 Глава 19. Шлюзы прикладного уровня
Интерфейс к модему коммутируемой линии передачи Операционная система с поддержкой TCP/IP Стандартный интерфейс сокетов к протоколам TCP/IP Домашний компьютер, в котором используется протокол SUP или РРР Хост сети Internet, в котором используются протоколы TCP/IP Рис. 19.5. Принципы работы шлюза slirp Прикладной шлюз — это программа, которая принимает запросы с помощью одного протокола высокого уровня и выполняет их с использованием другого протокола высокого уровня* Фактически каждый прикладной шлюз является сервером одной службы и клиентом другой. В настоящей главе описан прикладной шлюз, который принимает по электронной почте запросы на получение документов RFC, использует протокол FTP для приема указанного документа RFC и передает его отправителю запроса с помощью сообщения электронной почты. Как показывает этот пример, прикладной шлюз не обязательно должен быть сложным или написанным на языке низкого уровня. Приведенный здесь код состоит из сценария командного интерпретатора и программы awk. Прикладные шлюзы на многих сетевых узлах используются для проверки прав доступа и соблюдения правил защиты. Поскольку шлюз работает как обычная прикладная программа, в нем можно легко предусмотреть блокировку попыток несанкционированного доступа или регистрацию всех запросов. Есть и такой шлюз, под названием slirp, который поддерживает несколько служб. Программа slirp, предназначенная для использования по коммутируемой телефонной линии, позволяет обеспечить доступ пользователя домашнего компьютера к службам IP, не требуя присвоения этому компьютеру действительного IP-адреса. На домашнем компьютере работает программное обеспечение РРР или SLIP, которое передает дейтаграммы IP по коммутируемому соединению. Программа slirp получает входящие дейтаграммы, передает подтверждения от имени адресата и использует обычное программное обеспечение, применяемое в Internet, для взаимодействия с адресатом. Материал для дальнейшего изучения С дополнительной информацией о языке командного интерпретатора и языке программирования awk можно ознакомиться в документации, которая поставляется вместе с операционной системой. Программирование на языке awk подробно Материал для дальнейшего изучения 269
описано в книге [2]. А в книге [13] рассматривается командный интерпретатор, который был предшественником программы bash, и приведены примеры программ командного интерпретатора. Программа sendmail операционной системы Linux обеспечивает передачу электронной почты по многим транспортным протоколам; она может быть настроена на функционирование в качестве прикладного шлюза. С информацией о программе sendmail можно ознакомиться в оперативной документации. Почтовый протокол (POP —~ Post Office Protocol) определяет стандартную службу, позволяющую пользователю персонального компьютера обращаться к своей электронной почте, которая хранится на сервере. Версия 3 протокола POP (РОРЗ) описана в документе [104]. Упражнения 19.1. Предусмотрите проверку прав доступа в прикладном шлюзе, описанном в качестве примера в настоящей главе, чтобы с ее помощью системный администратор мог регламентировать доступ пользователей. 19.2. Предусмотрите средства ведения журнала в примере прикладного шлюза, рассматриваемом в этой главе, позволяющие составить список всех пользователей, запросивших документы RFC. 19.3. Как может отправитель запросить копию списка документов RFC с помощью прикладного шлюза, рассматриваемого в этой главе? 19.4. Обратите внимание на то, что при вызове пользователем сценария rf с для запроса документа RFC именно в тот же момент, когда прикладной шлюз использует этот сценарий для передачи запроса на получение того же документа RFC, пользователь может получить пустой или укороченный файл. Измените сценарий, чтобы исправить эту ошибку. 19.5. Как может системный администратор установить прикладной шлюз с помощью системы sendmail? 19.6. Может ли программное обеспечение slirp использоваться для эксплуатации клиентского программного обеспечения на домашнем компьютере? Может ли оно использоваться для эксплуатации сервера? Объясните ваш ответ. 19.7. Перепишите приведенный в этом примере сценарий awk с применением языка сценариев Perl. Какая из этих программ меньше? Какая из них удобнее для чтения? 270 Глава 19. Шлюзы прикладного уровня
20 Внешнее представление данных (XDR) 20.1. Введение В предыдущих главах описаны алгоритмы, средства и методы реализации клиентских и серверных программ. С этой главы начинается описание понятий и методов, позволяющих программистам использовать средства взаимодействия типа клиент/сервер, а также механизмов, предоставляющих программную поддержку для этих концепций. В частности, в настоящей главе рассматривается способ внешнего представления и оформления данных, фактически признанный в качестве стандарта, а также набор библиотечных процедур, используемых для преобразования данных. Кроме того, рассматриваются основные причины применения средств внешнего представления данных и приводятся сведения об одной конкретной реализации таких средств. В следующей главе показано, как стандарт внешнего представления данных позволяет упростить взаимодействие между клиентом и сервером, и приведен пример, подтверждающий, что стандарт обеспечивает возможность использования единообразного механизма дистанционного доступа для этого взаимодействия. 20.2. Различные способы представления данных В каждой компьютерной архитектуре принято собственное определение способа представления элементов данных. Даже если на двух компьютерах с разной архитектурой поддерживаются одинаковые типы данных, для них могут использоваться разные представления. Например, считается, что в компьютерной архитектуре применяется формат слова, оканчивающийся младшим байтом, если в ней наименее значимый байт целого числа имеет меньший адрес в памяти, или формат слова, оканчивающийся старшим байтом, если в ней меньший адрес имеет наиболее значимый байт. Некоторые архитектуры не принадлежат ни к тому, ни к другому типу, поскольку в них байты целого числа не занимают смежные байты в памяти. На рис. 20.1 показано представление 32-битовых целых чисел в формате слова, оканчивающегося старшим байтом и младшим байтом. На рис. 20.1а представлен формат слова, оканчивающегося старшим байтом, в котором наиболее значимый байт имеет меньший адрес в памяти, а на рис. 20.16 — формат слова, оканчивающегося младшим байтом, в котором меньший адрес в памяти имеет наименее значимый байт. Цифрами указано десятичное значение каждого 8-битового байта. Программисты, разрабатывающие программы для отдельно взятого компьютера, могут не учитывать разницу в представлениях ДАвных* Поскольку каждый конкретный компьютер обычно допускает только одньпредставление. Если про-
граммист объявляет переменную как целое число (например, с использованием объявления int языка С), транслятор использует внутреннее представление данных компьютера при распределении памяти для этого целого числа или при формировании кода выборки из памяти, записи в память или сравнения значений. Увеличение адресов памяти Старший Младший Уменьшение адресов памяти «^ байт байт > 0 0 1 4 (а) Младший Старший байт байт 4 1 0 0 (б) Рис. 20.1. Два представления значения 260, хранящегося в виде 32-битового двоичного целого числа Однако программисты, разрабатывающие клиентское и серверное программное обеспечение, должны учитывать способы представления данных, поскольку на обоих концах соединения должно быть согласовано точное представление всех данных, передаваемых по каналу связи от отправителя к получателю. Если на двух компьютерах используются разные внутренние представления данных, то необходимо предусмотреть преобразование данных, передаваемых из программы на одном компьютере в программу на другом. 20.3. Проблема асимметричного преобразования и быстрого роста числа его вариантов Основной и наиболее характерной для представления данных является проблема переносимости программного обеспечения. При ее решении специалисты иногда идут на такую крайность: вкладывают информацию о каждой компьютерной архитектуре в каждую пару клиентских и серверных программ и предусматривают обязательное преобразование данных одним из участников соединения в представление, подходящее для другого. Считается, что в проектах, в которых применяется принцип преобразования непосредственно в то представление, которое требуется для другого участника соединения, используется асимметричное преобразование данных, поскольку преобразование должен выполнять только один участник соединения. К сожалению, способ асимметричного преобразования данных требует создания отдельной версии программного обеспечения клиент/сервер для каждой пары архитектур, в которых будут использоваться эти программы. Построение отдельных версий программ может оказаться весьма дорогостоящим. Чтобы понять, с чем это связано, рассмотрим набор, состоящий из N компьютеров. Если на каждом компьютере для хранения чисел с плавающей точкой применяется разное представление данных, то должно быть подготовлено всего (N2 - К) 12 версий любых программ типа клиент/сервер, которые обмениваются числами с плавающей точкой. Эту проблему1 принято называть проблемой роста числа ва- Иногда в литературе эту проблему называют проблемой n*m вариантов, чтобы подчеркнуть, что клиенты могут работать в п архитектурах, а серверы — в m архитектурах. 272 Глава 20. Внешнее представление данных (XDR)
риантов преобразования пропорционально квадрату N, чтобы подчеркнуть, что объем работы по программированию пропорционален квадрату числа различных представлений данных. На проблему роста числа вариантов преобразования, пропорционального квадрату N, можно взглянуть и с другой стороны, если представить себе, какие трудозатраты необходимы для подключения компьютера с новой архитектурой к существующему набору, состоящему из N компьютеров. После поступления каждого нового компьютера необходимо разработать N новых версий каждой пары программ клиент/сервер, чтобы обеспечить взаимодействие этого нового компьютера с каждым из уже существующих. Даже если в процессе программирования используется условная трансляция (например, с применением конструкции ifdef препроцессора С), задачи разработки, отладки, сопровождения и управления многочисленными версиями программы могут стать очень трудоемкими. Кроме того, пользователям при вызове клиентской программы нужно будет учитывать, что для каждой конкретной конфигурации подходит только одна из многочисленных версий. Если в программном обеспечении клиент/сервер предусмотрено асимметричное преобразование из внутреннего представления данных клиента во внутреннее представление данных сервера, то число версий программного обеспечения растет пропорционально квадрату числа компьютерных архитектур, N. 20.4. Сетевой стандартный порядок байтов Чтобы избежать проблем, связанных с сопровождением числа версий программ клиент/сервер, пропорционального квадрату N, программисты не используют асимметричное преобразование, а создают клиентское и серверное программное обеспечение таким образом, чтобы каждую конкретную исходную программу можно было транслировать и вызывать на выполнение на различных компьютерах без каких- либо изменений. В результате этого программирование упрощается, поскольку разрабатываемый код может быть легко перенесен из одной архитектуры в другую. В конечном итоге упрощается также доступ к службе, поскольку пользователям достаточно только запомнить, как нужно вызывать одну версию клиента. Но как обеспечить правильную трансляцию единственной исходной программы на разных архитектурах, если в них используются различные варианты представления данных? Еще важнее найти ответ на вопрос о том, как может клиентская или серверная программа передавать данные программе на другом компьютере, если на этих двух компьютерах используются разные представления данных? В главе 5 описано, как решается проблема представления целочисленных данных в протокольных заголовках протоколов TCP/IP: в этих протоколах используются функции, выполняющие преобразование из внутреннего порядка байтов компьютера в сетевой стандартный порядок байтов. Фактически в протоколах TCP/IP для всех данных, передаваемых по сети в протокольных заголовках, используется одно и то же представление. Это означает, что в заголовках целочисленные данные представлены в стандартной, машинно-независимой форме. Принято считать, что в таких проектах применяется симметричное преобразование данных^ т.е. вместо преобразования непосредственно из одного представления в другое, каждый участник соединения берет на себя обязанности выполнять преобразование из локального представления в стандартное представление, используемое при передаче. Применение симметричного преобразования данных имеет одно важное следствие: оно позволяет иметь только одну версию программного обеспечения протокола. 20.4. Сетевой стандартный порядок байтов 273
Метод симметричного преобразования данных получил самое широкое распространение при разработке программного обеспечения клиент/сервер. Вместо преобразования непосредственно из представления данных одного компьютера в представление другого, и клиент, и сервер выполняют преобразование данных в единый формат. Перед передачей по сети данные преобразуются из внутреннего представления компьютера-отправителя в стандартное, машинно-независимое представление. Аналогичным образом, данные преобразуются из машинно- независимого представления во внутреннее представление компьютера-получателя после приема данных из сети. Стандартное представление, используемое для передачи данных по сети, принято называть внешним представлением данных. Применение стандартного внешнего представления данных имеет свои преимущества и недостатки. Основным преимуществом является расширение области применения: ни клиентская, ни серверная программа не должна иметь информации о том, в какой архитектуре работает другой участник соединения. Для доступа к серверу на любом компьютере может применяться только одна клиентская программа, в которой не должны учитываться сведения о его архитектуре. Общие затраты труда программистов становятся пропорциональными числу компьютерных архитектур, а не квадрату этого числа. Основным недостатком симметричного преобразования является рост издержек, связанных с преобразованием данных. В тех случаях, если и клиент, и сервер работают на компьютерах с одинаковой архитектурой, эти издержки кажутся неоправданными. Один из участников соединения преобразует все данные из внутреннего представления своей архитектуры во внешнее представление перед их передачей, а другой участник соединения снова преобразует данные из внешнего представления в первоначальное после их приема. Даже если клиентский и серверный компьютер не имеют одинаковой архитектуры, применение промежуточной формы представления данных вносит дополнительные издержки. Вместо преобразования данных непосредственно из представления отправителя в представление получателя, и клиент, и сервер должны расходовать процессорное время на прямое и обратное преобразование из локального представления во внешнее. Кроме того, поскольку внешнее представление может потребовать введения дополнительной информации в передаваемые данные или выравнивания их по границе слова, такое преобразование может привести к увеличению объема передаваемых данных сверх необходимого. Но несмотря на дополнительные вычислительные издержки и увеличение объема данных, передаваемых по сети, большинство программистов считают целесообразным использование симметричного преобразования данных, поскольку оно упрощает программирование, уменьшает количество ошибок и увеличивает способность к взаимодействию различных программ. Этот способ позволяет также упростить управление сетью и отладку, поскольку сетевой администратор может анализировать содержимое любых пакетов без учета того, какие архитектуры имеют компьютеры отправителей и получателей. 20.5. Внешнее представление данных, фактически признанное стандартным Компания Sun Microsystems разработала внешнее представление данных, которое определяет способы представления наиболее распространенных форматов данных при передаче по сети. Это представление компании Sun, известное под названием XDR (external Data Representation — Внешнее представление данных), фактически применяется в качестве стандартного в большинстве приложений клиент/сервер. Представление XDR определяет форматы данных для боль- 274 Глава 20. Внешнее представление данных (XDR)
шинства типов данных, которыми обмениваются клиенты и серверы. Например, представление XDR определяет, что 32-битовые двоичные целые числа должны быть представлены в формате слова, оканчивающегося старшим байтом (т.е. наиболее значимый байт должен иметь меньший адрес в памяти). 20.6. Типы данных XDR В табл. 20.1 перечислены типы данных, для которых определены стандартные представления XDR. Стандарт XDR указывает, как должны быть закодированы элементы данных каждого типа для передачи по сети. Таблица 20.1. Типы данных, для которых определено внешнее представление XDR Тип данных Размер Описание int unsigned int bool enum hyper unsigned hyper float double opaque string fixed array counted array structure discriminated union 32 бита 32 бита 32 бита произвольный 64 бита 64 бита 32 бита 64 бита произвольный произвольный произвольный произвольный произвольный произвольный void symbolic constant 0 битов произвольный 32-битовое целое число со знаком 32-битовое целое число без знака Логическое значение (ложное или истинное), представленное, соответственно, нулем или единицей Перечисление, значения которого закодированы целыми числами (например, RED=1, WHiTE-2, blue=3) 64-битовое целое число со знаком 64-битовое целое число без знака Число с плавающей точкой одинарной точности Число с плавающей точкой двойной точности Непреобразованные данные (т.е. данные во внутреннем представлении отправителя) Строка символов ASCII Массив постоянного размера, состоящий из данных другого типа, внешнее представление которого не содержит информации о его размере Массив переменного размера, состоящий из данных другого типа, внешнее представление которого содержит информацию о его размере Агрегат данных, подобный структуре struct языка С Структура данных, допускающая использование одной из нескольких альтернативных форм, которая напоминает объединение union языка С или вариантную запись языка Pascal Используется, если данные не представлены там, где элемент данных является необязательным (например, в структуре) Символическая константа и связанное с ней значение optional data произвольный Допускает нуль или одно вхождение данных 20.6. Типы данных XDR
Типы, представленные в табл. 20.1, охватывают большинство структур данных, используемых в прикладных программах, поскольку они позволяют программисту создавать составные типы из других типов. Например, кроме массива целых чисел, стандарт XDR допускает применение массива структур, причем каждая из этих структур может иметь многочисленные поля, представляющие собой, в свою очередь, массив, структуру или объединение. Таким образом, стандарт XDR предусматривает способы представления данных для большинства структур, которые могут быть определены в программе С. 20.7. Неявные типы Стандарт XDR определяет, как должен быть закодирован объект данных для каждого из типов данных, перечисленных в табл. 20.1. Однако эти кодировки содержат только элементы данных, но не информацию об их типах. Например, стандарт XDR определяет, что для 32-битовых двоичных целых чисел должен использоваться формат слова, оканчивающийся старшим байтом (такая же кодировка используется в протокольных заголовках TCP/IP). Если в прикладной программе для кодирования 32-битового целого числа применяется представление XDR, то полученный результат занимает точно 32 бита; эта кодировка не содержит дополнительных битов, которые применялись бы для обозначения элемента данных как целого числа или указания его длины. Поэтому клиенты и серверы, в которых используется стандарт XDR, должны согласовать между собой точный формат сообщений, которыми они обмениваются. Программа не может интерпретировать сообщение, закодированное с помощью стандарта XDR, не имея информации о том, какой именно формат и какие типы имеют все поля данных. 20.8. Программная поддержка для стандарта XDR Программисты, использующие представление XDR для симметричного преобразования данных, должны внимательно следить за тем, чтобы каждый элемент этих данных перед передачей по сети был преобразован во внешнюю форму. Аналогичным образом, в программе-получателе необходимо проконтролировать, чтобы каждый принятый элемент данных был преобразован во внутреннее представление. В главе 5 показан один из способов, который может использоваться для выполнения преобразования: вставка кода в вызов функции для преобразования каждого элемента данных в сообщении во внешнюю форму перед отправкой этого сообщения и вставка вызова функции для преобразования каждого элемента данных во внутреннюю форму при поступлении сообщения. Для большинства программистов не составит труда написать функции преобразования основной части форматов XDR. Однако для осуществления некоторых преобразований требуется значительный опыт (например, для преобразования из внутреннего представления в компьютере числа с плавающей точкой в стандартное представление XDR без потери точности необходимо владеть основами численного анализа). Чтобы можно было исключить потенциальные ошибки преобразования, реализация XDR включает библиотечные процедуры для выполнения всех необходимых преобразований. 20.9. Библиотечные процедуры XDR Библиотечные процедуры XDR на конкретном компьютере позволяют выполнять прямое и обратное преобразование элементов данных из внутреннего представления компьютера в стандартное представление XDR. В большинстве реализаций XDR для организации этой работы используется буфер, позволяющий создавать целое сообщение в форме XDR. 276 Глава 20. Внешнее представление данных (XDR)
20.10. Последовательная сборка сообщения Для применения средств преобразования XDR по принципу использования буфера необходимо распределить в программе буфер, достаточно большой для того, чтобы в нем можно было поместить внешнее представление сообщения и последовательно добавлять к нему элементы данных (т.е. поля). Например, в операционной системе Linux предусмотрена версия программного обеспечения XDR, предоставляющая процедуры преобразования, каждая из которых добавляет внешнее представление элемента данных к концу буфера в памяти. Программа вначале вызывает процедуру xdrmem_create для распределения буфера в памяти и передачи программному обеспечению XDR информации о том, что перед ним стоит задача составить в этом буфере внешнее представление сообщения. Процедура xdrmem_create инициализирует область памяти таким образом, чтобы эта область представляла поток данных XDR, который может использоваться для кодирования (преобразования в стандартное представление) или декодирования (преобразования во внутреннее представление) передаваемых или принимаемых данных. В результате этого вызова поток XDR инициализируется как пустой путем установки внутреннего указателя равным адресу начала буфера. Процедура xdrmem_create возвращает указатель потока, который должен затем применяться при последующих вызовах процедур XDR. Ниже приведены примеры объявлений и вызовов, необходимых для создания потока XDR в программе на языке С: ((include <rpc/xdr.h> tdefine BUFSIZE 4000 /* Размер области памяти, в которой */ /* будет выполняться кодирование */ XDR *xdrs; /* Указатель на "поток" XDR */ char buf[BUFSIZE]; /* Область памяти для хранения данных XDR */ xdrmem_create(xdrs, buf, BUFSIZE, XDRJ3NC0DE); После создания потока XDR в программе можно вызывать отдельные процедуры преобразования XDR для представления внутренних объектов данных во внешней форме. При каждом вызове таких процедур кодируется один объект данных, и закодированная информация присоединяется к концу потока (т.е. закодированные данные размещаются в следующем доступном участке буфера, после чего внутренний указатель потока обновляется). Например, процедура xdr_int преобразует 32-битовое двоичное целое число из внутреннего представления в стандартное представление XDR и присоединяет его к потоку XDR. В программе процедура xdr^int вызывается путем передачи ей указателя на поток XDR и указателя на целое число: int i; /* Целое число во внутреннем представлении */ ... /* Предполагается, что поток инициализируется для */ /* работы в режиме XDR_ENCODE */ i = 260; /* Переменной присваивается преобразуемое */ /* целочисленное значение */ xdr_int(xdrs, &i); /* Целое число преобразуется и */ /* присоединяется к потоку */ На рис. 20.2 показано, как в результате выполнения вызова процедуры xdr_int, приведенного в примере кода, к потоку XDR добавляются четыре байта данных. 20.10. Последовательная сборка сообщения 277
Г Заголовок Т потока 0 1 2 3 4 5 6 (а) г ч I Заголовок 1 потока 0 1 2 3 4 5 6 0 0 1 4 (б) Рис. 20.2. (а) Поток XDR, который был инициализирован для кодирования данных с использованием параметра XDR^ENCODE и уже содержит 7 байтов данных, и (б) тот же поток XDR после вызова процедуры xdr_int для присоединения 32-битового целого числа со значением 260 20.11. Процедуры преобразования библиотеки XDR Процедуры преобразования библиотеки XDR перечислены в табл. 20.2. Эти процедуры позволяют выполнять и прямое, и обратное преобразование данных, поскольку в них большинство параметров являются указателями на объекты данных, а не значениями этих данных. Таблица 20.2. Процедуры преобразования данных XDR, входящие в состав библиотеки XDR Процедура Параметры Преобразуемый тип данных xdr__bool xdrs, ptrbool xdr_bytes xdrs, ptrstr, strsize, maxsize xdr_char xdrs, ptrchar xdrjiouble xdrs, ptrdouble xdr_enum xdrs, ptrint xdr_float xdrs, ptrfloat xdr_int xdrs, ip xdr_long xdrs, ptrlong xdr_opaque xdrs, ptrchar, count xdr_pointer xdrs, ptrobj, objsize, xdrobj xdr_short xdrs, ptrshort xdr_string xdrs, ptrstr, maxsize xdr_u_char xdrs, ptruchar xdr_u_short xdrs, ptrushort Логическое значение (значение int в языке С) Строка байтов переменной длины Символ Число с плавающей точкой двойной точности Переменная перечислимого типа (значение int в языке С) Число с плавающей точкой одинарной точности 32-битовое целое число 64-битовое целое число Байты, передаваемые без преобразования Указатель (используемый в связанных структурах данных, таких как списки или деревья) 16-битовое целое число Строка языка С 8-битовое целое число без знака 16-битовое целое число без знака 278 Глава 20. Внешнее представление данных (XDR)
Окончание табл. 20.2 Процедура Параметры Преобразуемый тип данных xdr_u_int xdr_u_long xdrjinion xdr__vector xdr void xdrs, ptrint xdrs, ptrulong xdrs, ptrdiscrim, ptrunion, choicefen, default xdrs, ptrarray, size, elemsize, elemproc Отсутствуют 32-битовое целое число без знака 64-битовое целое число без знака Размеченное объединение Массив постоянной длины Преобразование не выполняется (используется для обозначения пустого элемента структуры данных) Для формирования сообщения необходимо вызвать в приложении процедуры преобразования XDR для каждого элемента данных в сообщении. После кодирования каждого элемента данных и размещения их в потоке XDR приложение может передать сообщение путем отправки результирующего потока. В приложении- получателе весь этот процесс должен быть выполнен в обратном порядке. В нем вызывается процедура xdrmem_create для создания в памяти буфера, предназначенного для размещения потока XDR, и в этот буфер записывается входящее сообщение. Программное обеспечение XDR регистрирует направление преобразования в самом потоке, в котором процедуры преобразования могут получить доступ к этому потоку. При создании потока XDR, который будет использоваться для ввода, получатель задает в качестве третьего параметра функции xdrmem_create константу XDRJDEC0DE. В результате при вызове получателем каждой отдельной процедуры преобразования с указанием в качестве параметра входного потока процедура извлекает из потока элемент данных и преобразует его во внутреннее представление. Например, если получатель установил поток XDR, используемый для ввода (т.е. в вызове процедуры инициализации был указан параметр XDR_DEC0DE), он может извлечь из потока 32-битовое целое число и преобразовать его во внутреннее представление путем вызова процедуры xdr_int: int i; /* Целое число во внутреннем представлении */ . . . /* Предполагается, что поток инициализируется для */ /* работы в режиме XDRJDECODE */ xdr_int(xdrs, &i); /* Целое число извлекается из потока */ Поэтому, в отличие от процедур преобразования htons и ntohs, предусмотренных в операционной системе Linux, отдельные процедуры преобразования XDR не позволяют определить его направление. Вместо этого они требуют, чтобы это направление было задано в программе при создании потока XDR. Отдельные процедуры преобразования XDR не позволяют указать направление преобразования. Вместо этого отдельная процедура, позволяющая выполнять и прямое и обратное преобразование, устанавливает направление преобразования путем определения характеристик используемого потока XDR. 20.11. Процедуры преобразования библиотеки XDR 279
20.12. Потоки XDR, ввод/вывод и протокол TCP В приведенных выше примерах кода создается поток XDR, связанный с буфером в памяти. Применение памяти для буферизации данных позволяет повысить эффективность работы программы, поскольку обеспечивает возможность преобразовывать в приложении во внешнюю форму большие объемы данных перед передачей их по сети. После преобразования элементов данных во внешнюю форму и размещения их в буфере, в приложении необходимо вызвать функцию ввода/вывода, такую как write, для передачи данных через соединение TCP. Кроме того, может применяться способ использования процедур преобразования XDR для автоматической передачи данных через соединение TCP после преобразования каждого элемента данных во внешнюю форму. Для этого в прикладной программе вначале создается сокет TCP, а затем вызывается функция f dopen для закрепления за этим сокетом стандартного потока ввода/вывода. Вместо вызова процедуры xdrmem_create в приложении вызывается процедура xdrstdio_create для создания потока XDR и подключения его к существующему дескриптору ввода/вывода. Потоки XDR, подключенные к сокету TCP, не требуют явного вызова функций write или read. При вызове в приложений каждой процедуры преобразования XDR автоматически выполняется операция буферизированного чтения или записи с использованием соответствующего дескриптора. Запись в поток равносильна требованию к программному обеспечению TCP передать исходящие данные в сокет, а чтение равносильно требованию прочитать входящие данные из сокета. В приложении для работы с потоком ввода/вывода могут также применяться обычные функции из стандартной библиотеки ввода/вывода. Например, если требуется отправить все накопленные данные, в приложении можно вызвать функцию fflush для освобождения буфера вывода после преобразования даже нескольких байтов данных. 20.13. Записи, границы записей и дейтаграммный ввод/вывод Описанный механизм XDR успешно работает в сочетании со средствами ввода/вывода сокета TCP, поскольку и программное обеспечение XDR, и программное обеспечение TCP основано на понятии потока. Для обеспечения возможности применять средства преобразования XDR в сочетании с протоколом UDP так же успешно, как и с протоколом TCP, был разработан второй интерфейс. В этом альтернативном проекте предусмотрена возможность применять в приложении интерфейс, обеспечивающий прием и передачу данных в виде записей. Для использования интерфейса обмена данными в виде записей в программе при создании потока XDR вызывается функция xdrrec_create. Этот вызов включает два параметра, inproc и outproc, которые обозначают процедуру ввода и процедуру вывода. При преобразовании данных во внешнюю форму каждая процедура преобразования проверяет буфер. После обнаружения того, что буфер заполнен, процедура преобразования вызывает функцию outproc для вывода содержимого буфера и освобождения места для новых данных. Аналогичным образом, при каждом вызове в приложении процедуры преобразования для декодирования из внешней формы во внутреннее представление процедура проверяет, имеются ли в буфере данные. Если буфер пуст, процедура преобразования вызывает функцию inproc для получения дополнительных данных. При использовании программного обеспечения XDR в сочетании с программным обеспечением UDP в приложении создается поток XDR, предусматривающий обмен данными в виде записей. В нем обеспечивается вызов в процедурах ввода и вывода функций read и write (или recv и send), связанных с этим потоком. После заполнения в приложении буфера процедура преобразования вызывает функцию 280 Глава 20. Внешнее представление данных (XDR)
write для передачи буфера в виде одной дейтаграммы UDP. Аналогичным образом, при вызове в приложении процедуры преобразования для извлечения данных из буфера, процедура преобразования вызывает функцию read для получения следующей входящей дейтаграммы и размещения ее в буфере. Потоки XDR, создаваемые процедурой xdrrec_create, во многом отличаются от других потоков XDR. Потоки, предусматривающие обмен данными в виде записей, позволяют обозначать в приложении границы записей. Кроме того, отправитель может указать, должна ли быть запись отправлена немедленно или программное обеспечение должно ожидать заполнения буфера перед ее отправкой. Получатель может обнаружить границы записей, пропустить определенное число записей при вводе или определить, получены ли дополнительные записи. 20.14. Резюме В компьютерах не используется единый формат представления данных, поэтому при разработке клиентских и серверных программ приходится решать проблему преобразования данных из одного представления в другое. Для решения этой проблемы могут применяться способы асимметричного или симметричного преобразования данных в процессе взаимодействия клиентов и серверов. Способ асимметричного преобразования предусматривает выполнение клиентом или сервером преобразования из его собственного представления во внутреннее представление компьютера другого участника обмена данными. При симметричном преобразовании используется стандартное сетевое представление; этот способ предусматривает выполнение клиентом и сервером прямого и обратного преобразования между сетевым стандартным представлением и локальным представлением. Основная проблема, возникающая при использовании асимметричного взаимодействия, связана с тем, что для реализации этого способа должно быть создано множество версий каждой программы. Бели сеть поддерживает N архитектур, то для обеспечения асимметричного взаимодействия требуются затраты труда программистов, пропорциональные N2. Хотя реализация симметричных проектов связана с некоторым увеличением вычислительных издержек, такие проекты обеспечивают взаимодействие между любыми компьютерами с применением одной программы для каждой архитектуры. Поэтому большинство разработчиков выбирает симметричные решения, поскольку для их реализации требуются затраты труда программистов, пропорциональные N. Компания Sun Microsystems определила внешнее представление данных, которое фактически признано стандартным. Стандарт компании Sun, известный под названием XDR, включает определения не только основных типов данных (таких как целые числа и символьные строки), но и агрегатов данных (таких как массивы и структуры). Библиотечные процедуры XDR обеспечивают прямое и обратное преобразование из внутреннего представления данных компьютера во внешнее стандартное представление. В клиентских и серверных программах процедуры XDR могут применяться для преобразования данных во внешнюю форму перед их передачей и во внутреннюю — после их приема. Процедуры преобразования могут быть связаны с функциями ввода и вывода, используемыми в программном обеспечении протоколов TCP или UDP. Материал для дальнейшего изучения Кодировка XDR и стандартные процедуры преобразования XDR определены в документе [152]. В документе [146] содержится современная версия, которая является проектом стандарта. С дополнительной информацией можно ознакомиться в оперативной документации, которая входит в поставку операционной системы Linux. 20.14. Резюме 281
Международная организация по стандартизации в документах [68] и [69] определила альтернативное внешнее представление данных, известное под названием ASN.l (Abstract Syntax Notation One — Абстрактная синтаксическая запись, точка, один). Хотя в некоторых протоколах TCP/IP используется представление ASN.1, большинство прикладных программистов используют XDR. В статье [113] показано, что стандарты XDR и ASN.1 обладают эквивалентными выразительными возможностями. Упражнения 20.1. Напишите программу С, которая определяет применяемый на локальном компьютере формат представления байтов целого числа. 20.2. Разработайте версию функции ntohs и проведите эксперимент по сравнению времени выполнения вашей версии и версии из библиотеки системы или из включаемых файлов. Объясните полученные результаты. 20.3. В чем именно упрощается процесс программирования при использовании буфера, предусмотренного в программном обеспечении XDR? 20.4. Разработайте внешнее представление данных, которое включает поле типа перед каждым элементом данных. В чем состоит основное преимущество такого решения? В чем его основной недостаток? 20.5. Прочитайте оперативное руководство, которое поставляется вместе с системой Linux, чтобы больше узнать о формате потока XDR. Какая информация хранится в заголовке потока? 20.6. Покажите, что исходные коды программ было бы проще читать, если бы разработчики программного обеспечения XDR предпочли использовать отдельные процедуры преобразования для кодирования и декодирования вместо регистрации информации о направлении преобразования в заголовке потока. В чем состоит недостаток применения отдельных процедур преобразования? 20.7. При каких обстоятельствах программисту может потребоваться передавать непреобразованные (opaque) объекты данных между клиентом и сервером? 20.8. Объясните, почему преобразование данных с плавающей точкой является особенно сложным. 282 Глава 20. Внешнее представление данных (XDR)
21 Принципы дистанционного вызова процедур (RPC) 21.1. Введение С предыдущей главы начинается описание методов и средств, позволяющих упростить разработку программ на основе организации взаимодействия типа клиент/сервер. В ней показаны преимущества использования симметричного преобразования данных и описано применение стандарта внешнего представления данных XDR и соответствующих ему библиотечных процедур для обеспечения симметричного преобразования. В настоящей главе продолжается описание промежуточного программного обеспечения (т.е. инструментальных средств и библиотек, используемых для разработки программного обеспечения клиент/сервер). В ней представлены принципы дистанционного вызова процедур и описана конкретная реализация дистанционного вызова процедур, в которой используется стандарт XDR для представления данных. Здесь показано, что такой подход упрощает разработку программного обеспечения клиент/сервер и позволяет создавать более простые для понимания программы. В следующих двух главах эта тема завершается описанием инструментального средства, применяемого для выработки основной части кода С для реализации программы, в которой используются дистанционные вызовы процедур. Приведен пример законченной работающей программы, в котором показано, что это инструментальное средство позволяет устранить значительную часть рутинной работы при создании клиентских и серверных программ. 21.2. Модель дистанционного вызова процедур До сих пор в этой книге программы клиент/сервер рассматривались по принципу отдельного изучения структуры клиентского и серверного компонентов. Однако при разработке приложения клиент/сервер нельзя сосредоточиться на анализе исключительно только одного компонента. Необходимо рассмотреть, как должна функционировать вся система и как будут взаимодействовать эти два компонента. Для упрощения работы по изучению и проектированию средств взаимодействия клиент/сервер была предложена концептуальная инфраструктура для разработки распределенных программ. В этой инфраструктуре, получившей название модели дистанционного вызова процедур или модели RPC (Remote Procedure Call), в качестве основы разработки распределенных приложений используются привычные понятия, применяемые при разработке нераспределенных программ.
21.3. Два подхода к разработке распределенных программ При разработке распределенных приложений можно воспользоваться одним из следующих двух подходов. ¦ Проект, в основе которого лежит обмен данными. Начать с разработки протокола связи. Спроектировать формат и синтаксис сообщений. Разработать клиентский и серверный компоненты, определяя, как тот и другой будет реагировать на входящие сообщения и вырабатывать исходящие. ¦ Проект, в основе которого лежит приложение. Начать с приложения. Разработать автономную прикладную программу для решения поставленной задачи. Построить и отладить работающую версию нераспределенной программы, функционирующей на отдельном компьютере. Разделить программу на две или несколько частей и применить протоколы связи, позволяющие обеспечить выполнение различных частей на разных компьютерах. При разработке проекта, основанного на обмене данными, иногда приходится сталкиваться с определенными затруднениями. Во-первых, сосредоточившись на протоколе связи, разработчик может пропустить важные тонкости в работе приложения и обнаружить, что протокол не предоставляет всех необходимых функциональных средств. Во-вторых, поскольку лишь немногие программисты имеют знания и опыт, которые необходимы для проектирования протоколов, они зачастую создают неуклюжие, неработоспособные или неэффективные протоколы. Даже небольшие упущения в проекте протокола могут в дальнейшем привести к появлению фундаментальных ошибок, которые остаются скрытыми до тех пор, пока программы не начинают испытывать жесткую нагрузку (например, создаются предпосылки появления тупиковой ситуации). В-третьих, поскольку разработка сосредотачивается на средствах связи, эти средства обычно начинают составлять основную часть кода результирующих программ, поэтому изучение или доработка таких программ намного усложняется. В частности, поскольку определение алгоритма работы сервера напоминает перечень сообщений и действий, выполняемых при получении каждого сообщения, иногда вообще трудно определить, какая последовательность сообщений требуется для достижения конкретного результата или для чего применяются те или иные сообщения. В модели дистанционного вызова процедур используется подход, основанный на приложении, в котором все усилия сосредотачиваются на решении поставленной задачи, а не на исследовании необходимого для этого взаимодействия. Модель дистанционного вызова процедур предусматривает предварительную разработку нераспределенной программы для решения поставленной задачи, а затем разделение этой программы на части, работающие на двух или нескольких компьютерах. При этом существует возможность придерживаться принципов качественного проектирования, позволяющих создавать модульный и управляемый код. В идеальной ситуации модель дистанционного вызова процедур из абстрактной концепции превращается в практическое руководство к действию. Она позволяет разработать, оттранслировать и проверить версию нераспределенной программы и убедиться в том, что программа правильно решает поставленную задачу, после чего разделить эту программу на части, работающие на отдельных компьютерах. Кроме того, поскольку модель RPC предусматривает разделение программ на границах процедур (т.е. на тех участках, где происходит вызов методов), то разбиение на локальную и удаленную части может быть выполнено без внесения существенных изменении в структуру программы. Иногда даже возникает возможность перенести часть процедур из программы на удаленные компьютеры без корректировки или перетрансляции всей программы. Таким образом, модель RPC позволяет отдельно решать две задачи: создание программы и обеспечение возможности ее функционирования в распределенной среде. 284 Глава 21. Принципы дистанционного вызова процедур (RPC)
Подход, связанный с использованием дистанционного вызова процедур в программировании, основывается на приложении. Он позволяет сосредоточиться на разработке нераспределенной программы, которая решает поставленную задачу, и только после этого приступить к разделению программы на части, работающие на нескольких компьютерах. 21.4. Концептуальная модель локального вызова процедур Модель дистанционного вызова процедур тесно связана с механизмом локального вызова процедур, применяемым в языках программирования, предназначенных для создания нераспределенных программ. Процедуры лежат в основе продуктивного подхода, позволяющего разделять программы на небольшие, управляемые и простые для понимания части. Процедуры особенно удобны, поскольку имеют несложную реализацию, основанную на концептуальной модели выполнения программы. Принцип использования процедур проиллюстрирован на рис. 21.1. Нераспределенная программа состоит из одной или нескольких процедур, которые обычно организованы в виде иерархии вызовов. На этом рисунке стрелка от процедуры п к процедуре т обозначает, что вызов процедуры т происходит в процедуре п. ргос. ? I РГ0С5 1 Главная процедура ^у х^^ ргос2 ргос3 X/ X ргос6 ргос7 ргос4 , 1 , 1 РГ0С8 I Рис. 21.1. Схема организации взаимодействия процедур в программе Компьютер! Компьютер2 ргос4 J ргос8 Рис. 21.2. Распределенная программа, которая показывает, как может быть расширена нераспределенная программа (см. рис. 21.1) для дистанционного вызова процедур с применением протокола Главная процедура ^^/ ^- - proc, proc2 ргос3 1 Х^ X proc5 proc6 proc7 21.4. Концептуальная модель локального вызова процедур 285
21.5. Расширение процедурной модели В модели дистанционного вызова процедур используется такая же организация функционирования по принципу вызова процедур, как и в нераспределенной программе, но при этом предусмотрена возможность переходить из файловой системы одного компьютера в файловую систему другого. На рис. 21.2 показано, как может использоваться принцип дистанционного вызова процедур для разделения программы на две части, выполняемые на разных компьютерах. Граница раздела проходит между главной процедурой и процедурой 4. Безусловно, локальный вызов процедуры не может быть непосредственно передан с одного компьютера на другой. Прежде чем появится возможность использовать дистанционные вызовы удаленных процедур, программа должна быть дополнена программным обеспечением протокола, позволяющим обеспечить ее взаимодействие с удаленной процедурой. 21.6. Выполнение локального вызова процедур и возврат управления Процедурная модель работы программы позволяет построить концептуальное описание принципов функционирования программы, которые могут быть непосредственно распространены на дистанционные вызовы процедур. Рассмотрим эти принципы на примере того, как используется поток управления для выполнения оттранслированной программы, загруженной в память. Например, на рис. 21.3 показано, как поток управления проходит через главную процедуру и две другие процедуры, а затем возвращается обратно. Согласно процедурной модели выполнения программы, через все процедуры проходит единственный поток управления (или поток выполнения). Процессор начинает выполнение программы с главной процедуры и обрабатывает ее код до тех пор, пока не встретит вызов процедуры, что приводит к выполнению кода указанной процедуры. Встретив еще один вызов, процессор выполняет команду перехода к следующей процедуре. Выполнение кода вызванной процедуры продолжается до тех пор, пока не будет обнаружен оператор возврата return, после обработки которого выполнение программы возобновляется с оператора, расположенного непосредственно после последнего оператора вызова. Например, на рис. 21.3 после выполнения оператора return в процедуре В управление снова перейдет в процедуру А, к оператору, расположенному непосредственно после вызова процедуры В. Единственный поток управления начинается в главной процедуре, проходит через процедуры А В, и, наконец, возвращается в главную процедуру. В этой концептуальной модели в любой момент времени существует только один поток выполнения. Поэтому выполнение процедуры должно на время "остановиться", пока процессор выполняет вызов другой процедуры. Процессор приостанавливает вызывающую процедуру, оставляя значения всех переменных зафиксированными на время этого вызова. Затем после возврата управления из вызванной процедуры, процессор возобновляет выполнение вызывающей процедуры, вновь предоставив доступ ко всем переменным. Вызванная процедура может выполнять дальнейшие вызовы процедуры, поскольку процессор запоминает последовательность вызовов и всегда возвращает управление последней по времени вызывающей процедуре. 286 Глава 21. Принципы дистанционного вызова процедур (RPC)
Код главной процедуры Код процедуры А Код процедуры В Главная процедура Вызов процедуры А Выход Вызов процедуры В - Возврат - Возврат Рис. 21.3. Концептуальная модель выполнения, которая показывает поток управления во время вызова процедуры и возврата управления 21.7. Процедурная модель в распределенных системах Модель выполнения вызовов процедур, применяемая при разработке нераспределенных компьютерных программ, позволяет понять, как происходит выполнение распределенной программы. Необходимо отказаться от подхода, при котором изучается обмен сообщениями между клиентской и серверной программами, и представить себе, что каждый сервер реализует (удаленную) процедуру и взаимодействие между клиентом и сервером осуществляется по принципу вызова этой процедуры и возврата управления. Запрос, передаваемый от клиента к серверу, соответствует вызову удаленной процедуры, а ответ, отправляемый с сервера клиенту, соответствует выполнению оператора return. Такая аналогия иллюстрируется на рис. 21.4. В распределенной среде применяется один поток управления. На данном рисунке пунктирные линии показывают, как управление передается от клиента к серверу во время дистанционного вызова процедур и как возвращается обратно после получения ответа от сервера. Главная процедура на компьютере 1 (клиент) ПроцедураА на компьютере 2 (сервер) Процедура В на компьютере 3 Дистанционный вызов процедуры А — Г - Выход Дистанционный вызов процедуры В " - г Ответ "вызывающей процедуре Ответ 'вызывающей процедуре Рис. 21.4. Модель выполнения, в которой используются дистанционные вызовы процедур 21.7. Процедурная модель в распределенных системах 287
21.8. Аналогия между взаимодействием типа клиент/сервер и дистанционным вызовом процедур Принцип дистанционного вызова процедур представляет собой плодотворную аналогию, позволяющую рассматривать все аспекты взаимодействия между клиентом и сервером в привычном контексте. Как и локальный вызов процедур, дистанционный вызов сводится к передаче управления вызываемой процедуре. К тому же, как и в локальном вызове процедур, операционная система приостанавливает выполнение вызывающей процедуры на время вызова и позволяет продолжить работу только вызванной процедуре. Выдача удаленной программой ответа равносильно выполнению возврата в локальном вызове процедуры. Управление передается вызывающей процедуре, а выполнение вызванной процедуры прекращается. Понятие вложенных вызовов процедур распространяется также и на дистанционные вызовы процедур. Одна удаленная процедура может вызвать другую удаленную процедуру. Как показано на рис. 21.4, вложенные дистанционные вызовы процедур соответствуют той ситуации, когда сервер становится клиентом другой службы. Безусловно, аналогия между дистанционным вызовом процедур и взаимодействием между клиентом и сервером не позволяет описать все тонкости. Например, известно, что локальная процедура остается полностью бездействующей до тех пор, пока к ней не перейдет поток управления (т.е. до тех пор, пока она не будет вызвана). В отличие от этого, серверный процесс должен существовать в удаленной системе и ожидать, когда настанет время сформировать ответ, прежде чем им будет получен первый запрос от клиента. Другие различия связаны с тем, как передаются данные в удаленную процедуру. Локальные процедуры обычно принимают лишь несколько параметров и возвращают небольшой объем результатов. Однако сервер может принимать или возвращать произвольные объемы данных (т.е. он может принимать или возвращать потоки данных произвольной длины через соединение TCP). Безусловно, было бы идеально, если бы локальные и дистанционные вызовы процедур были полностью аналогичны друг другу, но практически это невозможно осуществить из-за нескольких ограничений. Во-первых, из-за сетевых задержек выполнение дистанционного вызова процедур может потребовать гораздо больше времени по сравнению с локальным вызовом процедур. Во-вторых, поскольку в нераспределенных программах вызванная процедура функционирует в том же адресном пространстве, что и вызывающая, в таких программах в качестве параметров могут передаваться указатели, тогда как в дистанционном вызове процедур это невозможно, т.к. удаленная процедура функционирует совершенно в ином адресном пространстве, нежели вызывающая процедура. В- третьих, удаленная процедура не имеет общей среды с вызывающей процедурой, поэтому не имеет и прямого доступа к дескрипторам ввода/вывода или к функциям операционной системы вызывающей процедуры. Например, удаленная процедура не может записывать сообщения об ошибках непосредственно в стандартный файл сообщений об ошибках вызывающей процедуры. 21.9. Организация средств распределенных вычислений в виде программы Чтобы оценить значение принципа дистанционного вызова процедур, необходимо понять, что он позволяет легко разрабатывать распределенные программы, несмотря на все свои практические ограничения. Чтобы убедиться в этом, предположим, что для организации распределенных вычислении применяются отдельные программы, работающие в распределенной среде. Представьте себе, насколько 288 Глава 21. Принципы дистанционного вызова процедур (RPC)
проще было бы создание распределенных программ, если бы в них для получения доступа к удаленной службе достаточно было вызвать процедуру, а не разрабатывать клиентское и серверное программное обеспечение, которое обеспечивает взаимодействие между локальным и удаленным компонентами. Насколько упростилось бы создание программ, если в них поток выполнения мог переходить по сети на удаленный компьютер, вызывать на нем удаленную процедуру, а затем возвращаться к вызывающей. С точки зрения программиста, к удаленным службам было бы так же легко обращаться, как к локальным процедурам или к локальным службам операционной системы. Иными словами, разработка распределенных программ стала бы такой же простой, как и разработка нераспределенных программ, поскольку в них могли бы применяться навыки и опыт программистов, приобретенный при использовании локальных вызовов процедур. Кроме того, программисты, знакомые с механизмом передачи параметров процедуры, могли бы точно определять все тонкости взаимодействия между клиентом и сервером без использования какой-либо специальной системы обозначений или особого языка. Если распределенные вычисления организованы по принципу использования одной программы, в которой управление передается по сети в удаленную процедуру и обратно, то определение всех аспектов взаимодействия клиента и сервера упрощается, поскольку взаимодействие распределенных компонентов сводится к знакомым понятиям вызова процедур и возврата управления. 21.10. Определение дистанционного вызова процедур, предложенное компанией Sun Microsystems Компания Sun Microsystems определила конкретную форму дистанционного вызова процедур, которая известна под названием Sun RPC, ONC (Open Network Computing — Открытый стандарт сетевой обработки) RPC или просто RPC1. Определение дистанционного вызова процедур ONC получило широкое признание в компьютерной индустрии. Оно было применено в качестве механизма реализации многих приложений, включая NFS (Network File System — Сетевая файловая система2). Спецификация ONC RPC определяет формат сообщений, передаваемых вызывающей процедурой (клиентом) для вызова удаленной процедуры на сервере, формат параметров, а также формат результатов, возвращаемых из вызываемой процедуры в вызывающую. Эта спецификация позволяет использовать в вызывающей программе для передачи сообщений протокол UDP или TCP и предусматривает применение формата XDR для представления параметров процедуры, а также других элементов в заголовке сообщения RPC. И наконец, кроме определения протокола, в состав средств ONC RPC входит система трансляции, обеспечивающая автоматическое создание распределенных программ. 21.11. Удаленные программы и процедуры Спецификация ONC RPC расширяет модель дистанционного вызова процедур путем определения среды дистанционного выполнения. В этой спецификации в качестве основного компонента программного обеспечения, выполняемого на удаленном компьютере, определена удаленная программа. Каждая удаленная программа соответствует тому программному компоненту, который принято рассматривать как сервер, и содержит одну или несколько удаленных процедур, а также Во всей этой главе термин RPC используется как сокращение от ONC RPC, если не указано иное. 2 Система NFS подробно рассматривается в главе 24. 21.10. Определение дистанционного вызова процедур... 289
глобальные данные. Все процедуры удаленной программы имеют совместный доступ к глобальным данным. Поэтому набор взаимодействующих удаленных процедур может иметь совместный доступ к информации о состоянии. Это позволяет легко создать, например, простую удаленную базу данных на основе одной удаленной программы, которая включает структуры данных для хранения разделяемой информации и три удаленные процедуры для манипуляции с этими структурами данных: вставки, удаления и поиска. Как показано на рис. 21.5, все удаленные процедуры, которые входят в состав удаленной программы, могут иметь совместный доступ к одной базе данных, так же, как локальные процедуры имеют совместный доступ к глобальным данным в нераспределенной программе. Отдельная удаленная программа ргос1 > ргос2 А Разделяемые глобальные данные А ргос3 у Рис. 21.5. Концептуальная организация трех удаленных процедур в одной удаленной программе 21.12. Сокращение числа параметров Поскольку в большинстве языков программирования для представления параметров используется позиционное обозначение, вызов процедур, содержащий более двух или трех параметров, может стать сложным для восприятия. Программисты могут упростить формат вызова, собирая несколько параметров в один агрегат данных (например, в структуру struct языка С) и передавая полученный агрегат в виде одного параметра. В вызывающей процедуре каждому полю структуры перед ее передачей вызываемой процедуре присваивается значение; вызывающая процедура извлекает возвращенные значения из структуры после возврата управления. Использование структуры вместо нескольких параметров позволяет повысить удобство программы для чтения, поскольку имена полей структуры служат в качестве ключевых слов, позволяющих читателю определить назначение каждого параметра. Поскольку в дальнейшем описании предполагается, что во всех программах, основанных на использовании RPC, предусмотрена сборка параметров в виде структуры, то в каждой удаленной процедуре применяется только один параметр. 21.13. Обозначение удаленных программ и процедур В стандарте ONC RPC указано, что каждой удаленной программе, выполняемой на компьютере, должно быть присвоено уникальное 32-битовое целое число, применяемое вызывающей процедурой для ее обозначения. Кроме того, программное обеспечение RPC присваивает целочисленный идентификатор каждой удаленной процедуре, входящей в состав конкретной удаленной программы. Процедуры нумеруются последовательно3: 1, 2, ..., N. По сути, каждая конкретная удаленная процедура на некотором удаленном компьютере может быть обозначена парой чисел: 3 В соответствии с общепринятым соглашением, номер 0 всегда зарезервирован для эхо- процедуры, применяемой для проверки возможности доступа к удаленной программе. 290 Глава 21. Принципы дистанционного вызова процедур (RPC)
[prog, proc) Здесь prog обозначает удаленную программу, а ргос — удаленную процедуру программы. Чтобы обеспечить отсутствие конфликтов между номерами программ, определяемыми отдельными организациями, стандарт RPC предусматривает разделение набора номеров программ на восемь групп, как показано в табл. 21.1. Каждой удаленной программе присваивается уникальный номер. Таблица 21.1. Разделение на восемь групп 32-битовых чисел, используемых в программном обеспечении ONC RPC для обозначения удаленных программ От 0x00000000 0x20000000 0x40000000 0x60000000 0x80000000 0ха0000000 ОхсООООООО 0хе0000000 До Oxlfffffff 0x3fffffff OxSfffffff 0x7fffffff 0x9fffffff Oxbfffffff Oxdfffffff Oxffffffff Ответственный за распределение номеров Компания Sun Microsystems Системный администратор на сетевом узле пользователя Номера не присваиваются на продолжительный период времени (не являются постоянными) Зарезервированные номера Зарезервированные номера Зарезервированные номера Зарезервированные номера Зарезервированные номера Компания Sun Microsystems администрирует первую группу идентификаторов, позволяя любой организации подать заявку на присвоение стандартного номера программы RPC. Поскольку компания Sun публикует выполненные ею назначения, то во всех компьютерах, эксплуатирующих программное обеспечение RPC, используются стандартные значения. Из числа 229 номеров программ, доступных в первой группе, компания Sun присвоила лишь небольшое число номеров. В табл. 21.2 перечислены некоторые из этих присвоенных номеров. 21.14. Возможность применения нескольких версий одной удаленной программы Кроме номера программы, стандарт ONC RPC предусматривает использование целочисленного номера версии для каждой из удаленных программ. Обычно первой версии программы присваивается номер 1, а каждая последующая версия получает свой уникальный номер. Применение номеров версий позволяет вносить небольшие изменения в дистанционные вызовы процедур, не меняя номера программы. На практике каждое сообщение RPC определяет намеченного получателя на данном компьютере с помощью трехэлементного массива: (prog, vers, proc) Здесь prog обозначает удаленную программу, vers — версию программы, которой должно быть передано сообщение, а ргос — удаленную процедуру в составе удаленной программы. Спецификация RPC позволяет эксплуатировать на компьютере несколько версий удаленной программы одновременно, что дает возможность постепенно переходить на новую версию. При этом применяется следующий принцип. Поскольку сообщения ONC RPC обозначают удаленную программу, ее версию и удаленную процедуру, появляется возможность постепенно перейти 21.14. Возможность применения нескольких версий одной удаленной программы 291
с одной версии удаленной процедуры на другую и отладить новую версию сервера, в то время как старая версия продолжает эксплуатироваться. Таблица 21.2. Примеры номеров программ RPC, назначенных к этому времени компанией Sun Microsystems Имя portmap rstatd rusersd nfs ypserv mountd dbxd ypbind walld yppasswdd etherstatd rquotad sprayd selection_svc dbsessionmgr rexd office_auto lockd lockd statd bootparamd pcnfsd Назначенный номер 100000 100001 100002 100003 100004 100005 100006 100007 100008 100009 100010 100011 100012 100015 100016 100017 100018 100020 100021 100024 100026 150001 Описание Служба привязки портов Программы rstat, rup и perfmeter Служба дистанционной регистрации пользователей Сетевая файловая система Система ур (теперь именуется NIS) Программы mount, showmount Программа DBXprog (отладчик) Клиентская программа службы NIS Программы rwall, shutdown Программа yppasswd Служба сбора статистики локальной сети Ethernet Программы rquotaprog, quota, rquota Программа spray Служба selection Программы unify, netdbros, dbms Программы rex, remote_exec Программа alice Программа klmprog Программа nlmprog Служба контроля состояния Служба определения параметров начальной загрузки Система NFS для персонального компьютера 21.15. Применение принципа взаимного исключения к процедурам удаленной программы Механизм ONC RPC определяет, что в любой момент времени не может быть вызвано более одной удаленной процедуры в удаленной программе. Таким образом, механизм RPC обеспечивает автоматическое применение принципа взаимного исключения к процедурам каждой конкретной удаленной программы. Такое взаимное исключение может быть очень важным для удаленных программ, применяемых для сопровождения общих данных, к которым обращаются несколько процедур. Например, если удаленная программа базы данных включает удален- 292 Глава 21. Принципы дистанционного вызова процедур (RPC)
ные процедуры для выполнения операций вставки и удаления, то программисту не приходится задумываться над тем, что два дистанционных вызова процедур могут нарушить работу друг друга, поскольку механизм RPC допускает в любой момент выполнение только одного вызова. Система блокирует остальные вызовы до тех пор, пока не завершится выполнение текущего вызова. Механизм ONC RPC обеспечивает применение принципа взаимного исключения к вызовам удаленных процедур в одной удаленной программе; в любой момент времени в удаленной программе может выполняться не более одного вызова удаленных процедур. 21.16. Организация связи При выборе способа обмена данными для механизма ONC RPC разработчики могут применить один из двух вариантов. Во-первых, для обеспечения максимально возможного подобия между дистанционным и локальным вызовом процедур механизм RPC должен использовать надежный транспортный протокол, такой как TCP, гарантируя бесперебойную доставку данных. Механизм дистанционного вызова процедур должен либо передать вызов удаленной процедуре и получить ответ, либо сообщить, что связь невозможна. Во-вторых, для обеспечения возможности использовать эффективные транспортные протоколы без установления логического соединения, механизм дистанционного вызова процедур должен поддерживать связь с помощью такого дейтаграммного протокола, как UDP. Спецификация ONC RPC не устанавливает требований по обеспечению надежного обмена данными и позволяет выбирать в каждом приложении в качестве транспортного протокола TCP или UDP. Более того, в этом стандарте не регламентируется использование дополнительных протоколов или механизмов для обеспечения надежной доставки данных. Вместо этого в нем определено, что применяемый в RPC способ обмена данными основан на средствах обмена данными базового транспортного протокола. Например, поскольку протокол UDP допускает потерю или дублирование дейтаграмм, спецификация RPC указывает, что при выполнении вызовов удаленных процедур с использованием протокола UDP может происходить потеря или дублирование данных. 21.17. Обмен данными по принципу доставки не менее одного экземпляра дейтаграммы В спецификации ONC RPC способ обмена данными, применяемый в дистанционном вызове процедур, определен простейшим образом: в ней указано, что программа должна быть способна работать при самом неудачном развитии событий в ходе обмена данными. Например, при использовании протокола UDP запросное или ответное сообщение (вызов удаленной процедуры или возврат управления после этого вызова) может быть потеряно или продублировано. Бели не произойдет возврат после вызова удаленной процедуры, то в вызывающей процедуре невозможно будет определить, была ли вызвана удаленная процедура, поскольку ответ мог быть потерян даже в случае успешной передачи запроса. Если произошел возврат после дистанционного вызова процедуры, то в вызывающей процедуре можно определить, что удаленная процедура была вызвана не менее одного раза. Однако в вызывающей процедуре нельзя определить, что удаленная процедура действительно была вызвана один и только один раз, поскольку запрос мог быть продублирован, а одно из ответных сообщений — потеряно. В стандарте ONC RPC используется термин взаимодействие по принципу получения не менее одного ответа для описания выполнения вызова RPC, при ко- 21.16. Организация связи 293
тором вызывающая процедура обязательно получает ответ, а также термин взаимодействие по принципу получения от нуля и более ответов для описания такого хода выполнения дистанционного вызова процедур, при котором вызывающая процедура может не получить ответа. Взаимодействие по принципу получения от нуля и более ответов в ходе дистанционного вызова процедур, предусмотренное стандартом RPC, возлагает на программистов немалую ответственность. Программисты, которые выбрали UDP для использования в качестве транспортного протокола для приложения ONC RPC, должны строить свое приложение с учетом варианта взаимодействия, допускающего получение от нуля и более ответов. На практике взаимодействие по принципу получения от нуля и более ответов обычно означает, что в программе необходимо обеспечить, чтобы каждый дистанционный вызов процедуры был идемпотентным4. Например, рассмотрим приложение дистанционного доступа к файлу. Удаленная процедура, добавляющая данные к файлу, не является идемпотентной, поскольку при повторном ее выполнении данные будут добавлены к файлу повторно. С другой стороны, удаленная процедура, которая записывает данные с указанной позиции в файле, является идемпотентной, поскольку при ее повторном выполнении данные всегда будут записываться, начиная с одной и той же позиции. 21.18. Повторная передача запроса RPC Библиотечное программное обеспечение, поставляемое в составе программного обеспечения ONC RPC, предусматривает использование простых средств тайм- аута и повторной передачи, но не гарантирует надежной доставки в строгом смысле этого слова. Предусмотренный по умолчанию механизм тайм-аута реализует постоянный (неадаптивный) тайм-аут с постоянным числом повторных попыток. При отправке библиотечным программным обеспечением RPC сообщения, соответствующего дистанционному вызову процедуры, запускается таймер. Программное обеспечение передает запрос повторно, если установка таймера истекает до получения ответа. В конкретном приложении можно предусмотреть корректировку тайм-аута и предельного числа повторных попыток, но библиотечное программное обеспечение не адаптируется автоматически к продолжительным или непостоянным сетевым задержкам. Безусловно, простые средства повторной передачи не могут не только гарантировать надежность, но даже обеспечить формирование в вызывающем приложении обоснованного вывода о том, как протекает выполнение удаленной процедуры. Например, если в сети теряются все ответы, то вызывающая процедура может несколько раз выполнить повторную передачу запроса, и каждый запрос приведет к выполнению удаленной процедуры. Однако в конечном итоге библиотечное программное обеспечение на компьютере, где находится вызывающая процедура, достигнет предела числа попыток повторной передачи и сообщит, что удаленная процедура не может быть выполнена. Еще более важно то, что в приложении неудачное выполнение не может рассматриваться как свидетельство о том, что удаленная процедура действительно не была выполнена (фактически она могла быть выполнена несколько раз). Этот термин взят из математики, где операцию называют идемпотентной, если ее повторное выполнение приводит к получению одного и того же результата. 294 Глава 21. Принципы дистанционного вызова процедур (RPC)
21.19. Привязка удаленной программы к порту протокола В транспортных протоколах UDP и TCP для обозначения оконечных точек связи используются 16-битовые номера портов протокола. В предыдущих главах описано, как сервер создает пассивный сокет, привязывает его к стандартному порту протокола и переходит в состояние ожидания запросов от клиентских программ. Для обеспечения взаимодействия между клиентами и серверами предполагается, что каждой службе назначен уникальный номер порта протокола и эти назначения являются общепринятыми. Поэтому разработчики и клиента, и сервера используют согласованный порт протокола, через который сервер принимает запросы, поскольку они руководствуются официально опубликованным списком назначений портов. В ходе применения спецификации ONC RPC возникает интересная проблема: поскольку в ней для обозначения удаленных программ используются 32-битовые числа, то диапазон номеров программ RPC выходит за пределы диапазона номеров портов протокола. Поэтому невозможно непосредственно выполнить привязку номеров программ RPC к портам протокола. Еще более важно то, что всем программам RPC не может быть назначен уникальный порт протокола, поэтому программисты не могут использовать схему назначения портов, которая была бы основана на общепринятых назначениях портов протокола. Хотя потенциальное число программ RPC превышает число общепринятых назначений портов, служба RPC не так уж значительно отличается от других служб. В любой момент времени на каждом отдельном компьютере выполняется лишь небольшое число удаленных программ. Поэтому, если назначения портов являются временными, каждая программа RPC может получить номер порта протокола и использовать его для обмена данными. Если в программе RPC не применяется зарезервированный, общепринятый порт протокола, то клиенты не смогут обратиться к ней непосредственно. Для лучшего понимания того, с чем это связано, рассмотрим серверную и клиентскую компоненты. С началом своего выполнения сервер (удаленная программа) передает операционной системе запрос на выделение неиспользуемого номера порта протокола. Вновь распределенный порт протокола используется сервером для всех видов обмена данными. Система может выбирать другой номер порта протокола при каждом запуске сервера на выполнение (т.е. серверу при каждой перезагрузке системы может выделяться другой порт). Клиент (программа, выполняющая дистанционный вызов процедуры) имеет информацию об адресе компьютера и номере программы RPC той удаленной процедуры, к которой он должен обратиться. Но поскольку программа RPC (сервер) получает порт протокола только после запуска на выполнение, клиент не имеет информации о том, какой порт протокола получен сервером. Поэтому клиент не может обращаться непосредственно к удаленной программе. 21.20. Динамическая привязка портов Для решения проблемы назначения портов клиент должен иметь возможность установить соответствие между номером программы RPC и адресом компьютера, с одной стороны, и портом протокола, полученным сервером на компьютере назначения при запуске, с другой стороны. Такая привязка должна быть динамической, поскольку она может изменяться после перезагрузки компьютера или повторного запуска программы RPC на выполнение. Для предоставления клиентам возможности обращаться к удаленным программам, механизм ONC RPC включает службу динамической привязки. На каждом 21.19. Привязка удаленной программы к порту протокола 295
компьютере, предоставляющем доступ к программе RPC (т.е. обеспечивающем эксплуатацию сервера), ведется база данных о привязке портов и предусмотрен механизм, который позволяет вызывающей программе выполнить привязку номеров программ RPC к портам протокола. В механизме привязки портов RPC, который принято называть службой привязки портов RPC или иногда просто службой привязки портов, для сопровождения небольшой базы данных на каждом компьютере используется сервер. Как показано на рис. 21.6, служба привязки портов функционирует как отдельный серверный процесс. Каждая программа RPC регистрирует в службе привязки портов на локальном компьютере свой номер программы, номер порта протокола и номер версии. Вызывающая программа обращается к службе привязки портов для определения порта протокола, применяемого для конкретной программы RPC на данном компьютере. Регистрация программы RPC (программа, порт, версия) Служба привязки портов RPC (сервер) Сокет для порта, используемого в настоящее время программой RPC Сокет для общепринятого порта, через который принимает запросы служба привязки портов Рис. 21.6. Служба привязки портов ONC RPC 21.21. Алгоритм функционирования службы привязки портов RPC В соответствии с алгоритмом 21.1, на каждом компьютере, где реализована серверная часть программы RPC, функционирует отдельный сервер службы привязки портов. Служба привязки портов позволяет клиентам обращаться к удаленным программам, даже если эти удаленные программы динамически распределяют порты протокола. С началом выполнения удаленная программа (т.е. сервер) распределяет локальный порт протокола, предназначенный для использования при обмене данными. Затем удаленная программа обращается к службе привязки портов на своем локальном компьютере и вводит в базу данных этой службы трехэлементный массив целых чисел: (номер программы RPC, номер порта протокола, номер версии) После выполнения собственной регистрации программой RPC вызывающие программы с других компьютеров могут определить ее порт протокола, передав запрос службе привязки портов. 296 Глава 21. Принципы дистанционного вызова процедур (RPC)
Алгоритм 21.1. Алгоритм функционирования службы привязки портов ONC RPC 1. Создать пассивный сокет, привязанный к общепринятому порту A11), который назначен службе привязки портов ONC RPC. 2. Повторно принимать запросы на регистрацию номера программы RPC или на выполнение поиска порта протокола, соответствующего указанному номеру программы RPC. Запросы на регистрацию поступают из программ RPC, функционирующих на том же компьютере, что и служба привязки портов. Каждый запрос на регистрацию содержит трехэлементный массив, состоящий из номера программы RPC, порта протокола, применяемого в настоящее время для доступа к этой программе, и номера версии. При поступлении запроса на регистрацию служба привязки портов вводит этот трехэлементный массив в свою базу данных. Запросы на выполнение поиска могут поступать с различных компьютеров. В каждом из них содержится номер удаленной программы и номер версии и запрашивается номер порта протокола, с помощью которого можно обратиться к этой удаленной программе. Служба привязки портов отыскивает номер удаленной программы в своей базе данных и в ответ отправляет информацию о соответствующем порте протокола для этой программы. Служба привязки портов на конкретном компьютере функционирует аналогично телефонной справочной службе: абонент, а в данном случае вызывающая программа, запрашивает службу привязки портов о том, как обратиться к конкретной программе RPC на данном компьютере. Чтобы связаться с удаленной программой, вызывающая программа должна иметь адрес компьютера, на котором функционирует удаленная программа, номер программы RPC, назначенный этой программе и номер версии. Вызывающая программа вначале обращается к службе привязки портов RPC на намеченном компьютере, а затем передает этой службе номер программы RPC и номер версии. Служба привязки портов в ответ возвращает номер порта протокола, применяемый в настоящее время указанной удаленной программой. Вызывающая программа может всегда обратиться к службе привязки портов, поскольку для взаимодействия с ней используется общепринятый номер порта протокола 111. После получения номера порта протокола, применяемого намеченной удаленной программой, вызывающая программа получает возможность непосредственно обратиться к удаленной. 21.22. Формат сообщения ONC RPC В отличие от многих других протоколов TCP/IP, в службе ONC RPC не используется фиксированный формат сообщений. Стандарт протокола определяет, что для представления общего формата сообщений RPC, а также элементов данных в каждом из полей сообщений используется язык XDR. Поскольку конструкции этого языка напоминают объявления структур данных в языке С, программисты, знакомые с языком С, обычно читают и понимают конструкции языка XDR без подробных пояснений. В целом, этот язык определяет способ сборки последовательности элементов данных, из которых состоит сообщение. Каждый элемент кодируется с использованием стандарта представления XDR. Поле типа сообщения в заголовке сообщения RPC позволяет провести различия между сообщениями, применяемыми клиентом для инициализации дистанционного вызова процедуры, и сообщениями, которые используются для передачи ответа из удаленной процедуры. Константы, применяемые в поле типа сообщения, могут быть определены с помощью языка XDR. Например, в объявлениях: 21.22. Формат сообщения ONC RPC 297
enum msg_type { /* Константы типа сообщения RPC */ CALL = 0; REPLY=1; }? определяется, что символические константы CALL и REPLY должны представлять собой значения перечислимого типа msg_type. Структуры данных в языке XDR могут рассматриваться как последовательности типов XDR, а также интерпретироваться как инструкции по сборке сообщения путем объединения отдельных элементов данных с использованием XDR. Например, после объявления значений символических констант язык XDR позволяет определить формат сообщения RPC: struct rpcjnsg { /* Формат сообщения RPC */ unsigned int mesgid; /* Идентификатор согласованных */ /* друг с другом вызовов и ответов */ union switch(msg_type raesgt){ case CALL: call_body cbody; case REPLY: rply_body rbody; } body; }? В этом объявлении указано, что сообщение RPC, а именно rpcjnsg, состоит из целочисленного идентификатора сообщения mesgid, за которым следует представление размеченного объединения в формате XDR. В этом представлении XDR каждое объединение начинается с целого числа, в данном случае mesgt, которое определяет формат остальной части сообщения RPC. Идентификатор типа сообщения mesgt содержит значение, которое определяет, относится ли это сообщение к типу CALL или REPLY. Сообщение CALL включает дополнительные данные в формате call_body, а сообщение REPLY содержит данные в формате rply_body. Объявления форматов call_body и rply_body должны быть даны в другом месте. Например, стандарт RPC определяет, что формат call_body имеет вид: struct call_body { /* Формат сообщения RPC CALL */ unsigned int rpcvers; /* Обозначение версии RPC */ unsigned int rprog; /* Номер удаленной программы */ unsigned int rprogvers; /* Номер версии удаленной программы */ unsigned int rproc? /* Номер удаленной процедуры */ opaque_auth cred; /* Информация аутентификации для */ /* вызываемой программы */ opaque_auth verf; /* Поле проверки аутентификации */ /* ПАРАМЕТРЫ */ /* Параметры для удаленной процедуры */ }? Несколько первых элементов в этом формате представления тела дистанционного вызова процедуры вполне очевидны. Вызывающая программа должна указать номер версии протокола RPC в поле rpcvers для обеспечения того, чтобы и клиентом, и сервером использовался один и тот же формат сообщения. Целочисленные поля rprog, rprogvers и rproc обозначают вызываемую удаленную программу, необходимую версию этой программы и удаленную процедуру в программе. Поля cred и verf содержат информацию, которая может применяться вызываемой программой для проверки подлинности (аутентификации) вызывающей программы. 298 Глава 21. Принципы дистанционного вызова процедур (RPC)
21.23. Упорядочение параметров удаленной процедуры Поля в сообщении RPC, которые следуют за информацией аутентификации, содержат параметры для удаленной процедуры. Число параметров и тип каждого из них зависит от вызываемой удаленной процедуры. Программное обеспечение RPC должно представить все параметры во внешней форме, позволяющей передавать их с одного компьютера на другой. В частности, если любой из параметров, передаваемых в удаленную процедуру, представляет собой такую сложную структуру данных, как связанный список, он должен быть закодирован в виде компактного представления, которое может использоваться для передачи по сети. Процесс кодирования параметров для передачи обозначается терминами упорядочение, линеаризация, сериализация или маршалинг. Процесс подготовки параметров клиентской частью программного обеспечения RPC для передачи в виде сообщения принято называть упорядочением (маршалингом) параметров, а процесс извлечения параметров серверной частью принято называть их разупорядочением (демаршалингом). Следует помнить, что спецификация RPC позволяет включать в вызовы RPC сложные объекты данных, но упорядочение и разупорядочение крупных структур данных может потребовать существенных затрат процессорного времени и большой пропускной способности сети. Поэтому большинство разработчиков старается избегать передачи связанных структур в качестве параметров. 21.24. Аутентификация Спецификация RPC определяет несколько возможных форм установления подлинности, включая простую схему аутентификации, которая основана на использовании функций, доступных в операционной системе, и более сложную схему, в которой используется стандарт DES (Data Encryption Standard — Стандарт шифрования данных), который был первоначально опубликован организацией NBS (National Bureau Of Standards — Национальное бюро стандартовM. Информация аутентификации может принадлежать к одному из четырех типов, которые показаны в следующем объявлении: enum auth_type { /* Возможные формы аутентификации */ AUTH_NULL =0; /* Отсутствие аутентификации */ AUTHJJNIX =1; /* Аутентификация имени компьютера */ AUTHJ3H0RT =2; /* Сокращенная форма аутентификации сообщений, */ /* следующих за первым сообщением */ AUTHJ)ES =3; /* Стандарт DES организации NIST (NBS) */ }? В любом случае стандарт RPC предоставляет право выбора формата и интерпретацию информации аутентификации разработчикам подсистемы проверки подлинности. Поэтому в объявлении структуры аутентификации в сообщении RPC используется ключевое слово opaque (непреобразованный) для указания на то, что эта структура, присутствующая в сообщении, не требует какой-либо интерпретации: struct opaque_auth { /* Структура для представления */ /* информации аутентификации */ auth_type atype; /* Обозначение типа аутентификации */ 5 Организация NBS сменила свое название на NIST (National Institute For Standards and Technology — Национальный институт стандартов и технологий). 21.23. Упорядочение параметров удаленной процедуры / 299
opaque body<400>; /* Данные для указанного типа */ }; Безусловно, в каждом методе аутентификации используется определенный формат представления данных. Например, на большинстве сетевых узлов, передающих сообщения RPC, используются компьютеры, на которых функционируют всевозможные разновидности операционной системы UNIX. Средства аутентификации6 UNIX определяют, что структура данных с информацией аутентификации должна включать несколько полей: struct authjinix { /* Формат информации аутентификации,*/ /* в системе UNIX */ unsigned int timestamp; /* Целочисленная отметка времени */ strings machine<255>; /* Имя компьютера пользователя */ unsigned int userid; /* Идентификатор пользователя, */ /* передавшего запрос */ unsigned int grpid; /* Идентификатор группы пользователя, */ /* передавшего запрос*/ unsigned int grpids<10>; /* Идентификаторы других групп */ /* данного пользователя */ }? Работа службы аутентификации системы UNIX основана на том, что клиентский компьютер передает свое имя в поле smachine, а числовой идентификатор пользователя, отправившего запрос, — в поле userid. Клиент также указывает в поле timestamp свое локальное время, что может использоваться для определения порядка следования запросов. И наконец, клиент передает главный числовой идентификатор группы и второстепенные числовые идентификаторы групп пользователя-отправителя в полях grpid и grpids. 21.25. Пример представления сообщения RPC Стандарт XDR определяет размер и внешний формат каждого поля в сообщении RPC. Например, в этом стандарте указано, что целое число (со знаком или без знака) занимает 32 бита и хранится в формате слова, оканчивающегося старшим байтом. На рис. 21.7 показано сообщение CALL программного обеспечения RPC. Размер каждого поля задается в соответствии с его определением в стандарте RPC и со спецификацией размеров в стандарте XDR. Например, поле типа сообщения по определению является перечислимым и согласно стандарту XDR хранится в виде 32-битового целого числа. Первые поля этого сообщения имеют постоянный размер, а размеры следующих полей изменяются в зависимости от того, для чего они используются. 21.26. Пример поля аутентификации UNIX Размер поля аутентификации в сообщении RPC зависит от его содержания. Например, второе поле в структуре аутентификации UNIX представляет собой имя компьютера в формате переменной длины. Стандарт XDR предусматривает представление строки переменной длины в виде 4-байтового целочисленного поля с обозначением длины, за которым следуют байты самой строки. На рис. 21.8 показано представление поля аутентификации UNIX. В данном примере имя компьютера (merlin.cs.purdue.edu) содержит 20 символов. Значения для этого 6 Хотя здесь при описании механизма аутентификации упоминается только операционная система UNIX, это описание в равной степени относится и к Linux. 300 Глава 21. Принципы дистанционного вызова процедур (RPC)
примера взяты из сообщения, отправленного пользователем с числовым идентификатором регистрационной записи 30 компьютера merlin.cs.purdue.edu. 0 16 31 1 Идентификатор сообщения Тип сообщения @ обозначает CALL) | Номер версии RPC B) Г Удаленная программа @x186аЗ обозначает NFS) Версия удаленной программы B) Удаленная процедура A обозначает getattr) Аутентификация UNIX Параметры для удаленной процедуры (если они имеются) Рис. 21.7. Пример внешнего формата, применяемого для сообщения CALL программного обеспечения RPC 0 16 31 I I I I Тип аутентификации A обозначает UNIX) Длина следующего далее тела сообщения D8) Отметка времени (например, 0Х2В15025С) Длина следующего далее имени компьютера B0) Имя компьютера (merlin.cs.purdue.edu) Идентификатор пользователя отправителя C0) Идентификатор группы отправителя C0) Длина следующего далее списка идентификаторов групп B) Идентификатор группы 1 C0) Идентификатор группы 2 E9) мм Рис. 21.8. Пример представления информации аутентификации UNIX в сообщении ONCRPC 21.26. Пример поля аутентификации UNIX 301
21.27. Резюме Модель дистанционного вызова процедур упрощает проектирование и изучение распределенных программ, поскольку предусматривает применение средств взаимодействия типа клиент/сервер для осуществления вызова процедур. В модели дистанционного вызова процедур каждый сервер рассматривается как удаленная программа, в которой реализована одна или несколько процедур. Сообщение, переданное от клиента к серверу, соответствует "вызову" удаленной процедуры, а ответ сервера клиенту соответствует "возврату управления" из вызванной процедуры. Удаленные процедуры, как и локальные, принимают параметры и возвращают один или несколько результатов. Параметры и результаты, передаваемые между вызывающей и вызываемой процедурами, могут лежать в основе точного определения всех аспектов взаимодействия между клиентом и сервером. Применение модели дистанционного вызова процедур позволяет программисту сосредоточиться на разработке самого приложения, а не протокола связи. Программист может построить и отладить нераспределенную программу, которая решает конкретную задачу, а затем разделить ее на части, выполняемые на двух или нескольких компьютерах. Компания Sun Microsystems определила конкретную форму дистанционного вызова процедур, которая фактически признана в качестве стандарта. Спецификация ONC RPC определяет общий принцип обозначения удаленных процедур, а также стандартный формат сообщений RPC. В этом стандарте для обеспечения независимости передаваемых сообщений от архитектуры компьютера используется стандарт внешнего представления данных XDR. В программах ONC RPC, в отличие от обычных клиентских и серверных программ, не используются общепринятые порты протоколов. Вместо этого в них предусмотрен механизм динамической привязки, который позволяет выбрать для каждой программы RPC произвольный, неиспользуемый порт протокола с началом ее выполнения. Этот механизм привязки, известный под названием службы привязки портов RPC, требует, чтобы на каждом компьютере, предоставляющим доступ к программам RPC, работал сервер привязки портов с общепринятым портом протокола. Каждая программа RPC после получения порта протокола регистрируется в службе привязки портов на своем локальном компьютере. При возникновении необходимости обратиться к программе RPC клиент RPC вначале обращается к службе привязки портов на намеченном им компьютере. В ответ служба привязки портов сообщает клиенту, какой порт использует намеченная им для взаимодействия программа RPC. После получения правильного номера порта протокола намеченной программы RPC клиент обращается непосредственно к этой программе уже с применением данного порта. Материал для дальнейшего изучения В документе [153] определен стандарт для ONC RPC и описана основная часть концепций, представленных в этой главе. В документе [146] содержится новая версия RPC, которая представляет собой проект стандарта. С дополнительной информацией можно ознакомиться в оперативной документации, которая входит в поставку операционной системы Linux. 302 Глава 21. Принципы дистанционного вызова процедур (RPC)
Упражнения 21.1. Прочитайте спецификацию ONC RPC и подготовьте схему с указанием размеров полей в типичном ответном сообщении. 21.2. Проведите эксперимент по измерению издержек, связанных с использованием службы привязки портов. 21.3. В клиентской программе можно избежать ненужных издержек, кэши- руя информацию о привязке портов протокола. Это означает, что после передачи запроса к службе привязки портов для получения номера порта протокола намеченной программы RPC клиент может сохранить информацию о привязке в кэше, чтобы избежать повторного поиска. Как долго эта информация будет оставаться действительной? 21.4. Может ли принцип использования службы привязки портов быть распространен на службы, отличные от RPC? Поясните свой ответ. 21.5. В чем состоят основные преимущества и недостатки применения службы привязки портов вместо общепринятых портов? 21.6. При передаче запроса в службу привязки портов клиент RPC должен либо указать, что намеченная программа открыла порт UDP или TCP, либо узнать, так ли это на самом деле. Внимательно прочитайте спецификацию, чтобы найти ответ на вопрос о том, как клиент RPC различает эти две ситуации. 21.7. Если на вашем компьютере есть вспомогательная программа rpcinfo, прочитайте справочное руководство для определения ее возможностей. Воспользуйтесь программой rpcinfo для получения списка программ и версий RPC, доступных на вашем компьютере. 21.8. Прочитайте литературу по проектам RPC, предложенным другими поставщиками. Реализованы ли в них принципы, не представленные в спецификации ONC RPC? 21.9. Рассмотрите схему аутентификации, применяемую в ONC RPC. Обеспечивает ли эта схема полную защиту, позволяющую использовать ее для обмена данными в пределах одного предприятия? Между несколькими предприятиями? 21.10. Сравните спецификации DCE RPC и ONC RPC. В чем их различия? 21.11. Прочитайте литературу по архитектуре CORBA (Common Object Request Broker Architecture — Общая архитектура брокера объектных запросов). Какие новые средства предоставляет эта архитектура для RPC? Упражнения 303
22 Построение распределенных программ (принципы использования программы rpcgen) 22.1. Введение В предыдущей главе рассматривались принципы, лежащие в основе модели дистанционного вызова процедур. В ней описано понятие дистанционного вызова процедур и показано, как могут использоваться дистанционные вызовы процедур для построения программ, функционирующих по принципу взаимодействия клиент/сервер. И наконец, в ней описан механизм ONC RPC. Изучение этой темы продолжается и в настоящей главе. Здесь более подробно рассматривается структура программ, в которых применяется механизм RPC, a также показано, как могут быть разделены программы по границам между процедурами. В этой главе введено понятие процедуры-заглушки и описано инструментальное средство генератора программ, которое автоматизирует основную часть работы по построению кода, связанной с использованием механизма ONC RPC. Здесь также представлена библиотека процедур, позволяющая упростить задачу построения серверов, которые предоставляют доступ к удаленным процедурам, и клиентов, которые их вызывают. В следующей главе завершается описание генератора. В ней показана последовательность действий, выполняемых в процессе создания нераспределенной программы, а затем продемонстрировано разделение этой программы на локальные и удаленные компоненты. В этой главе представлен простой пример приложения, который затем используется в процессе построения распределенной программы. На примере, приведенном в следующей главе, завершается описание принципов дистанционного вызова процедур, рассматриваемых в настоящей главе, путем пояснения многих подробностей и демонстрации кода, выработанного генератором. 22.2. Применение дистанционных вызовов процедур Модель дистанционного вызова удаленных процедур является универсальной и может использоваться для выполнения любой из описанных ниже задач. ¦ Определение только требований к программе. Для этого необходимо на основе модели RPC определить все этапы взаимодействия между клиентом
и сервером в форме дистанционного вызова процедур или возврата управления. В параметрах вызова процедуры определяются данные, передаваемые между клиентом и сервером. Анализируя работу клиентского и серверного компонентов, можно игнорировать их внутреннюю структуру, но использовать процедурную спецификацию этих компонентов для проверки соответствия результирующей системы поставленным требованиям. ¦ Определение требований к программе, а также анализ ее структуры в процессе проектирования. Для реализации такого подхода разработка прикладной программы и протокола связи осуществляется на основе принципов дистанционного вызова процедур, а протокол связи проектируется так, что каждое сообщение соответствует одному из дистанционных вызовов процедур. ¦ Концептуальное проектирование и практическая реализация. Для включения средств RPC в программу разрабатывается обобщенный формат сообщения RPC и протокол передачи управления удаленной процедуре. При разработке средств обмена данными между клиентом и сервером точно соблюдается процедурная спецификация. В программе для кодирования параметров используется стандартное внешнее представление данных и строго соблюдаются спецификации типов данных, предусмотренные в проекте. Для прямого и обратного преобразования данных из внутреннего представления компьютера во внешнее представление, применяемое при передаче данных по сети, в ней вызываются стандартные библиотечные процедуры. ¦ Проектирование и реализация для создания всего программного обеспечения с нуля. Разрабатывается нераспределенное приложение, которое решает поставленную задачу, затем это приложение разделяется на части по границам между процедурами, и отдельные части переносятся на разные компьютеры. В программе при вызове удаленной процедуры используется формат сообщений ONC RPC (включая представление данных XDR) и схема нумерации программ ONC RPC. Практическая реализация создается исключительно только на основе спецификаций RPC и с использованием службы привязки портов ONC RPC для определения соответствия между номером удаленной программы и портом протокола. ¦ Проектирование и реализация с использованием стандартных библиотек. Приложение разрабатывается и разделяется на части с использованием спецификации ONC RPC, а при любой возможности применяются существующие библиотечные процедуры RPC. Например, библиотечные процедуры используются для регистрации удаленной программы в службе привязки портов, для формирования и передачи сообщения с вызовом удаленной процедуры, а также для формирования ответа. ¦ Автоматизированная реализация. В процессе разработки полностью соблюдается спецификация ONC RPC, а для обеспечения автоматического построения необходимых частей клиентского и серверного кода и вызова библиотечных процедур RPC применяются инструментальные средства автоматизированного построения программ. С помощью библиотечных процедур RPC выполняются такие задачи, как регистрация удаленной программы в службе привязки портов, формирование сообщения и передача вызова соответствующей удаленной процедуре удаленной программы. 22.3. Вспомогательные средства программирования RPC Приложения RPC создаются на основе спецификаций ONC RPC. В этих спецификациях подробно регламентируются весьма сложные требования, поэтому разработка приложений RPC без какого-либо вспомогательного программного 306 Глава 22. Построение распределенных программ...
обеспечения может оказаться утомительной и трудоемкой. Большинство программистов предпочитают не создавать снова и снова один и тот же код, а использовать для выполнения основной части работы библиотечные процедуры и инструментальные средства программирования. Программное обеспечение ONC RPC позволяет исключить значительную часть рутинной работы по программированию. В нем предусмотрены вспомогательные средства, которые подразделяются на четыре категории. 1. Библиотечные процедуры XDR, которые преобразуют отдельные элементы данных из внутренней формы в стандартное внешнее представление XDR. 2. Библиотечные процедуры XDR, которые формируют сложные агрегаты данных (например, массивы и структуры), используемые для определения сообщений RPC. 3. Процедуры библиотеки времени выполнения RPC, позволяющие вызывать в программе удаленную процедуру, регистрировать удаленную программу (сервер) в службе привязки портов или передавать входящий вызов соответствующей удаленной процедуре удаленной программы. 4. Инструментальные средства генератора программ1, которые вырабатывают значительную часть файлов исходного кода на языке С, необходимых для построения распределенной программы в рамках технологии RPC. В состав библиотеки времени выполнения RPC входят процедуры, предоставляющие основную часть функциональных средств, необходимых для дистанционного вызова процедур. Например, процедура callrpc передает сообщение RPC на сервер. Она имеет следующую форму: callrpc (host, prog, progver,procnum, inproc, in, outproc, out); Параметр host задает символьную строку, содержащую имя компьютера, на котором функционирует удаленная процедура. Параметры prog, progver и procnum определяют номер удаленной программы, версию применяемой программы и номер удаленной процедуры. Параметр inproc указывает адрес локальной процедуры, которая может быть вызвана для упорядочения2 (линеаризации и кодирования) параметров в виде сообщения RPC, а параметр in указывает адрес параметров для удаленной процедуры. Параметр outproc задает адрес локальной процедуры, которая может быть вызвана для разупорядочения (делинеаризации и декодирования) результатов, а параметр out определяет адрес в памяти, где должны быть помещены результаты. Хотя процедура callrpc выполняет основную часть рутинной работы, необходимой для передачи сообщения RPC, библиотека ONC RPC включает и другие процедуры. Например, в клиентской программе для получения целочисленного идентификатора, который может использоваться для отправки сообщений RPC, вызывается функция: handle=clnt_create (host, prog, vers,proto); В технологии RPC такой целочисленный идентификатор называется дескриптором; дескриптор применяется в качестве одного из параметров в нескольких библиотечных процедурах RPC. Параметры процедуры clnt_create задают имя удаленного хоста — host, удаленную программу на этом хосте — prog, версию этой программы — vers и протокол (TCP или UDP) — proto. Генератор программ часто называют генератором заглушек. Причина, по которой применяется такой термин, станет очевидной после знакомства с описанием работы генератора. Термины Упорядочение" и "разупорядочение" являются синонимами терминов "маршалинг" и "демаршалинг". 22.3. Вспомогательные средства программирования RPC 307
Библиотека содержит также процедуры для формирования, записи в память и обработки аутентификационной информации., Например, для создания аутен- тификационного дескриптора конкретного пользователя на определенном хост- компьютере предназначена процедура: authunixjcreate(host,uid,gidtlenfaup_gids)} Параметры этой процедуры задают удаленный хост — host, идентификатор регистрационной записи пользователя — uid, идентификатор основной группы пользователя — gidy а также набор дополнительных групп, к которым принадлежит пользователь, — aup_grids. Параметр 1еп определяет число элементов в наборе. Хотя приложения могут разрабатываться непосредственно на основе использования вызовов библиотечных процедур RPC, такой подход к разработке применяется редко. Для построения программ чаще всего используется инструментальное средство генератора программы, описанное далее в этой главе. Вырабатываемый им код содержит вызовы библиотечных процедур. 22.4. Разделение программы на локальные и удаленные процедуры Для лучшего понимания того, как работают инструментальные средства программирования RPC, необходимо знать способы разделения любой программы на локальные и удаленные процедуры. Рассмотрим вызовы процедур в нераспределенном приложении. Один из таких вызовов показан на рис. 22.1. На этом рисунке пунктирные линии показывают соответствие между параметрами в вызове процедуры и параметрами в вызываемой процедуре. Процедура А | Параметры вызова соответствуют формальным параметрам вызываемой процедуры Рис. 22.1. Пример вызова процедуры, который служит иллюстрацией процедурного интерфейса, применяемого в вызывающей и в вызываемой процедуре Каждая процедура имеет набор формальных параметров, а каждый вызов процедуры определяет набор параметров вызова. Общее число параметров вызова в вызывающей процедуре должно быть равно общему числу формальных параметров в вызываемой процедуре, а тип каждого параметра вызова должен совпадать с объявленным типом соответствующего формального параметра. Иными словами, параметры определяют интерфейс между вызывающей и вызываемой процедурами. 22.5. Добавление кода, необходимого для дистанционного вызова процедур Для перемещения одной или нескольких процедур на удаленный компьютер необходимо ввести некоторый код между вызовом процедуры и удаленной процедурой. В клиентской части новый код должен упорядочивать параметры и преобразовывать их в машинно-независимое представление, создавать сообщение CALL по Процедура В 308 Глава 22. Построение распределенных программ...
спецификации RPC, отправлять сообщение удаленной программе, ожидать результатов и снова преобразовывать результирующие значения во внутреннее представление клиентского компьютера. В серверной части новый код должен принимать входящий запрос RPC, преобразовывать параметры во внутреннее представление данных сервера, доставлять сообщение соответствующей процедуре, формировать ответное сообщение, преобразовывая значения в машинно-независимое представление данных, и возвращать полученный результат клиенту. Для сохранения в неизменном виде структуры программы и изоляции кода, реализующего технологию RPC, от кода, реализующего функции приложения, должен быть введен дополнительный код дистанционного вызова процедур в форме двух дополнительных процедур, которые полностью скрывают от приложения все тонкости сетевого обмена данными. Эти новые процедуры предоставляют необходимые функциональные средства без изменения интерфейса между первоначальными вызывающими и вызываемыми процедурами. Сохранение первоначального интерфейса позволяет уменьшить вероятность появления ошибок, поскольку все функции обмена данными изолируются от первоначального приложения. 22.6. Процедуры-заглушки Дополнительные процедуры, которые вводятся в программу для реализации дистанционного вызова процедур, называются процедурами-заглушками. Чтобы лучше понять, что такое процедура-заглушка, проще всего представить себе, что нераспределенная программа разделена на две программы путем перемещения одной из существующих процедур на удаленный компьютер. В вызывающей (клиентской части) процедура-заглушка заменяет вызываемую процедуру. В части удаленной процедуры (серверной части) процедура-заглушка заменяет вызывающую процедуру. Эти две заглушки обеспечивают выполнение всех функций связи, необходимых для дистанционного вызова процедур, в результате чего две первоначальные процедуры (вызывающая и вызываемая) остаются неизменными. На рис. 22.2 проиллюстрировано понятие заглушки и показано, что процедуры- заглушки позволяют разделить вызов процедуры, приведенный на рис. 22.1, на локальную и удаленную части. Поскольку в заглушках используется такой же интерфейс, как и в первоначальных вызовах, введение их в программу не требует внесения изменений в первоначальные процедуры — вызывающую и вызываемую. Компьютер 1 Компьютер 2 Процедура А 1 Г Клиентская заглушка ' I Дистанционный вызов 4 процедуры ,' р —^> Серверная заглушка \ Г Процедура В Рис. 22.2. Процедуры-заглушки, введенные в программу, реализуют дистанционный вызов процедур 22.6. Процедуры-заглушки 309
22.7. Применение нескольких удаленных процедур и доставка дистанционного вызова по назначению На рис. 22.2 технология RPC показана в упрощенном виде, поскольку изображен только один вызов удаленной процедуры. На практике серверный процесс обычно образован несколькими удаленными процедурами одной удаленной программы. Каждый вызов RPC представляет собой сообщение с обозначением конкретной удаленной процедуры. При поступлении сообщения RPC сервер использует номер удаленной процедуры в сообщении для доставки поступившего вызова соответствующей процедуре. Этот принцип проиллюстрирован на рис. 22.3. На этом рисунке показано, как связаны между собой технология RPC и обычная реализация взаимодействия типа клиент/сервер. Удаленная программа представляет собой единственный серверный процесс, который должен функционировать еще до поступления каких-либо сообщений. В дистанционном вызове процедуры, который может поступить от любого клиента, должен быть указан адрес компьютера, на котором работает сервер, номер удаленной программы на этом компьютере и вызываемая удаленная процедура. Серверная программа состоит из процедуры диспетчера, а также из удаленных процедур и серверных процедур-заглушек. Диспетчер имеет информацию о том, какие номера удаленных процедур соответствуют тем или иным серверным заглушкам, и использует эту информацию для перенаправления каждого входящего дистанционного вызова процедуры к соответствующей заглушке. Клиенты передают запросы RPC в единственную серверную программу. Сервер использует номер удаленной процедуры в сообщении для определения того, какая процедура должна получить этот вызов. В данном примере процедура Ах вызывает Ви а процедура А2 вызывает В2. Штриховые и пунктирные линии на этом рисунке показывают, какой интерфейс используется каждой процедурой. Процедура А1 Процедура А2 iiiiiiiiiiiiiiiiii i:: Клиентская заглушка для В2 Клиентская заглушка для В1 ' ' I Л- — — -^ 1 », Диспетчер / \ Серверная заглушка для В1 L:::i:::. Процедура В1 Серверная заглушка для В2 Процедура В2 Рис. 22.3. Доставка сообщения по назначению в сервере RPC 22.8. Имя клиентской процедуры-заглушки Переход от нераспределенного приложения к распределенной программе значительно упрощается, если клиентская заглушка получает такое же имя, как и вызываемая процедура. Чтобы лучше понять, с чем это связано, снова рассмотрим процедуры-заглушки, показанные на рис. 22.3. Первоначальная вызывающая процедура Аг содержит вызов процедуры В;. После разделения программы 310 Глава 22. Построение распределенных программ...
процедура At входит в состав клиентской части и должна вызвать заглушку для обмена данными с удаленной процедурой. Если программист присвоит клиентской заглушке имя Bt и построит ее точно с таким же интерфейсом, как и у первоначальной процедуры Bl9 то вызывающая процедура At не потребует внесения каких-либо изменений. Фактически может даже оказаться, что преобразование программы в распределенную вообще не требует перетрансляции процедуры Aj. Первоначально оттранслированный объектный модуль процедуры Аг может быть связан с новой клиентской заглушкой для процедуры Bt и тем самым может быть создана работоспособная клиентская программа. Возможность добавления клиентской заглушки без внесения изменений в первоначальную вызывающую процедуру позволяет изолировать код RPC от кода первоначальной прикладной программы. В результате этого упрощается программирование, а также уменьшается вероятность внесения ошибок. Безусловно, применение для клиентской заглушки имени Bt усложняет сопровождение исходного кода, поскольку означает, что распределенная версия программы будет иметь две процедуры с именем Bt: клиентскую заглушку и первоначальную процедуру, которая вошла в состав сервера. Однако эти две версии процедуры Вj никогда не войдут в состав одной и той же программы, скомпонованной редактором связей. В большинстве случаев они не выполняются на одном и том же компьютере. Поэтому при том условии, что будут использоваться правильные версии процедур при построении программ из объектных модулей, подход с применением заглушек вполне себя оправдывает. Для построения распределенной версии прикладной программы необходимо перенести одну или несколько процедур на удаленный компьютер. Если при этом используются процедуры-заглушки, то первоначальные вызывающие и вызываемые процедуры могут остаться неизменными, но при условии, что клиентская заглушка будет иметь то же имя, что и первоначальная вызываемая процедура. 22.9. Применение инструментального средства rpcgen для построения распределенных программ Теперь должно быть очевидно, что основная часть кода, необходимая для реализации сервера RPC на основе нераспределенной версии программы, не требует изменения. Например, если информация о том, как связаны между собой номера удаленных процедур и серверные заглушки, хранится в виде однотипной структуры данных, то на всех серверах может использоваться одна и та же процедура диспетчера. Аналогичным образом, на всех серверах может использоваться один и тот же код для регистрации удаленных процедур в службе привязки портов. Чтобы можно было исключить непроизводительные затраты труда программистов, в большинстве реализаций ONC RPC предусмотрено инструментальное средство, позволяющее вырабатывать основную часть кода, необходимого для автоматического создания распределенной программы. К числу таких инструментальных средств относится программа rpcgen, которая принимает на входе файл спецификации и вырабатывает в качестве выходных данных файлы исходного кода на языке С. Файл спецификации содержит объявления констант, глобальных типов данных, глобальных данных и удаленных процедур (включая параметры процедур и типы результатов). Файлы, вырабатываемые программой rpcgen, содержат основную часть исходного кода, необходимого для реализации клиентских и серверных программ, которые обеспечивают выполнение указанных вызовов удаленных процедур. В частности, программа rpcgen вырабатывает код клиентских и серверных процедур-заглушек, включая код, выполняющий 22.9. Применение инструментального средства rpcgen... 311
функции упорядочения параметров, передачи сообщения RPC, доставки входящего вызова соответствующей процедуре, передачи ответа, а также прямого и обратного преобразования параметров и результатов из внешнего во внутреннее представление данных. В сочетании с прикладной программой и несколькими файлами, подготовленными программистом, программа rpcgen позволяет создавать законченные клиентские и серверные программы. Поскольку в качестве результатов своей работы программа rpcgen вырабатывает исходный код, существует возможность отредактировать этот код (например, внести в него вручную средства оптимизации для повышения производительности) или объединить его с другими файлами. В большинстве случаев программа rpcgen применяется как средство выполнения максимально возможного объема рутинной работы. Программисты стараются не редактировать вручную результаты, полученные при выполнении программы rpcgen, чтобы весь процесс создания клиентского и серверного программного обеспечения продолжал оставаться автоматизированным. Если изменяются требования к программе или возникает необходимость ввести новые дистанционные вызовы процедур, программист может исправить файлы спецификаций и снова вызвать на выполнение программу rpcgen для подготовки новых версий клиентской и серверной программ без доработки их вручную. 22.10. Вывод программы rpcgen и интерфейсные процедуры В целях расширения области применения и обеспечения автоматической выработки значительной части кода заглушек, в программе rpcgen предусмотрено разделение каждой процедуры-заглушки на две части. Одна из этих частей, общая почти для всех приложений, в которых используется технология RPC, предоставляет основные средства взаимодействия между клиентом и сервером, а вторая часть предоставляет интерфейс к прикладной программе. Программа rpcgen автоматически формирует часть заглушки, относящуюся к обмену данными, исходя из описания удаленной процедуры и ее параметров. Поскольку программа rpcgen вырабатывает код для заглушки связи, она определяет параметры, применяемые в клиентской части, и последовательность вызовов в серверной части. При использовании заглушек связи, созданных программой rpcgen, программист должен соблюдать соглашения по вызову процедур, предусмотренные в программе rpcgen. Причина, по которой заглушка делится на процедуры обмена данными и интерфейсные процедуры, вполне очевидна: это позволяет выбрать в программе rpcgen соглашения по вызову процедур, применяемые в заглушках связи, и, вместе с тем, определить соглашения по вызову процедур, используемые в удаленных процедурах. Программист создает интерфейсные заглушки, позволяющие установить соответствие между соглашениями по вызову удаленных процедур и соглашениями, предусмотренными в процедурах-заглушках связи, построенных программой rpcgen. На рис. 22.4 показано, как взаимодействуют между собой все эти процедуры. Программа rpcgen строит основные заглушки связи автоматически; программист предоставляет две интерфейсные процедуры. Как показано на рис. 22.4, каждая из двух частей заглушки состоит из двух отдельных процедур. В клиентской части интерфейсная процедура вызывает процедуру связи. В серверной части процедура связи вызывает интерфейсную процедуру. Если интерфейсные процедуры-заглушки определены правильно, то первоначальные вызывающая и вызываемая процедуры могут оставаться неизменными. 312 Глава 22. Построение распределенных программ...
Компьютер 1 Компьютер 2 Процедура А i Клиентский интерфейс * Клиентская заглушка связи 4 Дистанционный / вызов / процедуры / / / / / / / / • / / Серверная заглушка связи * Серверный интерфейс .„...т..... Процедура В Рис. 22.4. Форма распределенной программы, создаваемой с использованием программы rpcgen 22.11. Входные и выходные данные программы rpcgen Программа rpcgen читает входной файл, который содержит спецификацию удаленной программы. Она вырабатывает четыре выходных файла, содержащих исходный код. Программа rpcgen назначает выходным файлам имена с учетом имени входного файла. Если файл спецификации имеет имя Q.x, имена всех выходных файлов начинаются с Q. В табл. 22.1 приведен перечень выходных файлов и описано их назначение. Как показывают их имена, выходные файлы содержат исходный код на языке С и для программ, и для объявления данных. Таблица 22.1. Выходные файлы, вырабатываемые программой rpcgen после получения входного файла Q.x Имя файла Назначение Q.h Объявления констант и типов, используемых ь коде, выработанном программой rpcgen для включения и в клиентскую, и в серверную программу Qjcdr.c Вызовы процедуры XDR, которые применяются в клиентской и серверной программе для упорядочения параметров Q_clnt.c Клиентская процедура-заглушка связи Q_svc.c Серверная процедура-заглушка связи 22.12. Применение программы rpcgen для построения и клиента, и сервера На рис. 22.5 показаны файлы, которые должны быть подготовлены для построения клиентской и серверной программ с использованием программы rpcgen. По сути, для этого необходимо написать само приложение, процедуры, которые в нем вызываются, и интерфейсные части клиентской и серверной заглушек. Затем приложение разбивается на драйверную программу (клиент) и набор процедур, которые включают удаленную программу (сервер). После этого составляется спецификация удаленной программы и с помощью программы rpcgen формируются остальные части. На этом рисунке прямоугольники с жирными рамками обозначают файлы, которые должны быть подготовлены программистом. 22.11. Входные и выходные данные программы rpcgen 313
Клиентское приложение Клиентский интерфейс Q.x Спецификация удаленной программы Q clnt.c Q.h Q xdr.c Q svc.c Клиент Сервер Удаленные процедуры Серверный интерфейс Рис. 22.5. Файлы, необходимые для построения клиентской и серверной программ на основе выходных результатов программы rpcgen, а также этапы трансляции, необходимые для их обработки После вызова на выполнение программа rpcgen считывает спецификацию и вырабатывает исходный код С, который должен быть оттранслирован и скомпонован для получения выполняемых программ. После вызова на выполнение программы rpcgen должны быть выполнены два отдельных этапа трансляции и компоновки (обработки редактором связей). На одном этапе создается выполняемая клиентская программа, а на втором — выполняемая серверная программа. Рис. 22.5 позволяет получить лишь общее представление о том, что должно поступать на вход программы rpcgen и что она вырабатывает на выходе. В следующей главе представлены дополнительные сведения по использованию этой программы. В этой главе представлен пример простого приложения и показаны этапы его преобразования в распределенную программу. В главе описан файл спецификации, который программа rpcgen принимает в качестве входного, а также код, вырабатываемый этой программой. 22.13. Резюме Дистанционный вызов процедур — это широкое понятие, позволяющее успешно разрабатывать программное обеспечение клиент/сервер. Технология RPC может применяться как для проектирования, так и для реализации распределенной программы. Средства ONC RPC позволяют придерживаться определенной спецификации и создавать приложение с нуля, использовать процедуры, входящие в состав библиотеки RPC, или применять инструментальное средство автоматизированного построения программ, называемое rpcgen. 314 Глава 22. Построение распределенных программ...
Технология RPC позволяет создать нераспределенную программу, а затем преобразовать ее в распределенную путем перемещения некоторых процедур на удаленный компьютер. В процессе этого существует возможность свести объем доработок к минимуму и уменьшить вероятность внесения ошибок. Для этого достаточно ввести в программу процедуры-заглушки, которые реализуют все необходимые средства взаимодействия между компонентами программы. При использовании заглушек первоначальные вызывающие и вызываемые процедуры могут оставаться неизменными. Поскольку большинство распределенных программ, в которых используются средства RPC, имеют одну и ту же общую архитектуру, программа rpcgen может применяться для автоматического создания основной части необходимого кода. Для этого, кроме файла спецификации, достаточно подготовить пару интерфейсных процедур, а также процедур, связанных с приложением. Программа rpcgen автоматически формирует остальные компоненты клиентских и серверных программ, в том числе процедуры, которые регистрируют сервер в службе привязки портов, обеспечивают обмен данными между клиентом и сервером и доставляют входящие вызовы соответствующим удаленным процедурам. Материал для дальнейшего изучения С дополнительной информацией об инструментальном средстве rpcgen можно ознакомиться в документации, которая поставляется вместе с этим программным обеспечением/В книге [151] приведены подробные сведения по обработке исключительных ситуаций в программном обеспечении RPC. Упражнения 22.1. Опишите последовательность действий, выполняемых сервером при поступлении сообщения CALL по спецификации RPC. Не забудьте указать, на каком этапе значения данных преобразуются из внешнего представления во внутреннее. 22.2. Прочитайте, что сказано о библиотечных процедурах RPC в документации, которая поставляется вместе с операционной системой. Каковы параметры функции svc_sendreply? Для чего предназначен каждый из них? 22.3. Библиотека RPC включает процедуры, позволяющие выполнить регистрацию сервера в службе привязки портов. Прочитайте документацию, чтобы узнать, для чего предназначена процедура pmapjinset. Почему она так необходима? 22.4. Если у вас есть доступ к исходному коду библиотечных процедур RPC, узнайте, из какого числа строк состоит этот код. Сравните объем исходного кода библиотеки с объемом исходного кода программы rpcgen. Почему программа rpcgen имеет такой большой объем? 22.5. Программа rpcgen вырабатывает исходный код на языке С, а не объектный код. В документации, которая поставляется вместе с программой rpcgen, сказано, что выработка исходного кода предусмотрена для того, чтобы в него можно было внести изменения. С чем может быть связана необходимость внесения изменений? 22.6. (Дополнение к предыдущему вопросу.) В чем состоят преимущества и недостатки внесения изменений в выходные файлы программы rpcgen? Материал для дальнейшего изучения 315
22.7. Один из вариантов механизма вызова удаленных процедур может предусматривать объединение всех процедур удаленной программы в одну процедуру и применение дополнительного параметра для определения того, какая процедура должна быть вызвана (например, в основе удаленной процедуры лежит использование оператора switch языка С, в котором применяется параметр new для выбора одного из нескольких альтернативных действий). Назовите основные преимущества использования такого подхода по сравнению с технологией ONC RPC. Каковы основные недостатки? 22.8. Если в серверной части программы ONC RPC используются сокеты, то какие методы могут применяться для реализации взаимного исключения (т.е. как можно гарантировать, чтобы в любой момент времени вызывалась только одна удаленная процедура)? Назовите преимущества и недостатки каждого из этих методов? Подсказка: рассмотрите опции сокета и вариант с созданием файла. 316 Глава 22. Построение распределенных программ...
23 Построение распределенных программ (пример использования программы rpcgen) 23.1. Введение В предыдущих главах описаны принципы, лежащие в основе модели дистанционного вызова процедур и представлен механизм ONC RPC. Определено понятие дистанционного вызова процедур и показано, как можно разделить программу по границе между вызовами процедур. Кроме того, описано использование инструментального средства rpcgen и связанных с ним библиотечных процедур для автоматической выработки значительной части кода программ, в которых применяется технология ONC RPC. В настоящей главе завершается описание программы rpcgen. В ней представлена последовательность этапов, выполняемых в процессе создания нераспределенной программы, а затем ее разделение на локальный и удаленный компоненты. В этой главе каждый этап указанного процесса прослеживается на примере одного приложения. Здесь приведены результаты выполнения программы rpcgen и показан дополнительный код, необходимый для создания клиентского и серверного компонентов распределенной программы, в которой используется технология RPC. 23.2. Иллюстрация применения программы rpcgen Для разъяснения принципов работы программы rpcgen и изложения дополнительных сведений воспользуемся конкретным примером. Поскольку этот пример должен служить только для описания работы программы rpcgen, в качестве него выбрано исключительно простое приложение. На практике, безусловно, лишь немногие программы RPC являются столь же простыми и удобными для понимания, как в этом примере, который следует рассматривать как учебный и не задаваться вопросом, действительно ли это приложение должно быть построено в виде распределенной программы. 23.3. Операции со словарем В качестве примера использования программы rpcgen рассмотрим приложение, реализующее простую базу данных. База данных обеспечивает выполнение четырех основных операций: I (сокращение от initialize) для перевода базы дан-
ных в исходное состояние (т.е. для уничтожения всех ранее хранимых в ней данных), i (сокращение от insert), для вставки нового элемента, d (сокращение от delete) для удаления элемента и 1 (сокращение от lookup) для поиска элемента. Предполагается, что элементы в базе данных представляют собой отдельные слова. Поэтому база данных функционирует как словарь. Приложение вставляет набор допустимых слов, а затем использует базу данных для проверки новых слов и определения того, имеется ли каждое из них в словаре. В целях упрощения данного примера предполагается, что для ввода данных в приложение применяется текстовый файл, в котором каждая строка содержит однобуквенную команду, а за ней следует слово. В табл. 23.1 перечислены команды и описано назначение каждой из них. За некоторыми командами должно следовать слово word, которое может рассматриваться как параметр команды. Таблица 23.1. Команды, применяемые во входном файле рассматриваемого в качестве примера приложения базы данных, и их назначение Однобуквенная команда Параметр Описание I Отсутствует Инициализирует базу данных, удаляя все слова i word Вставляет слово word в базу данных d word Удаляет слово word из базы данных 1 word Выполняет поиск слова word в базе данных q Отсутствует Завершает работу Например, ниже приведен входной файл, содержащий последовательность команд обработки данных. Эти команды инициализируют словарь, вставляют имена поставщиков компьютеров, удаляют часть этих имен и выполняют поиск трех имен: I " i Navy i IBM i RCA i Encore i Digital d RCA d Navy 1 IBM d Encore 1 CDC 1 Encore q После подачи этого командного файла на вход приложение базы данных словаря должно сообщить, что слово IBM в словаре найдено, а слова Encore или ( CDC — отсутствуют. 23.4. Восемь этапов создания распределенного приложения На рис. 22.51 показано, какие входные файлы требуются для программы rpcgen и какие выходные файлы она вырабатывает. Для создания необходимых файлов и Рис. 22.5 приведен на стр. 314. 318 Глава 23. Построение распределенных программ...
оформления полученных результатов в виде клиентской и серверной программ необходимо выполнить следующие восемь этапов. 1. Разработать и отладить нераспределенное приложение, которое решает поставленную задачу. 2. Разделить программу на две части, выбрав набор процедур, которые должны быть перенесены на удаленный компьютер. Поместить выбранные процедуры в отдельный файл. 3. Оформить спецификацию rpcgen для удаленной программы, в частности, указать имена и номера удаленных процедур и привести объявления их параметров. Выбрать номер удаленной программы и номер версии (обычно 1). 4. Вызвать на выполнение программу rpcgen для проверки спецификации и, если она не содержит ошибок, сформировать четыре файла исходного кода, которые будут использоваться в клиентской и серверной программах. 5. Написать интерфейсные процедуры-заглушки для клиентской и серверной части. 6. Выполнить трансляцию и компоновку клиентской программы, которая состоит из четырех основных файлов: первоначальной прикладной программы (из которой изъяты удаленные процедуры), клиентской заглушки (которая построена программой rpcgen), клиентской интерфейсной заглушки и процедур XDR (построенных программой rpcgen). После трансляции и компоновки всех этих файлов создается выполняемая программа, предназначенная для использования в качестве клиента. 7. Выполнить трансляцию и компоновку серверной программы, которая состоит из четырех основных файлов: процедур, взятых из первоначального приложения, которые теперь представляют собой удаленную программу, серверной заглушки (которая построена программой rpcgen), серверной интерфейсной заглушки и процедур XBR (построенных программой rpcgen). После трансляции и компоновки всех этих файлов создается выполняемая программа, предназначенная для использования в качестве сервера. 8. Запустить сервер на удаленном компьютере, а затем вызвать клиент на локальном компьютере. В следующих разделах эти этапы рассматриваются более подробно. В них для иллюстрации всех тонкостей также используется приложение базы данных словаря. 23.5. Этап 1: построение нераспределенной прикладной программы На первом этапе построения распределенной версии для рассматриваемого приложения базы данных словаря необходимо создать нераспределенную программу, которая решает поставленную задачу. Прикладная программа на языке С, которая решает задачу управления словарем, приведена в файле diet.с. /* Файл diet.с - главная процедура, процедуры initw, nextin, insertw, */ /* deletew, lookupw */ finclude <stdlib.h> ¦include <stdio.h> ¦include <ctype.h> 23.5. Этап 1: построение нераспределенной прикладной программы 319
tinclude <string.h> fdefine MAXWORD 50 /* Максимальная длина команды или слова */ ¦define DICTSIZ 100 /* Максимальное число записей в словаре */ char dict[DICTSIZ][MAXWORD+l]; /* Распределение памяти для слов словаря */ int nwords =0; /* Число слов в словаре */ int nextin(char *cmd, char *word), initw(), insertw(const char *word); int deletew(const char *word), lookupw(const char *word); /* * Главная процедура - вставка, удаление или поиск указанных слов в словаре */ int main(int argc, char argv[]) { char word[MAXWORD+l]; /* Область памяти для размещения введенного слова */ char crad; int wrdlen; /* Длина введенного слова */ while (l) { wrdlen = nextin(&cmd, word); if (wrdlen <0) exit@); word[wrdlen] = '\0'; switch (cmd) { case 'I': /* Команда инициализации словаря */ initw(); printf("Dictionary initialized to empty.\nM); break; case 'i': /* Команда вставки */ insertw(word); printf("%s inserted.\n",word); break; case 'd': 7* Команда удаления */ if (deletew(word)) printf(M%s deleted.\n",word); else printf("%s not found.\n",word); break; case '1': /* Команда поиска */ if (lookupw(word)) printf("%s was found.\n'\word); else printf("%s was not found.Nn^word); break; case 'q': /* Команда выхода */ priritf("program quits.\nH); exit@); default: /* Недопустимый формат ввода */ 320 Глава 23. Построение распределенных прогр
printf("command %c invalid.\nH, cmd)? break; } } } /* * Процедура nextin - чтение команды и (возможно) слова из очередной * введенной строхи *... « — —.————— .———.. .......... .... ... */ int nextin(char *cmd, char *word) { int i, ch; ch = getc(stdin); while (isspace(ch)) ch = getc(stdin); if (ch == EOF) return -1? *cmd = (char) ch; ch = getc(stdin); while (isspace(ch)) ch = getc(stdin); if (ch == EOF) return -1; if (ch == '\n') return 0; i = 0; while (lisspace(ch)) { if (++i> MAXWORD) { printf("error: word too long.Nn"); exit(l); } *word++ « ch; ch = getc(stdin); } return i; } /*... * Процедура initw - инициализация словаря с исключением из него всех слов * ... . ....... . . . . */ int initw() { nwords =0? return 1; > /* 23.5. Этап 1: построение нераспределенной прикладной программы 321
* Процедура insertw - вставка слова в словарь * , .. .... «__ т . штттл _ */ int insertw(const char *word) { strcpy(diet[nwords], word)? nwords++; return nwords? } /* —™_- -——-._:—-— * Процедура deletew - удаление слова из словаря * . . */ int deletew(const char *word) { int i; for (i=0 ; i<nwords ; i++) if (strcmp(word, dict[i]) =s 0) { nwords—; strcpy(dict[i], dict[nwords]); return 1; } return 0; } /* . —————— * Процедура lookupw - поиск слова в словаре * « . . —— «. ™ - - .— —————••••___•—. .._..—_- */ int lookupw(const char *word) { int i; for (i=0 ; i<nwords ; i++) if (strcmp(word, dict[i]) == 0) return 1; return 0; } Для того чтобы это приложение было простым и удобным для восприятия, в примере нераспределенной программы, приведенном в файле dxct.c, для хранения слов используется двухмерный массив. В любой момент времени можно узнать количество слов в словаре по значению глобальной переменной nwords. Главная процедура состоит из цикла, в котором при каждом проходе выполняется чтение и обработка одной строки ввода. В цикле вызывается процедура nextin для чтения команды (и возможно, слова) из очередной строки ввода, а затем используется оператор switch языка С для выбора одного из шести возможных вариантов. Эти 322 Глава 23. Построение распределенных программ...
варианты соответствуют пяти допустимым командам, а также действию, которое выполняется по умолчанию при получении строки недопустимого формата. В каждом варианте выбора (оформленном с помощью конструкции case) в основной программе вызывается процедура для выполнения конкретных действий. Например, в конструкции case, которая соответствует команде вставки i, вызывается процедура insertw. Процедура insertw добавляет новое слово к концу массива и увеличивает значение переменной nwords. Остальные процедуры действуют в соответствии с их назначением. Процедура deletew ищет слово, предназначенное для удаления. Найдя такое слово, процедура deletew заменяет его последним словом в словаре и уменьшает значение переменной nwords. И наконец, процедура lookupw выполняет последовательный поиск в массиве для определения того, присутствует ли в нем заданное слово. Она возвращает 1, если слово имеется в словаре, и 0 — в противном случае. Для подготовки выполняемой программы для этого приложения используется транслятор С. В большинстве версий системы Linux для подготовки файла выполняемой программы diet из исходного кода, который содержится в файле diet.с, применяется следующая команда: ее -о diet diet.с 23.6. Этап 2: деление программы на две части После формирования и отладки нераспределенное приложение можно разделить на локальный и удаленный компоненты. Для решения задачи разделения программы на части необходимо иметь концептуальную модель вызова процедур в программе, представленную в виде графа. Например, на рис. 23.1 показана процедурная организация первоначального приложения базы данных словаря. Представленный на этом рисунке граф вызовов описывает процедурную организацию программы. nextin Главная процедура insertw х1х initw deletew lookupw Рис. 23.1. Граф вызова процедур для первоначальной нераспределенной программы, которая решает поставленную задачу управления словарем Решая вопрос о составе процедур, которые могут быть перенесены на удаленный компьютер, необходимо учитывать, какие функциональные средства требуются для работы каждой процедуры. Например, процедура nextin читает и интерпретирует очередную строку ввода при каждом ее вызове. Поскольку для работы этой процедуры требуется доступ к стандартному файлу ввода программы, процедура nextin должна находиться на том же компьютере, где находится основная программа. Процедуры, выполняющие операции ввода/вывода или пользующиеся дескрипторами файлов для выполнения каких-либо иных действий, нельзя легко перенести на удаленный компьютер. 23.6. Этап 2: деление программы на две части 323
Необходимо также учитывать» где находятся данные, к которым обращается каждая процедура. Например, процедура lookupw должна иметь доступ ко всей базе данных слов. Если процедура lookupw выполняется на компьютере, отличном от того, где находится словарь, то в каждом вызове RPC этой процедуры lookupw в качестве параметра должен передаваться весь словарь. Передача удаленным процедурам крупных структур данных в качестве параметров является крайне неэффективной, поскольку при этом механизм RPC должен считывать и кодировать всю структуру данных при выполнении каждого дистанционного вызова процедуры. Процедуры должны выполняться на том же компьютере, где находятся данные, к которым они обращаются. Передача удаленным процедурам крупных структур данных в качестве параметров является неэффективной. После изучения первоначальной программы для работы со словарем, а также данных, к которым обращается каждая процедура, должно быть очевидно, что процедуры initw, insertw, deletew и lookupw должны находиться на том же компьютере, где находится сам словарь. Предположим, что решено перенести базу данных словаря и связанные с ним процедуры на удаленный компьютер. Чтобы понять, к чему приведет перемещение ч-асти процедур на удаленный компьютер, программисты обычно пытаются мысленно представить себе схему распределенной программы и структур данных или даже сделать набросок такой схемы. На рис. 23.2 показана новая структура приложения для работы со словарем, в которой данные словаря и процедуры доступа перенесены на удаленный компьютер. Удаленный компонент содержит данные словаря, а также процедуры для доступа и поиска в нем. Клиент на компьютере 1 Удаленная программа на компьютере 2 Главная процедура \ ч *"* * Г nextin Вызовы удаленных процедур «ч. »» „, """ "~ — «ч -¦ «* ч» ч» Ч. "* *, ч» «» _ ч* *" •» ^ ч» ** «» Ч» ""* Ч. "Ч ч» ч» ж initw insertw deletew lookupw Структуры данных для ведения словаря (разделяемые) Рис. 232. Концептуальная схема распределения программы словаря на локальный и удаленный компоненты Даже такая простая схема, которая приведена на рис. 23.2, может помочь продумать, как должна быть разделена программа на локальный и удаленный компоненты. Программист должен учесть, будет ли иметь каждая процедура доступ к необходимым ей данным и службам, а также определить, какие параметры потребуются для каждой удаленной процедуры и какие издержки будут связаны с передачей этой информации по сети. И наконец, схема может помочь оценить, как сетевые задержки будут влиять на производительность программы. После выбора подходящего варианта разбиения программы и принятия решения о переходе к дальнейшим действиям необходимо на следующем этапе разделить исходный код программы на локальный и удаленный компоненты. Программист определяет константы и структуры данных, применяемые в каждом компоненте, и по- 324 Глава 23. Построение распределенных программ...
мещает каждый компонент в отдельный файл. В этом примере приложения словаря задача разделения программы решается довольно просто, поскольку первоначальный исходный файл может быть разделен по границе между процедурами nextin и initw. Файл diet 1. с содержит главную процедуру и процедуру nextin. /* Файл dictl.c - главная процедура, процедура nextin */ tinclude <stdlib.h> ¦include <stdio.h> ¦include <ctype.h> fdefine MAXWORD 50 /* Максимальная длина команды или */ /* слова */ int nextin(char *cmd, char *word), initw(void), insertw(char *)f deletew(char *), lookupw(char *); /* * Главная процедура - вставка, удаление или поиск указанных слов в словаре *........... ............................................... — ..... */ int main(int argc, char *argv[]) { char word[MAXWORD+l]; /* Область памяти для размещения */ /* введенного слова */ char cmd; int wrdlen; . /* Длина введенного слова */ while (l) { wrdlen = nextin(&cmd, word); if (wrdlen <0) exit@); switch (cmd) { case 'I': /* Команда инициализации словаря */ initw(); printf("Dictionary initialized to empty.\nH); break; case 'i': /* Команда вставки */ insertw(word); printf("%s inserted.\n",word); break; case 'd': /* Команда удаления */ if (deletew(word)) printf("%s deleted.\nM,word); else printf(H%s not found.\nH,word); break; case '1': /* Команда поиска */ if (lookupw(word)) printf(H%s was found.\nn,word); else printf(H%s was not found.\n",word); break; case 'q': /* Команда выхода */ 23.6. Этап 2: деление программы на две части 325
printf("program quits.\n"); exit(O); default: /* Недопустимый формат ввода */ printf("command %c invalid.\n", cmd); break; } } } /* * Процедура nextin - чтение команды и (возможно) слова из очередной * введенной строки *. . . _-_- ... .. ..........—.—..— . ...— */ int nextin(char *cmd, char *word) { int i, ch; ch = getc(stdin); while (isspace(ch)) ch = getc(stdin); if (ch == EOF) return -1; *cmd = (char) ch; ch = getc(stdin)? while (isspace(ch)) ch = getc(stdin); if (ch == EOF) return -1; if (ch == '\n') return 0; i = 0; while (!isspace(ch)) { if (++i> MAXWORD) { printf("error: word too long.\n"); exit(l); } *word++ * ch; ch = getc(stdin); > return i; } Файл diet2.с содержит те процедуры первоначального приложения, которые войдут в состав удаленной программы. Кроме того, он содержит объявления тех глобальных данных, к которым будут иметь совместный доступ процедуры. В данный момент этот файл не содержит законченную программу; недостающий код будет добавлен позже. /* Файл dict2.c - процедуры initw, insertw, deletew, lookupw ¦*/ ¦include <string.h> fdefine MAXWORD 50 /* Максимальная длина команды или слова */ 326 Глава 23. Построение распределенных программ...
jtdefine DICTSIZ 100 /* Максимальное число записей в словаре */ char dict[DICTSIZ][MAXWORD+l];/* Распределение памяти для слов словаря */ int nwords = 0; /* Число слов в словаре *'¦/ /* - - * Процедура initw - инициализация словаря с исключением из него всех слов * */ int initw() { nwords =0; return 1; } /* * Процедура insertw - вставка слова в словарь * ... . _„_ ——— ... ._.———..—.....-.—.-.—..—— */ int insertw(char *word) { strcpy(diet[nwords], word); nwords++; return nwords; } /* — * Процедура deletew - удаление слова из словаря * . . */ int deletew(char *word) { int i; for (i-0 ; i<nwords ; i++) if (strcmp(word, dict[i]) == 0) { nwords—; strcpy(dict[i], dict[nwords]); return 1; } return 0? } /*. ...................... .... ... .......... * Процедура lookupw - поиск слова в словаре * . .. . ...... ... . .. —.. .. */ int lookupw(char *word) { 23.6. Этап 2: деление программы на две части
int i; for (i=0 ; i<nwords ; i++) if (strcmp(word, dict[i]) ==0) return 1; return 0; } Обратите внимание, что определение символической константы MAXWORD присутствует в обоих компонентах, поскольку и в том, и в другом компоненте объявляются переменные, применяемые для хранения слов. Однако только файл dict2.c содержит объявления структур данных, используемых для хранения словаря, поскольку структуры данных словаря будут включены только в удаленную программу. С практической точки зрения, разделение кода приложения на два файла позволяет оттранслировать клиентскую и серверную части отдельно. Транслятор проверяет обе части кода на отсутствие таких ошибок, как необъявленные символические константы, а редактор связей определяет, могут ли все структуры данных быть собраны в один модуль вместе с теми процедурами, которые на них ссылаются. В системе Linux для получения файлов объектного кода этих двух компонентов (а не законченных программ) можно применить следующие команды: ее -с dictl.c ее -с dict2.c Для получения выполняемой программы указанные компоненты могут быть скомпонованы редактором связей, но они транслируются не для этого; в данном случае этап трансляции применяется для проверки того, являются ли оба файла синтаксически правильными. Следует отметить, что очень удобно иметь возможность проверять код с помощью транслятора, т.к. большинство распределенных программ намного сложнее по сравнению с этим тривиальным примером. Трансляция позволяет найти в большой программе ошибки, не замеченные программистом. Обнаружив такие ошибки на раннем этапе (т.е. до вставки дополнительного кода), их можно сразу же устранить. 23.7. Этап 3: создание спецификации rpegen После определения структуры распределенной программы выполняется подготовка спецификации rpegen. По сути, файл спецификации rpegen содержит объявление удаленной программы, а также используемых в ней структур данных. Файл спецификации содержит константы, определения типов и объявления для клиентской и серверной программ. Точнее, файл спецификации содержит: ¦ объявления для констант, используемых в клиенте или, чаще всего, в сервере (в удаленной программе); ¦ объявления применяемых типов данных (в частности, в параметрах вызова удаленных процедур); ¦ объявления удаленных программ, процедур, содержащихся в каждой программе, и типов их параметров. Напомним, что спецификация RPC определяет использование номеров для обозначения удаленных программ и содержащихся в них удаленных процедур. Объявление программы в файле спецификации содержит такие сведения, как номер программы по спецификации RPC, номер ее версии и номера, присвоенные каждой процедуре в программе. 328 Глава 23. Построение распределенных программ...
Все спецификации должны быть определены на языке программирования RPC, а не на языке С. Хотя различия между этими языками невелики, иногда они могут привести к путанице. Например, в языке RPC для обозначения символьных строк с нулевым символом в конце применяется ключевое слово string, а в языке С используется тип данных char *. Даже опытным программистам иногда приходится несколько раз исправлять свою спецификацию для устранения подобных несоответствий. Пример спецификации rpcgen приведен в файле rdict.x. В этом файле содержатся примеры объявлений для версии программы базы данных словаря для RPC. /* Файл dict.x */ /* Объявления RPC для программы работы со словарем */ const MAXW0RD =50? /* Максимальная длина команды или слова */ const DICTSIZ = 100; /* Число записей в словаре */ struct example { /* Неиспользуемая структура, объявленная */ int exfieldl; /* здесь для иллюстрации того, как */ char exfield2; /* строятся программой rpcgen процедуры */ /* XDR для преобразования структур */ }? /*-- * RDICTPR06 - удаленная программа, обеспечивающая вставку, удаление и поиск '*«....... -— ..... - - .. - ........ - ..... - */ program RDICTPROG { • /* Имя удаленной программы (не используется) */ version RDICTVERS { /* Объявление версии (см. ниже) */ int INITW(void) = 1; /* Первая процедура в данной программе */ int INSERTW(string) ¦ 2; /* Вторая процедура в данной программе */ int DELETEW(string) я 3; /* Третья процедура в данной программе */ int LOOKUPW(string) = 4; /* Четвертая процедура в данной программе */ } » 1; . /* Определение версии программы */ }= 0x30090949; /* Номер удаленной программы /* (должен быть уникальным) */ Файл спецификации rpcgen не содержит записи для всех объявлений, находящихся в первоначальной программе. В нем определены только те константы и типы, которые используются совместно клиентской и серверной программами или требуются для определения параметров. Этот пример файла спецификации начинается с определения констант MAXW0RD и DICTSIZE. В первоначальном приложении обе эти константы определены как символические с использованием оператора define препроцессора языка С. В языке RPC не используются объявления символических констант С; в этом языке символические константы должны быть объявлены с применением ключевого слова const и им должно быть присвоено значение с помощью знака равенства (=). В соответствие с общепринятыми соглашениями, в файле спецификации для определения процедур и программ используются имена, состоящие из прописных букв. Как будет показано ниже, эти имена становятся символическими константами, которые могут применяться в программах С. Использование прописных букв не является абсолютно необходимым, но помогает избегать конфликтов имен. 23.7. Этап 3: создание спецификации rpcgen 329
23.8. Этап 4: выполнение программы rpcgen После разработки спецификации выполняется программа rpcgen для проверки на отсутствие синтаксических ошибок и создания четырех2 файлов кода, как показано на рис. 22.53. В системе Linux, как ив большинстве версий UNIX, команда вызова этой программы имеет форму: rpcgen rdict.x В программе rpcgen при формировании имен четырех выходных файлов используется имя входного файла. Например, поскольку имя входного файла начинается с rdict, выходные файлы получают имена rdict.h, rdict_clnt.c, rdictjsvc.c и rdictjcdr.c. 23.9. Файл .h, сформированный программой rpcgen В листинге 23.1 показано содержимое файла rdict.h, который включает допустимые объявления С для всех констант и типов данных, объявленных в файле спецификации. Кроме того, программа rpcgen добавляет в него определения удаленных процедур. В этом примере кода программа rpcgen определила константу с именем INSERTW, состоящим из прописных букв, равную 2, поскольку процедура INSERTW, объявленная в спецификации, является второй процедурой в удаленной программе. Объявления внешних процедур в файле rdict.h требуют пояснения. Объявленные процедуры представляют собой интерфейсную часть серверной заглушки. Имена процедур были сформированы путем преобразования в нижний регистр имен объявленных процедур, добавления символа подчеркивания и номера версии программы. Например, в рассматриваемом файле спецификации объявлено, что удаленная программа содержит процедуру DELETEW, поэтому файл diet.h включает объявление extern для процедуры deletew_l. Чтобы лучше понять, почему программа rpcgen сформировала объявления этих интерфейсных процедур, напомним назначение интерфейсной части заглушки: она позволяет пользователю программы rpcgen выбрать собственные соглашения по вызову процедур и оставить неизменными первоначальные вызываемые процедуры. В качестве примера именования интерфейсной заглушки рассмотрим процедуру insertw. Первоначальная процедура войдет в состав сервера и останется неизменной. Поэтому серверная программа должна включать процедуру insertw, имеющую те же параметры, что и в первоначальном приложении. Для предотвращения конфликта имен в сервере для интерфейсной процедуры-заглушки должно использоваться другое имя. Программа rpcgen предусматривает использование для серверной заглушки связи вызова интерфейсной процедуры- заглушки с именем insertw_l. В этом вызове используются параметры, выбранные программой rpcgen, а программисту предоставляется возможность разработать процедуру insertw_l таким образом, чтобы она вызывал процедуру insertw с использованием правильных параметров. 2 Бели какой-либо конкретный выходной файл должен остаться пустым, программа rpcgen его не создает. Поэтому при обработке некоторых спецификаций формируется менее четырех файлов. Рис. 22.5 приведен на стр. 314. 330 Глава 23. Построение распределенных программ...
Листинг 23.1. Файл rdict.h - пример файла .h, сформированного программой rpcgen4 * Не редактируйте этот файл. * Он создан с помощью программы rpcgen. */ ¦ifndef _RDICT_H_RPCGEN ¦define _RDICT_H_RPCGEN ¦include <rpc/rpc.h> ¦ifdef cplusplus extern "Си { ¦endif ¦define MAXWORO 50 ¦define DICTSIZ 100 struct example { int exfieldl; char exfield2; typedef struct example example; ¦define RDICTPROG 0x30090949 ¦define RDICTVERS 1 ¦if defined( STDC ) || defined( cplusplus) ¦define INITW 1 extern int * initw_l(void *, CLIENT *); extern int * initw 1 svc(void *, struct svc req *); ¦define INSERTW 2 extern int * insertw_l(char **, CLIENT *); extern int * insertw_l_svc(char **, struct svc req *); ¦define DELETEW 3 extern int * deletew_l(char **, CLIENT *); extern int * deletewJL_svc(char **, struct svc req *); ¦define LOOKUPW 4 extern int * lookupw_l(char **, CLIENT *); extern int * lookupw_l_svc(char **, struct svcjreq *); extern int rdictprog_l_freeresult (SVCXPRT *, xdrproc_tr caddr_t); ¦else /* Синтаксис языка С по определению Кернигана и Ритчи */ ¦define INITW l extern int * initw_l(); extern int * initw_l_svc(); Поскольку код, приведенный в данной главе, вырабатывается программой rpcgen, он может не соответствовать строгим стандартам ANSI. Кроме того, код не начинается с комментария, обозначающего файл исходного кода. 23.9. Файл .h, сформированный программой rpcgen 331
¦define INSERTW 2 extern int * insertwJL(); extern int * insertw 1 svc(); ¦define DELETEW 3 extern int * deletew_l(); extern int * deletew l_svc(); ¦define LOOKUPW 4 extern int * lookupw_l()? extern int * lookupw_l_svc(); extern int rdictprog_T_freeresult (); tendif /* Синтаксис языка С по определению Кернигана и Ритчи */ /* Функции XDR */ Ш defined! STDC ) || defined( cplusplus) extern bool_t xdr_example (XDR *, example*); ¦else /* Синтаксис языка С по определению Кернигана и Ритчи */ extern bool_t xdr_example (); tendif /* Синтаксис языка С по определению Кернигана и Ритчи */ tifdef cplusplus } jfendif ¦endif /* ! RDICT H RPCGEN */ 23.10. Файл вызова процедур преобразования XDR, сформированный программой rpcgen Программа rpcgen вырабатывает файл, содержащий вызовы процедур, которые выполняют преобразования XDR для всех типов данных, объявленных в удаленной программе. Например, файл rdict_xdr.c, показанный в листинге 23.2, содержит вызовы процедур преобразования типов данных, объявленных в программе базы данных словаря. Листинг 23.2. Файл rdict_xdr.c - пример файла с вызовами процедур преобразования XDR, сформированного программой rpcgen /* * Не редактируйте этот файл. * Он создан с помощью программы rpcgen. */ ¦include "rdict.h" boolj: xdr example (XDR *xdrs, example *objp) { register long *buf; 332 Глава 23. Построение распределенных программ...
if (lxdr_int (xdrs, &objp->exfieldl)) return FALSE; if (Ixdr_char (xdrs, &objp->exfield2)) return FALSE; return TRUE; } В данном примере единственное объявление типа, которое присутствует в файле спецификации, имеет имя example. Это объявление определяет структуру, имеющую одно целочисленное и одно символьное поле. Файл rdictjcdr.c содержит код, необходимый для прямого и обратного преобразования структуры типа example из внутреннего представления данных во внешнее. В коде, который был сформирован автоматически программой rpcgen, содержатся вызовы процедур библиотеки XDR для каждого поля структуры. Непосредственно после объявления объявленный тип может применяться для обозначения параметров удаленных процедур. Если в одной из удаленных процедур структура example используется в качестве параметра, то программа rpcgen вырабатывает код и в клиентской, и в серверной части для вызова процедуры xdr_example, выполняющей необходимое преобразование. 23.11. Код клиентской программы, сформированный программой rpcgen Для данного примера приложения базы данных словаря программа rpcgen вырабатывает файл rdict_clnt.c — исходный код программы, которая будет выполнять функции клиентской заглушки связи в распределенной версии приложения. Листинг 23.3. Файл rdict_clnt.c -пример клиентской заглушки, сформированной программой rpcgen /* * Не редактируйте этот файл. * Он создан с помощью программы rpcgen. */ ¦include <memory.h> /* Определение функции memset */ ¦include "rdict.h" /* Применяемый по умолчанию таймаут можно изменить с помощью */ /* функции clnt_control() */ static struct timeval TIMEOUT * { 25, 0 }; int * initw l(void *argp, CLIENT *clnt) { static int clntjres; memset((char *)&clnt res, 0, sizeof(clnt res)); if (clntj:all (clnt,"lNITW, (xdrproc_t) xdrjrcid, (caddr_t) argp, (xdrproc~t) xdr~int, (caddr_t) ficlntjres, 23.11. Код клиентской программы, сформированный программой rpcgen 333
TIMEOUT) != RPC_SUCCESS) { return (NULL)? } return (fcclntjres); int * insertw l(char **argp, CLIENT *clnt) { " '" :" ¦'•i--':-^ • ¦¦¦" r • static int clntjres; memset((char *)&clnt res, 0, sizeof(clnt res)); if (clnt_cali (clnt,~INSERTW, (xdrprocj:) xdrjrrapstring, (caddr_t) argp, (xdrprocj:) xdr_int, (caddr t) ficlnt res, TIMEOUT) != RPC_SUCCESS) { ~ return (NULL); } return (&clnt_res); int * deletew_l(char **argp, CLIENT *clnt) { static int clntjres; memset((char *)&clntj:es, 0, sizeof(clnt res)); if (clnt_call (clnt, DELETEW, (xdrproc_t) xdr^wrapstring, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr t) fcclntjres, TIMEOUT) != RPC_SUCCESS) { ~ return (NULL); } return (&clntj:es); int * lookupw_l(char **argp, CLIENT *clnt) { static int clntjres? memset((char *)&clnt_res, 0, sizeof(clntjres)); if (clnt_call (clnt, LOOKUPW, (xdrprocj:) xdrj/rapstring, (caddr_t) argp, (xdrprocj:) xdrjnt, (caddrJ:) &clntj:es, TIMEOUT) != RPCJ5UCCESS) { return (NULL); } return (fcclntjres); 334 Глава 23. Построение распределенных програ
Этот файл содержит процедуру-заглушку связи для каждой из процедур в удаленной программе. Как и в серверной программе, имена здесь были выбраны с учетом необходимости предотвращения конфликтов. 23.12. Код серверной программы, сформированный программой rpcgen В этом примере базы данных словаря программа rpcgen вырабатывает четвертый файл rdict_svc.c, который содержит код, необходимый для сервера. Этот файл содержит главную процедуру, вызываемую на выполнение после запуска серверной программы. Эта процедура получает порт протокола, регистрирует программу RPC в службе привязки портов, а затем переходит в состояние ожидания для получения вызовов RPC. Каждый поступивший вызов перенаправляется в соответствующую интерфейсную процедуру серверной заглушки. После получения ответа от вызванной процедуры сервер создает ответ RPC и отправляет его клиенту. Листинг 23.4. Файл rdict_svc.c - пример серверной заглушки, сформированной программой rpcgen /* * Не редактируйте этот файл. * Он создан с помощью программы rpcgen. */ ¦include "rdict.h" ¦include <stdio.h> ¦include <stdlib.h> ¦include <rpc/pmap_clnt.h> ¦include <string.h> ¦include <memory.h> ¦include <sys/socket.h> ¦include <netinet/in.h> ¦ifndef SIGJPF ¦define SIG_PF void(*)(int) ¦endif static void rdictprog_l(struct svc req *rqstp, register SVCXPRT *transp) { union { char *insertw_l_arg; char *deletew_l_arg; char *lookupw_l_arg; } argument; char *result; xdrproc_t jcdr_argument, jcdrjresult; char *(*local)(char *, struct svcjreq *)? switch (rqstp->rqj?roc) { case NULLPROC: (void) svc_sendreply (transp, (xdrprocj:) xdrjroid, (char *)NULL)j 23.12. Код серверной программы, сформированный программой rpcgen 335
return; case INITW: _xdr_argument * (xdrproc_t) xdrjroid; jcdrjresult a (xdrprocjj xdr_int; local = (char *(*)(char *, struct svcjreq *)) initw_l_svc; break; case INSERTW: jcdr_argument * (xdrprocj:) xdr_wrapstring; jcdrjresult * (xdrprocjj xdrj.nt; local ¦ (char *(*)(char *, struct svcjreq *)) insertwj_svc; break; case DELETEW: jcdr_argument s (xdrproc_t) xdr_wrapstring; jcdrjresult в (xdrproc^tj xdr_int; local ¦ (char *(*)(char *, struct svcjreq *)) deletewjjsvc; break; case LOOKUPW: jcdrjirgument « (xdrprocj:) xdrj/rapstring; jcdrjresult = (xdrproc^tj xdrj.nt? local * (char *(*)(char *, struct svcjreq *)) lookupwjjsvc; break? default: svcerrjioproc (transp); returnj } memset ((char *)(argument, 0, sizeof (argument)); if (Isvc_getargs (transp, jcdrjurgument, (caddr_t) (argument)) { svcerrjiecode (transp);"" return! > result * (*local)((char *)(argument, rqstp); if (result !* NULL (( Isvcj3endreply(transp, jcdrjresult, result)) { svcerr systemerr (transp); } if (Isvc_freeargs (transp, jcdrjurgument, (caddr J:) (argument)) { fprintf (stderr, "unable~to free arguments");"" exit A)? } return? int main (int argc, char **argv) { register SVCXPRT *transp; pmapjmset (RDICTPROG, RDICTVERS); 336 Глава 23. Построение распределенные
transp * svcudp create(RPC ANYSOCK); if (transp « NULL) { fprintf (stderr, "cannot create udp service.")? exit(l); } if (Isvcj:egister(transp, RDICTPROG, RDICTVERS, rdictprog_l, IPPROTO JJDP)) { fprintf (stderr, "unable to register (RDICTPROG, RDICTVERS, udp)."")? exit(l); } transp ¦ svctcp create(RPC ANYSOCK, 0, 0); if (transp == NULL) { fprintf (stderr, "cannot create tcp service."); exit(l); } if (!svc register(transp, RDICTPROG, RDICTVERS, rdictprog 1, IPPROTO TCP)) { fprintf (stderr, "unable to register (RDICTPROG, RDICTVERS, tcp)."")? exit(l); } svcjrun ()? fprintf (stderr, "svc run returned"); exit A); /* Недостижимый участок кода */ После выработки всех файлов их можно оттранслировать и сохранить в виде файлов объектного кода. В системе Linux для трансляции всех трех файлов, содержащих выработанный код, могут применяться следующие команды: ее -с rdict_clnt.c ее -с rdictjsvc.c се -с rdictjcdr.c Каждая команда принимает в качестве параметра файл исходного кода на языке С и вырабатывает соответствующий файл объектного кода. В именах файлов объектного кода расширение .с заменяется расширением5 .о. Например, оттранслированная версия файла rdict_clnt.c будет помещена в файл rdict_clnt.o. 23.13. Этап 5: подготовка интерфейсных процедур-заглушек 23.13.1. Клиентские интерфейсные процедуры Файлы, вырабатываемые программой rpegen, не составляют законченных программ. Для создания таких программ необходимо подготовить клиентские и серверные интерфейсные процедуры. Для каждой удаленной процедуры в удаленной программе должна быть предусмотрена отдельная интерфейсная процедура. Клиентская часть первоначальной прикладной программы управляет обработкой. В ней вызываются интерфейсные процедуры с использованием тех же 5 Программисты иногда называют файлы объектного кода файлами точка, о. 23.13. Этап 5: подготовка интерфейсных процедур-заглушек 337
имен процедур и типов параметров, которые первоначально служили для вызова этих процедур, ставших удаленными в распределенной версии. Каждая интерфейсная процедура должна преобразовывать свои параметры в форму, применяемую программой rpcgen, а затем вызывать соответствующую клиентскую процедуру связи. Например, поскольку первоначальная программа содержит процедуру insertw, которая принимает в качестве параметра указатель на символьную строку, то такую же процедуру должен включать клиентский интерфейс. Интерфейсная процедура вызывает insertw_l — клиентскую заглушку связи, сформированную программой rpcgen. Основное различие между параметрами локальной процедуры и параметрами, применяемыми в заглушках связи, состоит в том, что в параметрах всех процедур, выработанных программой rpcgen, используется косвенная адресация. Например, если в первоначальной процедуре имеется целочисленный параметр, то соответствующий параметр в заглушке связи для этой процедуры должен представлять собой указатель на целое число. В программе базы данных словаря для большинства процедур требуется параметр типа символьной строки, который в языке С объявляется как указатель на символ (char *). В соответствующих заглушках связи все подобные параметры должны представлять собой указатель, который ссылается на указатель на символ (char **). Файл rdict_cif .с может служить иллюстрацией того, как интерфейсные процедуры преобразовывают параметры в форму, приемлемую для кода, выработанного программой rpcgen. Этот файл содержит по одной клиентской интерфейсной процедуре для каждой из удаленных процедур программы. /* Файл rdict_cif.c - процедуры initw, insertw, deletew, lpokupw */ ¦include <rpc/rpc.h> ¦include <stdio.h> ¦define RPC_CLNT ¦include "rdict.h" /* Клиентские процедуры-заглушки, написанные программистом */ extern CLIENT *handle; /* Дескриптор для удаленной процедуры */ static int *ret; /* Область памяти для временного */ /* хранения кода возврата */ /* * Процедура initw - клиентская интерфейсная процедура, вызывающая * процедуру initw_l * . * — : Z ' . - */ int initw'O ret = initwJ.(Q, handle)? return ret==0¦¦? 0 : *ret; } ¦ /*_™_™_™- ..™™_™™__._™_- .-...-. * Процедура insertw - клиентская интерфейсная процедура, вызывающая * процедуру insertw_l *__ т, «.-——» — --- — -* — - ZL: - - - 338 Глава 23. Построение распределенных программ...
*/ • int insertw(char *word) { char **arg; /* Указатель на параметр */ arg = Sword; ret = insertw_l(arg, handle); return ret=ss(T? 0 : *ret; > '"' ¦" '¦ ¦ ¦',.. <'-".-'' /* . —— - - — ~ * Процедура deletew - клиентская интерфейсная процедура, вызывающая * процедуру deletew_l *. - . ..-. . - _-_.---.- -_ k - */ int deletew(char *word) { char **arg; /* Указатель на параметр */ . . arg * Sword; ret = deletew_l(arg, handle); return ret==0 ? 0 : *ret; } /* —~ —-—— ......... * Процедура lookupw - клиентская интерфейсная процедура, вызывающая * процедуру lookupw_l * . . */ int lookupw(char *word) { char **arg; /* Указатель на параметр */ arg = &word; ret = lookupw_l(arg, handle)? return ret==0 ? 0 : *ret; } 23.13.2. Серверные интерфейсные процедуры Интерфейсные процедуры серверной части принимают вызовы от заглушек связи, созданных программой rpcgen, и передают управление процедуре, которая выполняет указанный вызов. Как и в клиентской части, серверные интерфейсные процедуры должны преобразовать типы параметров, выбранные программой rpcgen, в типы параметров, которые применяются в вызываемых процедурах. В большинстве случаев различие между типами параметров заключается только в применении дополнительного уровня косвенной адресации: программа rpcgen предусматривает передачу указателя на объект вместо самого объекта. Для преобразования типа параметра в интерфейсной процедуре достаточно применить только оператор косвенной адресации языка С (*). Этот принцип проиллюстри- 23.13. Этац 5: подготовка интерфейсных процедур-заглушек 339
рован в файле rdict_sif .с, который содержит серверные интерфейсные процедуры для программы базы данных словаря. /* Файл rdict_sif.c - процедуры init_l, insert_l, delete_l, lookup_l_svc */ finclude <rpc/rpc.h> ¦define RPCJVC #include "rdict.h" /* Серверные процедуры-заглушки, написанные программистом */ static int retcode; int initw(void), insertw(char *), deletew(char *), lookupw(char *); /* * Процедура insertw_l_svc - серверный интерфейс к удаленной процедуре insertw * . Г « ... ———— .... . — . */ int * insertw lj3vc(char **w, struct svc req *rqstp) { retcode = insertw(*(char **)w); return firetcode? } /* * Процедура initw_l svc - серверный интерфейс к удаленной процедуре initw * ..... . . Г ......-.——.————.—. .. ...— . — ... */ int * initw 1 svc(void *w, struct svc req *rqstp) { retcode s initw(); return Sretcode; } /* -~ * Процедура deletew l_svc - серверный интерфейс к удаленной процедуре deletew *—.————......Г.Г—.... ....................................———— */ int * deletew 1 svc(char **w, struct svc req *rqstp) { retcode ¦ deletew(*(char **)w); return firetcode; } /* * Процедура lookupw_l_svc - серверный интерфейс к удаленной процедуре lookupw *—... .—.———.—Г-Г..—————-——.———.-————.—-—.— ...... */ int * lookupw 1 svc(char **w, struct svc req *rqstp) { retcode * lookupw(*(char**)w); 340 Глава 23. Построение распределенных программ...
return fcretcode; } 23.14. Этап 6: трансляция и компоновка клиентской программы После подготовки и ввода в файл исходного кода клиентских интерфейсных процедур можно выполнить их трансляцию. Например, файл rdict_cif .с содержит все интерфейсные процедуры для данного примера работы со словарем. В системе Linux для трансляции файла, полученного в результате выполнения описанной выше работы, применяется следующая команда: ее -с rdict_cif.c Транслятор вырабатывает выходной файл rdict_cif .о. Для завершения создания клиентской программы необходимо ввести несколько небольших изменений в первоначальную главную процедуру. Поскольку в новой версии используется технология RPC, для нее требуется включаемый файл на языке С для объявлений RPC. В этой процедуре должен быть также вызван включаемый файл rdict.h, поскольку он содержит объявления констант, применяемых и в клиенте, и в сервере. В клиентской программе необходимо также объявить и инициализировать дескриптор, который может использоваться в процедурах связи RPC для обмена данными с сервером. В большинстве клиентов такой дескриптор объявляется с применением заранее определенного типа CLIENT и инициализируется путем вызова библиотечной процедуры RPC, а именно clnt_create. Пример необходимого для этого кода приведен в файле rdict.c: /* Файл rdict.c - главная процедура, процедура nextin */ ¦include <rpc/rpc.h> ¦include <stdlib.h> ¦include <stdio.h> ¦include <ctype.h> ¦include "rdict.h" ¦define MAXWORD 50 /* Максимальная длина команды или слова */ ¦define RMACHINE "localhost" /* Имя удаленного компьютера */ CLIENT *handle; /* Дескриптор для удаленной процедуры */ int nextin(char *cmd, char *word), initw(), insertw(char *word); int deletew(char *word), lookupw(char *word); /* * Главная процедура - вставка, удаление или поиск указанных слов в словаре *..•.-_--_-.-•-..--...._.•-.-__._.•_..._._.._... — - --«_-«.—— */ int raain(int argc, char *argv[]) { char word[MAXWORD+l]; /* Область памяти для размещения введенного слова */ 23.14. Этап 6: трансляция и компоновка клиентской программы 341
char cmd; int wrdlen; /* Длина введенного слова */ /* Установление соединения для вызова удаленной процедуры */ handle = clnt_create(RMACHINE, RDICTPROG, RDICTVERS, "tcp"); if (handle == 0) { printf("Could not contact remote program.\n"); exit(l); } while A) { wrdlen = nextin(&cmd, word); if (wrdlen <0) exit@); word[wrdlen] = '\0'; switch (cmd) { case 'I': /* Команда инициализации словаря */ initw(); printf("Dictionary initialized to empty.\n"); break; case 'i': /* Команда вставки */ insertw(word); printf("%s inserted.\nH,word); break; case 'd': /* Команда удаления */ if (deletew(word)) printf("%s deleted.\n",word); else printf("%s not found.\n",word); break; case '1': /* Команда поиска */ if (lookupw(word)) printf("%s was found.\nH/word); else printf("%s was not found.\nw,word); break; case 'q': /* Команда выхода */ printf("program quits.\nH); exit@); default: /* Недопустимый формат ввода. */ printf("command %c invalid.\n", cmd); break; } } } " ' * Процедура nextin - чтение команды и (возможно) слова из очередной * введенной строки */ int 342 Глава 23. Построение распределенных г
nextin(char *cmd, char *word) { int i, ch? ch = getc(stdin); while (isspace(ch)) ch = getc(stdin); if (ch == EOF) return -1; *cmd = (char) ch; ch = getc(stdin); while (isspace(ch)) ch = getc(stdin); if (ch == EOF) return -1; if (ch == '\n') return 0; i = 0; while (!isspace(ch)) { if (++i> MAXWORD) { printf("error: word too long.\n"); exit(l); } *word++ = ch; ch = getc(stdin); } -¦ " ' ~" return i; } Сравните файл rdict.c с файлом dictl.c6, на основе которого он был создан, чтобы убедиться в том, что при этом потребовалась совсем небольшая доработка. В данном примере кода используется символическая константа RMACHINE для обозначения доменного имени удаленного компьютера. В целях упрощения отладки константа RMACHINE объявлена со значением localhost, а это означает, что клиент и сервер будут работать на одном и том же компьютере. Безусловно, после завершения отладки распределенной программы программист может изменить это определение, указав постоянное местонахождение сервера. Библиотечная процедура clnt_create предпринимает попытки установить соединение с указанным удаленным компьютером. Бели эта попытка соединения заканчивается неудачей, процедура clnt_create возвращает значение NULL, чтобы приложение могло передать пользователю сообщение об ошибке. В рассматриваемом примере кода при получении сообщения об ошибке от процедуры clnt_create программа завершает свою работу. На практике в клиентской программе можно выбрать вариант повторного выполнения соединения или попытаться установить соединение с одним из компьютеров, список которых ведется в этой программе. Файл rdict.c, как и другие файлы исходного кода С, может быть оттранслирован с использованием одной команды: ее -с rdict.c Файл dictl. с приведен на стр. 325. 23.14. Этап 6: трансляция и компоновка клиентской программы 343
После трансляции файла rdict.c и получения соответствующего файла объектного кода все файлы, входящие в состав клиентской программы, могут быть скомпонованы редактором связей для получения выполняемой программы. В системе Linux редактор связей для обработки файлов с расширением .о вызывается с помощью команды ее, которая может быть введена в командной строке с опцией -о для вывода полученных результатов в указанный файл. Например, приведенная ниже командная строка позволяет связать друг с другом все необходимые файлы .о и поместить полученную в результате выполняемую клиентскую программу в файл rdict: се -о rdict rdict.о rdict_clnt.o rdictjcdr.o rdict_cif.o 23.15. Этап 7: трансляция и компоновка серверной программы Выходные файлы, выработанные программой rpegen, включают основную часть кода, необходимую для сервера, но программист должен подготовить два дополнительных файла: интерфейсные процедуры сервера (которые решено поместить в файл rdict_sif.с) и сами удаленные процедуры. В рассматриваемом примере программы работы со словарем окончательная версия удаленных процедур включена в файл rdict_srp.c. В качестве кода этих процедур взят код, который содержался в первоначальном приложении: /* Файл rdictjsrp.c - процедуры initw, insertw, deletew, lookupw */ ¦include <rpc/rpc.h> ¦include <string.h> ¦include "rdict.h" /* Серверные удаленные процедуры и используемые в них глобальные данные */ char dict[DICTSIZ][MAXWORD+l]; /* Распределение памяти для слов словаря */ int nwords =0; /* Число слов в словаре */ /* * Процедура initw - инициализация словаря с исключением из него всех слов * . ... ... .. .. ...... —— —... */ int initw() { nwords = 0; return 1; } /* . * Процедура insertw - вставка слова в словарь *............._.........—_............ .—..-. ——*..—..—...— */ int insertw(char *word) { strcpy(diet[nwords], word); 344 Глава 23. Построение распределенных программ...
nwords++; return nwords; } /* - * Процедура deletew - удаление слова из словаря * - . . . */ int deletew(char *word) { int i; for (i=0 ; i<nwords ; i++) if (strcmp(word, dict[i]) == 0) { nwords—; strcpy(dict[i], dict[nwords]); return 1; } return 0? } /* * Процедура lookupw - поиск слова в словаре * . */ int lookupw(char *word) { int i; for (i=0 ; i<nwords j i++) if (strcmp(word, dict[i]) ==0) return 1; return 0; } Вначале выполняется трансляция файла, содержащего удаленные процедуры, с использованием команды: ее -с rdict_srp.c Затем для создания выполняемого файла осуществляется компоновка файлов объектного кода, входящих в состав сервера, с помощью следующей команды: ее -о rdictd rdict_svc.o rdictjcdr.o rdictjsif.o rdict_srp.o После выполнения этой команды создается файл rdictd7 с выполняемым кодом. В системе Linux, как и в большинстве версий UNIX, принято присваивать серверам имена с суффиксом "d" (сокращение от daemon — демон); термин демон применяется ко всем программам, работающим в фоновом режиме. 23.15. Этап 7: трансляция и компоновка серверной программы 345
23.16. Этап 8: Запуск серверной программы и вызов клиентской программы на выполнение Первое серьезное испытание всей системы происходит после запуска на выполнение и клиентского, и серверного компонентов на одном компьютере. Функционирование сервера должно начаться еще до того, как к нему попытается подключиться клиент, поскольку в противном случае клиент выведет сообщение о том, что не может связаться с удаленной программой, и остановится: Could not contact remote program. В системе Linux предусмотрена возможность запустить сервер на выполнение и освободить терминал пользователя для ввода других команд, указав в конце командной строки символ амперсанда: ./rdictd & Кроме того, пользователь может вызвать клиентскую программу для работы в интерактивном режиме, введя имя этой программы, а затем набрав входные данные. Иным образом, входные данные при вызове клиентской программы могут быть перенаправлены из файла. 23.17. Применение утилиты make Следует учесть, что сопровождение всех файлов, связанных с распределенной программой, может стать довольно трудоемким. Вспомогательная программа make позволяет упростить эту работу, поскольку обеспечивает автоматическое выполнение задачи определения того* какие файлы должны быть перетранслированы после внесения изменений. Утилита make считывает файл спецификации, в котором описаны зависимости между файлами программы, и выдает необходимые команды для автоматической перестройки сложных программ. Хотя изложение подробных сведений об утилите make выходит за рамки этой книги, ниже приведен наглядный пример, позволяющий убедиться в том, что даже простой файл спецификации позволяет обеспечить передачу утилите make информации обо всех файлах и командах, необходимых для построения первоначальной или распределенной версий программы работы со словарем. Такие спецификации содержатся в файле Makefile. > # Файл Makefile для примера приложения базы данных словаря t (нераспределенного и распределенного) # CFLAGS = -g ${INCLUDE} SRC = diet.с dictl.c dict2.c rdict.c rdict.x rdict_cif.c \ rdict sif.c rdict srp.c * f Определение всех файлов программы # PROGS = diet rdict rdictd t 346 Глава 23. Построение распределенных программ...
jf Определение всех файлов, создаваемых программой rpcgen GFILES = rdict.h rdict_clnt.c rdict_svc.c rdict_xdr.c # jf Определение клиентских и серверных файлов объектного кода ¦ RDICT_OBJ = rdict_clnt.o rdict_cif.o rdict.о RDICTD_OBJ = rdict_svc.o rdict_sif.o rdict_srp.o all: ${PROGS} dictl.o dict2.o install: all §echo nothing to install. clean: rm *.o ${PROGS} ${GFILES} * f Первоначальное (нераспределенное) приложение * diet: diet.с ${CC} ${CFLAGS} -o diet diet.с v jf Проверить, успешно ли проходит трансляция частей, на которые jf разделена программа dictl.o: dictl.c ${СС} ${CFLAGS} -с dictl.c dict2.o dict2.c ${CC} ${CFLAGS} -c dict2.c jf jf Зависимости между файлами, созданными программой rpcgen * ${GFILES}: rdict.h rdict.с rdict.h: rdict.x rpcgen rdict.x I jf Трансляция и компоновка клиентских модулей для распределенной версии # ${RDICT OBJ}: $JCC} ${CFLAGS} -с $*.с rdict: ${RDICT OBJ} rdict xdr.o ${CC} ${CFLAGS} -o $8 ${RDICT_OBJ} rdictjcdr.o chmod 755 rdict * 23.17. Применение утилиты make 347
I Трансляция и компоновка серверных модулей для распределенной версии * ${RDICTD_OBJ}: ${СС} ${CFLAGS} -с $*.с rdictd: ${RDICTD_OBJ} rdict xdr.o ${CC} ${CFLAGS} -o $§ ${RDICTD_OBJ} rdictjcdr.o f f Трансляция определений XDR f rdictjcdr.o: ${CC} ${CFLAGS} -c $*.c После подготовки файла Makefile перетрансляция клиентской и серверной программ выполняется полностью автоматически. Например, после запуска утилиты make на выполнение в каталоге, содержащем файл спецификации rpcgen, файл Makefile и все файлы исходного кода, описанные в настоящей главе, утилита make выполняет приведенную ниже последовательность команд: ее то diet diet.с се -с dictl.c се -с dict2.c rpcgen rdict.x ее -с rdict__clnt.c се -с rdict_cif.c ее -с rdict.с ее -с rdictjcdr.c ее -о rdict rdict_clnt.o rdict_cif.0 rdict.о rdict_xdr.o chmod 755 rdict cc -c rdict_svc.c cc -c rdict_sif.c cc -c rdict_srp.c cc -o rdictd rdict_svc.o rdict_sif.o rdict_srp.o rdictjcdr.o chmod 755 rdictd Таким образом, в результате вызова утилиты make на выполнение создается выполняемая клиентская программа rdict и выполняемая серверная программа rdictd. 23.18. Резюме Построение распределенной программы с применением инструментального средства rpcgen происходит в восемь этапов. Вначале создается нераспределенная прикладная программа для решения поставленной задачи, а затем принимается решение о том, как разделить эту программу на компоненты, выполняемые локально и удаленно; программа делится на две физические части, создается файл спецификации с описанием удаленной программы и вызывается на выполнение программа rpcgen для получения необходимых файлов. Затем программист подготавливает клиентские и серверные интерфейсные процедуры и объединяет их с кодом, выработанным программой rpcgen. И наконец, выполняется трансляция и 348 Глава 23. Построение распределенных программ...
компоновка клиентских файлов и серверных файлов для получения выполняемых программ — клиента и сервера. Хотя применение программы rpcgen позволяет исключить значительный объем работы по программированию, необходимый при использовании технологии RPC, все вопросы построения распределенной программы должны быть тщательно продуманы. Принимая решение о том, как разделить программу на локальный и удаленный компоненты, необходимо изучить, к каким данным обращается каждая часть программы, чтобы свести к минимуму перемещение данных. Следует также учесть, какую задержку будет вносить каждый дистанционный вызов процедур, и проанализировать, к каким средствам ввода/вывода должен иметь доступ каждый компонент программы. Пример приложения для работы со словарем, представленный в этой главе, показывает, какие значительные усилия требуются для преобразования в распределенную программу даже простейшего приложения. Для более сложных приложений нужны гораздо более сложные спецификации и интерфейсные процедуры. В частности, в значительной степени увеличивается объем кода при разработке таких приложений, которые передают структурированные данные в удаленные процедуры или проверяют права доступа клиента. Материал для дальнейшего изучения С дополнительной информацией об инструментальном средстве rpcgen можно ознакомиться в документации, которая поставляется вместе с этим программным обеспечением. Подробные сведения об утилите make представлены в оперативной документации системы Linux. Упражнения 23.1. Доработайте пример программы, представленной в данной главе, таким образом, чтобы процедура клиентского интерфейса записывала в кэш слова, поиск которых проводился в последнее время, и выполняла поиск в кэше перед выполнением дистанционного вызова процедур. Какие дополнительные вычислительные издержки возникают в связи с ведением кэша? Сколько времени позволяет сэкономить кэш, если в нем будет обнаружена необходимая запись? 23.2. Разработайте распределенное приложение, которое предоставляет доступ к файлам на удаленном компьютере. Включите в него удаленные процедуры, которые позволяют клиенту выполнять чтение или запись данных в указанной позиции указанного файла. 23.3. Разработайте распределенную программу, которая передает удаленной процедуре связанный список в качестве параметра. Подсказка: используйте относительные указатели вместо абсолютных адресов памяти. 23.4. Попытайтесь доработать представленную в качестве примера программу для работы со словарем таким образом, чтобы удаленные процедуры могли записывать сообщения об ошибках в стандартный файл сообщений об ошибках на клиентском компьютере. С какими проблемами вы столкнулись? Как вы их решили? 23.5. Инструментальное средство rpcgen могло быть спроектировано таким образом, чтобы оно автоматически назначало каждой удаленной процедуре уникальный номер: 1, 2 и т.д. Каковы преимущества и недостатки предоставления программисту возможности назначать номера Материал для дальнейшего изучения 349
удаленных процедур вручную в файле спецификации вместо использования автоматического назначения? 23.6. Какие ограничения имеет программа rpcgen? Подсказка: рассмотрите возможность построить сервер, который одновременно является клиентом другой службы. 23.7. Тщательно изучите код в файле rdict_clnt.c. Какие действия выполняются этой клиентской программой с начала выполнения? 23.8. Попытайтесь построить и отладить одновременно две версии распределенной программы для работы со словарем. Возможна ли отладка новых версий, если в серверной части используется общепринятый номер порта? Объясните ваш ответ. 23.9. В этом примере программы работы со словарем пробелы распознаются на входе как отдельные символы. Доработайте процедуру nextin таким образом, чтобы она позволяла вводить не только пробелы, но и символы табуляции. 23.10. Существует ли возможность преобразовать серверную часть программы работы со словарем в параллельную версию? Объясните ваш ответ. 350 Глава 23. Построение распределенных программ...
24 Принципы работы сетевой файловой системы (NFS) 24.1. Введение В предыдущих главах описана технология ONC RPC, показано, какая связь существует между дистанционным вызовом процедур и взаимодействием типа клиент/сервер, а также приведен пример использования технологии RPC для создания распределенной версии прикладной программы. Настоящая и следующая главы посвящены описанию конкретного приложения и протокола, которые определены, спроектированы и реализованы на основе средств RPC. В этой главе описаны общие принципы дистанционного доступа к файлам и рассмотрены концепции, лежащие в основе конкретного механизма дистанционного доступа к ним. Поскольку многие идеи или детали реализации этого механизма были позаимствованы из базовой операционной системы, в настоящей главе рассматривается файловая система Linux и организация выполнения операций с файлами. В ней описана иерархическая структура каталогов, составных имен файлов и каталогов, а также показано, как механизм дистанционного доступа к файлам позволяет осуществлять операции в этих иерархических структурах. В следующей главе приведены дополнительные сведения о протоколе дистанционного доступа к файлам и показано, как в спецификации протокола используются средства RPC для определения дистанционных операций с файлами. 24.2. Сравнение средств дистанционного доступа к файлам и передачи файлов Службы передачи файлов, предоставляющие пользователям перемещать копии файлов с одного компьютера на другой, появились на самых ранних этапах развития сетевых систем. В дальнейшем появились сетевые системы, предоставляющие службы доступа к файлам, которые позволяют обращаться к файлам на удаленном компьютере из любой прикладной программы. Механизм дистанционного доступа к файлам предусматривает хранение одной копии каждого файла и предоставляет возможность обращаться по требованию к этой копии из одной или нескольких прикладных программ. Приложения, в которых используется механизм дистанционного доступа к файлам, могут выполняться на том же компьютере, где находится файл, или на удаленном компьютере. Если приложение обращается к файлу, находящемуся на удаленном компьютере, то операционная система, в которой работает эта программа, вызывает клиентское программное обеспечение, вступающее во взаимо-
действие с сервером на удаленном компьютере и выполняющее затребованные операции с файлом. В отличие от службы передачи файлов, операционная система приложения не выполняет выборку всего файла и не сохраняет его на локальном компьютере. Вместо этого она при выполнении каждой операции запрашивает передачу одного небольшого блока данных. Для предоставления дистанционного доступа к некоторым или ко всем файлам, находящимся на компьютере, системный администратор должен предусмотреть эксплуатацию на этом компьютере сервера, отвечающего на запросы дистанционного доступа. Сервер проверяет каждый запрос для определения того, имеет ли клиент право доступа к указанному файлу, выполняет заданную операцию и возвращает результат клиенту. Компания Sun Microsystems определила механизм дистанционного доступа к файлам, который получил широкое признание во всей компьютерной индустрии. Этот механизм, получивший название NFS (Network File System — Сетевая файловая система), позволяет эксплуатировать на компьютере сервер, предоставляющий дистанционный доступ к некоторым или всем файлам этого компьютера, что дает возможность обращаться к этим файлам из приложений, работающих на других компьютерах1. 24.3. Операции с файлами на удаленном компьютере Система NFS позволяет выполнять с файлами на удаленном компьютере точно такие же операции, как и на локальном. Приложение может открыть файл на удаленном компьютере для получения доступа, прочитать данные из файла, записать данные в файл, перевести указатель в конкретную позицию файла (в начало, конец или в любое другое место)» а также закрыть файл по окончании его использования. 24.4. Доступ к файлам в разнородной сетевой среде Задача предоставления дистанционного доступа к файлам может оказаться сложной. Служба доступа к файлам должна предоставлять не только основные механизмы чтения и записи файлов, но и способы создания и уничтожения файлов, работы с каталогами, проверки прав на выполнение запросов, соблюдения правил защиты файлов, а также прямого и обратного преобразования информации с учетом того, что на разных компьютерах применяются различные представления данных. Поскольку служба дистанционного доступа к файлам соединяет два компьютера, в ней должны учитываться различия в том, как в клиентских и серверных системах именуются файлы, обозначаются пути перехода по каталогам и хранится информация о файлах. Еще более важно то, что в программном обеспечении дистанционного доступа к файлам должны учитываться различия в семантической интерпретации операций с файлами. Система NFS с самого начала проектировалась с учетом возможности применения в разнотипных компьютерных системах. Протокол, операции этой системы и способы их выполнения были выбраны в целях обеспечения взаимодействия самых различных систем. Безусловно, система NFS не позволяет учесть все тонкости работы файловых систем всевозможных компьютерных платформ. Вместо этого в спецификации NFS была предпринята попытка определить фай- Основные понятия и многие конкретные сведения, представленные в настоящей книге, относятся ко всем версиям протокола NFS. Хотя в системе. Linux ко времени публикации этой книги все еще использовалась версия 2, авторы решили подробно описать версию 3, поскольку вскоре ожидается переход системы Linux на эту версию. 352 7Глава 24. Принципы работы сетевой файловой системы (NFS)
ловые операции, подходящие для максимально возможного количества систем, без существенного снижения эффективности или чрезмерного усложнения. Как показала практика, основная часть решений, принятых при разработке этой спецификации, вполне себя оправдывает. 24.5. Серверы, не поддерживающие состояние Спецификация NFS предусматривает хранение информации о состоянии в клиентской части, что позволяет применять серверы, не поддерживающие состояние. Поскольку сервер не поддерживает состояние, то нарушения в процессе функционирования службы NFS не влияют на функционирование клиента. Например, теоретически клиентская программа сможет продолжить выполнение операции доступа к файлу после аварийного завершения работы сервера, не поддерживающего состояние, и перезагрузки серверного компьютера; прикладная программа, работающая в клиентской системе, может даже не получить никакой информации о перезагрузке сервера. Кроме того, поскольку сервер, не поддерживающий состояние, не должен распределять какие-либо ресурсы для каждого клиента, проект, не предусматривающий поддержки состояния, может масштабироваться в целях обеспечения обслуживания намного большего числа клиентов по сравнению с проектом, предусматривающим поддержку состояния. Проект сервера NFS, не поддерживающего состояние, оказал влияние и на протокол, и на его реализацию. Еще более важно то, что в таком сервере нельзя хранить какую-либо информацию о том, в какой позиции файла или каталога должен осуществляться доступ. После изучения операций, предусмотренных спецификацией NFS, станет очевидно, как реализован в системе NFS проект, не предусматривающий поддержки состояния. 24.6. Система NFS и организация работы с файлами в операционной системе UNIX Хотя система NFS была разработана в целях ее применения в разнотипных файловых системах, на ее общий проект, применяемую терминологию и многие тонкости реализации протокола значительное влияние оказала организация оригинальной файловой системы2 UNIX. Проектировщики NFS, определяя смысл отдельных операций дистанционного доступа к файлам, приняли за основу организацию этой файловой системы. Поэтому, чтобы понять, как работает NFS, необходимо вначале изучить файловую систему UNIX. В главе 3 приведено краткое описание средств ввода/вывода, а в следующем разделе это описание дополнено. В этом разделе показано, как организовано хранение файлов в системе UNIX и как к ним предоставляется доступ, и это описание сосредоточено на тех понятиях и сведениях, которые в большей степени касаются NFS. В остальных разделах данной главы показано, что разработчики NFS позаимствовали многие идеи непосредственно из проекта файловой системы UNIX и адаптировали основные особенности ее конструкции с незначительными изменениями. Для понимания работы системы NFS необходимо знать устройствд оригинальной файловой системы UNIX, поскольку в системе NFS используется такая же терминология и организация файловой системы. Хотя в данной главе упоминается оригинальная система UNIX, все эти понятия и термины относятся также и к системе Linux. 24.5. Серверы, не поддерживающие состояние 353
24.7. Обзор файловой системы UNIX В настоящем разделе рассматриваются понятия, термины и конкретные сведения о файловой системе UNIX, относящиеся и к системе NFS, а в следующих разделах описано, как эти понятия реализованы в NFS. 24.7.1. Основные определения С точки зрения пользователя, в системе UNIX файл определен как объект, состоящий из последовательности байтов. С теоретической точки зрения, файл UNIX может достигать произвольных размеров, но на практике размер файла ограничен объемом пространства, доступного на физическом запоминающем устройстве. Файлы UNIX могут расти динамически. Файловая система не требует предварительного объявления ожидаемого размера или распределения пространства. Размеры файла увеличиваются автоматически в соответствии с объемом данных, записанных в него приложением. Формально в системе UNIX байты в файле нумеруются, начиная с нуля. В любой момент времени размер файла определяется как число находящихся в нем байтов. Файловая система UNIX обеспечивает произвольный доступ к любому файлу с использованием номеров байтов для указания адреса доступа. Она позволяет приложению перейти в файле к позиции любого байта и обратиться к данным в этой позиции. 24.7.2. Последовательность байтов без границ между записями Каждые файл в системе UNIX представляет собой последовательность байтов; эта система не определяет для файла какую-либо дополнительную структуру, выходящую за пределы структуры самих данных. В частности, в системе UNIX нет такого понятия, как границы между записями, блокировка записей, индексированные файлы или типизированные файлы, как в других системах. Безусловно, в приложении имеется возможность создать файл, состоящий из записей, а затем обращаться к этим записям. Основная идея здесь состоит в том, что сама файловая система не предусматривает структуризации содержимого файла: формат файла должны определять приложения, в которых он используется. 24.7.3. Идентификаторы владельца и группы файла В системах UNIX предусмотрено применение учетных записей для пользователей, и каждому пользователю присвоен числовой идентификатор пользователя, который служит для учета выполняемых им действий и проверки прав во всей системе. Каждый файл имеет одного владельца, обозначенного числовым идентификатором пользователя, который создал этот файл. Информация о том, кому принадлежит файл, хранится вместе с файлом (т.е. в самом файле, а не в системе каталогов). Кроме доступа к файлу по принципу указания идентификатора пользователя, в системе UNIX предусмотрен совместный доступ к файлам в составе групп пользователей, что дает возможность системному администратору создавать подмножества пользователей, обозначенных числовым идентификатором группы. Любой пользователь в любое время может принадлежать к одной или нескольким группам. После вызова пользователем на выполнение прикладной программы (например, программы для работы с электронными таблицами или текстового редактора) работающая программа наследует идентификаторы пользователя и его группы. Каждый файл принадлежит к определенной группе, и числовой идентификатор его группы хранится вместе с ним. Операционная система сравнивает идентификаторы пользователя (владельца файла) и группы, хранящиеся в файле, с идентификаторами пользователя и группы конкретного прикладного процесса для определения того, какие операции данная программа может выполнить с файлом. 354 7Глава 24. Принципы работы сетевой файловой системы (NFS)
24.7.4. Защита и доступ Механизм защиты доступа системы UNIX позволяет владельцу файла регламентировать доступ к файлу отдельно для самого владельца, членов группы файла и всех прочих пользователей. Для каждой из этих трех категорий пользователей механизм защиты позволяет владельцу определить, имеют ли пользователи этой категории права на чтение, запись или выполнение файла. Как показано на рис. 24.1, права доступа к файлу могут быть представлены в виде матрицы битов защиты. Право на чтение Право на запись Право на выполнение Владелец Члены группы файла Другие пользователи 1 1 1 1 0 0 0 0 0 Рис. 24.1. Схематическое представление прав доступа к файлу в виде матрицы битов защиты На практике в системе UNIX матрица битов защиты, обозначающих права доступа к файлу, хранится в младших битах одного двоичного целого числа, а для обозначения целого числа, в котором закодированы биты защиты, используются термины режим файла, режим защиты или режим доступа к файлу. Как показано на рис. 24.2, в системе UNIX биты защиты файла закодированы в 9 младших битах целого числа с обозначением режима доступа к файлу. Кроме битов защиты, показанных на рис. 24.2, в системе UNIX применяются дополнительные биты целочисленного обозначения режима для определения других свойств файла (например, биты режима указывают, является ли этот файл обычным файлом или каталогом). Бит 1 показывает, что предоставлено определенное право доступа; бит О означает, что в этом праве отказано. В восьмеричном формате приведенные на этом рисунке права защиты принимают значение 0644. После записи значения целого числа с обозначением режима файла в восьмеричном формате три младших разряда определяют права доступа владельца, членов группы файла и других пользователей. Поэтому значение прав доступа 0700 указывает, что владелец имеет право на чтение, запись и выполнение файла, а все другие пользователи не имеют к нему доступа. Обозначение режима защиты 0644 говорит о том, что все пользователи могут читать этот файл, но только владелец имеет право записывать в него данные. 24.7. Обзор файловой системы UNIX 355
Другие биты режима Владелец файла Пользователи, входящие в группу файла Другие пользователи 1 Выполнение Запись Чтение Выполнение Запись Чтение Выполнение Запись Чтение Рис. 24.2. Информация о правах доступа к файлу, хранящаяся в младших девяти битах целого числа с обозначением режима файла 24.7.5. Организация работы с файлом по принципу "открыть-читать- писать-закрыть" В приложениях, работающих в таких системах, как Linux, доступ к файлам осуществляется по принципу "open-read-write-close" (открыть-читать-писать- закрыть). Для установления доступа к файлу в приложении необходимо вызвать функцию open, передав ей имя файла и параметры с описанием желаемого способа доступа. Функция open возвращает целочисленный дескриптор файла, используемый в приложении во всех остальных операциях с файлом. Например, чтобы открыть файл с именем filename, можно включить в программу следующий оператор: fdesc=open("filename",0_CREAT|0_RDWR,0644); Значение 0_CREAT во втором параметре указывает, что файл должен быть создан, если он еще не существует, а значение 0_RDWR указывает, что файл должен быть создан и для чтения, и для записи. Восьмеричное значение 0644 определяет биты режима защиты, которые будут назначены файлу, если он будет создан. В качестве второго параметра могут использоваться другие значения для указания того, нужно ли усечь этот файл (установить для него размер, равный нулю) и должен ли он быть открыт для чтения, записи или для обеих операций. 24.7.6. Передача данных В приложении функция read используется для передачи данных из файла в память, а функция write — для передачи данных из памяти в файл. Функция read принимает три параметра: дескриптор открытого файла, адрес буфера и число считываемых байтов. Например, следующий оператор представляет собой требование к операционной системе прочитать 24 байта данных из файла с дескриптором f desc: n=read(fdesc,buff,24); 356 7Глава 24. Принципы работы сетевой файловой системы (NFS)
Операции read и write начинают передачу данных с текущей позиции в файле и обновляют позицию в файле по окончании своей работы. Например, если приложение открывает файл, переходит в позицию 0 и читает 10 байтов из файла, то после завершения этой операции чтения позиция в файле будет равна 10. Поэтому в программе можно извлечь все данные из файла последовательно, начиная с позиции 0 и повторно вызывая функцию read. Если в приложении предпринимается попытка прочитать больше байтов, чем содержится в файле, функция read извлекает то число байтов, которое содержится в файле, и возвращает в качестве результата число считанных байтов. Если при вызове в приложении функции read позиция в файле установлена на конец файла, то вызов функции read возвращает значение нуль для указания на то, что обнаружен признак конца файла. 24.7.7. Право на поиск в каталоге В системе UNIX файлы организованы в виде иерархической структуры с использованием каталогов3 для хранения файлов и других каталогов. В этой системе для каталогов применяется такая же 9-битовая схема режимов защиты, как и для обычных файлов данных. Биты прав на чтение определяют, может ли приложение получить список файлов в каталоге, а биты прав на запись определяют, может ли приложение вставлять или удалять файлы в этом каталоге. Каждый отдельный файл имеет отдельный набор битов прав доступа, которые определяют, какие операции с содержимым этого файла разрешено выполнять пользователям различных категорий. Права доступа к каталогу указывают только то, какие операции разрешено выполнять с самим каталогом, и не относятся к содержащимся в нем файлам. Каталоги могут содержать прикладные программы, но сами они не состоят из оттранслированного кода. Это означает, что они не могут вызываться на выполнение как прикладные программы, поэтому права на выполнение для каталога не имеют смысла. В системе UNIX биты прав на выполнение применительно к каталогам означают права на выполнение поиска. Если приложение имеет право на поиск, оно может обращаться к файлу, находящемуся в каталоге; в ином случае, система не допускает использования в приложении каких-либо ссылок на этот файл. Права на поиск позволяют раскрыть для приложения или скрыть от него целое поддерево иерархии файлов, не изменяя прав доступа к отдельным файлам в этом поддереве. 24.7.8. Произвольный доступ При открытии файла позиция в нем может быть установлена на начало файла (например, для последовательного доступа к файлу) или на конец файла (например, для добавления данных в существующий файл). После открытия файла позиция в нем может быть изменена путем вызова функции lseek. Функция lseek принимает три параметра с указанием дескриптора файла, смещения и точки отсчета смещения. Третий параметр позволяет в приложении указать, определяет ли это смещение абсолютное положение в файле (например, байт 512), новое положение относительно текущей позиции (например, байт, находящийся на расстоянии 64 байтов от текущей позиции) или положение относительно конца файла (например, за 2 байта до конца файла). В частности, константа LJ5ET определяет, что система должна интерпретировать смещение как абсолютную величину. Таким образом, для указания на то, что текущая позиция в файле с дескриптором fdesc должна быть перемещена к байту номер 100, применяется вызов: lseek(fdesc,100L,LJ3ET); .. В других операционных системах для обозначения той структуры, которая в системе UNIX называется каталогом, иногда используется термин папка. 24.7. Обзор файловой системы UNIX 357
24.7.9. Выполнение поиска за пределами файла Файловая система UNIX позволяет в приложении перейти в любую позицию файла, даже если указанная позиция находится за пределами текущего конца файла. Если в приложении будет выполнена установка новой позиции в пределах файла и запись новых данных, эти данные заменят старые данные, находившиеся ранее в этой позиции. Если же в приложении позиция будет установлена за пределами файла и выполнена запись новых данных, то файловая система увеличит размер файла. С точки зрения пользователя, создается впечатление, что система заполняет промежуток между существующими данными и новыми данными, записанными за пределами файла, нулевыми байтами (байтами со значением нуль). В дальнейшем, при осуществлении в приложении попытки чтения байтов в позиции, относящейся к этому промежутку, файловая система возвращает байты с нулевыми значениями. Этот принцип проиллюстрирован на рис. 24.3. а b с d е f t Конец файла (а) а b с d е f t Конец файла (б) Рис. 24.3. (а) Файл, содержащий в 6 байтах символы от а до f, и (б) файл после перехода в приложении в позицию 8 и записи байта, содержащего символ х; создается впечатление, что незаписанные байты содержат нулевые значения Хотя в результате работы файловой системы создается впечатление, что незаписанные промежутки заполнены нулевыми байтами, фактически структура хранения просто дает возможность эмулировать нулевые байты, не представляя их на физическом носителе. Поэтому размер файла отражает максимальную байтовую позицию, в которую были записаны данные, а не общее число записанных байтов. 24.7.10. Позиция в файле и параллельный доступ Файловая система UNIX позволяет обращаться к файлу сразу нескольким прикладным программам. Дескриптор каждого открытого файла ссылается на структуру данных, в которой регистрируется текущая позиция в файле. При вызове в процессе функции fork для создания нового процесса, дочерний процесс наследует копии всех дескрипторов файлов, открытых родительским процессом ко времени вызова функции fork. Дескрипторы указывают на одну и ту же базовую структуру данных, применяемую для доступа к файлу. Поэтому, если один из двух процессов изменяет позицию в файле, эта позиция изменяется также и для другого процесса. 358 7Глава 24. Принципы работы сетевой файловой системы (NFS)
При каждом вызове функции open создается новый дескриптор с позицией в файле, независимой от полученной при предыдущих вызовах функции open. Поэтому, если в каждом из двух приложений будет вызвана функция open для открытия одного и того же файла, то каждое из них может управлять позицией в файле независимо от другого. Одно приложение может переместить указатель позиции в конец файла, а другое — оставить его в начале. При проектировании приложения необходимо принять решение о том, должен ли быть в нем предусмотрен указатель позиции в файле, применяемый совместно с другими процессами, или для каждого приложения должен применяться отдельный указатель позиции. Понимание, в чем состоит разница между указателем позиции в файле и самим файлом, необходимо при изучении операций с удаленными файлами. Сформулируем это понятие. При каждом вызове функции open создается новая структура доступа к файлу, в которой хранится информация о смещении в нем указателя. Отделение информации о текущей позиции в файле от самого файла позволяет обеспечить одновременный доступ к нему нескольких приложений без взаимных помех. Такая организация доступа позволяет также использовать операцию lseek для изменения приложением позиции в файле без изменения самого файла, 24.7.11. Организация записи при параллельном доступе Если две программы выполняют запись в файл одновременно, между ними могут возникать конфликты. Например, предположим, что каждая из двух параллельных программ читает из файла первые два байта, переставляет их местами и снова записывает в файл. Если планировщик операционной системы примет решение вызвать на выполнение одну программу, а затем другую, то первая программа, допустим, переставит местами два байта и снова запишет их в файл. После этого вторая программа считывает байты, находящиеся в обратном порядке, снова переставляет их в первоначальное положение и записывает в файл. Однако если планировщик запускает на выполнение сразу две программы и позволяет каждой из них выполнить чтение из файла до того, как будет выполнена какая-либо запись, то обе программы прочитают байты, находящиеся в первоначальном порядке, а затем обе запишут эти байты в обратном порядке. Поэтому можно сделать вывод, что порядок байтов в файле зависит от того, какой принцип предоставления ресурсов процессора двум программам принят в системном планировщике. В системе UNIX не предусмотрев взаимоисключающий доступ и не определена организация параллельного доступа, не считая того, что файл всегда содержит данные, записанные в последней по времени операции. Ответственность за правильную организацию работы возложена на программиста4. Поэтому программисты должны создавать параллельные программы таким образом, чтобы они всегда функционировали правильно и вырабатывали в любых условиях одни и те же результаты. 24.7.12. Имена и пути доступа к файлам В системе UNIX определено понятие иерархической структуры имен файлов. Каждый файл и каталог в файловой системе имеет отдельное имя, которое может В системе Linux, как и в других современных версиях UNIX, предусмотрен механизм рекомендательной блокировки, основанный на использовании функции flock. В ранних версиях этих систем использовалась схема блокировки, основанная на функции lockf, или требование по выполнению блокировки обозначалось с применением параметра 0_EXCL в вызове функции open. 24.7. Обзор файловой системы UNIX 359
быть представлено строкой ASCII. Кроме того, каждый каталог или файл имеет полное составное имя, обозначающее положение файла в этой иерархии. На рис. 24.4 показаны примеры имен файлов и каталогов в иерархической файловой системе. В данном случае каталог верхнего уровня содержит два файла (а и с) и два каталога (Ь и d). На практике файлы и каталоги почти никогда не имеют односимвольных имен. Рис. 24.4. Пример иерархической файловой системы; кружками обозначены каталоги, а квадратами — файлы Как показано на этом рисунке, верхний каталог файловой системы, называемый корневым, имеет полное составное имя "/" (обозначается символом косой черты). Полное составное имя файла можно рассматривать как конкатенацию с именем файла цепочки имен каталогов, по которым проходит путь в иерархии от корня к данному файлу, с использованием в качестве разделителя символа косой черты (/). Например, файл с именем а, который находится в корневом каталоге, имеет полное составное имя /а. Файл с именем е, находящийся в каталоге Ь, имеет полное составное имя /b/е, а файл с именем к имеет полное составное имя /d/g/k. 24.7.13. Индексный узел: информация, хранимая вместе с файлом Кроме самих данных, файловая система UNIX предусматривает хранение информации о каждом файле в энергонезависимой памяти. Эта информация хранится в структуре, называемой индексным узлом файла5. Индексный узел содержит много полей, в том числе поля с обозначением идентификаторов владельца и группы, целого числа с обозначением режима файла (см. раздел 24.7.4), времени последнего доступа, времени последнего изменения, размера файла, дисковода и файловой системы, в которой находится файл, числа записей каталога для этого файла, числа дисковых блоков, используемых в настоящее время этим файлом, и основного типа (который определяет, например, является ли он обычным файлом или каталогом). Понятие индексного узла позволяет описать некоторые особенности файловой системы UNIX, которые применяются также в системе NFS. Во-первых, в системе UNIX такая информация, как данные о владельце и битах защиты файла, от- Англоязычный эквивалент термина индексный узел, inode, произносится как "ай-ноуд" и является сокращением от index node. 360 7Глава 24. Принципы работы сетевой файловой системы (NFS)
делена от записей каталога для рассматриваемого файла. Это позволяет иметь несколько записей каталога, которые указывают на один и тот же файл. В системе UNIX для обозначения записи каталога, относящейся к файлу, используется термин ссылка или жесткая ссылка. Как показано на рис. 24.5, если имеется более одной жесткой ссылки на файл, то информация о нем присутствует в нескольких каталогах. На этом рисунке показаны две ссылки на файл h; одна из них находится в каталоге а, а вторая — в каталоге d. К файлу h можно обратиться по составному имени /a/h или /b/d/h. Рис. 24.5. Пример использования жестких ссылок К файлам, на которые указывают несколько жестких ссылок, можно обратиться по нескольким полным составным именам. Например, к файлу h, как показанному на рис. 24.5, можно обратиться по полному составному имени /a/h, поскольку ссылка на него находится в каталоге а. К нему можно также обратиться по полному составному имени /b/d/h, поскольку ссылка на него находится и в каталоге d. Как показывает этот пример, составные имена файла с несколькими ссылками могут иметь различную длину. Поскольку информация о файле хранится в индексном узле, а не в каталоге, то информация о владельце и правах защиты файла h остается неизменной, независимо от того, какое имя используется в приложении для доступа к файлу. Если владелец изменит режим защиты файла /a/h, изменится также режим защиты файла /b/d/h. 24.7.14. Назначение функции stat Системная функция stat извлекает информацию о файле из его индексного узла и возвращает эту информацию в вызывающий оператор. Вызов этой функции принимает два параметра: составное имя файла и адрес структуры, в которую должен быть помещен этот результат: stat(pathname,&result_struct); Второй параметр должен представлять собой адрес области в памяти, достаточно большой для размещения следующей структуры: 24.7. Обзор файловой системы UNIX 361
* Структура, возвращаемая функцией stat */ * Устройство, на котором находится индексный узел */ * Номер индексного узла файла */ * Биты защиты */ * Общее число жестких ссылок на файл */ * Идентификатор пользователя-владельца файла */ * Идентификатор группы, присвоенный файлу */ * Используется для устройств, а не файлов */ * Общий размер файла в байтах */ * Время последнего доступа к файлу */ * Не используется */ * Время последнего изменения */ * Не используется */ * Время последнего изменения индексного узла */ struct stat { dev__t stjlev; ino_t st_ino; u_short stjnode; short stjilink; short st_uid; short st_gid; dev_t stjrdev; long stjsize; time_t st_atime; int stjinusedl; time_t stjntime; int st_unused2; time_t st_ctime; }? Функцию stat для получения информации о файле может вызвать любой пользователь, даже не имеющий права на чтение этого файла. Однако вызывающий оператор должен иметь права доступа на поиск во всех каталогах вдоль пути к указанному файлу, так как в противном случае функция stat возвратит сообщение об ошибке. 24.7.15. Механизм именования файлов Хотя у пользователей создается впечатление, что все файлы и каталоги составляют часть единой иерархии, фактически такая многоуровневая структура создается за счет применения механизма именования файлов. Механизм именования позволяет системному администратору составить единую концептуальную иерархию из нескольких меньших иерархий. Пользователи редко задумываются над тем, как устроена базовая структура файловой системы или как формируется эта иерархия из различных компонентов, поскольку механизм именования полностью скрывает эту структуру. Ниже будет показано, как в системе NFS, работающей в такой операционной системе типа UNIX, как Linux, применяется механизм именования для объединения файлов удаленного компьютера с файлами локального компьютера. На первых порах механизм именования создавался с учетом того, что в компьютерных системах применялось несколько физических запоминающих устройств (например, несколько жестких дисков). Чтобы не вынуждать пользователей указывать не только имя файла, но и имя устройства, проектировщики предусмотрели возможность соединять иерархию каталогов одного диска с иерархией другого. В результате создается единое унифицированное пространство имен файлов, которое позволяет пользователю работать с файлами, не учитывая их местонахождение на конкретных дисководах. Этот механизм именования действует следующим образом. ¦ Иерархия каталогов одного из дисков обозначается как корневая. ¦ В корневой иерархии создается пустой каталог. Допустим, что полное составное имя пустого каталога задается строкой /alpha. ¦ Механизм именования получает указание наложить на каталог /alpha новую иерархию (обычно находящуюся на другом диске). После присоединения новой иерархии механизм именования автоматически преобразовывает имена в форме /alpha/betha в имя файла или каталога betha в подключенной иерархии. Отметим следующее важное понятие. 362 7Глава 24. Принципы работы сетевой файловой системы (NFS)
Механизм именования файлов предоставляет пользователям и прикладным программам доступ к общей единообразной иерархии файлов, даже если сами эти файлы разбросаны по нескольким физическим дискам. По сути, эта система является более общей, чем механизм включения в состав иерархии целых дисков. Она позволяет определить один физический диск как состоящий из одной или нескольких файловых систем. Каждая файловая система представляет собой независимую иерархию; в нее входят и файлы, и каталоги. Файловая система может быть присоединена к этой унифицированной иерархии в любой точке. В следующем разделе приведен пример, который позволяет пояснить работу механизма именования. 24.7.16. Монтирование файловой системы Действие механизма именования по построению унифицированной иерархии основано на использовании системного вызова mount. Функция mount применяется для указания того, как файловая система одного из дисков должна быть подключена к иерархической системе. Обычно необходимые операции монтирования выполняются автоматически во время начальной загрузки системы. На рис. 24.6 показаны три файловые системы, которые были смонтированы для формирования единой иерархии. После монтирования границы между дисками стираются. Например, файловая система 0 жесткого диска 2 выглядит как каталог /с. Диск1 Файловая / ) система О Диск1 Файловая ( / система 1 Диск 2 Файловая система О i i Рис. 24.6. Три файловые системы, смонтированные для формирования общей единообразной иерархии 24.7. Обзор файловой системы UNIX 363
Как показано на этом рисунке, файловая система 0 жесткого диска 1 была смонтирована в корне иерархии. Файловая система 1 того же жесткого диска была смонтирована в каталоге /а, а файловая система 0 жесткого диска 2 смонтирована в каталоге /с. Команда mount полностью замещает первоначальный каталог новой файловой системой. Обычно для монтирования создается отдельный пустой каталог. Но если каталог, в котором монтируется файловая система, до выполнения монтирования содержит какие-либо файлы, они будут полностью скрыты (т.е. станут недоступными даже для системного администратора) до тех пор, пока эта файловая система не будет снова размонтирована. За небольшими исключениями, операции монтирования полностью скрыты от пользователей6. После получения пользователем листинга содержимого каталога / система сообщит о наличии трех подкаталогов: a, b и с. При формировании листинга каталога /а система сообщит о наличии двух каталогов: g и h. В листинге каталога /c/s будет показан файл и и каталог v. Составные имена не обозначают границы между файловыми системами и пользователи не имеют информации о том, где находятся файлы. Например, к файлу п (находящемуся в файловой системе 1 диска 1) можно обратиться с использованием полного составного имени /a/g/j/n. После построения иерархии файловых систем с применением системной функции mount границы, разделяющие отдельные файловые системы, становятся прозрачными. Некоторые файлы и каталоги могут находиться на одном диске, а другие файлы и каталоги — на другом. Пользователь не имеет информации о том, с каким диском он работает, поскольку в результате монтирования создается единообразная система именования, стирающая все границы. На практике пользователь может узнать, как смонтированы файловые системы, если это его интересует. В операционной системе предусмотрена команда mount, которая запрашивает необходимую информацию, а затем отображает список смонтированных файловых систем. Системный администратор может также использовать команду mount для выполнения новых операций монтирования. Например, при выполнении команды mount на одном компьютере был получен следующий результат: /dev/hdal on / type ext2 (rw,noquota) /dev/hdbl on /usr type ext2 (rw,noquota) /dev/hda2 on /usr/src type ext2 (rw,noquota) В первой строке этого листинга показано, что файловая система hdal (первая файловая система на первом жестком диске IDE) смонтирована в каталоге 7 и образует корень иерархии. Слова type ext2 указывают, что было выполнено монтирование файловой системы Linux, а параметры в круглых скобках означают, что файловая система была смонтирована для чтения и записи, а дисковые квоты для учетных записей не предусмотрены. Файловая система hdbl (первая файловая система второго жесткого диска) смонтирована в каталоге /usr. Поэтому файлы, доступ к которым может быть получен в каталоге /usr, находятся на другом жестком диске по сравнению с файлами каталога /. И наконец, файловая система hda2 (вторая файловая система первого жесткого диска) смонтирована в каталоге /usr/src. Третья операция монтирования иллюстрирует любопытную особенность системы именования, поскольку она означает, что основная часть 6 Пользователь не может создать жесткую ссылку, которая пересекает границу между файловыми системами. / 364 7Глава 24. Принципы работы сетевой файловой системы (NFS)
файлов каталога /usr находится на втором жестком диске, тогда как файлы поддерева /usr/src находятся на первом жестком диске. Дело в том, что механизм монтирования позволяет объединить несколько файловых систем нескольких жестких дисков в общую единообразную иерархию каталогов; пользователи или прикладные программы обращаются к этой иерархии, не имея информации о том, где фактически находятся файлы. Принцип монтирования файловых систем для формирования единой иерархии обеспечивает невероятную гибкость. Он позволяет выбирать способ распределения файлов по жестким дискам с учетом требований экономии, уменьшения конкуренции за доступ к файлам или необходимости изоляции каталогов на случай непредвиденных отказов. Как будет показано ниже, этот принцип предоставляет также удобный способ включения в иерархию файлов удаленного компьютера. 24.7.17. Преобразование имен файлов После получения полного составного имени механизм файловой системы преобразует его в имя файла на диске; для этого механизм преобразования имен проходит по концептуальной иерархии каталогов. В системе Linux преобразование имен в конечном итоге сводится к поиску индексного узла, обозначающего файл. Преобразование полного составного имени в файловой системе начинается с корня иерархии и предусматривает последовательное прохождение по каталогам. Например, после получения имени типа /a/b/c/d файловая система открывает корневой каталог и ищет в нем подкаталог а. Найдя подкаталог /а, она открывает этот каталог и ищет в нем подкаталог Ь. Аналогичным образом, файловая система ищет в b каталог с, а в с отыскивает файл или подкаталог d. Программное обеспечение на каждом этапе поиска может извлечь один компонент полного составного имени, поскольку символ косой черты всегда разделяет отдельные компоненты. Как будет показано ниже, в системе NFS применяется такой же способ преобразования имен, как и в операционной системе Linux. Хотя для нас пока не имеют значения конкретные сведения о преобразовании имен файлов, отметим следующий важный принцип. Составное имя преобразуется поэтапно — по одному компоненту за один этап. Преобразование начинается с корня иерархии и предусматривает прохождение по компонентам составного имени. В этом последовательном процессе на каждом этапе извлекается очередной компонент составного имени и отыскивается файл или подкаталог с этим именем. 24.7.18. Символические ссылки Многие файловые системы допускают применение файлов особого типа, называемых символическими ссылками (ярлыками). Символическая ссылка представляет собой специальный текстовый файл, содержащий имя другого файла. Например, можно создать файл /a/b/с, который содержит символическую ссылку со значением /a/q. При открытии в программе файла /a/b/с система обнаруживает, что он содержит символическую ссылку, и автоматически переключается на файл /a/q. Основное преимущество использования символических ссылок заключается в их универсальности: поскольку символическая ссылка может содержать произвольную строку, она может указывать на любой файл или каталог. Например, хотя файловая система запрещает создание жестких ссылок на каталог, она позволяет пользователю создавать символические ссылки на каталог. Кроме того, поскольку символическая ссылка может указывать на произвольное составное шея, она может применяться для краткого обозначения длинных составных 24.7. Обзор файловой системы UNIX 365
имен или для значительного сокращения пути доступа к каталогу, находящемуся в отдаленной части иерархии. Основной недостаток символических ссылок связан с отсутствием в них единообразия и надежности. Например, пользователь может создать символическую ссылку на файл, а затем удалить этот файл, в результате чего останется оборванная символическая ссылка, которая указывает на несуществующий объект. Фактически может быть даже создана символическая ссылка на несуществующий файл, поскольку система не проверяет правильность символической ссылки при ее создании. Ничто не препятствует также созданию набора символических ссылок, образующих цикл, или двух символических ссылок, указывающих друг на друга. При вызове функции open для открытия файла, указанного с помощью такой ссылки, возникает ошибка периода выполнения. 24.8. Файлы в системе NFS В системе NFS используются многие определения файловой системы UNIX. В ней файл рассматривается как последовательность байтов, допускается неограниченный рост размеров файлов, а также обеспечивается произвольный доступ с использованием для указания в качестве места доступа позиций байтов в файле. В этой системе обеспечивается доступ по тому же принципу "open-read-write-close", как в файловой системе, и предоставляются во многом аналогичные службы. В системе NFS применяется также иерархическая система именования. В иерархии NFS используется терминология системы UNIX; в ней считается, что иерархия файлов состоит из каталогов и файлов. Каталог может содержать файлы и другие каталоги. В системе NFS применены также многие особенности конструкции файловой системы UNIX, причем одни из них остались неизменными, а другие претерпели лишь небольшие изменения. В следующих разделах описаны некоторые особенности системы NFS и показано, как они связаны с файловой системой UNIX, описанной выше. 24.9. Типы файлов NFS В системе NFS используются те же основные типы файлов, как и в UNIX. В ней определены перечислимые значения, которые могут применяться на сервере при указании типа файла: enum ftype3{ NF3REG=1, /* Обычный файл */ NF3DIR=2, /* Каталог*/ NF3BLK=3, /* Блочное устройство */ NF3CHR=4, /* Символьное устройство.*/ NF3LNK=5, /* Символическая ссылка */ NF3SOCK=6, /* Сокет*/ NF3FIFO=7 /* Именованный канал */ Этот набор типов, включая NF3BLK и NF3CHR, взят непосредственно из операционной системы UNIX. В частности, система UNIX позволяет вводить устройства ввода/вывода в пространство имен файловой системы, что дает возможность открывать в прикладных программах устройства ввода/вывода и выполнять передачу данных в прямом и обратном направлениях по принципу "open-read-write- close". В системе NFS принята терминология UNIX, в которой предусмотрена 366 7Глава 24. Принципы работы сетевой файловой системы (NFS)
классификация устройств ввода/вывода на блочные устройства (такие как жесткий диск, в котором обмен данными всегда происходит в виде блоков по 512 байтов) и символьные устройства (такие как последовательный порт, в котором данные передаются по одному символу). В документации NFS для обозначения имен устройств иногда используется термин специальный файл системы UNIX. Файл, который соответствует блочному устройству, относится к типу специального блочного файла, а файл, который соответствует символьному устройству, относится к типу специального символьного файла. 24.10. Режимы работы с файлами в системе NFS В системе NFS, как и в операционной системе UNIX, предполагается, что каждый файл или каталог имеет обозначение режима, в котором указан его тип и содержится информация о правах доступа. Обозначение режима файла хранится в виде целого числа. В табл. 24.1 показаны отдельные биты целочисленного обозначения режима файла NFS и описано их назначение. В этой таблице для представления битов используются шестнадцатеричные числа; эти определения непосредственно соответствуют данным, возвращаемым функцией stat системы UNIX. Таблица 24.1. Описание битов в целочисленном обозначении режима файла NFS. Эти определения взяты непосредственно из системы UNIX Бит режима Описание 0x0800 0x0400 0x0200 0x0100 0x0080 0x0040 0x0020 0x0010 0x0008 0x0004 0x0002 0x0001 Установить идентификатор пользователя после вызова на выполнение (setuid) Установить идентификатор группы после вызова на выполнение (setgid) Сохранить текст, выведенный на внешнее устройство (не определено в стандарте POSIX) Право на чтение для владельца Право на запись для владельца Право на выполнение для владельца файла или право на поиск в каталоге для владельца каталога Право на чтение для группы Право на запись для группы Право на выполнение для группы файла или право на поиск в каталоге для группы каталога Право на чтение для прочих пользователей Право на запись для прочих пользователей Право на выполнение для прочих пользователей или право на поиск в каталоге для прочих пользователей Хотя в системе NFS определены типы файлов для устройств, в ней не допускается Дистанционный доступ к устройствам (например, клиент не может выполнять чтение или запись на удаленном устройстве). Таким образом, хотя клиенту разрешено получение информации об именах файлов, ему запрещено выполнять операции с устройствами, даже если биты защиты доступа это допускают. 24.10. Режимы работы с файлами в системе NFS 367
Хотя в системе NFS определены биты защиты файлов, в соответствии со значениями которых клиент может выполнять чтение или запись в конкретном файле, система NFS запрещает доступ с удаленного компьютера ко всем устройствам, даже если биты защиты доступа указывают, что доступ разрешен. 24.11. Атрибуты файла NFS В системе NFS, как и в операционной системе UNIX, предусмотрен способ получения информации о файле. В системе NFS информация о файле представлена в виде атрибутов файла. Атрибуты файла, рассматриваемые в системе NFS, представлены в структуре fattr3: * Атрибуты файла NFS */ */ struct fattr3 { ftype3 type; mode3 mode; uint32 nlink; uid3 uid; gid3 gid; size3 size; size3 used; specdata3 rdev; uint64 fsid; fileid3 fileid; nfstime3 atime; nfstime3 mtime; nfstime3 ctime; }? Как показывает эта структура, принцип ее организации и основная часть представленных сведений исходят из информации, возвращаемой функцией stat операционной системы UNIX. Тип: файл, каталог и т.д. Биты защиты файла */ Общее число жестких ссылок на файл */ Идентификатор пользователя-владельца файла */ Идентификатор группы, присвоенный файлу */ Общий размер файла в байтах */ Фактически используемое дисковое пространство */ Описание, если файл является устройством*/ Идентификатор файловой системы */ Уникальный идентификатор файла (индексный узел) */ Время последнего доступа к файлу */ Время последнего изменения */ Время последнего изменения индексного узла */ 24.12. Клиент и сервер NFS Сервер NFS функционирует на компьютере, на котором имеется локальная файловая система. Сервер предоставляет возможность обращаться к некоторым локальным файлам с удаленных компьютеров. Клиент NFS может работать на любом компьютере и обращаться к файлам, находящимся на одном или нескольких удаленных компьютерах, на которых функционируют серверы NFS. Чаще всего сервер NFS устанавливается на компьютере с большим объемом дискового пространства. Такой компьютер называют компьютером файлового сервера. Обычно на компьютере файлового сервера NFS запрещается эксплуатация прикладных программ в целях уменьшения его нагрузки и обеспечения быстрого отклика на запросы доступа. Применение отдельного компьютера для размещения файлового сервера позволяет также добиться того, чтобы обработка трафика дистанционного доступа к файлам не занимала процессорного времени, необходимого шя выполнения прикладных программ. В большинстве реализаций клиентских программ NFS предусмотрено объединение файлов NFS с собственной файловой системой компьютера, в результате чего информация о местонахождении файлов становится скрытой от прикладных программ и пользователей. Например, рассмотрим среду Windows. В ней имена файлов имеют форму X: alpha, где X — односимвольный идентификатор устройства ввода/вывода, a alpha обозначает составное имя файла на этом устройстве. В сис- 368 7Глава 24. Принципы работы сетевой файловой системы (NFS)
теме Windows для разделения компонентов составного имени используется символ обратной косой черты (\). Поэтому имя файла C:\D\E\F обозначает файл F в подкаталоге Е каталога D на системном жестком диске. Если в этой системе будет установлен клиент NFS и настроен на доступ к файлам удаленного сервера, в этой клиентской программе можно будет использовать схему именования Windows. Например, для всех удаленных файлов могут быть выбраны имена в форме Rtbetha, где betha обозначает составное имя файла в удаленной файловой системе R. При вызове в прикладной программе функции open для получения доступа к файлу операционная система использует обозначение составного имени файла для определения того, должны ли применяться процедуры локального или дистанционного доступа к файлам. Если составное имя относится к удаленному файлу, то в системе для доступа к этому файлу применяется клиентское программное обеспечение NFS, а если составное имя относится к локальному файлу, то в системе для доступа к этому файлу используется стандартное программное обеспечение файловой системы компьютера. На рис. 24.7 показано, как взаимодействуют между собой модули операционной системы при выборе соответствующего программного обеспечения. В системе для определения того, должна ли использоваться система NFS, которая открывает удаленный файл, или стандартная файловая система, которая открывает локальный файл, анализируется составное имя файла. V Код системной функции open \ } Программные средства интерпретации составного имени и выбора модуля доступа / \ Клиентские программные средства NFS для удаленного доступа к файлам ч Программные средства файловой системы для локального доступа к файлам Рис. 24.7. Процедуры операционной системы, вызываемые при открытии файла в приложении 24.13. Клиентские операции в системе NFS Напомним, что система NFS проектировалась с учетом возможности ее применения в разнотипных компьютерных системах. При установке клиентского кода NFS в операционной системе должно быть обеспечено объединение схемы именования, предоставляемой системой NFS, со схемой именования файлов, применяе- 24.13. Клиентские операции в системе NFS 369
мой в операционной системе компьютера. Однако синтаксис составных имен, применяемый в удаленной файловой системе, может отличаться от синтаксиса составных имен на клиентском компьютере. Например, при использовании клиентского программного обеспечения NFS на компьютере с операционной системой Windows для подключения к серверу NFS, работающему на компьютере с операционной системой UNIX, в клиентской системе в качестве разделителя служит обратная косая черта (\), а в файловой системе сервера — косая черта (/). Для учета потенциальных различий в синтаксисе составных имен файлов в клиентском и серверном компьютерах в системе NFS применяется простое правило: полные составные имена интерпретируются только в клиентской части. Для прослеживания компонентов полного составного имени в иерархической системе каталогов сервера клиент последовательно передает на сервер отдельные компоненты составного имени. Например, если в клиентской программе в качестве разделителя для поиска составного имени /a/b/с на сервере используется косая черта, то клиент вначале должен получить информацию о корневом каталоге сервера. Затем клиент передает серверу запрос на выполнение поиска имени а в этом каталоге. Сервер возвращает информацию о каталоге а. Предположим, что в ответе будет указано, что а представляет собой каталог. Затем клиент отправляет на сервер запрос выполнить поиск имени b в каталоге а. После получения ответа от сервера клиент проверяет, является ли b каталогом, и в случае получения положительного ответа передает серверу запрос на выполнение поиска имени с в каталоге Ь. И наконец, сервер отвечает, передавая информацию о файле с. Основной недостаток такой организации работы, при которой задача интерпретации составных имен возложена на клиентскую часть, должен быть очевиден: для этого требуется обмен данными по сети при определении каждого компонента составного имени. Основное преимущество также известно: приложения любого компьютера могут обращаться к файлам на удаленном компьютере с использованием того же синтаксиса составного имени, который служит для обозначения локальных файлов. Еще более важно то, что приложения и клиентские программы могут разрабатываться для доступа к удаленным файлам без учета того, где находятся эти файлы и какие соглашения об именовании используются в файловых системах серверов. Поэтому ни одно из клиентских приложений не требует замены при установке новых версий программного обеспечения на серверном компьютере, даже если эти обновления будут связаны с переходом на другую операционную систему или иную схему именования файлов по сравнению с первоначальной. Для того чтобы работа приложений на клиентских компьютерах не зависела от местонахождения файлов и от типа операционной системы серверного компьютера, система NFS требует, чтобы полные составные имена интерпретировались только в клиентских программах. Клиент прослеживает составное имя файла по иерархии каталогов сервера, передавая на сервер каждый раз по одному компоненту и получая информацию об указанном в этом компоненте файле или каталоге. 24.14. Клиент NFS в системе UNIX Напомним, что в такой версии^системы UNIX, как Linux, для построения единой унифицированной иерархии имен на основе отдельных файловых систем, находящихся на нескольких жестких дисках, используется механизм монтирования. В реализациях клиентского программного обеспечения NFS для системы UNIX предусмотрена расширенная версия механизма монтирования для включения удаленных файловых систем в общую иерархию имен наряду с локальными файловыми системами. С точки зрения прикладного программиста, основным преимуществом исполь- 370 7Глава 24. Принципы работы сетевой файловой системы (NFS)
зования механизма монтирования является единообразие, поскольку все имена файлов имеют одинаковую форму. В прикладной программе невозможно определить, является ли файл локальным или удаленным, исходя лишь из одного синтаксиса имени файла. Если путь, указанный в вызове функции open, включает каталог, смонтированный в системе NFS* то операционная система в конечном итоге находит точку монтирования файловой системы удаленного компьютера и передает управление клиентскому программному обеспечению NFS. Клиент NFS завершает операцию открытия файла, продолжая интерпретировать и отыскивать компоненты составного имени. В конечном итоге клиент NFS доходит до конца составного имени и возвращает информацию о файле на удаленном компьютере. Система создает дескриптор для этого удаленного файла, а приложение использует этот дескриптор для последующего выполнения операций чтения или записи. Таким образом, при открытии удаленного файла приложение получает для доступа к этому файлу целочисленный дескриптор, точно так же как и при открытии локального файла. Связанная с этим дескриптором информация, которая указывает, что этот файл является удаленным и доступ к нему осуществляется через NFS, хранится только в операционной системе. При выполнении в приложении любой операции с дескриптором файла (например, операции чтения) система проверяет, относится ли этот дескриптор к локальному файлу или удаленному файлу. Если это — локальный файл, операционная система выполняет операцию, как обычно. Если файл является удаленным, операционная система вызывает клиентскую программу NFS, которая преобразует эту операцию в эквивалентную операцию NFS и передает на сервер дистанционный вызов процедуры. 24.15. Выполнение операций монтирования в системе NFS При вводе записей монтирования NFS в таблицу монтирования UNIX необходимо указать удаленный компьютер, на котором функционирует сервер NFS, каталог на этом сервере, локальный каталог, в котором будет выполнено монтирование, и дополнительные сведения об операции монтирования. Например, ниже показана часть результатов выполнения команды mount системы UNIX и приведена информация об операциях монтирования NFS, выполненных на одном из компьютеров в Университете Пердью (записи, не относящиеся к операциям монтирования NFS, были удалены): arthur:/pl on /pi type nfs (rw,grpid,intr,bg,noquota) arthur:/p4 on /p4 type nfs (rw,grpid,intr,bg,noquota) ector: /u4 on /u4 type nfs (rw,grpid,softrbg,noquota) gwen: / on /gwen type nfs (rw,grpid,soft,bg,noquota) gwen: /u5 on /u5 type nfs (rw,grpid,soft,bg,noquota) Каждая строка в этом листинге соответствует одной операции монтирования файловой^ системы NFS. В первом поле каждой строки указан компьютер, на котором работает^сервер NFS, и каталог на этом сервере, а в третьем поле указан локальный каталог, в котором смонтирована удаленная файловая система. Например, запись arthur :/pl указывает каталог /pi на компьютере arthur, который смонтирован в локальном каталоге /pi. Файловая система /pi компьютера arthur смонтирована в локальном каталоге с тем же именем, чтобы пользователи обоих компьютеров могли обращаться к файлам с использованием одинаковых имен. Все операции монтирования, показанные в этом примере, принадлежат к типу nfs, а это означает, что они относятся к удаленным файловым системам, доступ к которым предоставляется через NFS. Кроме того, в каждой строке приведены параметры, заключенные в круглые скобки, которые сообщают дополнительные сведения о монтировании. Как и при монтировании локальных 24.15. Выполнение операций монтирования в системе NFS 371
файловых систем, для удаленной файловой системы можно указать, допускает ли она чтение и запись (rw) или только чтение (г). Система NFS предусматривает два основных метода дистанционного монтирования. Метод мягкого монтирования означает, что в клиенте NFS необходимо применять механизм тайм-аута и сообщать пользователю о том, что сервер находится в автономном режиме, если установка тайм-аута истекает до успешного завершения попытки монтирования. Применение жесткого монтирования означает, что в клиенте NFS не должен использоваться механизм тайм-аута. Обычно выполнение всех операций монтирования происходит автоматически во время запуска системы. После завершения монтирования NFS прикладные программы и пользователи не имеют возможности различать между собой локальные и удаленные файлы. Пользователь может применять обычную прикладную программу для работы с удаленным файлом так же легко, как и с локальным файлом. Например, пользователь может вызвать на выполнение стандартный текстовый редактор для редактирования удаленного файла, а редактор выполняет операции с этим файлом точно так же, как и с локальным файлом. Кроме того, пользователь может перейти в удаленный каталог или вернуться в локальный каталог, просто указав составное имя, которое пересекает одну из точек монтирования. 24.16. Дескриптор файла После указания имени удаленного файла и его открытия в клиентской программе должен быть предусмотрен способ обозначения того, что файл предназначен для выполнения последующих операций (например, чтения и записи). Кроме того, в клиентской программе должен быть предусмотрен способ обозначения отдельного каталога или файла, применяемый в процессе прохождения по иерархии имен файлов сервера. Для решения этих задач в системе NFS предусмотрено, что сервер присваивает каждому открытому файлу уникальный дескриптор файла, который применяется в качестве идентификатора. После открытия клиентом файла сервер создает дескриптор и передает его клиенту. Клиент, передавая запросы на выполнение операций с файлом, отправляет на сервер дескриптор. С точки зрения клиента, дескриптор файла представляет собой 64-байтовую строку, используемую серверов для обозначения файла. С точки зрения сервера, в качестве дескриптора файла может быть выбран любой подходящий набор байтов, который однозначно обозначает отдельный файл. Например, в дескрипторе файла может быть закодирована информация, позволяющая серверу быстро декодировать дескриптор и найти файл. Согласно терминологии NFS, дескриптор файла является для клиента непрозрачным; это означает, что клиент не может декодировать дескриптор или создать его самостоятельно. Дескрипторы файлов создаются только серверами, и каждый сервер распознает только те дескрипторы, которые были им созданы. Кроме того, в защищенных реализациях серверов NFS используются сложные алгоритмы шифрования, которые исключают возможность получения в клиентской программе какой-либо информации из дескриптора файла. В частности, в серверах некоторые биты дескриптора устанавливаются случайным образом, что позволяет исключить возможность подделки дескриптора в клиентской программе. В целях повышения уровня защиты серверы могут также ограничивать продолжительность использования дескриптора файла. Это позволяет исключить возможность захвата клиентом дескриптора на неопределенно долгое время. Для ограничения времени жизни дескриптора в сервере предусматривается вставка в дескриптор зашифрованной отметки времени. По истечении времени жизни дескриптора сервер отказывается выполнять дальнейшие операции с файлом с применением такого устаревшего дескриптора. Прежде чем продолжить доступ 372 7Глава 24. Принципы работы сетевой файловой системы (NFS)
к файлу, клиентская программа должна получить свежий дескриптор. Обычно на практике отметки времени NFS служат достаточно долго и позволяют выполнять всевозможные операции доступа. Приложения, в которых файл используется на законных основаниях, всегда могут получить свежий дескриптор с помощью клиентского программного обеспечения NFS (необходимые для этого действия могут быть полностью скрыты от приложения). 24.17. Применение дескрипторов вместо составных имен файлов Чтобы понять, с чем связана необходимость применения дескрипторов, рассмотрим, какие имена файлов используются клиентами в удаленной иерархии каталогов. Напомним, что система NFS требует, чтобы интерпретация всех составных имен файлов выполнялась клиентом, поскольку это позволяет не учитывать в клиентских программах синтаксис составных имен файлов сервера и обеспечить доступ к иерархическим файловым системам с разнотипных компьютеров. В результате этого исключается возможность использовать в клиентской программе полное составное имя для обозначения файла при передаче запроса на выполнение операции с этим файлом. Вместо этого клиентская программа должна получить дескриптор, который может применяться для ссылки на файл. Возможность получения от сервера NFS не только дескриптора файла, но и дескриптора каталога позволяет в клиентской программе прослеживать путь в иерархии каталогов сервера. Чтобы понять, с чем это связано, напомним, что дистанционное монтирование может выполняться в любой точке иерархии каталогов. Следовательно, поиск NFS должен быть объединен с обычным поиском в файловой системе. При указании файла в приложении должен быть указан полный составной путь. В операционной системе для преобразования составного имени в имя файла на внешнем устройстве используется обычный алгоритм: поиск начинается с корня файловой системы и рассматривается ка- ждый\ компонент составного имени. После достижения точки дистанционного монтирования локальная операционная система передает управление клиенту NFS. Поскольку остальные компоненты составного имени относятся к каталогам, находящимся на удаленном компьютере, клиентская программа NFS не может выполнить поиск этих каталогов непосредственно. Вместо этого для поиска отдельного компонента клиент должен обратиться к соответствующему серверу NFS и получить дескриптор. Затем этот дескриптор применяется для передачи серверу запроса на выполнение поиска в каталоге. На рис. 24.8 показано, какими сообщениями обмениваются клиент и сервер в процессе поиска клиентом файла с составным именем /a/b/с в иерархии каталогов сервера. На этом рисунке дескриптор файла или каталога с именем х обозначен как Нх, а символом "/"обозначен корневой каталог и разделитель компонентов, используемый клиентом. На практике для получения дескриптора корневого каталога предусмотрен отдельный протокол. Как показано на этом рисунке, сервер возвращает дескриптор для каждого каталога в процессе прохождения по компонентам составного имени. Клиент использует каждый дескриптор в очередном дистанционном вызове процедуры. Например, после получения дескриптора каталога а клиент снова передает этот дескриптор серверу и запрашивает, чтобы сервер выполнил поиск подкаталога b в каталоге а. Клиент не может указать имя этого подкаталога в форме /а/b, так же как не может указать имя каталога верхнего уровня в форме /а, 24.17. Применение дескрипторов вместо составных имен файлов 373
поскольку не имеет информации о том, какой синтаксис составных имен применяется на сервере. Клиент Передаваемые сообщения Получить дескриптор корневого каталога Время Рис. 24.8. Сообщения, которыми обмениваются клиент и сервер в процессе поиска клиентом файла, имеющего полное составное имя /a/b/c 24.18. Определение позиции в файле при работе с сервером, не поддерживающим состояние Поскольку в системе NFS используется проект сервера, не поддерживающего состояние, сервер не может хранить информацию о позиции указателя в файле для каждого приложения, которое работает с этим файлом. Вместо этого вся информация о позиции указателя в файле хранится в клиентской программе и в каждом запросе, переданном на сервер, должна быть указана нужная позиция в файле. Хранение информации о позиции в клиентской программе позволяет также оптимизировать операции, выполнение которых приводит к изменению позиции в файле. Например, в реализации системы NFS, применяемой в операционной системе Linux, для хранения информации о позиции в файле используется локальная таблица файлов по такому же принципу, по какому в ней записывается информация о позициях в локальных файлах. При вызове в клиентской программе функции lseek система регистрирует в этой таблице новую позицию в файле, не передавая на сервер никаких сообщений. При последующих операциях доступа информация о позиции в файле извлекается из таблицы и передается на сервер наряду с запросом доступа. 374 7Глава 24. Принципы работы сетевой файловой системы (NFS)
Поскольку выполнение функции lseek не предусматривает передачу каких-либо сообщений по сети, операция изменения позиции указателя в удаленном файле выполняется столь же эффективно, как и в локальном. 24.19. Операции с каталогами Формально в системе NFS каталог определен как набор пар, состоящих из имени файла и указателя на названный файл. Каталог может иметь произвольную длину; какие-либо ограничения на длину каталога не устанавливаются. В системе NFS предусмотрены операции, позволяющие клиенту вставить файл в каталог, удалить файл из каталога, выполнить поиск в каталоге файла с указанным именем и прочитать содержимое каталога. 24.20. Получение листинга каталога с применением операции, не поддерживающей состояние Поскольку каталоги могут иметь произвольную длину, а сети связи налагают ограничение на размер отдельного сообщения, то для чтения содержимого каталога может потребоваться выполнение нескольких запросов. Поскольку серверы NFS не поддерживают состояние, сервер не может вести учет того, в какой позиции каждый клиент выполняет чтение имен файлов в данном каталоге. В проекте NFS это ограничение, связанное с применением серверов, не поддерживающих состояние, преодолено следующим образом: в проекте предусмотрено, что сервер NFS возвращает идентификатор позиции, отвечая на запрос, предусматривающий получение определенной записи каталога. Клиент использует этот идентификатор позиции при формировании следующего запроса для указания на то, какие записи он уже получил, а какие ему еще нужны. Таким образом, при возникновении необходимости прочитать содержимое удаленного каталога, в клиентской программе выполняется поэтапный просмотр каталога путем повторного выполнения запросов, в каждом из которых указан идентификатор позиции, полученный в ответе на предыдущий запрос. Системные программисты для обозначения идентификатора позиции в каталоге системы NFS применяют неформальный термин магический ключ (или магический пароль). Этот термин подразумевает, что идентификатор не только не интерпретируется клиентом, но и не может быть подделан в клиентской программе. Только сервер может создать магический ключ (поэтому он и называется магическим); клиент может лишь вернуть назад магический ключ, предоставленный ему сервером. Магический ключ не гарантирует неразрывность (атомарность) операции, а также не блокирует каталог. Поэтому, если операции в каком-то каталоге выполняются двумя приложениями, они могут нарушить работу друг друга. Например, допустим, что сервер выполняет запросы от двух или нескольких клиентов одновременно. Предположим, что после чтения трех записей из одного и того же каталога D первый клиент получает магический ключ, который ссылается на позицию, предшествующую четвертой записи, а тем временем другой клиент выполняет ряд операций, которые приводят к вставке и удалению файлов в каталоге D. При попытке выполнить чтение оставшихся записей в каталоге D с использованием устаревшего магического ключа первый клиент может не получить данных обо всех выполненных изменениях. На практике эта потенциальная проблема параллельного доступа к каталогу редко приводит к нарушениям в работе пользователей, поскольку выполняемые ими действия, как правило, не зависят от операций массовой вставки или удаления файлов. Фактически многие обычные операционные системы обнаруживают 24.19. Операции с каталогами 375
такое же поведение, как и система NFS, поэтому пользователям редко приходится изучать подробные сведения о выполнении в системе параллельных операций с каталогами, поскольку у них почти никогда не возникает такая необходимость. 24.21. Поддержка нескольких иерархий в сервере NFS Напомним, что для обеспечения возможности предоставлять дистанционный доступ к файлам с разнотипных компьютеров проектировщики системы NFS предусмотрели интерпретацию всех составных имен в клиентских программах. Это позволяет использовать и в клиенте, и в сервере собственную схему именования файлов, соответствующую применяемой в них операционной системе, и исключить необходимость закладывать в тот или иной программный компонент информацию об операционной среде другого компонента. Ограничение на использование полных составных имен практически не отражается на выполнении большинства операций с файлами. Однако при дистанционном доступе к файлам такое ограничение создает серьезную проблему, поскольку означает, что в клиентской программе нельзя применять полное составное имя для обозначения удаленной файловой системы или каталога. В ранних версиях протокола NFS предполагалось, что каждый сервер предоставляет доступ только к одной удаленной иерархии каталогов. Первоначальный протокол включал функцию NFSPROC_ROOT, которая могла быть вызвана в клиентской программе для получения дескриптора корневого каталога в иерархии каталогов сервера. После получения в клиентской программе дескриптора корневого каталога можно было выполнять чтение записей каталогов и следовать по произвольному пути через эту иерархию. Современные версии NFS позволяют использовать один сервер для предоставления дистанционного доступа к файлам, находящимся в нескольких иерархиях каталогов. В подобных случаях недостаточно иметь лишь процедуру, которая возвращает дескриптор единственного корневого каталога. Для обеспечения поддержки одним сервером нескольких иерархий в системе NFS должен использоваться дополнительный механизм, позволяющий клиенту указать одну из возможных иерархий каталогов и получить дескриптор ее корневого каталога. 24.22. Протокол монтирования В текущей версии NFS для решения задачи поиска корневого каталога применяется отдельный протокол, получивший название протокола монтирования и определенный с использованием средств дистанционного вызова процедур. Однако протокол монтирования не составляет часть программы дистанционного доступа NFS. Хотя он и требуется для сервера NFS, протокол монтирования поддерживается отдельной удаленной программой. Программное обеспечение протокола монтирования предоставляет четыре основных службы, которые требуются клиентам для работы с системой NFS. Во- первых, оно позволяет клиенту получить список иерархий каталогов (т.е. файловых систем), к которым клиент может получить доступ с помощью системы NFS. Во-вторых, оно принимает полные составные имена и позволяет указывать в клиентской программе конкретную иерархию каталогов. В-третьих, это программное обеспечение выполняет аутентификацию запросов каждого клиента и проверяет права клиента на доступ к затребованной иерархии. В-четвертых, оно возвращает дескриптор файла для корневого каталога иерархии, указанной клиентом. Клиент сохраняет дескриптор корневого каталога, возвращенный программным обеспечением протокола монтирования, и использует этот дескриптор при выполнении последующих запросов к серверу NFS. 376 7Глава 24. Принципы работы сетевой файловой системы (NFS)
Имя протокола монтирования и основные принципы работы были позаимствованы в системе UNIX; в этой операционной системе протокол монтирования применяется для монтирования удаленной файловой системы в собственном пространстве имен. В клиентской системе протокол монтирования используется для обеспечения взаимодействия с сервером и проверки прав доступа к удаленной файловой системе перед введением соответствующей точки монтирования в локальное иерархическое пространство имен. Если программное обеспечение протокола монтирования откажет в предоставлении доступа, то клиентское программное обеспечение сообщит об ошибке системному администратору. Если же программное обеспечение протокола монтирования разрешит доступ, то клиентское программное обеспечение сохранит дескриптор корневого каталога удаленной файловой системы, чтобы дать возможность использовать этот дескриптор при осуществлении попытки открыть файл в этой файловой системе. 24.23. Транспортные протоколы, применяемые в системе NFS Первоначальная версия системы NFS была разработана для использования в среде локальной сети. В связи с этим проектировщики в качестве транспортного протокола выбрали UDP. Основные преимущества UDP связаны с низкими издержками и меньшими затратами ресурсов операционной системы сервера; главным недостатком является его ненадежность. Для повышения надежности в протокол включены основные средства тайм-аута и повторной передачи запросов. Однако на практике при обмене данными по локальной сети потеря пакетов происходит редко. К сожалению, средства повторной передачи, предусмотренные в ранних версиях NFS, не подходят для работы в глобальной сети. Средства повторной передачи не являются адаптивными и не предусматривают применения средств устранения заторов или сквозного управления потоком данных. Но несмотря на недостатки этого протокола, в середине 90-х годов поставщики стали предлагать модифицированные версии системы NFS для использования в Internet; в большинстве модифицированных версий в качестве транспортного протокола применяется TCP. В связи с этим была разработана версия 3, предусматривающая использование транспортной службы с установлением логического соединения. В отличие от версии 2, в которой определяется предельно допустимый объем данных, передаваемых в отдельной операции, в версии 3 это ограничение ослаблено и допускается передача через соединение TCP всего файла. В результате появились реализации NFS, в которых применяется протокол TCP для обеспечения надежного доступа к файлам с компьютеров, подключенных к Internet. 24.24. Резюме Компания Sun Microsystems определила механизм дистанционного доступа к файлам, получивший название NFS, который фактически стал промышленным стандартом. Для обеспечения доступа к серверу многочисленных клиентов и защиты серверов от воздействий, связанных с нарушением работы клиентских программ, в системе NFS используются серверы, не поддерживающие состояние. Поскольку система NFS спроектирована для разнородной среды, то клиентская программа не может иметь информации о том, какой синтаксис составных имен файлов применяется на каждом из серверов. Для обеспечения способности к взаимодействию разнотипных систем спецификация NFS требует, чтобы клиент выполнял интерпретацию составных имен и поиск каждого отдельного компонента имени. После получения от клиента каждого запроса на поиск конкретного компонента сервер возвращает 64-байтовый дескриптор файла, который используется клиентом в последующих операциях в качестве ссылки на файл или каталог. 24.23. Транспортные протоколы, применяемые в системе NFS 377
Многие понятия и принципы организации файловой системы NFS позаимствованы из операционной системы UNIX. Система NFS поддерживает иерархическую систему каталогов, в ней файл рассматривается как последовательность байтов, допускается динамический рост размеров файлов, предоставляется последовательный или произвольный доступ, а также может быть получена почти такая же информация о файлах, как при использовании функции stat системы UNIX. Многие концептуальные операции с файлами, применяемые в NFS, определены по аналогии с операциями, которые поддерживаются в файловой системе UNIX. В системе NFS принят принцип "open-read-write-close" (открыть-читать- писать-закрыть), используемый в системе UNIX, а также поддерживаются такие же основные типы файлов и режимы защиты файлов. В сочетании с системой NFS применяется протокол монтирования, позволяющий предоставить с помощью единственного сервера NFS доступ к нескольким иерархиям каталогов. Программное обеспечение протокола монтирования обеспечивает проверку прав доступа и позволяет клиенту получить дескриптор корневого каталога конкретной иерархии каталогов. После получения дескриптора корневого каталога в клиентской программе могут использоваться процедуры NFS для доступа к файлам и каталогам в этой иерархии. В системе Linux, как и в других версиях UNIX, для установки точек монтирования удаленных иерархий каталогов в иерархическом пространстве имен используется протокол монтирования. Материал для дальнейшего изучения В статье [135] рассмотрена оригинальная файловая система UNIX и описана реализация методов доступа к файлам с использованием индексных узлов и дескрипторов. Функционирование файловой системы UNIX рассматривается в книге [6]. В статье [95] описана быстродействующая файловая система, на основе которой была создана операционная система Linux. В документе [17] представлен протокол NFS версии 3 и протокол монтирования; в этом документе даны определения большинства понятий, рассматриваемых в настоящей главе. В документе [140] изложены предварительные сведения по созданию версии 4 протокола NFS, а в документе [65] приведено соглашение о координации работ по созданию этой версии между компанией Sun Microsystems и IETF (Internet Engineering Task Force — Инженерная группа проектирования Internet). В документах [18], [19] и [20] рассматривается интеграция NFS и World Wide Web. Упражнения 24.1. Разработайте программу, которая получает от сервера NFS магический ключ. Воспользуйтесь этой программой для получения магического ключа для нескольких каталогов на нескольких серверах и выведите на печать их содержимое в шестнадцатиричном формате. Попытайтесь догадаться, какая информация содержится в ключе и как он закодирован сервером. 24.2. Воспользуйтесь сетевым анализатором для отслеживания сообщений, которыми обмениваются клиент и сервер NFS. Сколько пакетов передается при выполнении каждой из следующих операций: открытие файла, не находящегося на верхнем уровне каталога, чтение 10 байтов из файла и закрытие файла? 24.3. Предположим, что сеть, соединяющая между собой клиент и сервер NFS, доставляет пакеты с нарушением порядка следования. Какие 378 7Глава 24. Принципы работы сетевой файловой системы (NFS)
ошибки могут возникнуть в результате изменения порядка следования пакетов с запросами на выполнение операций NFS? 24.4. Предположим, что сеть, соединяющая между собой клиент и сервер NFS, может не только изменить порядок следования пакетов, но и продублировать эти пакеты. Какие ошибки могут возникать в результате дублирования и изменения порядка запросов на выполнение операций NFS? Сравните ваш ответ с ответом на предыдущее упражнение. Является ли дублирование пакетов предпосылкой возникновения дополнительных аварийных ситуаций? Объясните ваш ответ. 24.5. Изучите версии 3 и 4 спецификации NFS. В чем их основные различия? Предлагаются ли в версии 4 такие изменения, которые были бы заметны или важны для программистов? 24.6. Хотя система NFS была первоначально спроектирована как не поддерживающая состояние, механизм дескриптора файла вводит в протокол поддержку состояния. Могут ли дескрипторы файлов стать источником каких-либо проблем? Как можно решить эти проблемы? (Подсказка: многие реализации спецификации NFS требуют, чтобы на всех компьютерах с активными клиентскими программами была выполнена перезагрузка после аварийного завершения работы и перезагрузки компьютера файлового сервера.) •ажнения 379
25 Протоколы сетевой файловой системы (протокол NFS и протокол монтирования) 25.1. Введение В предыдущей главе рассматриваются понятия, лежащие в основе системы NFS (Network File System — Сетевая файловая система) компании Sun, и показано, что основная часть этих понятии и многие особенности реализации были взяты из файловой системы UNIX. В настоящей главе продолжается описание NFS с точки зрения реализованного в ней протокола1. Внимание акцентируется на том, что система NFS может быть определена как удаленная программа с использованием технологии ONC RPC и что каждая операция с файлом соответствует дистанционному вызову процедуры. 25.2. Применение средств дистанционного вызова процедур для определения протокола В главах с 21 по 23 показано, как может применяться технология RPC для разделения программы на компоненты, выполняемые на отдельных компьютерах. Многие программисты используют технологию RPC именно так, как описано в этих главах: они вначале разрабатывают нераспределенную программу, а затем с помощью средств RPC создают ее распределенную версию. В настоящей главе был выбран другой подход. В ней показано, как использовать средства RPC для определения протокола без привязки его к какой-либо конкретной программе. Основное различие между подходами, в которых RPC применяется для построения распределенной версии программы и для определения протокола общего назначения, связано с тем, какие вопросы при этом рассматриваются. При построении распределенной версии программы работа начинается с анализа существующей программы, которая включает и процедуры, и структуры данных, а при создании протокола работа начинается с формирования набора желаемых служб и определения абстрактных процедур для их поддержки. В процессе проектирования протокола необходимо продумать, как будут использоваться службы и как должны быть реализованы программы, предоставляющие эти службы. Проектировщик должен выбрать такое определение прото- 1 Сведения, приведенные в этой главе, относятся к версии 3 протокола.
кола, в котором соблюдается равновесие между точностью и общностью. Протокол должен быть достаточно точным для обеспечения взаимодействия между программами, которые его придерживаются, но в то же время достаточно общим, чтобы он мог допускать возможность создания целого ряда реализаций. Кроме того, проект протокола не может быть успешно разработан без учета многих дополнительных требований. Проектировщик может успешно составить спецификацию протокола только при том условии, что он хорошо знает все тонкости организации связи и имеет опыт в разработке таких проектов. Опыт обычно накапливается только в результате глубокого изучения компьютерных систем, прикладных программ и других протоколов связи. Поэтому проектирование протокола может оказаться гораздо сложнее, чем это представляется на первый взгляд. В частности, даже если такой протокол, как NFS, внешне кажется очень простым, это не означает, что его проектирование не потребует значительных усилий. Хотя технология RPC предоставляет удобный способ определения протокола, она не упрощает проектирование протокола, а также не гарантирует его эффективность. 25.3. Определение протокола с использованием структур данных и процедур Для определения протокола с использованием технологии RPC необходимо выполнить следующее: ¦ подготовить объявления для констант, типов и структур данных, применяемых в качестве параметров процедур или результатов выполнения функций; ¦ подготовить объявление для каждой удаленной процедуры с указанием параметров, результатов и назначения выполняемых ею действий; ¦ определить назначение каждой удаленной процедуры, указав, как в ней обрабатываются параметры и вычисляется возвращаемое значение. С теоретической точки зрения, в протоколе, который определен с использованием технологии RPC, сервер должен представлять собой отдельную удаленную программу. Запрос на выполнение операции, передаваемый клиентом серверу, соответствует дистанционному вызову процедуры, а сообщение, возвращаемое сервером клиенту, соответствует возврату управления из процедуры. Поэтому в любом протоколе, который определен с использованием технологии RPC, клиент должен быть инициатором всех операций, а сервер может только отвечать на отдельные запросы клиента. В случае применения NFS требование, чтобы инициатором каждой операции выступал клиент, действительно имеет смысл, поскольку протокол доступа к файлам может быть спроектирован как действующий под управлением клиентской программы. Сервер предоставляет процедуры, позволяющие клиенту создавать или удалять файлы, каталоги и символические ссылки; выполнять чтение или запись данных; проводить поиск в каталоге указанного файла; получать информацию о состоянии всей удаленной файловой системы или отдельного файла и каталога. Хотя другие прикладные протоколы не могут быть описаны с помощью спецификации RPC так же полно, как протокол NFS, опыт показывает, что большинство вариантов взаимодействия типа клиент/сервер может быть представлено на основе принципов дистанционного вызова процедур. Протокол NFS представляет собой интересный пример использования спецификации RPC, поскольку он является достаточно сложным и требует еще нескольких удаленных процедур и типов данных, но в то же время его можно назвать вполне понятным и концептуально простым для изучения. В следую- 382 Глава 25. Протоколы сетевой файловой системы...
щем разделе приведены примеры объявлений констант и типов, применяемых в протоколе NFS. За ним следуют разделы, в которых показано, как эти константы и типы используются в объявлениях процедур для определения параметров и результатов. 25.4. Объявления констант, типов и данных NFS Стандарт протокола NFS определяет имена констант, типов, а также структуры данных, применяемые во всех объявлениях процедур. Во всех объявлениях используется синтаксис объявлений RPC. 25.4.1. Константы NFS Спецификация NFS определяет основные константы, в которых заданы значения, применяемые в протоколе. Эти объявления представлены на языке RPC. Например, ниже показано объявление символической константы FSF3J3YMLINK со значением 2: const FSF3JYMLINK = 0x0002; Кроме основных констант, протокол определяет также перечислимый набор констант, используемый для формирования сообщений об ошибках. После каждого дистанционного вызова процедуры возвращается одно из этих значений результатов. Этот набор носит имя nf sstat3 и объявлен следующим образом: enum nfsstat3 { NFS3_0K =0 /* Успешный вызов */ NFS3ERR_PERM =1, /* Пользователь не является владельцем файла *./ или возникла другая ошибка */ 2, /* Файл не существует */ Ошибка устройства ввода/вывода */ Устройство или адрес не существует */ Доступ запрещен */ Указанный файл уже существует */ Попытка создать жесткую ссылку на */ другое устройство */ Указанное устройство не существует */ Указанный элемент не является каталогом */ Указанный элемент является каталогом */ Недопустимый параметр */ Файл слишком велик для сервера */ На устройстве (диске) отсутствует */ свободное место */ Запись в файловой системе, допускающей */ только чтение */ Слишком много жестких ссылок */ Файл имел слишком длинное имя */ Каталог не пуст */ Превышена квота дискового пространства */ Устаревший дескриптор файла */ Слишком много компонентов в составном */ имени удаленного файла */ Поврежденный/недопустимый дескриптор файла */ Нарушение синхронизации, установленной */ функцией setattr */ Устаревший магический ключ */ Операция не поддерживается */ NFS3ERR_N0ENT NFS3ERR 10-5, NFS3ERR~NXI0 = 6, NFS3ERR~ACCES = 13, NFS3ERR~EXIST = 17, NFS3ERRJCDEV =18, NFS3ERR NODEV =19, NFS3ERR~N0TDIR = 20, NFS3ERR~ISDIR = 21, NFS3ERR~INVAL = 22, NFS3ERR~FBIG = 27, NFS3ERRJTOSPC = 28, NFS3ERR_R0FS = 30, NFS3ERR MLINK = 31, NFS3ERR~NAMET00L0NG = 63, NFS3ERR~N0TEMPTY = 66, NFS3ERR~DQU0T= 69, NFS3ERRJTALE = 70, NFS3ERR_REM0TE = 71, NFS3ERR BADHANDLE = 10001, NFS3ERR~NOT_SYNC = 10002, NFS3ERR BAD COOKIE = 10003, NFS3ERR~N0TSUPP = 10004, 25.4. Объявления констант, типов и данных NFS 383
NFS3ERRJT00SMALL = 10005, /* Буфер или запрос слишком мал */ NFS3ERR~SERVERFAULT ¦ 10006, /* Ошибка сервера, отличная от */ /* перечисленных выше */ NFS3ERR_BADTYPE = 10007, /* Требуемый тип не поддерживается */ NFS3ERR_JUKEBOX = 10008 /* Клиент должен повторно выдать запрос */ У, Каждый из этих кодов ошибок имеет смысл в контексте определенного вызова. Например, если клиентская программа предпринимает попытку применить к обычному файлу операцию, которая должна выполняться с каталогом, сервер возвращает код ошибки NFS3ERR_NOTDIR, а если к каталогу применяется операция, предназначенная для работы с обычным файлом, сервер возвращает код ошибки NFS3ERR_ISDIR. 25.4.2. Объявления typedef спецификации NFS В целях упрощения структуры объявлений в стандарте протокола NFS определены имена для типов, которые затем используются во многих структурах. Например, тип filename3 определен как массив символов, достаточно большой для размещения составного имени файла. В синтаксисе RPC для объявления массива символов служит ключевое слово string. Поэтому для указания на то, что dirpath представляет собой имя типа, которое может использоваться для объявления переменных, содержащих полное имя файла, в программе должно быть предусмотрено следующее определение: typedef string dirpath<MNTPATHLEN>; Аналогичным образом, стандарт определяет тип fhandle3 как 64-байтовый массив, содержащий дескриптор файла. Этот тип объявлен как непрозрачный, поскольку в клиентской программе его внутренняя структура не анализируется: typedef opaque fhandle3<FHSIZE3>; 25.4.3. Структуры данных NFS После определения констант и типов в процессе проектирования протокола можно перейти к определению типов всех применяемых структур данных. В спецификации протокола NFS соблюдается соглашение, в соответствии с которым все параметры дистанционного вызова процедуры должны быть объединены в одну структуру. Поэтому стандарт определяет структуру, применяемую в качестве параметра, для каждой удаленной процедуры, а также отдельную структуру для каждого результата выполнения процедуры. Кроме того, в стандарте определено несколько структур, совместно используемых в нескольких процедурах. Например, в главе 24 описана структура fattr3, которая в спецификации NFS служит для определения атрибутов файла. Состав структуры fattr3 определен с учетом данных, представленных структурой stat. В некоторых полях структуры fattr3 регистрируется время последнего изменения файла или доступа к файлу. Такие поля объявлены как принадлежащие к типу nf stime3, который представляет следующую структуру: struct nfstime3 { /* Значения даты и времени, используемые в NFS */ uint32 seconds; /* Число секунд, прошедших с начала эпохи */ /* A января 1970 года) */ uint32 nseconds; /* Число дополнительных наносекунд */ >; Согласно этому объявлению, спецификация NFS предусматривает хранение значений времени в виде двух 32-битовых целых чисел. Первое целое число 384 Глава 25. Протоколы сетевой файловой системы...
представляет число секунд, прошедших с начала эпохи, а второе целое число содержит дополнительное значение, выраженное в наносекундах (что позволяет повысить точность представления времени). В системе NFS для измерений значений времени в качестве даты начала эпохи используется 1 января 1970 года2. Остальные объявления в основном служат для определения типов параметров, передаваемых удаленной процедуре, или результатов, возвращаемых процедурой. Например, при вызове процедуры, которая выполняет операцию в каталоге (например, удаление файла), клиент должен передать имя файла. В спецификации NFS соответствующий тип параметра определен как структура diropargs3 (сокращение от directory operation arguments — параметры операции с каталогом, версия 3): struct diropargs3 { /* Параметры операции с каталогом */ nfsJEh3 dir; /* Дескриптор каталога, в котором находится файл */ filename3 name; /* Имя файла в этом каталоге */ >; Эта структура показывает, что параметр включает в себя дескриптор файла для данного каталога и имя файла в каталоге. Чтобы понять, с чем связана необходимость в применении структуры diropargs3, напомним, что клиент NFS раскладывает все составные имена на компоненты. Поэтому в клиентской программе для обозначения файла не может применяться полное составное имя. Вместо этого спецификация NE3 требует, чтобы все операции с файлами были определены с указанием дескриптора каталога, в котором находится файл, и имени файла в этом каталоге. Кроме объявлений типов параметров, стандарт определяет типы, возвращаемые удаленными процедурами. Объявление операции, выполняющей чтение из каталога, принадлежит к числу наиболее сложных, поскольку такая операция возвращает список записей, каждая из которых обозначает файл. Любая запись в этом списке имеет следующую форму: struct entry3{ fileid3 fileid; /* Идентификатор текущего файла */ ШепатеЗ пате; /* Имя файла */ cookie3 cookie; /* Магический ключ для данной записи */ entry3 *nextentry; /* Указатель на следующую запись */ >? Ниже приведено объявление, в котором указано, что dirlist3 представляет собой заголовок списка записей. Логическое значение eof служит для указания того, содержит ли список последние элементы в каталоге (т.е. позволяет определить, имеются ли еще элементы каталога, предназначенные для чтения): struct dirlist3 { /* Список записей каталога */ entry3 *entries; /* Указатель на следующую запись */ bool eof; /* Принимает истинное значение, если список содержит */ /* последнюю запись в каталоге */ }? После объявления списка записей перечисленные выше объявления могут использоваться для определения типов значений, возвращаемых в случае успешного или неудачного выполнения операции чтения каталога READDIR: struct READDIR3resok { /* Значение, возвращаемое в случае успешного */ /* выполнения */ 2 Разработчики спецификации NFS приняли решение о том, что начало эпохи отсчета времени NFS должно совпадать с началом эпохи, которое определено в системе UNIX. 25.4. Объявления констант, типов и данных NFS 385
post_op_attr dir_attributes; /* Атрибуты каталога */ cookieverf3 cookieverf; /* Магический ключ, полученный при */ /* предыдущем вызове */ dirlist3 reply; /* Список записей каталога */ }? struct READDIR3resfail { /* Значение, возвращаемое в случае */ /* неудачного выполнения */ post_op_attr dir_attributes; /* Атрибуты каталога */ }? И наконец, для объявления одного типа, который описывает результаты и успешного, и неудачного завершения, может применяться размеченное объединение: union READDIR3res switch (nfsstat3 status) { /* Возвращаемое значение */ case NFS3_0K: /* Применяется, если status == NFS3_0K */ READDIR3resok resok; default: /* Применяется, если status != NFS3__0K */ READDIR3resfail resfail; }; Кроме объявлений, позволяющих представить результаты выполнения всех процедур, стандарт включает объявления параметров. Например, для процедуры чтения требуется три параметра: дескриптор файла, смещение в файле, с которого начинается чтение, и число считываемых октетов: struct READ3args { /* Параметры процедуры чтения READ */ nfs_fh3 file; /* Дескриптор файла, предназначенного для чтения */ offset3 offset; /* Смещение в файле */ count3 count; /* Число считываемых символов */ }? Аналогичным образом, структура WRITE3args определяет параметры, применяемые при вызове функции записи в файл: struct WRITE3args { /* Параметры процедуры записи WRITE */ nfs_fh3 file; /* Дескриптор файла, предназначенного для записи */ offset3 offset; /* Смещение в файле */ count3 count; /* Число записываемых символов */ stablejiow stable; /* Выводить данные на диск перед возвратом управления */ /* из процедуры? */ opaque data<>; /* Записываемые данные */ }? Процедура READLINK позволяет прочитать в клиентской программе содержимое символической ссылки (ярлыка). Ее результаты определены структурой READLINK3resok: struct READLINK3resok{ post_op_attr symlink_attributes; /* Атрибуты ссылки */ nfspath3 data; /* Составное имя, содержащееся */ /* в символической ссылке */ }; 25.5. Процедуры NFS После объявления констант и типов данных остается только определить удаленные процедуры, реализующие данный протокол. Сервер NFS предоставляет доступ к 386 Глава 25. Протоколы сетевой файловой системы...
удаленной программе, которая реализует 21 процедуру (не считая пустой процедуры NULL). Эта программа может быть объявлена на языке RPC следующим образом: program NFS PROGRAM { version NFS VERSION { void NFSPR0C3JNULL(void) =0; GETATTR3res NFSPROC3JSETATTR(GETATTR3args) - 1; SETATTR3res NFSPROC3JETATTR(SETATTR3args) = 2; L00KUP3res NFSPR0C3 LOOKUP(LOOKUP3args) = 3; ACCESS3res NFSPROC3~ACCESS(ACCESS3args) = 4; READLINK3r.es NFSPROC3_READLINK(READLINK3args) = 5; READ3res NFSPROC3_READ(READ3args) = 6; WRITE3res NFSPROC3_WRITE(WRITE3args) = 7; CREATE3res NFSPROC3_CREATE(CREATE3args) =8; MKDIR3res NFSPROC3_MKDIR(MKDIR3args) = 9; SYMLINK3res NFSPROC3_SYMLINK(SYMLINK3args) = 10; MKN0D3res NFSPROC3_MKNOD(MKNOD3args) =11; REM0VE3res NFSPROC3 REMOVE(REMOVE3args) =12; RMDIR3res NFSPROC3_RMDIR(RMDIR3args) =13; RENAME3res NFSPROC3_RENAME(RENAME3args) =14; LINK3res NFSPROC3_LINK(LINK3args) =15; READDIR3res NFSPROC3_READDIR(READDIR3args) = 16; READDIRPLUS3res NFSPROC3_READDIRPLUS(READDIRPLUS3args) =17; FSSTAT3res NFSPROC3_FSSTAT(FSSTAT3args) =18; FSINF03res NFSPR0C3 FSINF0(FSINF03args) = 19; PATHC0NF3res NFSPROC3_PATHCONF(PATHCONF3args) = 20; C0MMIT3res NFSPROC3_COMMIT(COMMIT3args) =21; } = 3; /* Текущая версия протокола NFS */ } = 100003; /* Номер программы RPC, назначенный программному обеспечению NFS */ 25.6. Назначение операций NFS Основная часть операций NFS предназначена для выполнения таких же операций, какие осуществляются с использованием файловых операций в системах типа Linux. В каждом из следующих разделов описано назначение одной из удаленных процедур NFS. 25.6.1. NFSPROC3_NULL (процедура 0) В соответствии с общепринятым соглашением, процедура 0 в любой программе RPC является пустой, поскольку не выполняет никаких действий. Такая процедура может быть вызвана в приложении для проверки того, отвечает ли на запросы определенный сервер. 25.6.2. NFSPROC3_GETATTR (процедура 1) Процедура 1 вызывается в клиентской программе для получения атрибутов файла, которые включают такие элементы, как режим защиты, владелец, размер и время последнего доступа. 25.6.3. NFSPROC3_SETATTR (процедура 2) Процедура 2 позволяет установить в клиентской программе некоторые атрибуты файла. Клиентом могут быть установлены не все атрибуты (например, в клиентской программе невозможно изменить зарегистрированный размер файла, 25.6. Назначение операций NFS 387
кроме как путем добавления байтов к файлу или его усечения). В случае успешного выполнения этого вызова возвращаемый результат содержит атрибуты файла после внесения изменений. 25.6.4. NFSPROC3_LOOKUP (процедура 3) В клиентской программе процедура 3 может быть вызвана для поиска файла в каталоге. В случае ее успешного выполнения возвращаемое значение содержит дескриптор указанного файла и его атрибуты. 25.6.5. NFSPROC3_ACCESS (процедура 4) Процедура 4 вызывается в клиентской программе для проверки того, может ли быть получен доступ к элементу данных без передачи всего элемента. 25.6.6. NFSPROC3_READLINK (процедура 5) Процедура 5 позволяет прочитать в клиентской программе значение из символической ссылки (ярлыка). 25.6.7. NFSPROC3_READ (процедура 6) Процедура 6 обеспечивает выполнение одной из наиболее важных функций, поскольку позволяет читать данные из файла в клиентской программе. Результат, возвращаемый сервером, представляет собой объединение. В случае успешного выполнения этой операции результат содержит атрибуты файла, а также затребованные данные. Если операция оканчивается неудачей, то значение кода состояния содержит код ошибки. 25.6.8. NFSPROC3.WRITE (процедура 7) Процедура 7 обеспечивает выполнение еще одной важной функции; она позволяет осуществить в клиентской программе запись данных в удаленный файл. Результат вызова этой процедуры представляет собой объединение, которое содержит либо код ошибки, если операция завершилась неудачей, либо атрибуты файла в случае успешного выполнения операции. 25.6.9. NFSPROC3.CREATE (процедура 8) В клиентской программе процедура 8 вызывается для создания файла в указанном каталоге. Этот файл не должен существовать перед вызовом процедуры, так как в противном случае вызов возвратит код ошибки. Результат вызова представляет собой объединение, которое содержит либо информацию об ошибке, либо дескриптор нового файла вместе с его атрибутами. 25.6.10. NFSPROC3_MKDIR (процедура 9) В клиентской программе процедура 9 вызывается для создания нового каталога (папки) на сервере. В случае успешного выполнения этой операции процедура возвращает дескриптор нового каталога наряду со списком его атрибутов, а в случае неудачного выполнения вызова возвращаемое значение информации о состоянии указывает на причину ошибки. 25.6.11. NFSPROC3_SYMLINK (процедура 10) Процедура 10 позволяет создать символическую ссылку (ярлык). Параметры ее вызова должны включать дескриптор каталога и имя создаваемого файла, а также стро- 388 Глава 25. Протоколы сетевой файловой системы...
ку, которая станет содержимым символической ссылки. Сервер создает символическую ссылку, а затем возвращает информацию о состоянии, которая либо указывает на успешное завершение операции, либо сообщает причину неудачи. В случае успешного выполнения процедура возвращает дескриптор файла для вновь созданной ссылки. 25.6.12. NFSPROC3JVIKNOD (процедура 11) В клиентской программе процедура 11 вызывается для создания на сервере специального файла. Параметры ее вызова должны указывать тип специального файла (например, сокет), а процедура возвращает дескриптор нового файла. 25.6.13. NFSPROC3_REMOVE (процедура 12) В клиентской программе процедура 12 вызывается для удаления существующего файла. Результаты выполнения вызова содержат информацию о состоянии. Эта информация либо указывает, что операция была выполнена успешно, либо содержит код ошибки, позволяющий определить причины неудачи. 25.6.14. NFSPROC3_RMDIR (процедура 13) В клиентской программе процедура 13 может применяться для удаления каталога. Допускается уничтожение только пустых каталогов. Поэтому для удаления целого поддерева в клиентской программе необходимо пройти по всему поддереву, удалить все файлы, а затем удалить оставшиеся пустые каталоги. Обычно удаление файлов и пустых каталогов выполняется в одной операции с использованием алгоритма обхода дерева каталогов в обратном порядке. 25.6.15. NFSPROC3_RENAME (процедура 14) Процедура 14 позволяет переименовать файл в клиентской программе. Поскольку параметры этой процедуры позволяют указать в клиентской программе не только новое имя файла, но и новый каталог, эта операция переименования соответствует команде mv (сокращение от move— переместить). Спецификация NFS гарантирует, что операция переименования на сервере будет выполнена как атомарное действие (т.е. не может быть прервана). Обеспечение атомарности действия является очень важным, поскольку оно означает, что прежнее имя файла не будет удалено до тех пор, пока в каталоге не появится новое имя. Поэтому у пользователя не создается впечатление, что файл на время выполнения операции переименования исчезает с диска. 25.6.16. NFSPROC3J.INK (процедура 15) В клиентской программе процедура 15 вызывается для формирования жесткой ссылки на существующий файл. Спецификация NFS гарантирует, что даже при наличии нескольких жестких ссылок файл всегда имеет одинаковые атрибуты, независимо от того, какая ссылка применяется для доступа к нему. 25.6.17. NFSPROC3_READDIR (процедура 16) В клиентской программе процедура 16 вызывается для чтения записей из каталога. Параметры этой процедуры указывают дескриптор просматриваемого каталога, магический ключ и максимальное число считываемых символов. При первоначальном вызове в клиентской программе задается магический ключ, содержащий значение нуль, что приводит к выполнению сервером чтения записей с начала каталога. Возвращаемое значение содержит связанный список, содер- 25.6. Назначение операций NFS 389
жащий от нуля и более записей каталога, и логическое значение, которое указывает, находится ли последняя возвращенная запись в конце каталога. После успешного возврата управления из процедуры каждая запись каталога в связанном списке содержит имя файла, уникальный идентификатор файла, магический ключ, который указывает позицию файла в каталоге, и указатель на следующую запись в списке. Для чтения последовательности записей в каталоге работа клиентской программы начинается с вызова процедуры NFSPR0C3_READDIR со значением магического ключа, равным нулю, и числом символов, равным размеру внутреннего буфера. Сервер возвращает число записей каталога, которое может поместиться в указанном буфере. Клиент проходит по циклу через список записей и обрабатывает каждое имя файла. Если возвращаемое значение показывает, что был достигнут конец каталога, клиент прекращает обработку. В ином случае, клиент использует магический ключ в текущей записи для выполнения на сервере еще одного вызова процедуры NFSPR0C3_READDIR и получения дополнительных записей. Клиент продолжает чтение групп записей до достижения конца каталога. 25.6.18. NFSPROC3_READDIRPLUS (процедура 17) Процедура 17 действует аналогично процедуре 16, за исключением того, что возвращает не просто имя и дескриптор каждого файла, а полную информацию о файле. Если в клиентской программе необходимо получить все сведения о каждом файле в каталоге (например, о его атрибутах), то процедура NFSPR0C3_READDIRPLUS позволяет ускорить работу по сравнению с выполнением для той же цели вызовов процедур NFSPR0C3_READDIR и NFSPR0C3_GETATTR. 25.6.19. NFSPROC3_FSSTAT (процедура 18) Процедура 18 позволяет получить в клиентской программе информацию об удаленной файловой системе, в которой находится файл. Возвращаемый результат содержит поля с такой информацией, как общий размер файловой системы и максимальное число файлов. Кроме того, процедура сообщает объем свободного пространства на диске и число свободных записей в каталоге. 25.6.20. NFSPROC3_FSINFO (процедура 19) Процедура 19 возвращает информацию, которая может использоваться в клиентской программе для оптимизации обмена данными. Например, в полученных результатах указаны максимальные и предпочтительные размеры блоков информации, передаваемых по запросам READ, WRITE и READDIR. Кроме того, в ответе сервера должно быть указано, поддерживаются ли им такие средства, как символические ссылки. 25.6.21. NFSPROC3_PATHCONF (процедура 20) В клиентской программе процедура 20 применяется для получения с сервера информации о конфигурации составного имени, которая включает сведения о том, какую максимальную длину может иметь компонент имени файла, являются ли имена чувствительными к регистру и усекает ли система имена, длина которых превышает максимальную. 25.6.22. NFSPROC3_COMMIT (процедура 21) Процедура 21 вынуждает сервер выполнить сброс его буферов. Это означает, что в результате выполнения этой операции сервер должен сохранить в энерго- 390 Глава 25. Протоколы сетевой файловой системы...
независимой памяти все данные, ранее переданные для записи в файл. Таким образом, после возврата управления процедурой NFSPR0C3_C0MMIT клиентская программа получает подтверждение того, что данные, переданные на сервер, останутся на нем даже после перезагрузки. 25.7. Протокол монтирования Протокол монтирования, описанный в главе 24, также определен с применением технологии RPC. Хотя сервер NFS всегда используется в сочетании с сервером монтирования, эти два серверных компонента должны быть определены как отдельные удаленные программы. Поэтому в стандарте протокола монтирования указаны константы, типы и набор удаленных процедур, которые входят в состав сервера. 25.7.1. Определения констант протокола монтирования Хотя оба указанных протокола определены отдельно, значения многих констант и типов, заданных для протокола монтирования, соответствуют константам, применяемым в протоколе NFS. Действительно, оба эти протокола не могли бы успешно взаимодействовать, если бы в них не использовались одинаковые представления для таких объектов, как дескрипторы файлов. Например, в протоколе монтирования размеры имени файла, составного имени и дескриптора файла определены следующим образом: const MNTPATHLEN = 1024; /* Максимальное число символов в составном имени */ const MNTNAMLEN = 255; /* Максимальное число символов в имени */ const FHSIZE3 =64; /* Число октетов в дескрипторе файла NFS */ В этих объявлениях используется синтаксис RPC; в каждом из них длина выражается числом байтов. 25.7.2. Определения типов монтирования Протокол монтирования содержит также определения типов, которые соответствуют их аналогам, применяемым в протоколе NFS. Например, в протоколе монтирования определен следующий тип дескриптора файла: typedef opaque fhandle3<FHSIZE3>; Аналогичным образом, в протоколе монтирования указано, что составное имя представляет собой массив символов: typedef string dirpath<MNTPATHLEN>; 25.7.3. Структуры данных монтирования Поскольку протокол монтирования был определен с использованием технологии RPC, он соответствует общепринятому соглашению по объявлению структуры для параметров и результатов каждой удаленной процедуры. Например, одна из основных процедур этого протокола возвращает дескриптор файла для корневого каталога в именованной иерархии. Возвращаемое значение представляет собой объединение mountres3, объявленное следующим образом: union mountres3 switch(mountstat3fhs_status){ case MNT_0K: /* В случае успешного выполнения */ mountres3_ok mountinfo; /* информация об указанном корневом каталоге */ default: /* В ином случае */ void? /* пустое значение */ }? 25.7. Протокол монтирования 391
Как и в протоколе NFS, каждая удаленная процедура в протоколе монтирования возвращает информацию о состоянии наряду с другой информацией. В случае неудачного завершения операции информация о состояний указывает причину неудачи. Кроме процедуры, возвращающей дескриптор файла, в протоколе монтирования предусмотрена процедура, позволяющая определить в клиентской программе, какие файловые системы предназначены для доступа. Процедура возвращает результаты в виде связанного списка, который называется списком экспорта9. Тип узла в списке экспорта объявлен как структура exportnode; определен также тип указателя для этой структуры — exports: typedef struct exportnode *exports; struct exportnode { /* Список доступных иерархий */ dirpath exjiir; /* Составное имя пути для этой иерархии */ groups ex_groups; /* Группы, которым разрешен доступ к ней */ exports exjiextj /* Указатель на следующий элемент в списке */ }; Поле ex_groups узла exportnode содержит указатель на связанный список, который определяет, какие группы защиты имеют право доступа к именованной иерархии. Узлы в списке заданы как экземпляры структуры типа groupnode, a тип groups представляет собой указатель на узел groupnode: typedef struct groupnode *groups; struct groupnode { /*. Список имен групп */ name gr_name; /* Имя одной группы */ groups grjiext; /*¦ Указатель на следующий элемент в списке */ У Протокол монтирования позволяет также определить в клиентской программе, к каким удаленным файловым системам на указанном компьютере происходит доступ. Это дает возможность построить с помощью системы NFS список перекрестных ссылок для ряда компьютеров. Для этой цели каждому компьютеру из списка передается запрос на получение перечня удаленных файловых систем, к которым имеет доступ данный компьютер. Следует отметить, что перечень удаленных файловых систем, доступных для данного компьютера, не совпадает с перечнем локальных файловых систем, экспортируемых компьютером в целях предоставления доступа к ним с других компьютеров. Ответ с перечнем доступных файловых систем, формируемый программным обеспечением протокола монтирования, представляет собой связанный список, каждый узел которого имеет тип mountbody: typedef struct mountbody *mountlist; struct *mountbody { /* Список удаленных монтируемых файловых систем */ name mljiostname; /* Компьютер, на котором находятся файлы */ dirpath ml^directory; /* Составное имя корневого каталога */ /* иерархии каталогов */ mountlist ml_nextentry; /* Указатель на следующий элемент в списке */ У Термин "экспорт" подчеркивает аналогию с тем, что сервер экспортирует часть своих файлов на другие компьютеры. 392 Глава 25. Протоколы сетевой файловой системы...
25.8. Процедуры протокола монтирования В протоколе монтирования, как и в протоколе NFS, все операции определены как вызовы процедуры в удаленной программе. Ниже приведены объявления программы монтирования в формате RPC: program M0UNT_PR0GRAM { version M0UNT_V3 { void M0UNTPR0C3_NULL(void) = 0; mountres3 M0UNTPR0C3 MNT(dirpath) = 1; mountlist M0UNTPR0C3~DUMP(void) = 2; void M0UNTPR0C3_UMNTidirpath) = 3; void M0UNTPR0C3JJMNTALL(void) =4; exports M0UNTPR0C3_EXP0RT(void) = 5; } ¦ 3; /* Версия 3 протокола монтирования соответствует версии 3 */ /* протокола NFS */ } = 100005; /* Номер программы RPC, назначенный программному обеспечению */ /* протокола монтирования */ 25.9. Назначение операций монтирования Назначение каждой из операций, перечисленных выше, определено в протоколе монтирования, а в следующих разделах вкратце описана каждая из этих операций. 25.9.1. MOUNTPROC3_NULL (процедура 0) В соответствии с общепринятым соглашением технологии RPC, процедура О не выполняет никаких действий. 25.9.2. MOUNTPROC3JVINT (процедура 1) В клиентской программе процедура 1 вызывается для получения дескриптора корневого каталога конкретной иерархии. Параметр вызова процедуры содержит составное имя, применяемое сервером для определения одной из иерархий, экспортируемых им для доступа по сети; возвращаемый результат имеет тип fhstatus. Имена иерархий, доступных на конкретном сервере, можно получить с использованием вызова M0UNTPR0C3_EXP0RT (см. ниже), 25.9.3. MOUNTPROC3.DUMP (процедура 2) Процедура 2 позволяет получить в клиентской программе список удаленных файловых систем, которые применяются на конкретном компьютере. Информация, предоставляемая процедурой M0UNTPR0C3J)UMP, редко используется в стандартных приложениях; она предназначена для системных администраторов. 25.9.4. MOUNTPROC3JJMNT (процедура 3) В клиентской программе процедура 3 применяется для передачи другому компьютеру информации о том, что доступ к службе на время прекращается. Например, если на компьютере А смонтированы одна или несколько файловых систем, предоставляемых сервером на компьютере В9 то в программе на компьютере В процедура MOUNTPR0C3JJMNT может использоваться для передачи на компьютер А информации о том, что конкретная файловая система будет какое-то время не доступна (например, на время обслуживания диска). Это позволяет ис- 25.8. Процедуры протокола монтирования 393
ключить необходимость отправки с компьютера А на компьютер В дополнительных запросов, пока файлы находятся в автономном режиме. 25.9.5. MOUNTPROC3_UMNTALL (процедура 4) Процедура 4 позволяет передать с одного компьютера на другой сообщение о том, что все его файловые системы NFS будут недоступными. Например, сервер может передать клиентам сообщение о том, что все его файловые системы должны быть размонтированы в связи с предстоящей перезагрузкой. 25.9.6. MOUNTPROC3_EXPORT (процедура 5) Процедура 5 предоставляет доступ к важной службе: она позволяет получить в клиентской программе имена всех иерархий, доступных на указанном сервере. В результате вызова этой процедуры возвращается связанный список, содержащий по одному узлу типа exportlist (список экспорта) для каждой доступной файловой системы. Клиент должен использовать одно из составных имен каталога, находящихся в этом списке экспорта, при вызове процедуры M0UNTPR0C3__MNT (процедура 1). 25.10. Протокол NFS и средства аутентификации протокола монтирования Протокол NFS предусматривает использование протокола монтирования для аутентификации запросов. Протокол монтирования выполняет проверку подлинности запросов каждого отдельного клиента на получение дескриптора корневого каталога, а протокол NFS не обеспечивает проверку подлинности каждого отдельного клиентского запроса. Любопытно отметить, что протокол монтирования не обеспечивает высокий уровень защиты. В нем для аутентификации клиента используется функция AUTH_N0NE протокола RPC. Однако после получения клиентом дескриптора корневого каталога средства защиты отдельных файлов почти не играют никакой роли. Например, после получения программистом широких прав доступа к своей собственной рабочей станции, он может также получить возможность обращаться к произвольным файлам не только на своем компьютере, но и на сервере NFS. Кроме того, если программист сможет догадаться, как устроен непрозрачный дескриптор файла, он получит возможность самостоятельно строить дескрипторы для произвольных каталогов сервера. В целях усложнения задачи расшифровки структуры дескрипторов, во многих версиях NFS информация о файле или каталоге, содержащаяся в дескрипторе, закодирована. Эта информация размещается в отдельных полях, чтобы невозможно было установить очевидную связь между дескриптором для каталога и дескрипторами для файлов в этом же каталоге. В ранних версиях NFS применялись поля постоянной длины для обеспечения эффективной интерпретации дескрипторов. Например, как показано в табл. 25.1, в одной из реализаций NFS дескриптор состоял из десяти полей; дескрипторы файлов строятся таким образом не на всех серверах. Таблица 25.1. Назначение полей в дескрипторе файла NFS на сервере NFS, работающем под управлением операционной системы Berkeley UNIX Поле < Размер Назначение fileid 4 Старший и младший номера устройства, соответствующего файлу one 1 Всегда равно 1 394 Глава 25. Протоколы сетевой файловой системы...
iengthl 2 Общая длина следующих трех полей zerol 2 Всегда равно О Окончание табл. 25.1 Поле Размер Назначение inode igener length2 zero2 rinode 4 4 2 2 4 Номер индексного узла, соответствующего файлу Номер поколения индексного узла файла (для защиты от несанкционированного доступа в это поле записывается случайное число) Общая длина следующих трех полей Всегда равно О Номер индексного узла, соответствующего корневому каталогу файловой системы rigener 4 Номер поколения индексного узла корневого каталога (для защиты от несанкционированного доступа в это поле записывается случайное число) Основная опасность использования формата с полями постоянной длины для дескриптора файлов связана с тем, что протокол NFS почти не обеспечивает защиту от несанкционированного доступа. Программист, желающий получить доступ к файлам из клиентской программы, может обойти протокол монтирования, составив вручную дескриптор для произвольного файла. Затем он может получить информацию об атрибутах этого файла, включая данные о владельце файла и битах защиты. Таким образом, он сможет получить доступ к файлам, предназначенным для чтения всеми пользователями локального компьютера, даже если протокол монтирования не разрешает доступ. Кроме того, если клиент имеет права пользователя root на локальном компьютере, он сможет передавать серверу запросы, которые содержат произвольные пользовательские идентификаторы. Таким образом, если клиент имеет права привилегированного пользователя root на локальном компьютере, он сможет отправлять на сервер запросы, содержащие произвольные идентификаторы пользователей. Поэтому пользователь клиентской программы с правами привилегированного пользователя может вначале узнать, кто является владельцем конкретного файла, а затем отправить запрос, выдавая себя за этого владельца. В целях усложнения задачи расшифровки структуры дескрипторов файлов многие системные администраторы используют утилиту, позволяющую заменить номера поколений индексных узлов случайными числами. Эта утилита проходит по всем индексным узлам диска и записывает в поле номера поколения индексного узла случайное число. В протоколе монтирования номер поколения применяется при передаче клиенту дескриптора файла, а программное обеспечение NFS проверяет, совпадает ли значение в дескрипторе с номером поколения, находящимся в индексном узле. Замена номеров поколения случайными числами повышает уровень защиты, но не приводит к увеличению вычислительных издержек, связанных с формированием дескриптора, поскольку замена случайными числами выполняется до запуска на выполнение программного обеспечения протокола. Однако замена номеров поколения случайными числами намного усложняет расшифровку структуры дескрипторов файлов. Клиент, пытающийся получить несанкционированный доступ, должен выбрать одно из 232 возможных значений номера поколения. Поскольку 25.10. Протокол NFS и средства аутентификации протокола монтирования 395
вероятность угадывания действительного дескриптора невелика, вероятность получения несанкционированного доступа к файлу также является весьма низкой. 25.11. Блокировка файла Поскольку программное обеспечение NFS версии 2 и последующих версий не поддерживает состояния, для блокировки файла используется дополнительный протокол, известный под названием NLM (Network Lock Manager — Сетевой диспетчер блокировки). Он позволяет клиенту получить в исключительное использование файл, смонтированный в системе NFS (т.е. заблокировать этот файл), а затем освободить блокировку. Хотя основной принцип блокировки файла не меняется, начиная с версии 2 программного обеспечения NFS, многие подробности использования механизма блокировки изменились. В частности, расширены определения всех процедур NLM для включения номера версии протокола, что позволяет исключить путаницу между старыми и новыми версиями. К сожалению, номера версий NLM не соответствует номерам версий NFS. Версии 1 или 3 протокола NLM могут использоваться с версией 2 протокола NFS, a для версии 3 протокола NFS требуется версия 4 протокола NLM. К числу самых существенных изменений, связанных с внедрением версии 4 протокола NLM, относится увеличение полей с обозначением длины файла и смещения с 32 до 64 битов. 25.12. Изменения в протоколе NFS, связанные с переходом от версии 3 к версии 4 Хотя между версиями 3 и 4 протокола NFS есть много небольших различий, основная часть изменений касается защиты. После ее окончательного завершения4 версия 4 позволит лучше выполнять аутентификацию операций обмена данными, чтобы можно было исключить возможность несанкционированного доступа. Эта версия позволит также шифровать передаваемые данные для обеспечения конфиденциальности передаваемых данных. 25.13. Резюме При использовании технологии RPC для определения протокола необходимо подготовить определения для всех констант и типов данных, применяемых в спецификации протокола, определения процедур, предоставляемых сервером, типов всех параметров и результатов процедур, а также указать назначение каждой процедуры. Определение протокола с помощью RPC представляет собой иную задачу по сравнению с использованием RPC для создания распределенной версии программы, поскольку в первом случае проектировщик должен оперировать абстрактными концепциями, а не преобразовывать структуру существующей программы. Протокол NFS определен с использованием технологии RPC. В стандарт протокола входят определения 21 процедуры, которые предоставляются сервером. Кроме операций, позволяющих выполнять чтение или запись файлов клиентской программы, протокол определяет структуры данных и операции, позволяющие выполнять в клиентской программе чтение записей каталога, создавать, удалять, переименовывать файлы или получать информацию о них. 4 Ко времени выхода этой книги версия 4 протокола NFS была определена, но еще не внедрена на практике. 396 Глава 25. Протоколы сетевой файловой системы...
Протокол монтирования, используемый в сочетании с протоколом NFS, обеспечивает аутентификацию клиента и позволяет клиенту найти дескриптор корневого каталога в соответствующей иерархии каталогов. Протокол монтирования дает возможность экспортировать на конкретном сервере несколько иерархий и позволяет клиенту указывать конкретную иерархию с применением полного составного имени. При решении задачи защиты доступа протокол NFS опирается на протокол монтирования. В нем предполагается, что клиент может передавать запросы на доступ к файлам после получения дескриптора корневого каталога. В большинстве реализаций NFS дескриптор файла строится так, что в нем информация о файле закодирована. Кроме прочих элементов данных, в серверах NFS, работающих в таких версиях UNIX, как Linux, часто предусматривается кодирование номера поколения индексного узла файла, содержащегося в дескрипторе. Чтобы исключить для клиента возможность расшифровать структуру дескриптора файла, а затем использовать эти сведения для получения несанкционированного доступа, многие системные администраторы применяют инструментальное средство, позволяющее заменить номера поколения индексного узла случайными числами. В результате такой замены задача подготовки пользователем клиентской программы вручную действительного дескриптора файла значительно усложняется. Версия 4 протокола NFS содержит небольшие изменения, способствующие повышению уровня защиты. Хотя уже определен стандарт для версии 4, в большинстве реализаций протокола NFS используются версии 2 или 3. Материал для дальнейшего изучения В документе [17] с использованием средств RPC определена версия 3 как протокола NFS, так и протокола монтирования. В нем предусмотрены объявления констант, типов и процедур, которые входят в состав программного обеспечения каждого из этих протоколов, а также приведено описание предполагаемого назначения процедур и даны рекомендации по их реализации. В документе [140] рассматриваются основные особенности проекта версии 4 протокола NFS. Упражнения 25.1. Напишите программу, в которой используется протокол монтирования для получения дескрипторов файлов. Применяется ли в вашей системе аутентификация UNIX? 25.2. С применением программы, описанной в предыдущем упражнении, проверьте несколько файлов на конкретном сервере Linux, чтобы узнать, заменил ли администратор номера поколений индексных узлов в файловых системах этого сервера случайными числами. 25.3. Ознакомьтесь с версией 2 стандарта протокола, чтобы получить сведения об операции WRITECACHE. Каково ее назначение? 25.4. В спецификации протокола NFS упоминается, что некоторые операции в определенных условиях могут оказаться неидемпотентными. Найдите эти операции и объясните, при каких обстоятельствах они могут стать неидемпотентными. 25.5. Организация функционирования NFS гарантирует, что запрос на запись не считается выполненным до тех пор, пока данные не будут записаны, на энергонезависимом запоминающем устройстве (например, на диске). Оцените, насколько быстрее выполнилась бы операция записи на локальном сервере, если бы протокол разрешал копировать на сервере Материал для дальнейшего изучения 397
данные в выходной буфер и выполнять возврат управления после вызова процедуры, не ожидая записи данных из буфера на диск. 25.6. Протокол NFS спроектирован так, что обеспечивает эксплуатацию клиентов и серверов в разнородной среде. Объясните, какие проблемы могут быть связаны с применением символических ссылок. (Подсказка: тщательно изучите протокол, чтобы определить, интерпретируются ли клиентом или сервером символические ссылки). 25.7. Прочитайте спецификацию протокола и выясните, какие атрибуты файла могут быть установлены клиентом. 25.8. Если в программном обеспечении NFS используется дистанционный вызов процедур по протоколу UDP, то дистанционный вызов может быть продублирован, задержан или доставлен не в том порядке, в каком был отправлен. Объясните, как в этом случае может выглядеть отправленный клиентом допустимый ряд вызовов NFS для создания файла и записи его данных, который, тем не менее, приводит к созданию файла нулевой длины. 25.9. Предположим, что в клиентской программе вызывается процедура NFSPR0C3_FSSTAT для определения предпочтительного объема передаваемых данных. Какие ограничения могут привести к тому, что передача такого объема данных перестанет быть оптимальной? 25.10. Прочитайте спецификацию протокола, чтобы узнать, что такое устаревшие дескрипторы файлов. Почему сервер может объявить о том, что дескриптор стал устаревшим? Как можно обеспечить повышение уровня защиты путем объявления дескриптора устаревшим при определенных условиях? 25.11. Напишите программу, которая вызывает процедуру M0UNTPR0C3JJMNTALL на некотором сервере S. Отразится ли этот вызов на последующих вызовах процедур сервера S? Объясните ваш ответ. 25.12. Напишите программу, которая вызывает процедуру M0UNTPR0C3_EXP0RT. Вызовите эту программу на выполнение и выведите на печать список имен экспортируемых файловых систем. 398 Глава 25. Протоколы сетевой файловой системы...
26 TELNET (структура программы) 26.1. Введение В предыдущих главах для иллюстрации понятий и методов, реализованных в программном обеспечении клиент/сервер, служили простые примеры. В настоящей и следующих главах показано, как принципы взаимодействия типа клиент/сервер применяются в сложном прикладном протоколе. В качестве примера протокола используется TELNET — один из наиболее широко распространенных прикладных протоколов в наборе протоколов TCP/IP. В настоящей главе рассматривается общая структура программы. Предполагается, что читатель знаком с основами протокола TELNET, поэтому изложение материала в этой главе в основном сосредоточено на описании реализации. В ней рассматривается проект клиентского программного обеспечения, структура процессов/ потоков и применение конечных автоматов (КА) для управления обработкой. Здесь также рассматривается терминальный ввод/вывод и описано, как в клиентской программе обмен данными по протоколу TELNET связан с операциями терминала. В следующей главе это описание завершается. В ней в основном представлены подробные сведения о процедурах, вызываемых для выполнения действий, связанных с переходами между состояниями в конечных автоматах. Пример кода, рассматриваемый в обеих главах, наглядно показывает, насколько в этом коде доминируют средства программной реализации и как они усложняют данную реализацию. 26.2. Краткий обзор 26.2.1. Терминал пользователя Протокол TELNET определяет средство интерактивной связи, позволяющее пользователям обмениваться данными со службой, функционирующей на удаленном компьютере. В большинстве случаев пользователи применяют протокол TELNET для обмена данными со службой удаленного доступа. Как показывает пример, приведенный в главе 1, тщательно спроектированный клиент TELNET позволяет пользователям обращаться также и к другим службам. Протокол TELNET определяет интерактивное средство связи с символьной организацией. В спецификации этого протокола дано определение сетевого виртуального терминала (NVT — Network Virtual Terminal), который состоит из клавиатуры и экрана монитора. Протокол определяет также набор символов для виртуального терминала. Некоторые клавиши терминала соответствуют не значениям данных, а концептуальным операциям. Например, нажатие одной из клавиш вызывает прерывание или аварийное прекращение работы. Основным преимуществом использования сетевого виртуального терминала является то, что он позволяет
клиентам подключаться к службе с компьютеров разных типов. Как и стандарт XDR, описанный в главе 20, спецификация TELNET предусматривает использование симметричного представления данных. Каждый клиент при передаче данных выполняет преобразование из символьного представления локального терминала в символьное представление NVT, а при приеме данных выполняет преобразование из представления NVT в набор символов локального компьютера. TELNET —- это протокол с символьной организацией, в котором при передаче данных используется стандартная кодировка. 26.2.2. Команды и управляющая информация Протокол TELNET позволяет клиенту и серверу, кроме символьных данных, обмениваться командами или управляющей информацией. Поскольку вся связь между клиентом и сервером осуществляется через единственное соединение TCP, протокол предусматривает, что команды или управляющая информация должны быть закодированы, чтобы получатель мог отличить их от обычных данных. Поэтому значительная часть спецификации протокола посвящена описанию того, как должен кодировать команду отправитель и как ее должен распознавать получатель. 26.2.3. Терминалы, окна и файлы Протокол TELNET определяет требования к средствам связи между терминалом пользователя и удаленной службой. В спецификации протокола подразумевается, что терминал состоит из клавиатуры, на которой пользователь может вводить символы, и экрана монитора, способного отображать сразу несколько строк текста. На практике пользователь может вызвать клиентскую программу и открыть в ней входной файл вместо клавиатуры или выходной файл вместо дисплея. Иным образом, пользователь может вызвать клиентскую программу из окна на растровом дисплее. Хотя для реализации таких вариантов требуется внести в код небольшие изменения, проще будет понять и сам протокол, и его реализацию, если предположить, что каждый из пользователей, вызывающих клиентскую программу TELNET, имеет обычный терминал. Клиентское программное обеспечение TELNET предназначено для обеспечения интерактивной связи с терминалом пользователя. 26.2.4. Необходимость параллельной организации работы Клиент TELNET фактически передает между терминалом пользователя и удаленной службой не блоки данных, а отдельные символы. С одной стороны, в нем в процессе взаимодействия с терминалом пользователя применяются функции локальной операционной системы. С другой стороны, в клиенте TELNET в процессе обмена данными с удаленной службой используется соединение TCP. Этот принцип проиллюстрирован на рис. 26.1. Как показано на этом рисунке, клиент должен передавать символы с клавиатуры терминала пользователя в удаленную службу, а также передавать символы из удаленной службы на дисплей терминала пользователя. Терминал пользователя Удаленная служба Рис. 26.1. Принципиальное назначение клиента TELNET 400 Глава 26. TELNET (структура программы)
Для обеспечения дуплексной связи между терминалом пользователя и удаленной службой клиент TELNET должен выполнять одновременно две задачи: ¦ считывать символы, введенные пользователем с клавиатуры, и передавать их через соединение TCP в удаленную службу; ¦ считывать символы, поступающие из соединения TCP, и отображать их на экране терминала пользователя. Поскольку удаленная служба может в любой момент вывести выходную информацию, а пользователь может в любое время ввести символы на клавиатуре, клиентская программа не имеет информации о том, какой источник данных первым перейдет в состояние готовности к вводу/выводу. Поэтому клиентская программа не может заблокироваться на неопределенно долгое время, ожидая ввода из одного источника, но не проверяя при этом ввода из другого. Иными словами, клиентская программа должна быть способна передавать данные одновременно в обоих направлениях. 26.2.5. Модель процессов для клиента TELNET Для обеспечения параллельной передачи данных клиентская программа должна либо предусматривать использование нескольких потоков управления, работающих одновременно, либо обеспечивать параллельный ввод/вывод в рамках одного потока. В приведенном здесь примере кода реализован последний вариант: в клиентской программе используется один поток, который блокируется до тех пор, пока один из двух источников не будет готов к вводу /выводу. Выбранная авторами структура процессов показана на рис. 26.2. В одном потоке выполнения осуществляется чтение символов с клавиатуры пользователя и передача их в удаленную службу через соединение TCP. В этом же потоке происходит чтение символов из удаленной службы и передача их на экран монитора пользователя. Дескриптор Дескриптор Сокет ввода вывода для соединения TCP с клавиатуры на дисплей с удаленным сервером Клиентское приложение TELNET (однопотоковое) Операционная система Рис. 26.2. Структура процессов в примере с клиентом TELNET 26.2. Краткий обзор 401
26.3. Алгоритм клиента TELNET Алгоритм 26.1 определяет принцип функционирования однопотокового клиента TELNET. Как и в проекте однопотокового параллельного сервера, описанного в главе 13, в реализации однопотокового параллельного клиента в системе Linux для осуществления этапа 3 алгоритма 26.1 используется системная функция select. Параметры вызова в клиентской программе функции select определяют, что поток должен быть заблокирован до тех пор, пока не поступят входные данные в дескриптор файла, соответствующий клавиатуре пользователя, или в дескриптор сокета, соответствующий соединению TCP. После перехода в состояние готовности любого из дескрипторов происходит возврат управления функцией select и клиентская программа выполняет чтение из дескриптора, готового к вводу. Единственный поток выполнения передает символы в обоих направлениях; он блокируется до тех пор, пока не появятся данные, доступные для ввода с клавиатуры или из сокета. Алгоритм 26.1. Клиент TELNET 1. Интерпретировать параметры и инициализировать структуры данных. 2. Открыть соединение TCP с указанным портом на указанном удаленном хосте. 3. Заблокироваться до тех пор, пока пользователь не введет символы с клавиатуры или не поступят данные из соединения TCP. 4. При поступлении данных, введенных с клавиатуры, прочитать их, обработать, преобразовать в представление NVT и передать через соединение TCP. В противном случае — прочитать данные из соединения TCP, обработать, преобразовать в локальное символьное представление и передать их на дисплей терминала пользователя. 5. Перейти к описанному выше этапу 3. 26.4. Терминальный ввод/вывод в системе Linux Алгоритм 26.1 может показаться чрезвычайно простым. Однако в связи со сложностями реализации протокола TELNET и терминального ввода/вывода код усложняется. Чтобы лучше понять, как клиентское программное обеспечение TELNET взаимодействует с терминалом пользователя, необходимо знать во всех тонкостях, как осуществляется терминальный ввод/вывод в клиентской операционной системе. В настоящем разделе описано, как терминальный ввод /вывод организован в системе Linux, и приведены примеры кода, управляющего терминалом пользователя. В системе Linux ввод/вывод на терминальное устройство осуществляется по такому же принципу open-read-write-close (открыть-читать-писать-закрыть), как и ввод/вывод в файлы или сокеты. В прикладной программе вызывается функция open для получения дескриптора ввода/вывода для клавиатуры или экрана терминала, функция read вызывается для приема данных, введенных пользователем с клавиатуры, и write — для передачи данных на экран терминала. На практике в большинстве приложений не предусматривается применение функции open для создания дескриптора терминала пользователя, поскольку командный интерпретатор предоставляет такие открытые дескрипторы автоматически. Командный интерпретатор обычно оставляет клавиатуру подключенной к дескриптору 0 (стандартное устройство ввода), а экран терминала — к дескриптору 1 (стандартное устройство вывода). 402 Глава 26. TELNET (структура программы)
Хотя терминальный ввод/вывод следует тому же основному принципу, что и файловый ввод/вывод, он усложняется бесчисленным множеством подробностей. Многие из этих подробностей связаны с тем, что аппаратное обеспечение терминала является исключительно простым. Например, хотя большинство пользователей привыкли рассматривать клавиатуру и экран как единое устройство, в аппаратном обеспечении ввод с клавиатуры отделен от вывода на экран, и эти два устройства рассматриваются отдельно. Поэтому аппаратное обеспечение не отображает автоматически каждый символ, введенный пользователем. Вместо этого аппаратура просто доставляет символы, введенные с клавиатуры, в операционную систему, которая затем выводит копию каждого символа на экран. Большинство приложений рассчитано на то, что пользователь должен видеть копию каждого набранного им символа на экране, но некоторые приложения должны подавлять вывод введенных символов. Например, чтобы исключить возможность для постороннего лица узнать чужой пароль, приложения, требующие пароль, обычно прекращают отображать вводимые символы, в то время как пользователь набирает пароль. Чтобы исключить необходимость учитывать в каждом приложении все тонкости, связанные с осуществлением терминального ввода/вывода и управлением символьным экраном, в операционной системе Linux предусмотрено программное обеспечение для автоматической поддержки этих устройств. Как показано на рис. 26.3, это программное обеспечение, называемое драйвером терминального устройства, находится в ядре операционной системы. Весь терминальный ввод/вывод проходит через драйвер устройства. Драйвер устройства связывает ввод с клавиатуры с выводом на экран соответствующего терминала. Ему может быть передана команда выполнять эхо-повтор вводимых символов (т.е. отображать на экране каждый символ, введенный пользователем) или подавлять эхо-повтор (т.е. отключить отображение вводимых символов). Драйвер может дать возможность пользователю исправлять ошибки с помощью клавиш забоя <Backspace> и удаления <Del> или может передавать все символы, включая символы забоя и удаления, непосредственно в приложение. Драйвер может также вырабатывать специальную последовательность символов для размещения курсора в начале новой строки после получения от приложения символа конца строки1. И наконец, драйвер устройства может распознавать специальный символ (или символы), вызывающий прерывание или аварийное завершение текущего процесса операционной системой. При вводе пользователем специального символа драйвер устройства преобразует его в сигнал аварийного завершения прикладной программы. 26.4.1. Управление драйвером устройства С началом выполнения клиент TELNET должен изменить правила работы терминала (например, для того чтобы нажатие клавиши аварийного завершения не приводило к прекращению работы программы, а воспринималось как ввод данных). В клиентской программе вызывается функция операционной системы tcgetattr для получения копии всех параметров драйвера устройства. Затем в ней вызывается системная функция tcsetattr для задания новых параметров. И наконец, прежде чем клиентская программа завершит или приостановит свою работу, в ней вызывается функция tcsetattr для восстановления первоначаль- 1 Большинство терминалов требует, чтобы операционная система для перемещения курсора к началу следующей строки передавала не только символ перевода строки, но и символ возврата каретки. Символ перевода строки перемещает курсор на одну строку вниз по вертикали, а символ возврата каретки перемещает курсор в начало текущей строки. 26.4. Терминальный ввод/вывод в системе Linux 403
ных значений параметров. Поэтому, хотя с точки зрения пользователя параметры терминала и назначения некоторых клавиш на время работы клиентской программы становятся иными, они возвращаются к первоначальным значениям после завершения или приостановки работы клиентской программы. Процедура tcgetattr извлекает текущие параметры из драйвера устройства, связанного с клавиатурой (устройство 0) и сохраняет их в структуре типа termios. Процедура tcsetattr принимает параметры, ранее записанные в структуре termios, и передает их драйверу устройства. Каждая из этих процедур принимает параметры, которые указывают устройство, выполняемую операцию и адрес применяемой структуры. Прикладной процесс [ Приложение ] L \ т Драйвер устройства \ 1 1 Интерфейсные аппаратные средства Дескрипторы ' терминала Операционная система Интерфейс последовательной линии Последовательная линия (RS 232C) Терминал пользователя Рис. 26.3. Драйвер терминального устройства в операционной системе Linux и пути следования данных при их передаче в систему с клавиатуры или из системы на экран терминала 26.5. Установление параметров работы терминала С началом работы в клиентской программе вызывается процедура ttysetup для установления параметров драйвера терминального устройства. Файл ttysetup.с содержит код: /* Файл ttysetup - процедура ttysetup .*/ tinclude <unistd.h> 404 Глава 26. TELNET (структура программы)
¦include <termios.h> Iinclude <stdio.h> ¦include <string.h> ¦include "local.h" /* - . * Процедура ttysetup - процедура настройки терминала *__...•__.. ————_—™ -..——.- ..... — . . */ int ttysetup(void) { extern struct termios tntty; if (tcgetattr@, fcoldtty) <0) /* Сохранение первоначальных данных */ /*' о состоянии терминала */ errexit("can't get tty modes: %s\n", strerror(errno)); sg_erase * oldtty.c_cc[VERASE]; sgjdll я oldtty.c_cc[VKiLL]; t Tntrc s oldtty.c"cc[VINTR]; tjjuitc = oldtty.c~cc[VQUIT]; tjlushc = oldtty.c_cc[VDISCARD]; tntty = oldtty; /* Создание копии для обеспечения */ /* возможности внесения изменений */ /* Отмена некоторых tntty.c_cc[VINTR] = tntty.c_cc[VQUIT] = tntty.с cc[VSUSP] = ¦ifdef VDSUSP tntty.c_cc[VDSUSP] ¦endif if (tcsetattr@, TCSADRAIN, &tntty) <0) errexit("can't set tty modes: %s\n", strerror(errno)); } В процедуре ttysetup вызывается функция tcgetattr для регистрации начальных параметров режима терминала в глобальной переменной oldtty. Поскольку функция tcgetattr позволяет в программе извлекать или загружать сразу все параметры, в клиентской программе нельзя устанавливать каждый параметр отдельно. Для изменения значений одного или нескольких параметров в клиентской программе необходимо создать копию начальных значений параметров в локальной переменной tntty, отредактировать эту копию для изменения одного или нескольких параметров, а затем вызвать функцию tcsetattr для передачи новых параметров драйверу терминального устройства. Изменения, внесенные в процедуре ttysetup, представляют собой отмену действия символов, вызывающих прерывание, завершение работы, приостановку и отсроченную приостановку работы. Фактически установка указанных значений равными _POSIX_VDISABLE исключает для локального драйвера устройства возможность прервать или приос- специальных символов */ POSIX_VDISABLE; ~POSIX_VDISABLE? _POSIX_VDISABLE; = POSIX VDISABLE; 26.5. Установление параметров работы терминала 405
тановить работу клиентской программы. Поскольку в процедуре ttysetup эти изменения применяются к копии начальных параметров терминала, эта процедура оставляет все остальные характеристики терминала неизменными. Например, если пользователь определил назначение комбинации клавиш <Ctrl+C> как средство аварийного завершения работы программы, то отмена этой функции в драйвере устройства означает, что клиентская программа сможет читать символ, представляющий комбинацию клавиш <Ctrl+C>, как и любой другой символ. Но поскольку работа процедуры ttysetup начинается с создания копии параметров, то отмена первоначальной интерпретации комбинации клавиш <Ctrl+C> не влияет на другие характеристики терминала (например, на эхо-повтор). Как будет показано ниже, в клиентской программе сохраненная запись с исходными параметрами терминала используется для определения того, какой символ был задан пользователем в качестве сигнала прерывания. Клиент передает серверу команду прерывания при вводе пользователем именно этого символа. 26.6. Глобальные переменные, применяемые для хранения информации о состоянии Объявление глобальной переменной oldtty, наряду с другими глобальными переменными, относящимися к локальному терминалу, находится в файле local.h. Кроме того, этот файл содержит объявления функций errexit, ttwrite, sowrite и f smbuild, которые описаны далее в этой главе. /* Файл local.h */ #include <termios.h> extern FILE *scrfp; extern char scrname[]; extern struct termios oldtty; extern char t_flushc, t_intrc, tjjuitc, sg_erase, sgjcill; extern int errno; int errexit(const char *format, ...), cerrexit(const char *format, ...); int ttwrite(FILE *sfp, FILE *tfp, unsigned char *buf, int cc); int sowrite(FILE *sfp, FILE *tfp, unsigned char *buff int cc); int fsmbuild(void); В этом коде переменная oldtty объявлена как структура типа termios, которая определена в системе как состоящая из шести полей: struct termios { tcflag_t c_iflag; /* Параметры ввода с терминала */ tcflag_t c_oflag; /* Параметры ввода/вывода на терминал */ tcflag_t c_cflag; /* Параметры управления терминалом */ tcflag__t c_lflag; /* Параметры работы линии связи */ char c_line; /* Режим работы линии связи */ cc_t c_cc[17]; /* Управляющие символы */ 406 Глава 26. TELNET (структура программы)
26.7. Восстановление параметров работы терминала перед выходом из программы С началом работы клиентская программа сохраняет первоначальные параметры работы терминала в переменной oldtty. Перед завершением работы в клиентской программе вызывается процедура ttyrestore для восстановления записанных ранее параметров работы терминала. В частности, процедура dcon разрывает соединение TCP с сервером; в клиентской программе процедура dcon вызывается при получении от пользователя запроса на прекращение сеанса. Процедура dcon выводит сообщение, восстанавливает первоначальные параметры работы терминала и выполняет нормальное завершение работы (т.е. в ней используется код завершения 0). Файл dcon,с содержит следующий код: /* Файл dcon - процедура dcon */ ¦include <stdlib.h> ¦include <stdio.h> ¦include <termios.h> ¦include "local.h" /* * Процедура dcon - процедура отключения от удаленного участника соединения * */ int dcon(FILE *sfp, FILE *tfp, int c) { fprintf(tfp, "disconnecting.\n"); (void) tcsetattr@, TCSADRAINr fioldtty); exit@)? } В этом коде предусмотрено, что в случае возникновения любой неисправимой ошибки работа клиентской программы должна быть завершена. Перед завершением работы клиентской программы должны быть восстановлены первоначальные параметры терминала, так как в противном случае пользовательский терминал не будет действовать обычным образом. Необходимый для этого код содержится в файле cerrexit.c. /* Файл cerrexit - процедура cerrexit */ ¦include <stdarg.h> ¦include <stdio.h> ¦include <stdlib.h> ¦include <termios.h> ¦include "local.h" /*' -—- * Процедура cerrexit - процедура для очистки кодов результата и выхода с * сообщением об ошибке * 26.7. Восстановление параметров работы терминала перед выходом... 407
int cerrexit(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); (void) tcsetattr@, TCSADRAIN, &oldtty); va end(args); exlt(l); } Процедура cerrexit, как и dcon, выводит сообщение и восстанавливает параметры терминала перед завершением своей работы. Однако в процедуре cerrexit, в отличие от dcon, для указания на аварийное завершение используется код завершения 1. 26.8. Приостановка и возобновление работы клиентской программы Операционная система Linux позволяет пользователю приостановить на время работу программы. Пока программа находится в приостановленном состоянии, управление пользовательским терминалом возвращается к другому процессу, обычно к командному интерпретатору. При получении клиентом TELNET сигнала, вызывающего приостановку процесса, он должен восстановить перед приостановкой первоначальные параметры терминала, а затем снова установить значения этих параметров, применяемые в клиентской программе после возобновления процесса. Файл suspend.с содержит код функции suspend, которая восстанавливает первоначальные параметры терминала перед приостановкой клиентского процесса. После возобновления работы клиентской программы функция suspend снова берет на себя управление, восстанавливает параметры терминала и возвращает управление вызывающей процедуре. /* Файл suspend - процедура suspend */ ¦include <sys/types.h> ¦include <sys/signal.h> ¦include <stdio.h> ¦include <string.h> ¦include "local.h" extern struct termios tntty; /* * Процедура suspend - процедура временной приостановки выполнения *«..«.«. ———. —— */ int suspend(FILE *sfp, FILE *tfp, int c) { if (tcgetattr@, fctntty) <0) 408 /* Сохранить данные о текущем */ Глава 26. TELNET (структура программы)
/* состоянии терминала */ errexit("can't get tty modes: %s\n", strerror(errno)); if (tcsetattr@, TCSADRAIN, fcoldtty) <0) /* Восстановить данные о предыдущем */ /* состоянии терминала */ errexit("can't set tty modes: %s\n", strerror(errno)); (void) kill@, SIGTSTP); if (tcgetattr@, fioldtty) <0) /* Может потребовать внесения изменений */ errexit("can't get tty modes: %s\n", strerror(errno)); if (tcsetattr@, TCSADRAIN, &tntty) <0) /* Возврат к предыдущим */ /* характеристикам режима работы протокола TELNET */ errexitf'can't set tty modes: %s\n", strerror(errno)); return 0; 26.9. Спецификация конечного автомата Протокол TELNET определяет правила передачи клиентской программой символов в удаленную службу и правила отображения данных, возвращенных этой службой. Основная часть трафика, проходящего через соединение, состоит из отдельных символов данных. Символы данных поступают в клиентскую программу при нажатии пользователем клавиш на клавиатуре; они также формируются на сервере при выработке выходных данных в сеансе дистанционного доступа. Кроме символов данных, протокол TELNET позволяет также клиенту и серверу обмениваться управляющей информацией. В частности, клиент может передать на сервер последовательность символов, представляющую собой команду управления работой удаленной службы, а также передать такую управляющую последовательность, которая может прервать работу удаленной прикладной программы. В большинстве реализаций протокола TELNET для точного определения синтаксиса и интерпретации управляющих последовательностей применяются конечные автоматы. Конечный автомат как инструментальное средство определения спецификации позволяет составить точное описание протокола. Он показывает, как отправитель должен встраивать управляющие последовательности в поток данных, и определяет, как получатель должен интерпретировать такие последовательности. Еще более важно то, что конечный автомат может быть непосредственно преобразован в программу, соответствующую данному протоколу. Поэтому появляется возможность проверить, соответствует ли результирующая программа спецификации протокола. Поскольку TELNET — это протокол с символьной организацией, в котором последовательности команд включены в потоки данных, проходящие между клиентом и сервером в прямом и обратном направлениях, в большинстве его реализаций для точного определения правил работы используются конечные автоматы. 26.10. Внедрение команд в поток данных TELNET В основе работы протокола TELNET лежит простой принцип: эта программа вставляет в поток данных специальный зарезервированный символ каждый раз при возникновении в клиентской или серверной программе необходимости передать управляющую последовательность вместо обычных данных. Для обозначения этого зарезервированного символа применяется аббревиатура IAC (Interpret 26.9. Спецификация конечного автомата 409
As Command — Интерпретировать как команду). После обнаружения символа IAC во входящем потоке данных получатель обрабатывает следующие октеты как принадлежащие к управляющей последовательности. Для передачи символа IAC в качестве данных отправитель вставляет перед ним еще один символ IAC. Любая управляющая последовательность может содержать либо запрос на установку опции, либо ответ на запрос. В запросе содержится требование к получателю применять (или не применять) определенную опцию TELNET; в ответе содержится подтверждение, что запрос принят, и указывается, будет ли получатель применять данную опцию. В протоколе определены два глагола, DO и D0NT, которые могут использоваться отправителем для формирования запроса. Как и в большинстве правил, определенных протоколом TELNET, стандарт протокола указывает, что каждый глагол и каждая опция должны быть закодированы одним символом. Поэтому запрос при его появлении в потоке данных обычно состоит из трех символов: IAC verb option Здесь verb обозначает условный символ, соответствующий глаголу DO или DONT, a option — условный символ, соответствующий одной из опций TELNET. В качестве примера рассмотрим опцию эхо-повтора протокола TELNET. Обычно сервер выполняет эхо-повтор каждого полученного символа (т.е. отправляет его копию на экран терминала пользователя). Для отключения дистанционного эхо-повтора символов клиент передает три условных символа, которые соответствуют следующей команде: IAC DONT ECHO 26.11. Согласование опций Как правило, получатель отвечает на запрос с применением глаголов WILL или WONT. Он передает глагол WILL для указания того, что будет применять указанную опцию, и WONT — для указания того, что не будет ее применять. Ответ на запрос служит для отправителя подтверждением получения запроса и сообщает ему о том, согласен ли получатель выполнить запрос. Например, с началом работы клиент и сервер согласуют между собой, кто из них будет выполнять эхо-повтор символов, вводимых пользователем. Обычно клиент передает символы на сервер, а сервер выполняет их эхо-повтор для последующего вывода на экран терминала пользователя. Однако если сетевые задержки приводят к нарушению работы, пользователь может предпочесть, чтобы эхо-повтор символов выполняла локальная система. Прежде чем клиент организует эхо-повтор символов в локальной системе, он передаст серверу последовательность: IAC DONT ECHO После получения этого запроса сервер передает трехсимвольный ответ: IAC WONT ECHO Обратите внимание, что глагол WONT относится к опции; он не всегда означает, что сервер отверг запрос. Например, в данном случае сервер дал свое согласие отключить эхо-повтор в соответствии с запросом. 26.12. Симметрия запросов и предложений Любопытно отметить, что протокол TELNET позволяет одному из участников соединения предложить конкретную опцию, прежде чем другой участник ее затре- 410 Глава 26. TELNET (структура программы)
бует. Для этого участник соединения, предлагающий применять (или не применять) какую-либо опцию, передает сообщение, содержащее глагол WILL (или WONT). Поэтому глаголы WILL или WONT либо подтверждают предыдущий запрос, либо соответствуют предложению применять (или не применять) опцию. Например, такие приложения, как текстовые редакторы, часто передают специальные управляющие последовательности для позиционирования курсора. В них не может использоваться кодировка сетевого виртуального терминала, поскольку она не поддерживает все возможные 8-битовые символы. Поэтому в большинстве систем сервер TELNET при подключении к нему каждого нового клиента автоматически, посылает глагол WILL в качестве предложения передавать двоичные опции; это служит для клиента предложением передавать 8-битовые двоичные (незакодированные) символы вместо применения кодировки сетевого виртуального терминала (NVT). Клиент должен ответить, передав управляющую последовательность с указанием, в котором содержится требование передавать или не передавать данные в двоичном режиме: DO transmit Ыnary или DONT transmitj)inary. 26.13. Определение символов TELNET Определения констант, применяемых в протоколе, содержатся в файле telnet.h: /* Файл telnet.h */ typedef unsigned char u_char; /* Коды команд TELNET: */ ¦define TCSB (u_charJ50 ¦define TCSE (u_charJ40 ¦define TCNOP (u_charJ41 ¦define TCDM (u charJ42 ¦define ¦define ¦define ¦define ¦define ¦define ¦define ¦define ¦define ¦define ¦define ¦define TCBRK TCIP TCAO TCAYT TCEC TCEL TCGA TCWILL TCWONT TCDO TCDONT TCIAC (u_charJ43 (iTcharJ44 (u_charJ45 (u_charJ46 (u_charJ47 (iTcharJ48 (u_charJ49 (u_charJ51 (u charJ52 (\fcharJ53 (iTcharJ54 (u charJ55 /* Коды опций протокола TELNET: */ ¦define TOTXBINARY (u_char) 0 ¦define TOECHO (iTchar) 1 ¦define TONOGA (u_char) 3 ¦define TOTERMTYPE (u_char) 24 * Начать уточнение опции */ * Закончить уточнение опции */ * Применить пустую операцию */ * Передать символ DATA MARK (для */ * синхронизации) */ * Передать символ BRK кодировки NVT */ * Прервать процесс .*/ ¦ * Аварийно прервать вывод */ * Выполнить функцию проверки абонента */ * "Are You There?" */ * Стереть символ */ * Стереть строку */ * Выполнить функцию передачи управления */ * "Go Ahead" */ * Предложить/подтвердить применение опции *¦/. * Отказаться от применения опции */ * Потребовать применять опцию */. * Потребовать НЕ применять опцию */ * Определить управляющий символ, */ * интерпретируемый как команда */ * Опция передачи в двоичном коде */ * Опция эхо-повтора */ * Опция подавления передачи управления */ * Опция согласования типа "терминала */ /* Специальные символы сетевого виртуального терминала */ ¦define VPLF '\n' /* Перевод строки */ 26.13. Определение символов TELNET 411
tdefine VPCR '\r' /* Возврат каретки */ ¦define VPBEL '\a' /* Звонок (сигнал привлечения внимания) */ fdefine VPBS '\b' /* Забой (<Backspace>) */ ¦define VPHT '\t' /* Горизонтальная табуляция */ ¦define VPVT '\V /* Вертикальная табуляция */ ¦define VPFF '\f /* Прогон страницы */ /* Клавиатурные управляющие ¦define ¦define ¦define ¦define ¦define ¦define KCESCAPE 035 KCDCON KCSUSP 032 KCSCRIPT 's' KCUNSCRIPT 'u' KCSTATUS 024 ¦define KCNL ¦define KCANY '\n' символы: */ * Локальный управляющий символ ('А]') */ * Управляющий символ прекращения сеанса */ * Управляющий символ приостановки сеанса ('AZ')*/ * Управляющий символ начала ведения протокола */ * Управляющий символ конца ведения протокола */ * Управляющий символ вывода информации */ * о состоянии ('АТ') */ * Символ перехода на новую строку */ (NCHRS+1) /* Константы уточнения опции: */ ¦define TT_IS 0 /* Команда "IS" опции согласования типа терминала */ ¦define TT_SEND 1 /* Команда "SEND" опции согласования типа терминала */ /* Логические переменные с обозначением опции и состояния */ extern char synching, doecho, sndbinary, rcvbinary; Следует отметить, что в этом файле содержатся символические имена для всех символов, используемых в протоколе TELNET, включая такие глаголы, как WILL и WONT, а также коды опций. 26.14. Конечный автомат для обработки данных, поступающих с сервера На рис. 26.4 показана принципиальная схема конечного автомата, который определяет протокол TELNET, включая его состояния, соответствующие согласованию опций, описанному выше. Этот конечный автомат можно рассматривать как определение способа обработки клиентом последовательности символов, полученных с сервера. Приведенные на рисунке имена состояний и символов взяты непосредственно из исходного кода программного обеспечения. Сокращение TCANY обозначает "любой символ, отличный от перечисленных явно". В схеме конечного автомата используется стандартная система обозначений. Каждый переход из одного состояния в другое имеет метку в форме alpha! betha, где alpha обозначает конкретный входной символ, вызывающий переход, а betha — действие, которое должно быть выполнено в процессе перехода. Метка alpha!betha на переходе из состояния X в состояние У означает следующее: если во время нахождения конечного, автомата в состоянии X поступает символ alpha, выполнить действие betha, а затем перейти в состояние У. Имена состояний и символы, показанные на этом рисунке, взяты из исходного кода программного обеспечения. Например, файл telnet.h определяет-константу TCIAC, соответствующую символу IAC протокола TELNET. Сокращение TCANY соответствует любому символу, отличному от символов перехода, перечисленных явно. 412 Глава 26. TELNET (структура программы)
Чтобы понять, как работает этот конечный автомат, представьте себе, что клиент использует его для определения дальнейших действий при получении от сервера любых данных, поступивших через соединение TCP. При получении любого символа от сервера клиент следует по переходу в конечном автомате. Некоторые из переходов оставляют конечный автомат в том же состоянии, а другие переводят его в новое состояние. 26.15. Переходы между состояниями С началом выполнения клиент переводит свой конечный автомат в состояние, отмеченное как TSDATA. Состояние TSDATA соответствует той ситуации, когда клиент ожидает поступления обычных символов данных и передачи их на экран терминала пользователя (т.е. клиент еще не приступил к чтению управляющей последовательности). Например, при поступлении символа q клиент остается в состоянии TSDATA и выполняет действие с меткой К (т.е. клиент вызывает процедуру ttputc для отображения символа на экране терминала пользователя, а затем проходит по циклу снова в то же состояние). При поступлении символа TCIAC во время нахождения конечного автомата в состоянии TSDATA клиент следует по переходу в состояние TSIAC и выполняет действие, обозначенное меткой Е на данной схеме. В перечне условных обозначений к схеме указано, что действие Е соответствует пустой операции (по_ор). После перехода в состояние TSIAC клиент начинает интерпретировать управляющую последовательность. Если символ, следующий за символом TCIAC, представляет собой глагол (например, TCD0), клиент следует по переходу в одно из состояний обработки опции. Конечный автомат для протокола TELNET должен иметь только шесть состояний, поскольку интерпретация протокола зависит лишь от краткой предыстории поступления символов. Например, вслед за символом TCIAC сервер может послать один из запросов на установку опций или ответов на запросы (TCDO, TCDONT, TCWILL или TCW0NT) или отправить запрос на уточнение опции. Уточнение опции позволяет отправителю включить в опцию строку переменной длины (например, опция, применяемая клиентом для передачи на сервер строки с обозначением типа терминала, требует уточнения, чтобы в нее можно было включить строку, содержащую имя терминала). Хотя уточнение позволяет передавать управляющие последовательности переменной длины, в конечном автомате достаточно иметь для их обработки только два состояния, поскольку строка уточнения оканчивается двухсим- вольной последовательностью. Клиент сразу после обнаружения запроса на уточнение переходит в состояние TSSUBNEG. Он входит в состояние TSSUBIAC при получении символа TCIAC и полностью выходит из состояния уточнения, если за ним непосредственно следует символ TCSE. При появлении любой другой двухсим- вольной последовательности конечный автомат остается в состоянии TSSUBNEG. 26.16. Реализация конечного автомата Поскольку существует возможность построить эффективную реализацию конечного автомата и в связи с тем, что такие конечные автоматы позволяют легко описывать протоколы с символьной организацией, в нашем примере кода используется три отдельных конечных автомата. Один из них управляет тем, как отвечает клиент на символы, введенные с клавиатуры, другой управляет обработкой клиентом символов, поступающих от сервера через соединение TCP, а третий выполняет все действия, связанные с уточнением опций. Во всех трех конечных автоматах используются структуры данных одинакового типа, что позволяет совместно применять некоторые процедуры, выполняющие манипуляции со структурами данных. 26.15. Переходы между состояниями 413
TCANY/C TOECHO/A TONOGA/B TOTXBINARY/D TCANY/K TCDM/J TCIAC/K TCNOP/E TCANY/E TCANY/H TCWILL/F TCWONT/F TCIAC/E TCANY/L TOTXBINARY/N TOTERMTYPE/M TSDOPT TCSE/G TCANY/H Исполнительные процедуры A - do_echo H - subopt В - do_noga С - do_notsup D - dojxbinary E - no_op F - recopt G - subend J - tcdm К - ttputc L - willjiotsup M - willjermtype N - willjxbinary Рис. 26.4. Конечный автомат, который описывает, как протокол TELNET представляет управляющие последовательности наряду с данными Для повышения эффективности обработки в данной реализации переходы конечного автомата представлены в виде матрицы переходов, как показано на рис. 26.5. Каждая строка этой матрицы соответствует состоянию, а каждый столбец — одному из возможных входных символов. Во время выполнения клиентской программы для регистрации текущего состояния используется целочисленная переменная. После поступления символа текущее значение переменной состояния и числовое значение символа используется в качестве индекса матрицы переходов. 414 Глава 26. TELNET (структура программы)
Входной символ 0 1 2 Текущее состояние N-1 0 1 2 255 Рис. 26.5. Конечный автомат, представленный в виде матрицы переходов 26.17. Компактное представление конечного автомата Подготовка исходного кода С для инициализации большой матрицы может оказаться довольно утомительной. Кроме того, если каждый элемент матрицы переходов будет содержать полную информацию о выполняемом действии и следующем состоянии, то для размещения матрицы потребуется большой объем памяти. Для обеспечения возможности создания небольшой матрицы переходов и упрощения ее инициализации в нашем коде применяется компактное представление конечного автомата. Фактически эти структуры данных позволяют программисту создать компактную структуру данных, которая представляет конечный автомат, а затем предусмотреть развертывание в программе соответствующей матрицы переходов во время выполнения. Файл tnfsm.h содержит объявление структуры fsm_trans, применяемой в компактном представлении. /* Файл tnfsm.h */ /* Состояния конечного ¦define TSDATA «define TSIAC «define TSWOPT «define TSDOPT «define TSSUBNEG «define TSSUBIAC автомата вывода в сокет TELNET: */ 0 /* Обработка обычных данных */ 1 /* Обнаружен символ IAC */ 2 /* Обнаружены символы IAC-{WILL/WONT} *'/ 3 /* Обнаружены символы IAC-{D0/D0NT} */ 4 /* Обнаружены символы IAC-SB */ 5 /* Обнаружены символы IAC-SB-...-IAC *•/ «define NTSTATES 6 /* Число состояний TS* */ /* Состояния конечного автомата ввода с клавиатуры TELNET: */ «define «define KSREMOTE KSLOCAL «define KSCOLLECT 0 У* Введенные данные передаются в сокет */ . . 1 /* Введенные данные передаются в локальную */ /* функцию */ 2 /* Введенные данные представляют собой имя */ /* файла протокола */ «define NKSTATES /* Число состояний KS* */ /* Состояния конечного автомата уточнения опции TELNET: */ 26.17. Компактное представление конечного автомата 415
tdefine SSJ3TART О ¦define SSJTERMTYPE 1 tdefine SS END 2 /* Начальное состояние */ /* Команда опции уточнения типа терминала */ /* Состояние после ввода допустимого символа */ ¦define NSSTATES 3 /* Число состояний SS * */ tdefine FSINVALID Oxff /* Недопустимый номер состояния */ ¦define tdefine NCHRS 256 TCANY (NCHRS+1) /* Число допустимых символов */ /* Символ, обозначающий любой символ, кроме */ /* явно заданного */ struct fsm_trans { unsigned char ftjstate; /* Текущее состояние */ short ftTchar; unsigned char ftjiext; /* Входной символ */ /* Следующее состояние */ int (*ftraction)(FILE *sfp, FILE *tfp, int c); /* Выполняемое действие */ }; Компактное представление конечного автомата состоит из одномерного массива структур fsm_trans. Каждый элемент определяет один переход. Поле ft_state задает состояние конечного автомата, с которого начинается переход. Поле ft_char указывает символ, который вызывает переход (или TCANY для обозначения всех символов, отличных от указанных явно в качестве причины перехода). Поле ftjiext определяет состояние, в котором завершается переход, а поле ft_action содержит адрес вызываемой процедуры, которая выполняет действие, связанное с переходом. 26.18. Форма компактного представления, применяемая во время выполнения В рассматриваемом примере клиентской программы не предусмотрено копирование всей информации из компактного представления в матрицу переходов. Вместо этого в нем компактное представление остается неизменным и служит для хранения информации о переходах. Для этой цели в программном обеспечении предусмотрено хранение в каждом элементе матрицы переходов целого числа. Это целое число определяет индекс записи в компактном представлении, соответствующей переходу. Соответствующие структуры данных показаны на рис. 26.6. 0 1 2 3 4 Компактное представление Матрица переходов Рис. 26.6. Структуры данных конечного автомата во время выполнения записи матрицы переходов содержат индекс, который указывает на элемент компактного представления 416 Глава 26. TELNET (структура программы)
26.19. Реализация компактного представления Файл ttfsm.c содержит пример компактного представления для конечного автомата, принципиальная схема которого приведена на рис. 26.4. /* Файл ttfsm.c */ ¦include <sys/types.h> ¦include <stdio.h> ¦include "telnet.h" ¦include "tnfsm.h" ¦include "local.h" extern int do_echo(FILE *,FILE *,int)r do noga(FILE *,FILE *,int), do notsup(FILE *,FILE *,int), do status(FILE *,FILE *,int), no~op(FILE *fFILE *,int), recoptjFILE *,FILE *,int), subend(FILE *,FILE *,int), subopt(FILE *,FILE *,int), tcdm(FILE *,FILE *,int), ttputc(FILE *,FILE *,int), will notsup(FILE *,FILE *,int), will termtype(FILE *,FILE *,int), wiH""txbinary(FILE *,FILE *,int), tnabort(FILE *,FILE *,int), do txbinary(FILE *,FILE *,int)j struct fsm_trans ttstab[] ¦ /•Состояние /* { TSDATA, { TSDATA, { TSIAC, { TSIAC, Ввод TCIAC, TCANY, TCIAC, TCSB, /* Команды TELNET */ { TSIAC, { TSIAC, /* Согласование { TSIAC, { TSIAC, { TSIAC, { TSIAC, { TSIAC, TCNOP, TCDM, опции */ TCWILL, TCWONT, TCDO, TCDONT, TCANY, /* Уточнение опции */ { TSSUBNEG, { TSSUBNEG, { TSSUBIAC, { TSSUBIAC, { TSWOPT, { TSWOPT, { TSWOPT, { TSWOPT, TCIAC, TCANY, TCSE, TCANY, TOECHO, TONOGA, TOTXBINARY, TCANY, { След. состояние TSIAC, TSDATA, TSDATA, TSSUBNEG, TSDATA, TSDATA, TSWOPT, TSWOPT, TSDOPT, TSDOPT, TSDATA, TSSUBIAC, TSSUBNEG, TSDATA, TSSUBNEG, TSDATA, TSDATA, TSDATA, TSDATA, Действие no_op ttputc ttputc no_op no_op tcdm recopt recopt recopt recopt no_°P no_op subopt subend subopt do_echo dojioga */ */ h h Ь Ь }, h h Ь h ь ь ь ь ь ь } t } 1 do_txbinary } do_notsup ), { TSDOPT, { TSDOPT, { TSDOPT, TOTERMTYPE, TSDATA, TOTXBINARY, TSDATA, TCANY, TSDATA, will_termtype }, will_txbinary }, will jiotsup }, 26.19. Реализация компактного представления 417
{ FSINVALID, TCANY, FSINVALID, tnabort }, ¦define NTRANS (sizeof(ttstab)/sizeof(ttstab[0])) int ttstate; u_char ttfsm[NTSTATES][NCHRS]; Массив ttstab содержит 22 действительные записи, каждая из которых соответствует одному из переходов, показанных на рис. 26.4 (а также дополнительную запись, отмечающую конец массива). Каждая запись в массиве состоит из структуры fsm_trans, которая определяет один переход. Следует отметить, что массив ttstab является и компактным, и удобным в определении. Он является компактным, поскольку не содержит пустых записей; он является удобным в определении, поскольку каждая запись непосредственно соответствует одному из переходов конечного автомата. 26.20. Построение матрицы переходов конечного автомата Преимущества этого компактного представления станут очевидными сразу после описания того, как оно может применяться для создания матрицы переходов. Этот код содержится в файле fsminit.c. /* Файл fsminit - процедура fsminit */ ¦include <sys/types.h> ¦include <stdio.h> ¦include "tnfsm.h" ¦define TINVALID Oxff /* Недопустимый индекс перехода */ /* - * Процедура fsminit - процедура инициализации конечного автомата * ± , */ int fsminit(unsigned char fsm[][NCHRS], struct fsm_trans ttab[], int nstates) { struct fsm_trans *pt; int sn, ti, en; for (cn=0; cn<NCHRS; ++cn) for (ti=0; ti<nstates; ++ti) fsm[ti][cn] = TINVALID; for (ti=0; ttab[ti].ftjstate != FSINVALID; ++ti) { pt = &ttab[ti]; sn = pt->ft state; if (pt->ft_char « TCANY) { for (cn=0; cn<NCHRS; ++cn) if (fsm[sn][cn] == TINVALID) 418 Глава 26. TELNET (структура программы)
fsm[sn][cn] = ti? } else fsm[sn][pt->ft_char] = ti; ¦ У /* Установить в качестве значения всех неинициализированных элементов */ /* массива недопустимый индекс перехода */ for (cn=0; cn<NCHRS; ++сп) for (sn=0; sn<nstates; ++sn) if (fsm[sn][cn] == TINVAIiID) fsm[sn][cn] = ti; I .... Процедура fsminit принимает три параметра. Параметр fsm указывает матрицу переходов, которая должна быть инициализирована. Параметр ttab задает адрес компактного представления конечного автомата, а параметр nstates определяет число состояний в результирующем конечном автомате. Процедура fsminit вначале выполняет инициализацию всей матрицы переходов, устанавливая в ней значения TINVALID. Затем она обрабатывает в цикле каждый элемент компактного представления и вводит информацию о переходе между состояниями, определяемом этим элементом, в матрицу переходов. И наконец, эта процедура снова обрабатывает в цикле матрицу переходов и изменяет все незаполненные переходы таким образом, чтобы они указывали на недействительный переход, находящийся в конце компактного представления. Основная часть кода процедуры fsminit является несложной. Однако при вводе информации о переходах в процедуре fsminit необходимо проводить различия между явным переходом и его сокращенным обозначением. Чтобы понять этот код, напомним, что в компактном представлении символ TCANY обозначает все символы, которые не были указаны явно. Поэтому при обработке информации об отдельном переходе в процедуре fsminit выполняется проверка символа, вызывающего переход. Если в записи указан символ ТС ANY, процедура fsminit обрабатывает в цикле все возможные символы и добавляет переход к любому символу, который еще не был инициализирован. Если в записи указан любой символ, отличный от TCANY, процедура fsminit заполняет элемент матрицы перехода, соответствующий одному этому символу. 26.21. Конечный автомат для вывода в сокет Конечный автомат, приведенный на рис. 26.4, определяет действия, выполняемые клиентом при получении каждого символа, который поступил от сервера. Отдельный и более простой конечный автомат описывает способ обработки в клиентской программе символов, поступающих с клавиатуры. Мы называем конечный автомат, связанный с вводом данных на клавиатуре, конечным автоматом вывода в сокет. На первый взгляд его название кажется необычным. Но, как показано на рис. 26.7, клиентское программное обеспечение организовано так, что такое имя становится вполне оправданным. Главный поток читает данные либо из сокета, либо с клавиатуры и вызывает процедуру конечного автомата для их обработки. Конечный автомат, обрабатывающий данные, которые поступают с клавиатуры, связан с выводом в сокет. Конечный автомат для вывода в сокет показан на рис. 26.8. Вместо определения состояния для каждой операции, в этом конечном автомате предусмотрено применение только трех состояний. Большинство операций выполняется в одном переходе, который переводит клиентскую программу из состояния, представ- 26.21. Конечный автомат для вывода в сокет 419
ляющего локальную обработку, в состояние, которое представляет (обычное) взаимодействие с удаленной системой. Клиент передает основную часть символов данных на удаленный сервер. Однако этот проект позволяет пользователю на время выйти из соединения передачи данных и вступить во взаимодействие с локальной клиентской программой. Дескриптор вывода на дисплей Дескриптор ввода с клавиатуры и Главный КА КА вывода всокет \л Дескриптор JJ сокета Рис. 26.7, Организация клиентского программного обеспечения KCANY/G KCANY/B KCSUSP/J KCDCON/A KCSTATUS/H KCESCAPE/G KCUNSCRIPT/K Исполнительные процедуры А - dcon В - sonotsup С • по_ор D - scrgetc Е - sennit F - scrwrap G - soputc H - status J -suspend К - unscript Рис. 26.8. Конечный автомат для вывода в сокет применяется для определения действий, выполняемых при обработке каждого символа, введенного пользователем 420 Глава 26. TELNET (структура программы)
Работа конечного автомата для вывода в сокет начинается с состояния KSREM0TE, что заставляет клиента передавать каждый символ, введенный с клавиатуры, на удаленный сервер. При нажатии пользователем на клавиатуре клавиши выхода <Esc> клиент переходит в состояние KSLOCAL, в котором он ожидает новых нажатий клавиш. Большинство нажатий клавиш, которые следуют за нажатием клавиши <Esc>, не имеют для этого конечного автомата никакого смысла, но некоторые из них заставляют клиентскую программу выполнить определенные действия и вернуться в состояние KSREM0TE. Только одно из них, KCSCRIPT, заставляет клиентскую программу перейти в состояние KSCOLLECT, в котором она собирает символы имени файла, применяемого для ведения протокола. 26.22. Определения конечного автомата для вывода в сокет Компактное представление конечного автомата для вывода в сокет приведено в файле sofsm.c. /* Файл sofsm.c */ tinclude <sys/types.h> ¦include <stdio.h> ¦include "telnet.h" finclude "tnfsm.h" /* Специальные символы: */ char t_flushc, t_intrc, tjjuitc, sg_erase, sgjcill; extern int soputc(FILE *?FILE *,int), scrinit(FILE *,FILE *,int), scrgetc(FILE *,FILE *,int), scrwrap(FILE *,FILE *,int), unscript(FILE *,FILE *,int), dcon(FILE *,FILE *,int), suspend(FILE *,FILE *,int), status(FILE *fFILE *,int), sonotsup(FILE *,FILE *,int), no_op(FILE *, FILE *, int), tnabortfFILE *,FILE *,int); struct fsm_trans sostab[] /•Состояние /* /* Ввод данных { KSREMOTE, { KSREMOTE, Ввод */ KCESCAPE, KCANY, /* Команды, выполняемые в { KSLOCAL, { KSLOCAL, { KSLOCAL, { KSLOCAL, { KSLOCAL, {KSLOCAL, { KSLOCAL, /* Сборка имени {KSCOLLECT, { KSCOLLECT, KCSCRIPT, KCUNSCRIPT KCESCAPE, KCDCON, KCSUSP, KCSTATUS, KCANY, -¦{ След. состояние Действие KSLOCAL, no_op KSREMOTE, soputc режиме работы с локальной прог KSCOLLECT, scrinit ', KSREMOTE, unscript KSREMOTE, soputc KSREMOTE, dcon KSREMOTE, suspend KSREMOTE, status KSREMOTE, sonotsup файла протокола */ KCNL, KCANY, KSREMOTE, scrwrap KSCOLLECT, scrgetc */ */ h h раммой Ь Ь Ь Ь ь ь ь ь ' Ь 2&.22. Определения конечного автомата для вывода в сокет 421
{ FSINVALID, KCANY, FSINVALID, tnabort }, }? Idefine NTRANS (sizeof(sostab)/sizeof(sostabfO])) int sostate; u_char sofsm[NKSTATES][NCHRS]; Компактное представление содержится в массиве sostab, а переменная sostate содержит целое число, которое определяет текущее состояние конечного автомата для вывода в сокет. 26.23. Конечный автомат для уточнения опции На рис. 26.9 показан третий конечный автомат, используемый в клиенте. Он обрабатывает последовательность символов, поступающую во время уточнения опции. Поскольку этот конечный автомат распознает лишь один возможный запрос на уточнение опции (тип терминала), для него требуются только три состояния. Клиентская программа выполняет повторную инициализацию этого конечного автомата после каждого очередного уточнения опции. Исполнительные процедуры А - по_ор В - subtermtype Рис. 26.9. Простой конечный автомат, применяемый для уточнения опции Проще всего можно понять, что такое уточнение опции, если представить себе, что эта операция описывает внутреннюю структуру состояния TSSUBNEG в приведенной выше принципиальной схеме конечного автомата. В то время как основной конечный автомат функционирует в состоянии TSSUBNEG, он вызывает процедуру subopt для обработки каждого входящего символа. Процедура subopt обращается к конечному автомату, уточняющему опцию. Как показано на рис 26.9, конечный автомат, предназначенный для уточнения опции, немедленно 422 Глава 26. TELNET (структура программы)
принимает решение в зависимости от опции. Если конечный автомат обнаруживает уточнение опции с обозначением типа терминала, он переходит в состояние SSJTERMTYPE. В ином случае он переходит непосредственно в состояние SSJ3ND и игнорирует оставшуюся часть строки уточнения функции. После перехода в состояние SSJTERMTYPE конечный автомат проверяет глагол уточнения опции. Он вызывает процедуру subtermtype, если глаголом является TTJSEND, а в ином случае игнорирует запрос на уточнение опции. Назначение и функционирование конечного автомата для уточнения опции станут более понятными после рассмотрения того, как клиент отвечает на запрос опции с указанием типа терминала. 26.24. Определения конечного автомата, уточняющего опции Объявления на языке С для конечного автомата, уточняющего опции, приведены в файле subfsm. с. /* Файл subfsm.с */ ¦include <sys/types.h> ¦include <stdio.h> ¦include "telnet.h" ¦include "tnfsm.h" extern int no_op(FILE *, FILE *, int), subtermtype(FILE *, FILE *, int), tnabort(FILE *, FILE *, int); struct fsm_trans substab[] * { /•Состояние Ввод След. состояние Действие */ /* */ { SS START, TOTERMTYPE, SS TERMTYPE, no_op }, { SS~START, TCANY, SS~END, no_op }, { SS TERMTYPE, TT SEND, SS END, subtermtype }, { SSJTERMTYPE, TCANY, SSJSND, no_op }, { SS END, TCANY, SS END, no_op }, { FSINVALID, TCANY, FSINVALID, tnabort }, }J int substate; u_char subfsm[NSSTATES][NCHRS]; 26.25. Инициализация конечного автомата С началом выполнения клиентской программы вызывается процедура fsmbuild для инициализации всех конечных автоматов. Как показывает код, приведенный в файле ttinit.c, процедура fsmbuild вызывает процедуру fsminit для построения требуемых структур данных всех конечных автоматов и устанавливает начальное значение переменной состояния каждого конечного автомата. 26.24. Определения конечного автомата, уточняющего опции 423
/* Файл ttinit - процедура ttinit */ ¦include <sys/types.h> ¦include <stdio.h> ¦include "tnfsm.h" extern struct fsm_trans ttstab[], sostab[]r substabf]; extern unsigned char ttfsm(][NCHRS], sofsm[][NCHRS], subfsm[][NCHRS]; extern int ttstate, sostate, substate; int fsminit(unsigned char fsm[][NCHRS], struct fsra_trans ttab[], int nstates); /* ~ * Процедура fsmbuild - формирование структур данных конечного автомата *_ . ... . .. ... . . */ int fsmbuild() { fsminit(ttfsm, ttstab, NTSTATES); ttstate = TSDATA; fsminit(sofsm, sostab, NKSTATES); sostate = KSREMOTE? fsminit(subfsm, substab, NSSTATES); substate = SS START; } 26.26. Параметры клиентской программы TELNET Файл tclient.c содержит код главной процедуры, которая выполняется при вызове пользователем клиентской программы TELNET: /* Файл tclient - главная процедура */ ¦include <stdlib.h> char *host = "localhost"; /* Применяемое по умолчанию имя хоста */ int errexit(const char *formatr ...); int telnet(const char *host, const char *service); /* * Главная процедура - клиент TCP для службы TELNET * ™™ ™ « — - ... -.—.-..— . . .... */ int main(int argc, char *argv[]) { 424 Глава 26. TELNET (структура программы)
char *service = "telnet"; /* Применяемое по умолчанию имя службы */ switch (argc) { case 1: break; case 3: service ¦ argv[2]; /* Выполняется также следующая ветвь оператора switch */ case 2: host s argv[l]; break; default: errexit("usage: telnet [host [port]]\n"); } telnet(host, service); •xit(O); > Пользователь может не ввести ни одного параметра командной строки либо ввести один или два параметра командной строки, которые интерпретируются программой. При отсутствии параметров (argc=l) клиент обращается к серверу яа локальном хосте и использует службу telnet. При наличии одного параметра (argc*2) клиент использует этот параметр в качестве имени удаленного хоста, на котором работает сервер. И наконец, при получении двух параметров клиент рассматривает второй параметр как имя службы на удаленном компьютере, а первый параметр — как имя удаленного хоста. После интерпретации параметров в главной процедуре вызывается функция telnet. 26.27. Основа клиента TELNET После вызова клиентская программа выполняет необходимую инициализацию. Затем клиентская программа входит в бесконечный цикл, в котором применяется функция select для перехода в состояние ожидания готовности дескрипторов к вводу/выводу. Код клиентской программы содержится в файле telnet.с. 7* Файл telnet - процедура telnet */ ¦include <sys/types.h> ¦include <sys/socket.h> ¦include <sys/time.h> ¦include <sys/signal.h> ¦include <sys/errno.h> ¦include <unistd.h> ¦include <stdlib.h> ¦include <string.h> ¦include <stdio.h> ¦include "local.h" void rcvurg(int); ¦define BUFSIZE 2048 /* Размер буфера чтения */ struct termios oldtty; ¦ffiiSl. Основа клиента TELNET 425
int connectTCP(const char *host, const char *service); int ttysetup(void); /* * Процедура telnet - подключение к указанному хосту и порту по протоколу * TELNET */ int telnet(const char *host, const char *service) { unsigned char buf[BUFSIZE]; int s, nfds; /* Дескриптор сокета и несколько дескрипторов файлов */ int cc; int on = 1; fd__set arfds, awfds, rfds, wfds; FILE *sfp; s = connectTCP(host, service); ttysetup(); fsmbuild(); /* Настройка конечного автомата */ (void) signal(SIGURG, rcvurg); (void) setsockopt(s, SOL_SOCKET, SOJDOBINLINE, (char *)&on, sizeof(on)); nfds = getdtablesize(); FDJERO(Sarfds)? FDJERO(&awfds); FD_SET(s, barfds); /* Сокет */ FDJ5ET@, Sarfds)? /* Стандартное устройство ввода */ sfp = fdopen(s, "w"); /* Подготовка к буферизованному выводу */ while A) { memcpy(&rfds, fcarfds, sizeof(rfds)); memcpy(&wfds, &awfds, sizeof(rfds)); if (select(nfds, &rfds, &wfds, (fd_set *H, (struct timeval *H) <0) { if (errno * EINTR) continue; /* Просто сигнал */ cerrexit("select: %s\n", strerror(errno)); } if (FD_ISSET(s, Srfds)) { cc = read(s, (char *)buf, sizeof(buf)); if (cc <0) cerrexit("socket read: %s\n", strerror(errno)); else if (cc == 0) { printf("\nconnection closed.\n"); 426 Глава 26. TELNET (структура про
if (tcsetattr@, TCSADRAIN, fcoldtty) <0) errexit("tcsetattr: %s\nH, strerror(errno)); exit(O)? } else ttwrite(sfpr stdout, buf, cc); } if (FD_ISSET@, &rfds)) { cc = read@, (char *)buf, sizeof(buf)); if (cc <0) cerrexit("tty read: %s\n", strerror(errno)); else if (cc == 0) { FD_CLR@, Sarfds); (void) shutdowns, 1); } else sowrite(sfp, stdout, buf, cc); } (void) fflush(sfp); (void) fflush(stdout); } } Процедура telnet принимает два параметра с указанием имени удаленного компьютера и службы на этом компьютере. Код начинается с вызова функции connectTCP для распределения сокета и формирования соединения TCP с сервером. В этой процедуре вызывается процедура ttysetup для инициализации параметров локального терминала и процедура f smbuild — для инициализации трех конечных автоматов. Затем в ней вызывается системная функция signal, создающая дескриптор для срочных данных. Если программное обеспечение TCP получит срочные данные от сервера после вызова на нем функции signal, операционная система вызовет для их обработки процедуру rcvurg. Для обеспечения переносимости кода в данной реализации не определено максимальное число дескрипторов файлов. Вместо этого перед инициализацией дескрипторов файлов в процедуре telnet вызывается системная функция getdtablesize для определения числа дескрипторов файлов, доступных в системе, после чего выполняется регистрация полученного результата в переменной nfds. При каждом проходе по главному циклу процедура telnet передает это значение в качестве первого параметра функции select. Поскольку процедуры tcdm и rcvurg являются весьма небольшими, они не были помещены в отдельные файлы. Вместо этого код обеих процедур включен в файл sync.с: /* Файл sync - процедуры tcdm, rcvurg */ ¦include <stdio.h> char synching; /* Отличен от нуля, если происходит синхронизация TELNET */ /*_ * Процедура tcdm - обрабатывает команду DATA MARK протокола TELNET * (которая обозначает конец режима синхронизации) *.. . . */ 26.27. Основа клиента TELNET
int tcdro(FILE *sfp, FILE *tfp, int c) { if (synching> 0) synching—; return 0; } /* - ~ г--------.--*----*--------------- * Процедура rcvurg - получение сигнала срочных данных (который указывает * на начало режима синхронизации TELNET) •———————————.-------------------.-¦—.*-i—u——————^—.—- ¦¦ */ int rcvurg(int sig) { synching++; } Протокол TELNET указывает, что при получении срочных данных клиент должен выполнить синхронизацию своего состояния с сервером. Для синхронизации клиент пропускает все символы в потоке данных, поступающих с сервера до тех пор, пока не встретит символ DATA MARK. Таким образом, при получении сигнала срочных данных процедура rcvurg просто наращивает значение глобальной переменной synching для перевода клиентской программы в режим синхронизации. После установки переменной synching исполнительные процедуры конечного автомата просто отбрасывают входящие данные, не отображая их на экране терминала пользователя. В дальнейшем после поступления символа DATA MARK клиент вызывает процедуру tcdm, чтобы снова присвоить глобальной переменной synching нулевое значение, после чего клиентская программа возвращается в режим нормальной обработки. После завершения инициализации процедура telnet входит в бесконечный цикл. В этой процедуре при каждом проходе по циклу вызывается функция select для перехода в состояние ожидания готовности к вводу/выводу сокета или клавиатуры (т.е. стандартного устройства ввода, связанного с дескриптором 0). Вызов функции select возвращает значение EINTR, если программа получает сигнал, а процесс в это время заблокирован. Если возникает такая ситуация, клиент затем переходит к выполнению следующей итерации цикла. Если вызов функции select возвращает любой другой код ошибки, клиент выводит сообщение для пользователя и завершает работу. При нормальном возврате управления из функции select в программе могут быть приняты данные, введенные с клавиатуры, поступившие из сокета или полученные из того или иного устройства ввода. Процедура telnet вначале проверяет сокет для определения того, имеются ли данные, поступившие с сервера. Если такие данные имеются, в процедуре вызывается функция read для получения данных и функция ttwrite — для вывода данных на экран терминала пользователя. Как будет показано ниже, в процедуре ttwrite реализована принципиальная схема конечного автомата, который интерпретирует входящий поток данных и обрабатывает управляющие символы и встроенные управляющие последовательности. После проверки наличия входящих данных в сокете процедура telnet проверяет дескриптор клавиатуры. Если данные поступили с клавиатуры, в процедуре telnet вызывается функция read для получения данных и процедура sowrite — для выво- 428 Глава 26. TELNET (структура программы)
да их в сокет. Процедура sowrite содержит код, вызывающий на выполнение конечный автомат, предназначенный для локальной обработки управляющих символов. В отдельных случаях клиент интерпретирует признак конца файла как запрос на прекращение соединения. При получении признака конца файла (т.е. при возврате функцией read значения 0) процедура telnet вызывает процедуру shutdown для передачи признака конца файла на сервер. В любом случае в процедуре telnet при каждом проходе по циклу вызывается функция fflush для обеспечения того, чтобы процедуры вывода не хранили в буфере выведенные данные. Функция fflush активизирует вызов системной процедуры write. 26.28. Реализация основного конечного автомата Процедура ttwrite реализует конечный автомат, приведенный на рис. 26.4, который интерпретирует данные, поступающие с сервера. Этот код представлен в файле ttwrite «с. /* Файл ttwrite - процедура ttwrite */ . ¦include <sys/types.h> linclude <stdio.h> ¦include "tnfsm.h" extern struct fsm_trans ttstab[]; extern unsigned char ttfsm[][NCHRS]; extern int ttstate; /* . * Процедура ttwrite - обработка выходных данных, поступающих на (локальный) * сетевой виртуальный терминал *.......... ........—.-• . ..................... ... ——— */ int ttwrite(FILE *sfp, FILE *tfp, unsigned char *buf, int cc) { struct fsm_trans *pt; int i, ti; for (i=0; i<cc; ++i) { int с * buf[i]; ti = ttfsm[ttstate][c]j pt ¦ &ttstab[ti]; (pt->ftraction)(sfp, tfp, c)j ttstate s pt->ft next? } } При каждом ее вызове процедура ttwrite извлекает ее символов из буфера buf. После извлечения каждого символа в процедуре ttwrite этот символ и текущее состояние (значение переменной ttstate) применяются для определения 26.28. Реализация основного конечного автомата 429
индекса матрицы переходов. Матрица переходов возвращает ti — индекс перехода в компактном представлении (ttstab). Процедура ttwrite вызывает процедуру» связанную с этим переходом (поле ftraction), и устанавливает текущую переменную в состояние, соответствующее следующему состоянию (поле f t jiext). 26.29. Резюме TELNET принадлежит к числу наиболее широко применяемых прикладных протоколов в наборе протоколов TCP/IP. Он обеспечивает интерактивный обмен символьными сообщениями между клиентом и сервером. Обычно клиентская программа подключает пользовательский терминал к серверу через соединение TCP. В настоящей главе приведен пример клиентского программного обеспечения, функционирующего на основе одного потока выполнения, в котором используется функция операционной системы select для обеспечения параллельного выполнения операций ввода/вывода. В протоколе TELNET для внедрения в поток данных команд и управляющей информации используются управляющие последовательности. В целях упрощения кода в рассматриваемом примере реализации клиентской программы для интерпретации последовательностей символов применяется три конечных автомата. Один из них обрабатывает данные, поступающие из сервера, другой обрабатывает данные, поступающие с клавиатуры терминала пользователя, а третий обеспечивает уточнение опций. Пример кода, приведенный в этой главе, может служить иллюстрацией не только к реализации основного клиентского процесса, но и к структурам данных, которые служат для построения конечных автоматов. В следующей главе приведены сведения о процедурах, выполняющих действия, связанные с переходами в конечных автоматах. Материал для дальнейшего изучения В документе [120] приведен стандарт основного протокола TELNET, в частности, кодировки сетевого виртуального терминала. В документе [121] даны подробные сведения об операциях согласования опции и уточнения опции. Дополнительную информацию об отдельных опциях можно найти в других документах RFC. В документе [157] описана опция определения типа терминала. В документе [123] рассматривается опция эхо-повтора, а в документе [122] описана опция передачи в двоичном режиме. В книге [151] описан протокол RLOGIN (разновидность протокола TELNET, применяемая во многих системах UNIX, включая Linux) и показан пример простого клиента и сервера. В книге [92] представлены дополнительные сведения о терминальном вводе/выводе. Упражнения 26.1. Сравните реализацию клиента TELNET, в которой применяется несколько потоков выполнения в одном процессе, с проектом, представленном в настоящей главе. Каковы преимущества и недостатки каждого из них? 26.2. Прочитайте оперативную документацию, чтобы больше узнать о терминальных устройствах и драйверах устройств. Что такое режим с обработкой (cooked mode), режим с прерыванием после ввода каждого символа (cbreak mode) и режим без обработки (raw mode)? 430 Глава 26. TELNET (структура программы)
26.3. Напишите программу, которая отменяет эхо-повтор символов на терминале пользователя. Что происходит с параметром эхо-повтора после завершения работы программы? 26.4. Прочитайте документацию Linux для получения информации о команде stty. Что произойдет после перенаправления вывода из команды stty в файл? Почему? 26.5. Каким образом код, приведенный в файле suspend.с, приостанавливает клиентский процесс? 26.6. Доработайте принципиальную схему конечного автомата и включите в нее состояния, предназначенные для уточнения опций. 26.7. В этом примере реализации конечного автомата для экономного распределения памяти применяется компактное представление. Рассчитайте, какой объем памяти потребуется для реализации рассматриваемой принципиальной схемы конечного автомата с использованием обычного представления, и сравните это значение с объемом памяти, требуемым для компактного представления. 26.8. При каких условиях после выполнения функции read для чтения данных с терминала будет возвращено значение О? 26.9. Прочитайте спецификацию протокола TELNET, чтобы ознакомиться с точными правилами синхронизации. Как отправитель передает срочные данные? Для чего нужна синхронизация? 26.10. В сервере может возникнуть необходимость отправить дополнительные данные после получения от клиента признака конца файла. Каким образом клиент получает информацию о том, что нужно закрыть соединение с сервером? 26.11. Доработайте процедуру ttwrite таким образом, чтобы в ней не использовался конечный автомат. Каковы преимущества и недостатки каждой реализации? фажнения 431
27 Клиент TELNET (практическая реализация) 27.1. Введение В предыдущей главе рассматривается структура клиентской программы TELNET и показано, как в ней используются конечные автоматы для управления обработкой. В настоящей главе это описание завершается; в ней описана реализация методов обработки символов с использованием соответствующих исполнительных процедур. 27.2. Исполнительные процедуры конечного автомата В рассматриваемой программе основная часть функций протокола TELNET реализована с применением конечных автоматов. Они управляют обработкой, вырабатывают ответы на запросы и выполняют действия в соответствии с входящими управляющими последовательностями. При осуществлении в клиентской программе каждого перехода в конечном автомате вызывается процедура для выполнений действий, связанных с данным переходом. На рис. 26.41 указаны имена процедур, соответствующих переходам в конечном автомате, в которых обрабатываются символы, поступающие с сервера. Действие может быть простым (например, уничтожение входящего символа) или сложным (например, передача ответа путем отправки строки с обозначением локального терминала). Поскольку каждое действие оформлено в виде процедуры, спецификация конечного автомата становится единообразной и упрощается структура кода. Однако в связи с тем, что программное обеспечение разделено на ряд процедур, выполняющих отдельные действия, связь между процедурами невозможно понять без изучения работы конечного автомата, объединяющего эти процедуры. В каждом из следующих разделов описаны отдельные исполнительные процедуры, связанные с переходами конечного автомата. За ними следует раздел с описанием формы вызова исполнительных процедур. 27.3. Регистрация типа запроса опции В состоянии TSIAC (т.е. вслед за поступлением символа TCIAC) получение символа TCWILL или TCW0NT вызывает переход в состояние TSW0PT. Согласно специфи- 1 Указанный рисунок приведен на стр. 414.
кации конечного автомата, при этом переходе должна быть вызвана процедура recopt. Эта процедура регистрирует символ, вызвавший переход, для его дальнейшего использования. Аналогичным образом, в конечном автомате процедура recopt применяется для регистрации символа TCDO или TCDONT во время перехода в состояние TSD0PT. Файл recopt.с содержит следующий код. /* Файл recopt - процедуры recopt, no_op */ ¦include <sys/types.h> ¦include <stdio.h> unsigned char option_cmd; /* Может принимать значение WILL, WONT, DO */ /* или DONT */ /* * Процедура recopt - регистрация типа опции * « ... */ int recopt(FILE *sfp, FILE *tfp, int c) { option^cmd « c; return 0; } /* * Процедура по_ор - пустая операция * ..Г « ? « */ int no_op(FILE *sfp, FILE *tfp, int c) { return 0; } 27.4. Выполнение пустой операции Файл recopt.с содержит также код процедуры по_ор (сокращение от по operation — пустая операция). Поскольку в конечном автомате должны быть предусмотрены действия для всех возможных сочетаний состояния и входного символа, процедура по_ор может применяться для тех переходов, которые не требуют никаких действий. Например, не требует выполнения каких-либо действий переход из состояния TSDATA в состояние TSIAC. Поэтому в конечном автомате ему соответствует вызов процедуры по_ор. 27.5. Ответ на сообщения WILL/WONT, соответствующие запросу опции эхо-повтора На запрос опции ECHO сервер отвечает сообщением WILL или WONT, информируя клиентскую программу, будет ли он по-прежнему выполнять эхо-повтор симво- 434 Глава 27. Клиент TELNET (практическая реализация)
лов или прекратит эхо-повтор. Согласно спецификации конечного автомата, при поступлении такого сообщения должна быть вызвана процедура do_echo. /* Файл do_echo - процедура do_echo */ ¦include <sys/types.h> ¦include <termios.h> ¦include <stdio.h> ¦include "telnet.h" char doecho; /* Имеет ненулевое значение, если эхо-повтор */ /* выполняется сервером */ extern u_char option_cmd; /*-. . - — * Процедура do_echo - обработка команд HILL/WONT, связанных с согласованием * опции эхо-повтора протокола TELNET *._.«_-« . . */ int do echo(FILE *rfp, FILE *tfp, int c) {" • struct termios tio; static char savec[2]; int ok, tfd = fileno(tfp); if (doecho) { if (option_cmd == TCWILL) return 0; /* Эхо-повтор уже выполняется */ } else if (option_cmd == TCWONT) return 0; /* Эхо-повтор уже НЕ выполняется */ if (ok = tcgetattr(tfd, &tio) *= 0) { if (option_cmd == TCWILL) { tio.c_lflag &= "(ECHO | ICANON); /* Значения VMIN и VTIME будут стерты при поступлении других */¦. /* символов, поэтому сохранить их, а затем восстановить */ savec[0] = tio.c_cc[VMIN]; savec[l] = tio.с cc[VTIME]; tio.c__cc[VMIN] *~1; tio.c_cc[VTIME] * 0; > else { tio.c_lflag |= (ECHO | ICANON); tio.c_cc[VMIN] = savec[0]; , tio.c_cc[VTIME] = savec[l]; } ok &= tcsetattr(tfd, TCSADRAIN, &tio) == 0; } if (ok) doecho « Idoecho; (void) putc(TCIAC, rfp); 27.5. Ответ на сообщения WILL/WONT... 435
if (doecho) (void) putc(TCDO, rfp); else (void) putc(TCDONT, rfp); (void) putc((char)c, rfp); return 0; } В спецификации протокола TELNET указано, что сервер может передать сообщение WILL или WONT, чтобы объявить о своей готовности применять конкретную опцию; одно из этих сообщений может быть также отправлено в ответ на запрос клиента. Поэтому, если клиент передал запрос, содержащий команду DO или D0NT, сообщение, переданное сервером, представляет собой ответ; в ином случае оно является извещением о готовности сервера применять конкретную опцию. В клиентской программе для принятия решения о том, какие действия должны быть выполнены в ответ на получение сообщений WILL или WONT, используется информация о текущем состоянии конечного автомата. Глобальная переменная doecho содержит ненулевое значение, если клиент в настоящее время ожидает, что эхо-повтор символов будет осуществляться сервером. Если сервер прислал сообщение WILL и в клиентской программе уже разрешен дистанционный эхо- повтор, клиент не отвечает на это сообщение. Аналогичным образом, клиент не отвечает, если сервер прислал сообщение WONT и в клиенте запрещен дистанционный эхо-повтор. Однако если сервер прислал сообщение WILL, а клиент в настоящее время отключил дистанционный эхо-повтор, в клиентской программе вызывается процедура tcsetattr для отмены локального эхо-повтора символов. Если в клиенте при получении сообщения WONT отменен локальный эхо-повтор, то в клиентской программе предполагается, что сервер запретил дистанционный эхо- повтор. Поэтому в клиентской программе вызывается процедура tcsetattr для разрешения локального эхо-повтора. Клиентская программа передает ответ DO или D0NT только при изменении в ней режима эхо-повтора. 27.6. Ответ на сообщения WILL/WONT, соответствующие не поддерживаемым опциям При получении клиентской программой запроса WILL или WONT на применение опции, которая в ней не предусмотрена, вызывается процедура dojiotsup для формирования ответа D0NT. /* Файл dojiotsup - процедура dojiotsup */ ¦include <sys/types.h> ¦include <stdio.h> ¦include иtelnet.hH extern u^char optionjand; /* * Процедура dojiotsup - обработка запросов WILL/WONT на выполнение * ~ неподдерживаемых опций TELNET *———————————————————————— —————........ 436 Глава 27. Клиент TELNET (практическая реализация)
*/ int do notsup(FILE *sfp, FILE *tfp, int c) (void) putc(TCIAC, sfp); (void) putc(TCDONT, sfp); (void) putc((char)c, sfp); return 0; 27.7. Ответ на сообщения WILL/WONT, соответствующие опции подавления символов GA В клиентской программе процедура dojioga используется для формирования ответа при получении от сервера запросов WILL или WONT на подавление символов GA (Go-Ahead — Передача управления), предусмотренных протоколом TELNET. /* Файл dojioga - процедура dojioga */ ¦include <sys/types.h> ¦include <stdio.h> ¦include "telnet.hM extern u_char option_cmd; * Процедура dojioga - отказ от выполнения функции передачи управления * " (Go-Ahead) протокола TELNET */ int do noga(FILE *sfp, FILE *tfp, int c) { " static noga; if (noga) { if (option jand ¦¦ TCWILL) return 0; } else if (optionj:md и TCWONT) return 0; noga ¦ inoga; (void) putc(TCIAC, sfp); if (noga) (void) putc(TCDO, sfp); else , . . (void) putc(TCDONT, sfp); (void) putc((char)c, sfp); return 0; } Как и в случае с другими опциями, клиент не отвечает, если его текущая установка для этой опции соответствует запросу сервера. Если же сервер требует изменить установку этой опции, клиент меняет текущую установку на противо- 27.7. Ответ на сообщения WILL/WONT... 437
положную, применяя операцию отрицания к глобальной целочисленной переменной пода, и передает ответ DO или D0NT. 27.8. Выработка ответа DO/DONT на запрос опции двоичной передачи Сервер может передавать клиенту символы либо в закодированном виде с использованием кодировки сетевого виртуального терминала, либо в незакодиро- ванном виде с использованием 8-битовых двоичных значений. Глобальная переменная rcvbinary управляет тем, будет ли клиент ожидать получения данных в виде двоичных символов или символов в кодировке NVT. Для формирования ответа при получении от сервера запроса WILL или WONT на применение опции двоичной передачи клиент вызывает процедуру do_txbinary. Как и другие процедуры управления опциями, процедура do_txbinary была разработана так, что она может вызываться для формирования запроса к серверу на использование двоичного режима или для подготовки ответа на извещение, полученное от сервера. В этой процедуре для определения способа дальнейших действий используется глобальная переменная option_cmd и предполагается, что эта переменная содержит входящий запрос. Процедура do_txbinary проверяет, ожидает ли клиент, что сервер будет передавать данные в двоичном коде, устанавливает значение переменной rcvbinary в соответствии с входящим запросом и передает серверу ответ, если значение переменной rcvbinary изменилось. /* Файл do_txbinary - процедура do_txbinary */ ¦include <sys/types.h> ¦include <stdio.h> tinclude "telnet.hH char rcvbinary; /* Имеет ненулевое значение, если сервер выполняет */ /* передачу в двоичном режиме */ extern u_char option_cmd; /* * Процедура do_txbinary - обработка запросов WILL/WONT на применение опции * "-,.., передачи в двоичном ходе протокола TELNET * - ¦«.._ ;'—--. "' ..._« '. - « — */ int do txbinary(FILE *sfp, FILE *tfp, int c) { " if (rcvbinary) { if (option_cmd « TCWILL) return 0 j ..•'•--¦ } else if (option_cmd '*« TCWONT) return 0; rcvbinary = !rcvbinary? (void) putc(TCIAC, s?p); if (rcvbinary) (void) putc(TCD0, sfp); else (void) putc(TCDONT, sfp); 438 Глава 27. Клиент TELNET (практическая реализация)
(void) putc((char)cf sfp); return 0; } 27.9. Ответ за запросы DO/DONT, содержащие требования по применению неподдерживаемых опций Сервер передает клиенту сообщения DO или D0NT, содержащие требования к клиенту, соответственно, разрешить шга запретить указанную опцию. Выражая свою готовность применять указанную опцию, клиент отвечает сообщением WILL; если же клиент не будет применять эту опцию, он отвечает сообщением WONT. Как показывает схема конечного автомата, приведенная на рис. 26.4, если клиент не поддерживает конкретную опцию, он вызывает процедуру willjiotsup. Процедура willjiotsup отправляет серверу сообщение WONT о том, что данная опция клиентом не поддерживается. /* Файл willjiotsup - процедура willjiotsup */ ¦include <sys/types.h> finclude <stdio.h> finclude "telnet.h" /* . . * Процедура willjiotsup - обработка запросов DO/DONT на применение * неподдерживаемых опций TELNET * . . */ int will notsup(FILE *sfp, FILE *tfp, int c) { (void) putc(TCIAC, sfp); (void) putc(TCWONT, sfp); (void) putc((char)c, sfp); return 0; } 27.10. Ответ на запрос DO/DONT по применению опции двоичной передачи Непосредственно после запуска клиентская программа использует кодировку сетевого виртуального терминала NVT для всех данных, передаваемых на сервер. Хотя кодировка NVT включает большинство печатаемых символов, в ней не предусмотрены коды для всех управляющих символов. Обычно серверы, которые функционируют в операционных системах, поддерживающих приложения с экранным интерфейсом, должны иметь возможность передавать произвольные символьные данные. Поэтому такие серверы обычно извещают клиентскую программу о своей готовности передавать двоичные данные и запрашивают клиентскую программу, будет ли она также передавать двоичные данные. Сервер передает запрос DO на применение опции двоичной передачи, требуя, чтобы клиент приступил к использованию 8-битовой, незакодированной передачи. После получения такого запроса клиент вызывает процедуру will_txbinary. 27.9. Ответ за запросы DO/DONT... 439
/* Файл will_txbinary - процедура will_txbinary */ finclude <sys/types.h> ¦include <stdio.h> ¦include "telnet.hM char sndbinary; /* Имеет ненулевое значение, если применяется режим */ /* передачи в двоичном коде */ extern u_char option_cmdj /*__. - ^——...-......-. - * Процедура will_txbinary - обработка запросов D0/D0NT на применение опции * " передачи в двоичном коде TELNET */ int will txbinary(FILE *sfp, FILE *tfp, int c) { if (sndbinary) { if (optionjand ¦¦ TCDO) return Oj } else if (option_cmd ¦¦ TCDONT) return Oj sndbinary ¦ Isndbinary; (void) putc(TCIAC, sfp); if (sndbinary) (void) putc(TCWILL, sfp); else (void) putc(TCWONT, sfp); (void) putc((char)c, sfp); return 0; } Для управления режимом передачи в клиенте используется глобальная переменная sndbinary. Если запрос требует изменения состояния, клиент подтверждает запрос, передавая ответ WILL или WONT. 27.11. Ответ на запрос DO/DONT с требованием передать информацию о типе терминала Для передачи клиентом серверу информации о типе терминала должны быть выполнены два действия. Во-первых, сервер запрашивает клиента, применяется ли в нем опция tenntype (тип терминала). Во-вторых, если клиент выражает свое согласие на применение опции termtype, сервер использует опцию уточнения для передачи запроса на получение строки с обозначением типа терминала пользователя. В операционной системе Linux клиент находит информацию о типе терминала пользователя, проверяя переменную TERM в среде процесса. Для этого клиент вызывает библиотечную функцию getenv, которая находит указанную переменную и возвращает указатель на ее строковое значение. При поступлении запроса на применение опции termtype клиент вызывает процедуру will_termtype. Этот код находится в файле will_termtype.c. 440 Глава 27. Клиент TELNET (практическая реализация)
/* Файл will^termtype - процедура will_termtype */ ¦include <sys/types.h> Iinclude <stdlib.h> ¦include <stdio.h> ¦include "telnet.h" char termtype; /* Имеет ненулевое значение, если получена команда */ /* DO TERMTYPE */ char *term; /* Имя терминала */ extern u_char option_cmd; int do_txbinary(FILE *,FILE *,int), will_txbinary(FILE *,FILE *,int); /* * Процедура will_termtype - обработка запросов DO/DONT на применение опции * " согласования типа терминала TELNET *. ------- .™«™——«.«.. «. « —— - */ int will termtype(FILE *sfp, FILE *tfp, int c) { if (termtype) { if (option_cmd ¦¦•TCDO) return (J; } else if (option_cmd ¦¦ TCDONT) return 0; termtype = !termtype? if (termtype) if (Iterm && I(term = getenv("TERM"))) termtype ¦ Itermtype; /* Запрос не может быть выполнен */ (void) putc(TCIAC, sfp); if (termtype) (void) putc(TCWILL, sfp); else (void) putc(TCWONT, sfp); (void) putc((char)c, sfp); if (termtype) { /* Установить режим передачи двоичных данных, передать */ /* команды WILL и DO */ option cmd s TCWILL; (void)~do_txbinary(sfp, tfp, TOTXBINARY); option cmd ¦ TCDO; (void)""will txbinary(sfp, tfp, TOTXBINARY); } return 0; } • l '; "'" ; r " Процедура will_termtype действует так же, как и другие обработчики опций. В ней используется глобальная переменная termtype для регистрации информации о том, что сервер ранее уже запрашивал опцию termtype, и выполняется проверка того, приведет ли текущий запрос к изменению состояния. В случае положительного ответа вызывается функция getenv для получения значения, 27.11. Ответ на запрос DO/DONT... 441
связанного с переменной среды TERM. Если такая переменная не существует, клиент отвечает, что им не будет выполнен данный запрос. Сервер запрашивает информацию о типе терминала для того, чтобы выполняемые на нем приложения могли подготовить вывод, приемлемый для пользовательского терминала. Например, в текстовом редакторе информация о типе терминала используется при подготовке последовательностей управляющих символов, которые представляют собой команды очистки экрана, перемещения курсора или выделения текста. Поэтому клиент ожидает, что после получения информации о типе терминала удаленное приложение будет передавать управляющие последовательности, подходящие для этого терминала. Поскольку такие последовательности нельзя передавать в кодировке NVT, процедура will termtype передает сообщение WILL, которое извещает о готовности клиента использовать двоичную передачу, и сообщение DO с запросом к серверу на применение двоичной передачи. Поскольку функции do_txbinary и will_txbinary могут быть вызваны из кода обработки опций конечного автомата, в них для управления обработкой используется глобальная переменная option__cmd. При непосредственном вызове этих функций другие процедуры должны инициализировать переменную option_cmd явно, как если бы клиент перед вызовом этих функций получил соответствующее сообщение WILL или DO от сервера. 27.12. Уточнение опции После передачи клиентом согласия на обработку опции типа терминала termtype сервер использует уточнение опции для запроса имени терминала. В отличие от всех обычных опций, имеющих постоянную длину, команда уточнения позволяет отправителю вставить в поток данных строку произвольной длины. Для этого отправитель как бы заключает строку в скобки, передавая заголовок уточнения, данные для конкретного уточнения опции и концевик, который обозначает конец уточнения. При обнаружении управляющей последовательности уточнения главный конечный автомат (рис. 26.4) переходит в состояние TSSUBNEG. После перехода в состояние TSSUBNEG клиент вызывает процедуру subopt при получении каждого символа. Как показывает код, приведенный в файле subopt.c, в процедуре subopt для выполнения операции уточнения применяется конечный автомат уточнения опции. /* Файл subopt - процедура subopt */ ¦include <sys/types.h> ¦include <stdio.h> ¦include "telnet.hM ¦include "tnfsm.h" extern struct fsm_trans substabf]; extern int substate; extern u_char subfsm[][NCHRS]; /* --— * Процедура subopt - выполнение команд, связанных с переходами конечного * автомата уточнения опции *. —— — ... - —— —... - ....... . */ 442 Глава 27. Клиент TELNET (практическая реализация)
int SUbopt(FILE *sfp, FILE *tfp, int c) { struct fsm_trans *pt; int ti; ti = subfsm[substate][c]; pt = &substab[ti]; (pt->ft_action)(sfp, tfp, c); substate = pt->ft_next; return 0; n > 27.13. Передача информации о типе терминала Для формирования ответа на запрос о типе терминала конечный автомат уточнения опции2 вызывает процедуру subtermtype. Сервер для запроса типа терминала передает управляющую последовательность: IAC SUBNEG TERMTYPE SEND IAC SUBEND Клиент отвечает, передавая сообщение: IAC SUBNEG TERMTYPE IS term^type^string IAC SUBEND Здесь term^type^string — строка с обозначением типа терминала. Файл subtermtype.с содержит код: /* Файл subtermtype - процедура subtermtype */ ((include <sys/types.h> ¦include <stdio.h> if include "telnet.h" extern char *term; /* Имя терминала, установленное в время */ /* инициализации процедуры */ /* . * Процедура subtermtype - применение опции уточнения типа терминала *.. -..-.—. - - « ™ */ int subtermtype(FILE *sfp, FILE *tfp, int c) { /* Получены команды IAC.SB.TERMTYPE.SEND */ (void) putc(TCIAC, sfp); (void) putc(TCSB, sfp); (void) putc(TOTERMTYPE, sfp); (void) putc(TT_IS, sfp); fputsfterm, sfp); (void) putc(TCIAC, sfp); Описание конечного автомата уточнения опции приведено на стр. 422. 27.13. Передача информации о типе терминала 443
(void) putc(TCSE, sfp); return 0; } После получения запроса SEND конечный автомат уточнения опции вызывает процедуру subtermtype. Перед этим клиент должен был передать положительный ответ на запрос о применении опции типа терминала, поэтому глобальная переменная term уже должна указывать на строку, содержащую обозначение типа терминала. Процедура subtermtype передает ответ, вызывая функцию putc для отправки отдельных управляющих символов и функцию fputs для отправки строки с информацией о типе терминала. 27.14. Завершение операции уточнения При обнаружении признака конца операции уточнения опции конечный автомат, схема которого приведена на рис. 26.4, снова переходит в состояние TSDATA. При выполнении каждого такого перехода вызывается процедура subend. Эта процедура просто переустанавливает конечный автомат уточнения опции в его начальное состояние для подготовки к выполнению следующей операции уточнения опции. Этот код содержится в файле subend.с. /* Файл subend - процедура subend */ tinclude <sys/types.h> tinclude <stdio.h> ¦include "tnfsm.h" extern int substate; /* * Процедура subend - завершение операции уточнения опции; установка конечного * автомата в исходное положение * .._ .. */ int subend(FILE *sfp, FILE *tfp, int c) { substate = SS_START; return 0; } 27.15. Передача символа на сервер Клиент вызывает процедуру soputc для преобразования кодировки выходного символа в кодировку сетевого виртуального терминала и передачи символа через сокет TCP на сервер. Файл soputc.с содержит следующий код. /* Файл soputc - процедура soputc */ iinclude <sys/types.h> ¦include <stdio.h> 444 Глава 27. Клиент TELNET (практическая реализация)
¦include иtelnet.hM ¦include "local.h" /t ~ . . * Процедура soputc - перемещение символа из клавиатуры в сокет *——..... -.-_ . . ... .. ...... . .... .. . :*/ kt SOputc(FILE *sfp, FILE *tfp, int c) { if (sndbinary) { if (c == TCIAC) (void) putc(TCIAC, sfp); /* Вставка дополнительного символа IAC */ /* при его обнаружении в данных */ (void) putc(c, sfp); return 0j } с &* 0x7f; /* Только семибитовые символы ASCII */ if (с яв t intrc || с == t quite) { /* Прерывание */ (void) putc(TCIAC, sfp)T (void) putc(TCIP, sfp); } else if (c == sg erase) { /* Стереть символ */ (void) putc(TCIAC, sfp)? (void) putc(TCEC, sfp)? } else if (c ¦* sgjcill) { /* Стереть строку */ (void) putc(TCIAC, sfp); (void) putc(TCEL, sfp); } else if (c =- t_flushc) { /* Аварийно прервать вывод */ (void) putc(TCIAC, sfp); (void) putc(TCAO, sfp); } else (void) putc(c, sfp); return 0; } Если передача ведется в двоичном режиме, вставка символов должна выполняться только применительно к символам IAC в потоке данных. Это означает, что процедура soputc должна заменять каждый символ IAC двумя символами IAC. Что касается любых других символов, то процедура soputc просто вызывает функцию putc для их передачи. Если передача ведется в обычном режиме, локальный набор символов должен быть преобразован процедурой soputc в набор символов сетевого виртуального терминала. Например, если поступающий символ соответствует либо символу прерывания, либо управляющему символу, процедура soputc передает двухеим- вольную последовательность: IAC IP В этой процедуре выполняется явная проверка каждого из специальных символов, которые определены в кодировке NVT. Процедура soputc должна также обрабатывать символы, для которых отсутствует кодировка NVT. Однако в протоколе NVT указано, что если сервер не требует выполнения клиентом передачи 27.15. Передача символа на сервер 445
в двоичном режиме, сервер должен отбрасывать основную часть управляющих символов, вводимых пользователем. 27.16. Отображение входящих данных на терминале пользователя Данные, поступающие через соединение TCP от сервера, могут быть либо не- закодированными (если сервер дал согласие на передачу в двоичном режиме), либо они могут состоять из символов, закодированных в соответствии с правилами, установленными протоколом NVT. Клиент вызывает процедуру ttputc для отображения входящих символов на экране пользователя. /* Файл ttputc - процедура ttputc */ ¦include <sys/types.h> ¦include <stdio.h> ¦include "telnet.h" int tcout(char *cap, FILE *tfp); int xputc(char ch, FILE *fp); /* .... * Процедура ttputc - вывод одного символа на сетевой виртуальный терминал * . *—• - - — - - - .... - • */ int ttputc(FILE *sfp, FILE *tfp, int c) { static last_char; int tc; if (rcvbinary) { (void) xputc(c, tfp); /* Выводить данные без интерпретации */ return 0; } if (synching) /* He выводить данные, если применяется */ /* режим синхронизации */ return 0; if ((last char == VPCR && с == VPLF) || (last~char == VPLF && с == VPCR)) { (void) xputc(VPLF, tfp); last_char =0; return 0; } if (last_char == VPCR) (voidf tc&ut(*cr*> tfp); else if (last_char » VPLF) (void) tcoutCdo", tfp); if (c>= ' ' && с <TCIAC) /* Печатаемый символ ASCII */ (void) xputc(c, tfp); =- else { /* Специальный символ NVT */ switch (c) { case VPLF: /* Определить наличие символа CR */ 446 Глава 27. Клиент TELNET (практическая реализация)
case VPCR: tc = 1; /* Определить наличие символа LF */ break; case VPBEL: tc = tcoutCbl", tfp); break; case VPBS: tc = tcout("bc*, tfp); break; case VPHT: tc = tcout(HtaH, tfp); break; case VPVT: tc * tcoutC'do", tfp); break; case VPFF: tc * tcout(-cl\ tfp); break; default: tc = 1; break; /* Никакое действие не выполняется */ } if (Itc) /* Если переменная termcap не установлена, применяется */ /* режим ASCII */ (void) xputc(c, tfp); } last_char = с; return 0; } Если сервер дал согласие на передачу данных в двоичной кодировке, процедура ttputc для их отображения вызывает процедуру xputc. Этот код содержится в файле xput.c. /* Файл xput - процедуры xputc, xfputs */ ¦include <stdio.h> extern FILE *scrfp; /* . ™ -_—_. * Процедура xputc - функция putc, дополненная необязательными средствами * вывода протокола работы в файл *.•......——.... - . - _¦ « */ int xputc(char ch, FILE *fp) { if (scrfp) (void) putc(ch, scrfp); return putc(ch, fp); } /* ... * Процедура xfputs - функция fputs, дополненная необязательными средствами * вывода протокола работы в файл * —™ ——— « ¦,.,. */ int xfputs(char *str, FILE *fp) { if (scrfp) 27.16. Отображение входящих данных на терминале пользователя 447
fputs(str, scrfp); fputs(str, fp); } Процедура xputc отличается от обычной функции putc, поскольку клиент в ней предоставляет средства поддержки ведения протокола. Если поддержка ведения протокола разрешена, процедура xputc выводит копию выходного символа и на терминал, и в файл протокола. В ином случае, она выводит копию только на терминал. Если сервер не передает двоичные данные, процедура ttputc должна преобразовать символы из кодировки NVT в символьную последовательность, соответствующую терминалу пользователя. При этом могут возникнуть две ситуации: клиент может находиться в обычном режиме или в режиме синхронизации. Клиент переходит в режим синхронизации после получения команды TELNET SYNCH. Находясь в режиме синхронизации, клиент читает и отбрасывает все данные. Клиент снова переходит в обычный режим после получения команды DATA MARK протокола TELNET. Сервер передает команду SYNCH в виде срочных данных. При поступлении этих срочных данных, предназначенных для клиента, операционная система передает клиенту сигнал SIGURG, что вынуждает клиента вызвать на выполнение обработчик сигнала rcvurg. Процедура rcvurg устанавливает глобальную переменную synching, что вынуждает клиента перейти в режим синхронизации и искать очередной символ DATA MARK в потоке данных. В режиме синхронизации клиент отбрасывает весь ввод и не отображает его. Поэтому процедура ttputc проверяет переменную synching и отбрасывает каждый выходной символ, если значение этой переменной отлично от нуля. После проверки того, находится ли клиент в режиме синхронизации, процедура ttputc должна интерпретировать остальные символы с использованием кодировки NVT. Поскольку в кодировке NVT частично используются двухсим- вольные последовательности, процедура ttputc хранит копию предыдущего символа в глобальной переменной last_char. Вначале процедура ttputc обрабатывает символы возврата каретки (CR — сокращение от carriage return) и перевода строки (LF — сокращение от linefeed). Она распознает, обозначают ли двухсимвольные последовательности CR-LF или LF-CR конец строки, и преобразует их в один символ LF в соответствии с соглашением, принятым в операционной системе Linux. Безусловно, если встречается отдельный символ возврата каретки или перевода строки, процедура ttputc выполняет действие, связанное с этим символом. Для этого в ней вызывается процедура tcout с указанием в качестве первого параметра операции перемещения курсора. Например, при обнаружении символа перевода строки процедура ttputc вызывает процедуру tcout со строковым параметром do (сокращение от down — вниз). Процедура ttputc вызывает процедуру xputc для непосредственного вывода любых печатаемых символов ASCII. В ином случае она выполняет обработку специальных символов. Например, если символ, предназначенный для вывода на экран, представляет собой символ NVT BEL (в коде этой процедуры — VPBEL), процедура ttputc вызывает процедуру tcout с применением параметра Ы. 448 Глава 27. Клиент TELNET (практическая реализация)
27.17. Применение обозначений опций termcap для управления терминалом пользователя Процедура tcout принимает в качестве параметров имя стандартной опции терминала termcap3 и указатель на выходной файл терминала. В ней используется функция getenv для извлечения информации о типе терминала из переменной среды TERM, а затем вызывается процедура tgetstr для поиска необходимой последовательности символов, позволяющей добиться указанного эффекта на экране терминала пользователя. И наконец, в ней вызывается процедура xfputs для вывода результирующей последовательности символов на терминал. /* Файл tcout - процедура tcout */ lifdef BSD tinclude <newcurses.h> lelse ¦include <curses.h> fendif finclude <stdlib.h> ¦include <stdio.h> ¦define TBUFSIZE 2048 int xfputs(char *str, FILE *fp); /* * Процедура tcout - вывод данных об указанной характеристике терминала * в указанный поток * */ int tcout(char *сар, FILE *tfp) { static init; static char *term; static char tbuf[TBUFSIZE], buf[TBUFSIZE], *bp = bufj char *sv; if (linit) { init = 1; term = getenv("TERM"); } if (term == 0 || tgetent(&tbuf[0], term) != 1) return 0; if (sv = tgetstr(cap, &bp)) { xfputs(sv, tfp); return 1; } return 0; } В некоторых версиях UNIX библиотека termcap называется termlib. 27.17. Применение обозначений опций termcap для управления терминалом пользователя 449
27.18. Вывод блока данных на сервер Для вывода блока данных на сервер в процедуре telnet вызывается процедура sowrite. /* Файл sowrite - процедура sowrite */ tinclude <sys/types.h> finclude <stdio.h> tinclude "tnfsm.h" extern struct fsm^trans sostab[]; extern int sostate; extern unsigned char sofsm[][NCHRS]; /* * Процедура sowrite - обработка информации, выводимой в сокет * */ int sowrite(FILE *sfp, FILE *tfp, unsigned char *buf, int cc) { struct fsm_trans *pt; int i, ki; for (i=0; i<cc; ++i) { int с = buf[i]; ki = sofsm[sostate][c]; pt = &sostab[ki]; if ((pt->ft_action)(sfp, tfp, c) <0) sostate = KSREMOTE; /* Возникла ошибка */ else sostate = pt->ft_next; } } Процедура sowrite обрабатывает в цикле каждый символ в указанном блоке и вызывает на выполнение конечный автомат sof sm для обработки каждого символа. 27.19. Взаимодействие с клиентским процессом Как и большинство клиентских программ TELNET, наша реализация предусматривает для пользователя возможность взаимодействия с клиентским процессом. Для этого пользователь вводит на клавиатуре управляющий символ, а затем команду. В табл. 27.1 приведен перечень возможных команд, которые могут следовать за управляющим символом, и указано их назначение. В соответствии с системой обозначений, принятой в протоколе TELNET, запись "X указывает, что символ формируется при удержании клавиши <Ctrl> и одновременном нажатии клавиши <Х>. 450 Глава 27. Клиент TELNET (практическая реализация)
Таблица 27.1. Вводимые с клавиатуры символы, которые клиент TELNET интерпретирует как команды, если они следуют за командой KCESCAPE Имя символа Введенный символ Описание KCSTATUS KCESCAPE KCSCRIPT KCUNSCRIPT ЛТ Ч s U KCSUSP AZ Приостановить на время клиентский процесс KCDC0N Разорвать соединение TCP с сервером Вывести информацию о состоянии текущего соединения Передать на сервер управляющий символ в виде данных Начать вывод протокола работы в указанный файл Прекратить вывод протокола работы Файл telnet.h содержит символические определения для каждого из символов клавиатурных команд. Например, в нем определено, что клавиатурный управляющий символ KCESCAPE соответствует символу "] (<Ctrl+]>), т.е. символу с восьмеричным значением 035. При обнаружении введенного с клавиатуры управляющего символа клиентская программа переводит конечный автомат вывода в сокет из состояния KSREM0TE в состояние KSLOCAL и интерпретирует следующий символ как команду5. Поскольку большинство команд состоят из одного символа, конечный автомат вывода в сокет обычно снова переходит в состояние KSREM0TE и выполняет исполнительную процедуру» связанную с этой командой. Например, если конечный автомат вслед за символом KCDC0N обнаруживает символ KCESCAPE, он вызывает процедуру dcon. 27.20. Ответ на недопустимые команды Если пользователь вслед за введенным с клавиатуры управляющим символом вводит нераспознанный символ, конечный автомат вывода в сокет вызывает исполнительную процедуру sonotsup, которая выводит сообщение об ошибке. Файл sonotsup.с содержит следующий код: /* Файл sonotsup - процедура sonotsup */ ¦include <stdio.h> /* * Процедура sonotsup - обработка не поддерживаемых команд управления *«..«.. «. */ int sonotsup(FILE *sfp, FILE *tfp, int c) { fprintf(tfp, fprintf(tfp, fprintf(tfp, fprintf(tfp, fprintf(tfp, \nunsupported escape: %c.\n", c); s - turn on scripting\t\t"); u - turn off scripting\n"); . - disconnect\t\t\tH); AZ - suspend\n"); Файл telnet .h приведен на стр. 411. Схема конечного автомата вывода в сокет приведена на рис. 26.8 на стр. 420. 27.20. Ответ на недопустимые команды 451
fprintf(tfp, ,,AT - print statusV); return 0; } 27.21. Вывод протокола в файл В нашем примере клиента TELNET реализовано одно новое средство, которое отсутствует в большинстве других клиентских программ: этот клиент позволяет пользователю динамически создавать файл протокола работы, содержащий копию всех данных, передаваемых на экран терминала пользователя. В основе создания такого протокола работы лежит предположение, что пользователю может потребоваться вести регистрацию всего сеанса TELNET или его части. Функция ведения протокола работы является динамической, поскольку пользователь может ее запустить или остановить в любое время. Кроме того, пользователь может сменить файл, в который клиентская программа записывает протокол. Таким образом, для перехвата вывода одной дистанционно выполняемой команды пользователь может зарегистрироваться в удаленной системе, отменив вывод протокола, затем разрешить вывод протокола и выдать одну или несколько команд, для которых необходимо сохранить вывод, и наконец, снова отменить ведение протокола работы. Файл протокола будет содержать копию всей информации, отображаемой клиентом на терминале пользователя на протяжении того промежутка времени, когда было разрешено ведение протокола. 27.22. Реализация средств ведения протокола Способ ведения протокола клиентом определяет конечный автомат вывода в сокет, показанный на рис. 26.8. Если пользователь вводит символы ~]s (<Ctrl+]>, <s>), что равнозначно символу KCESCAPE, за которым следует символ KCSCRIPT, конечный автомат вывода в сокет вызывает исполнительную процедуру scrinit и переходит в состояние KSCOLLECT. До тех пор, пока пользователь не введет символ конца строки (т.е. KCNL), конечный автомат будет оставаться в состоянии KSCOLLECT и вызывать процедуру scrgetc для сбора строки символов, которая задает имя файла протокола. После завершения пользователем ввода строки с именем файла конечный автомат вызывает процедуру scrwrap для открытия файла протокола и снова переходит в состояние KSREM0TE. В каждом из следующих разделов рассматривается одна из исполнительных процедур, связанных с выполнением функции ведения протокола. 27.23. Инициализация средств ведения протокола Сразу после обнаружения запроса на ведение протокола конечный автомат вывода в сокет вызывает исполнительную процедуру scrinit. /* Файл scrinit - процедура scrinit */ ¦include <unistd.h> ¦include <termios.h> ¦include <string.h> ¦include <stdio.h> ¦include "telnet.h" ¦include "local.h" 452 Глава 27. Клиент TELNET (практическая реализация)
extern int scrindex; extern struct termios tntty; /* - * Процедура scrinit - инициализация параметров работы терминала для сбора * имени файла протокола * */ int Scrinit(FILE *sfp, FILE *tfp, int c) { struct termios newtty; if (Jdoecho) { fprintf(tfp, "\nscripting requires remote ECH0.\n"); return -1; } if (scrfp) { fprintf(tfp,"\nalready scripting to \"%s\".\n", scrname); j return -1; } scrindex =0; if (tcgetattr@, &tntty)) /* Сохранить текущие установки терминала */ errexit("can't get tty modes: %s\n", strerror(errno)); newtty = oldtty; newtty.c_cc[VINTR] = _POSIX_VDISABLE; /* Отменить прерывание */ newtty.c~cc[VQUIT] = _POSIX_VDISABLE; /* Отменить прерывание */ newtty.с cc[VSUSP] = _POSIX_VDISABLE; /* Отменить приостановку */ fifdef VDSUSP newtty.c_cc[VDSUSP] = _POSIX_VDISABLE; /* Отменить приостановку */ lendif if (tcsetattr@, TCSADRAIN, Snewtty)) errexit("can't set tty modes: %s\n", strerror(errno)); fprintf(tfp, "\nscript file: "); (void) fflush(tfp); return 0; } Процедура scrinit вначале проверяет, используется ли клиентом дистанционный эхо-повтор (т.е. поступают ли все отображаемые символы с сервера, а не из локального драйвера устройства). Она также проверяет, не было ли уже разрешено пользователем ведение протокола. Процедура scrinit устанавливает значение глобальной переменной scrindex равным нулю. Эта переменная будет применяться другой процедурой для подсчета символов по мере чтения имени файла протокола, вводимого пользователем. И наконец, прежде чем вывести приглашение к вводу имени файла протокола, процедура scrinit сменяет режим терминала пользователя таким образом, чтобы локальный драйвер терминала выводил на экран символы имени файла по мере ввода их пользователем. 27.23. Инициализация средств ведения протокола 453
27.24. Сбор символов имени файла протокола Для чтения последовательности символов, которые будут использоваться в качестве имени файла протокола в конечном автомате вывода в сокет, применяется исполнительная процедура scrgetc. Файл scrgetc.с содержит следующий код. /* Файл scrgetc - процедура scrgetc */ #include <termios.h> tinclude <string.h> tinclude <stdio.h> tinclude "local.h" tdefine SFBUFSZ 2048 /* Размер буфера для ввода имени файла протокола */ struct termios tntty; FILE *scrfp; char scrname[SFBUFSZ]; int scrindex; /* - * Процедура scrgetc - начало записи протокола сеанса в файл * */ int scrgetc(FILE *sfp, FILE *tfp, int c) { scrname[scrindex++] = c; if (scrindex>= SFBUFSZ) { /* Имя файла слишком велико */ fprintf(tfp, "\nname too long\n"); if (tcsetattr@, TCSADRAIN, Soldtty) <0) errexit("tcsetattr: %s\n", strerror(errno)); return -1; } return 0; } При поступлении каждого символа клиент вызывает процедуру scrgetc, которая добавляет символ к строке scrname. 27.25. Открытие файла протокола При обнаружении признака конца строки, введенной пользователем, клиент вызывает процедуру scrwrap для открытия файла протокола. /* Файл scrwrap - процедура scrwrap */ tinclude <sys/file.h> tinclude <termios.h> tinclude <stdio.h> tinclude <string.h> 454 Глава 27. Клиент TELNET (практическая реализация)
¦include "local.h" extern struct termios tntty; extern char scrname[]; extern int scrindex, errno; /* * Процедура scrwrap - завершение сборки имени файла протокола *.. */ int scrwrap(FILE *sfp, FILE *tfp, int c) { int fd; if (scrindex) { scrname[scrindex] = '\0'; scrindex = 0; fd = openfscrname, 0_WRONLY |0_CREAT 1OJTRUNC, 0644); if (fd <0) fprintfftfp, "\ncan't write \"%s\": %s\n\ scrname, strerror(errno)); else scrfp = fdopen(fd, V); } if (tcsetattr@, TCSADRAIN, Stntty) <0) errexit("tcsetattr: %s\n", strerror(errno)); return 0; } Процедура scrwrap добавляет к собранной строке нулевой завершающий символ, переустанавливает для дальнейшего использования глобальную переменную scrindex и вызывает функцию open для открытия файла протокола. В случае успешного получения нового дескриптора файла протокола процедура scrwrap вызывает функцию fdopen для создания стандартного указателя файла ввода/вывода, соответствующего файлу протокола, и помещает этот указатель в глобальную переменную scrfp. Перед возвратом управления процедура scrwrap вызывает процедуру tcsetattr для переустановки режимов терминала в значения, которые они имели до их изменения процедурой scrinit. 27.26. Прекращение ведения протокола После того как пользователь принимает решение прекратить ведение протокола, конечный автомат вывода в сокет вызывает исполнительную процедуру unscript. /* Файл unscript - процедура unscript */ ¦include <sys/types.h> ¦include <sys/stat.h> ¦include <stdio.h> ¦include "local.h" 27.26. Прекращение ведения протокола 455
/*.™ * Процедура unscript - завершение записи протокола сеанса в файл * */ int unscript(FILE *sfp, FILE *tfp, int c) { struct stat statb; if (scrfp == 0) { fprintf(tfp, "\nNot scripting.\nH)? return 0; } (void) fflush(scrfp); if (fstat(fileno(scrfp), &statb) == 0) fprintf(tfp, H\n\H%s\": %d bytes.\n", scrname, statb.stjsize); (void) fclose(scrfp); scrfp = 0; return 0; } Процедура unscript выводит для пользователя информационное сообщение о том, что клиент прекратил ведение протокола, вызывает системную функцию f stat для получения информация о результирующем файле протокола и выводит сообщение с информацией о размерах файла протокола. И наконец, процедура unscript закрывает файл протокола и очищает глобальный указатель файла scrfp. 27.27. Вывод информации о состоянии Пользователь может получить информацию о состоянии текущего соединения, введя команду KCSTATUS вслед за вводом на клавиатуре управляющего символа. Для вывода информации о состоянии соединения в конечном автомате вывода в сокет вызывается процедура status. /* Файл status - процедура status */ ¦include <sys/types.h> finclude <sys/socket.h> tinclude <netinet/in.h> finclude <stdio.h> extern char doecho, sndbinary, rcvbinary; /* Опции TELNET */ extern char *host, scrname[]; extern FILE *scrfp; /* * Процедура status - вывод информации о состоянии соединения * */ int 456 Глава 27. Клиент TELNET (практическая реализация)
status(FILE *sfp, FILE *tfp, int c) { struct sockaddr_in sin; int sinlen; fprintf(tfpf "\nconnected to \"%s\" ", host); sinlen = sizeof(sin); if (getsockname(fileno(sfp), (struct sockaddr *)&sin, fcsinlen) == 0) fprintf(tfp, "local port %d "f ntohs(sin.sinjport)); sinlen = sizeof(sin); if (getpeername(fileno(sfp)f (struct sockaddr *)&sin, fcsinlen) == 0) fprintf(tfp, "remote port %d ", ntohs(sin.sinjport)); (void) putc('\n/, tfp); if (doecho || sndbinary || rcvbinary) { printf("options in effect: "); if (doecho) fprintf(tfpf "remote_echo "); if (sndbinary) fprintf(tfp, "sendjDinary "); if (rcvbinary) fprintf(tfpf "receive binary "); (void) putc('\n', tfp); ~ } if (scrfp) fprintfftfp, "scripting to file \"%s\"\n", scrname); return 0; } Процедура status выводит такую информацию, как имя удаленного хоста, локальный и удаленный порты протокола TCP, применяемые для соединения, и список действующих опций. 27.28. Резюме В нашем примере клиентской программы TELNET для интерпретации последовательностей символов, поступающих с сервера или введенных пользователем на клавиатуре, применяются три конечных автомата. Каждый входящий символ вызывает переход в конечном автомате. При выполнении каждого перехода в клиентской программе вызывается процедура, которая реализует действие, связанное с переходом. В настоящей главе описаны исполнительные процедуры трех конечных автоматов, из которых состоит рассматриваемый пример клиентской программы. Некоторые из этих действий являются простыми, а другие — весьма сложными. Основным недостатком организации клиентского программного обеспечения в виде исполнительных процедур для конечного автомата является то, что программа становится сложной для восприятия. Результирующий код может оказаться затруднительным для понимания, поскольку невозможно определить, как связаны между собой отдельные процедуры, не обращаясь к схемам конечных автоматов. 27.28. Резюме 457
Материал для дальнейшего изучения Дополнительные сведения об опциях TELNET и стандарты протокола по поддержке каждой из опций с примерами кода приведены в нескольких документах RFC. В документе [124] рассматривается опция передачи управления, а в документе [123] описана опция эхо-повтора символов. В документе [122] рассматривается опция, которая управляет 8-битовой двоичной передачей. И наконец, в документе [157] обсуждается опция согласования типа терминала и связанная с ней опция уточнения типа терминала. Упражнения 27.1. Терминалы некоторых типов поддерживают несколько режимов эмуляции, в результате чего появляется возможность определить целый ряд имен типов терминала для одного терминального устройства. Прочитайте документ RFC 1091. Как может использоваться в клиенте список имен терминалов при согласовании типа терминала с сервером? 27.2. Прочитайте стандарт протокола TELNET, чтобы узнать, когда именно сервер должен переключиться с передачи данных, закодированных с использованием кодировки сетевого виртуального терминала, на передачу 8-битовых двоичных данных. В частности, как осуществляется передача данных сервером после того, как он выразил свою готовность передавать двоичные данные, но не получил подтверждение? 27.3. Передает ли клиент сообщение WILL или DO, требуя от сервера применения конкретной опции? Что передает сервер, требуя применения опции от клиента? 27.4. Что означает параметр режима 0_WR0NLY10_CREAT10JTRUNC в вызове функции open, содержащемся в процедуре scrwrap? 27.5. Определите затраты времени на вывод сообщения в клиенте после получения запроса на применение опции. Воспользуйтесь доработанной версией клиента для обращения к разным серверам. Какие запросы на применение опций они передают автоматически? 27.6. Что произойдет, если клиент и сервер одновременно отправят друг другу, соответственно, сообщения DO ECHO и WILL ECHO? 27.7. Что произойдет, если клиент отправит сообщение DO ECHO на сервер, в котором уже разрешен режим ECHO? 458 Глава 27. Клиент TELNET (практическая реализация)
28 Потоковая передача аудио- и видеоинформации (принципы организации и проект протокола RTP) 28.1. Введение В двух предыдущих главах приведены подробные сведения о взаимодействии типа клиент/сервер, на котором основаны типичные приложения с символьной организацией обмена данными. В настоящей и следующих главах в основном рассматриваются приложения, предназначенные для передачи и аудио- или видеоинформации. В этой главе описаны основные понятия потоковой службы, представлен протокол, применяемый для потоковой передачи и показана организация библиотеки функций обработки данных в реальном времени. В следующей главе рассматривается пример программы, предназначенной для приема и воспроизведения потока аудиоинформации в реальном времени. 28.2. Потоковая служба В отличие от приложений, передающих ограниченный объем данных при каждом запросе или ответе, потоковая служба обеспечивает передачу непрерывного потока данных произвольной длины. Поток данных не ограничен ни по размерам, ни по продолжительности передачи. В большинстве случаев этот поток можно рассматривать как бесконечный, поскольку отправитель не прекращает передачу данных. В других случаях поток поддерживается очень долго (например, в течение нескольких часов), даже если время его передачи ограничено. Потоковая служба обычно применяется для прямой передачи аудио- или видеоинформации. Видеокамера или микрофон, установленный в передающем устройстве, вырабатывает непрерывный аналоговый сигнал, который преобразовывается в цифровую форму и передается по Internet. В приемном устройстве поток цифровых данных снова преобразуется в аналоговый сигнал и воспроизводится. Поскольку данные вырабатываются непрерывно, поток никогда не заканчивается, по меньшей мере до тех пор, пока продолжает работать видеокамера или микрофон.
28.3. Доставка данных в реальном времени Приложение рассматривается как функционирующее в реальном времени, если данные в нем должны быть доставлены с точным соблюдением тех же временных соотношений между отдельными фрагментами данных, с которыми они были созданы. Для доставки данных в реальном времени часто применяется потоковая передача; в частности, данные передаются в виде потока и воспроизводятся в аудио- и видеоприложениях в реальном времени. Например, в реальном времени транслируются передачи в прямом эфире, поскольку приемник должен воспроизводить звуки точно в такой же временной последовательности, в Какой они были созданы. В программном обеспечении протокола передачи данных в реальном времени должны учитываться две характеристики потока данных: ¦ порядок — входящие данные должны воспроизводиться точно в том же порядке, в каком они были созданы; ¦ время — для обеспечения своевременного воспроизведения входного сигнала получатель должен знать точное время выработки каждого пакета; получатель преобразует полученные цифровые данные в аналоговую форму и воспроизводит каждый фрагмент аналогового сигнала точно в то же время с начала передачи, в какое этот фрагмент присутствовал во входном сигнале. 28.4. Предусмотренные протоколом средства компенсации неравномерной задержки К сожалению, базовая сеть может нарушить и порядок, и время прохождения пакетов. Например, при передаче трафика через Internet может существенным образом изменяться значение задержки передачи каждого очередного пакета. Фактически организация доставки данных с применением протоколов Internet допускает настолько значительный сдвиг продолжительности передачи текущей дейтаграммы по отношению к предшествующей или следующей, что доставка дейтаграммы может быть выполнена с нарушением порядка следования. Для обозначения непостоянства задержки применяется термин флуктуация. Если базовая сеть характеризуется наличием флуктуации, то прикладное программное обеспечение не может обеспечить бесперебойное воспроизведение с использованием только средств данной сети. Например, если флуктуации возникают во время передачи аудиоинформации, то даже при условии, что отправитель передает дейтаграмму через каждые 20 мс, после прохождения по сети дейтаграммы не будут поступать к получателю с точным соблюдением интервала между ними, равного 20 мс. Для обеспечения бесперебойного воспроизведения данных в реальном времени программное обеспечение протокола должно исправлять ошибки, обусловленные поступлением пакетов с нарушением порядка следования, а также связанные с тем, что интервал между пакетами, передаваемыми по сети с небольшой флуктуацией, является непостоянным. Для решения этих двух задач применяются два поля каждого пакета. Отправитель помещает в каждый пакет порядковый номер, а получатель использует информацию о порядке следования пакетов для обеспечения обработки пакетов в том же порядке, в каком они были отправлены. Отправитель помещает также в каждый пакет отметку времени с указанием того момента, в который были записаны данные, находящиеся в пакете, а получатель использует отметку времени для воспроизведения данных в нужный момент времени относительно предшествующих пакетов. Для протокола реального времени очень важно предусмотреть возможность использования не только информации о порядке следования пакетов, но и информации о времени их создания, поскольку это позволяет раздельно выполнять 460 Глава 28. Потоковая передача аудио- и видеоинформации...
операции обработки пакетов и определения момента времени воспроизведения данных. Например, рассмотрим способ кодировки аудиоинформации, обеспечивающий передачу стереосигнала, в котором чередуются выборки данных, полученные из левого канала, с выборками из правого. При такой схеме два последовательных пакета могут иметь одну и ту же отметку времени. В подобных случаях применение отдельных порядковых номеров позволяет получателю восстановить порядок следования пакетов. Аналогичным образом, разделение необходимо при использовании таких кодировок данных, которые не предусматривают передачу данных при отсутствии сигнала на входе. Чтобы понять, с чем это связано, рассмотрим кодировку аудиоинформации, которая предусматривает приостановку передачи на период отсутствия звукового сигнала. Хотя пакеты не передаются, аппаратные часы, которые служат для формирования отметок времени, продолжают работать. Поэтому после того как по завершении периода тишины, составляющего N единиц времени, передающее приложение снова начнет отправлять данные, отметка времени будет на N единиц времени больше по сравнению с отметкой времени в предыдущем пакете. С точки зрения получателя, создается впечатление, что при получении двух последовательных пакетов отметка времени увеличилась скачкообразно на N единиц. Но несмотря на пропуск части отметок времени, получатель может определить, что пакеты не потеряны, поскольку порядковый номер увеличился только на единицу. 28.5. Повторная передача, потеря и восстановление Протоколы передачи данных в реальном времени позволяют устранять ошибки, связанные с задержкой и нарушением порядка следования пакетов, но в них не предусмотрено использование подтверждений и повторной передачи для устранения ошибок, связанных с потерей пакетов. Чтобы понять, с чем это связано, напомним, что в приложениях, работающих в реальном времени, применяется принцип воспроизведения с соблюдением такого же интервала времени между пакетами, как и при передаче данных: данные передаются с постоянной скоростью и с такой же скоростью должно выполняться их воспроизведение. Например, рассмотрим, к чему приведет повторная передача пакета с аудиоинформацией. Если короткий фрагмент аудиоинформации не поступит к тому моменту, когда в процессе воспроизведения потребуются недостающие данные, придется на время приостановить вывод звукового сигнала. Если же в протоколе будет предусмотрено, что на время ожидания повторной передачи воспроизведение должно приостанавливаться, то новые данные будут продолжать поступать, а очередь невоспроизве- денных данных будет расти. После поступления повторно переданного пакета и возобновления воспроизведения очередь невоспроизведенных данных останется заполненной, поскольку новые данные будут по-прежнему поступать с той же скоростью, с какой происходит воспроизведение. Это означает, что размер очереди возрастает при каждой повторной передаче и больше не уменьшается. Фактически по мере дальнейшей повторной передачи очередь возрастает неограниченно. Для предотвращения формирования очередей неопределенно большой длины в протоколах передачи данных в реальном времени не используется повторная передача. Вместо этого в них выполняется либо опережающее исправление ошибок, либо воспроизведение с пропусками. Метод опережающего исправления ошибок предусматривает включение в каждый пакет дополнительной информации, которая используется получателем для восстановления данных, относящихся к пропущенному пакету. Метод воспроизведения с пропусками предусматривает замену недостающего фрагмента другой информацией на интервал времени, равный продолжительности воспроизведения недостающих данных. Например, если потерян пакет с данными, относящимися к сеансу передачи аудиоинформа- 28.5. Повторная передача, потеря и восстановление 461
ции, то получатель может прекратить вывод данных, повторить звуковой фрагмент, полученный в последнюю очередь, или выработать белый шум (например, усилить фоновые помехиI. 28.6. Транспортный протокол передачи данных в реальном времени В сети Internet для решения проблем, связанных с нарушением порядка доставки дейтаграмм и неравномерного интервала между их поступлением, применяется транспортный протокол передачи данных в реальном времени (RTP — Real-time Transport Protocol). Как и можно было предположить, каждый пакет RTP включает и порядковый номер, и отметку времени, которые используются получателем для управления воспроизведением. Поскольку протокол RTP должен быть достаточно универсальным для поддержки широкого класса приложений, в нем не определяется точная интерпретация отметки времени, скорость выборки данных или метод кодировки. Вместо этого в протоколе RTP определен только заголовок постоянного формата, который должен находиться в начале каждого пакета, и предусмотрена возможность определить формат пакета, кодировку данных и назначение отдельных частей тела пакета в отдельной спецификации. На рис. 28.1 показаны поля в заголовке RTP с постоянным форматом. 0 13 8 16 31 VER Р X СС М PTYPE Порядковый номер Отметка времени Идентификатор источника синхронизации Идентификатор дополнительного источника Рис. 28.1. Заголовок с постоянным форматом, который находится в начале каждого пакета RTP В спецификации протокола RTP определено назначение двух полей заголовка, которые применяются при обработке каждого входящего пакета. Двухбитовое поле VER обозначает версию протокола; текущей версией является версия 2, а 16-битовое поле порядкового номера позволяет программному обеспечению RTP расставлять полученные пакеты по порядку. Начальный порядковый номер для любого сеанса выбирается случайным образом по двум причинам. Во-первых, случайный выбор порядкового номера позволяет исключить воздействие пакетов, относящихся к другому сеансу; эта проблема связана с тем, что пакеты одного сеанса, задержавшиеся в сети, могут быть приняты как относящиеся к другому сеансу. Во-вторых, применение начального порядкового номера, выбранного случайным образом, повышает степень защиты, поскольку затрудняет для взломщиков получение информации о порядковых номерах пакетов. Интерпретация большинства остальных полей в заголовке RTP зависит от семибитового поля PTYPE, которое определяет тип данных пакета. Например, тип данных пакета 0 служит для обозначения одной из кодировок аудиоинформации, РСМ (Pulse-Code Modulation — импульсно-кодовая модуляция). При использовании неко- Полное прекращение воспроизведения применяется редко, поскольку эксперименты показали, что люди предпочитают, чтобы был слышен белый шум, а не наступала полная тишина. 462 Глава 28. Потоковая передача аудио- и видеоинформации...
торых других типов данных пакета данные должны передаваться в виде блоков по- СТоянного размера. Если пакеты шифруются, то для реализации некоторых схем шифрования также должны использоваться блоки данных постоянного размера. В Подобных случаях для обозначения того, применялось ли дополнение нулями для уШйчения размера пакета до необходимой длины, служит бит Р (сокращение от jWMJLding); последний октет заполнения содержит число, указывающее длину заполнения. При использовании некоторых других типов данных пакета выполняется йфркировка; для этой цели предназначен бит М (сокращение от marker). Например, Йри передаче видеоинформации бит М служит для маркировки начала фрейма. Для правильного определения времени воспроизведения данных в пакете применяется 32-битовое поле с отметкой времени. Значение отметки времени соответствует тому моменту, когда была осуществлена выборка первого октета данных в пакете. Еще более важно то, что отправитель должен постоянно увеличивать показания часов, которые служат для формирования отметки времени, даже если входной сигнал не распознается и данные не передаются. Это позволяет отправителю не вырабатывать ненужные пакеты во время отсутствия сигнала, а получатель по отметке времени очередного пакета может определить, какую длину должна иметь пауза во время воспроизведения. ,. .Спецификация протокола RTP указывает, что начальная отметка времени для сеанса должна быть выбрана случайным образом, но при этом она не определяет единицы измерения, применяемые для формирования значения отметки времени, или точную интерпретацию этого значения. Вместо этого спецификация RTP позволяет устанавливать точность показаний часов в соответствии с типом данных пакета. Это позволяет выбрать точность синхронизации ввода и вывода сигнала с учетом типа приложения. Например, при передаче аудиоинформации по протоколу RTP целесообразно выбрать частоту формирования логических отметок времени, равную частоте выборки аудиоинформации. Однако при передаче видеоинформации для обеспечения бесперебойного воспроизведения частота формирования отметок времени должна в несколько раз превышать частоту кадров. И наконец, стандарт RTP допускает наличие в двух или нескольких пакетах одного и того же значения отметки времени, если выборка содержащейся в них информации осуществлялась в одно и то же время. Кроме того, повторяющиеся значения отметки времени могут, например, возникать при разбиении крупного кадра на несколько мелких пакетов. Поскольку кадр регистрируется с привязкой к одному моменту времени, то значение отметки времени в каждом из пакетов должно быть одинаковым. 28.7. Преобразование и смешивание потоков Протокол RTP позволяет смешивать (т.е. объединять в один поток или, как принято называть эту операцию, мультиплексировать) два или несколько потоков, а также преобразовывать поток (т.е. менять кодировку данных). Чтобы получатель имел возможность разделять (демультиплексировать) входящие пакеты, каждый источник обозначается уникальным 32-битовым целым числом. Идентификатор источника находится в поле идентификатора источника синхронизации. Если смешивание двух потоков для формирования нового потока выполняется в промежуточной системе (мультиплексоре), то источником нового потока становится промежуточная система. Спецификация RTP предусматривает возможность при обработке пакетов данных в мультиплексоре включить в них одно или несколько полей с идентификатором дополнительного источника для обозначения первоначальных отправителей. При мультиплексировании данных из нескольких источников для обозначения номера источника, участвующего в передаче, применяется четырехбитовое поле СС. 28.7. Преобразование и смешивание потоков 463
28.8. Отсроченное воспроизведение и флуктуационные буферы Поскольку поток данных, передаваемых в реальном времени, является непрерывным, в программном обеспечении воспроизведения полученных данных должно быть учтено наличие непостоянной задержки (для ее обозначения применяется термин флуктуация), поэтому воспроизведение не может начинаться сразу после поступления данных. Чтобы лучше понять, с чем это связано, рассмотрим, что произойдет, если воспроизведение полученной аудиоинформации будет начинаться немедленно, без какой-либо паузы. Предположим, что программное обеспечение воспроизведения обрабатывает данные из текущего пакета. Если следующий пакет поступит точно в тот же момент, когда программное обеспечение закончит воспроизведение данных из текущего пакета, то воспроизведение продолжится без перерыва. Но если следующий пакет поступит с большей задержкой, воспроизведение должно быть приостановлено до поступления этого пакета. Еще более важно то, что после своего возобновления воспроизведение не может выполняться с самого начала пакета (так как в этом случае станет расти очередь невоспроизведенных данных). Вместо этого воспроизведение должно быть продолжено с того места в пакете, которое соответствует текущему времени. Поэтому слушатель пропустит небольшой фрагмент звукового сигнала, равный увеличению задержки второго пакета, и сразу услышит сигнал, соответствующий тому фрагменту, который был бы воспроизведен полностью, если бы пакет поступил вовремя. Поскольку флуктуация является неотъемлемым свойством сети, небольшие отклонения во времени поступления пакетов являются вполне обычными. Для предотвращения появления пропуска в выходном сигнале при каждом увеличении задержки, в программном обеспечении воспроизведения используется эвристический метод, известный под названием отсроченного воспроизведения. При использовании этого метода получатель не начинает воспроизведение сразу после получения первого пакета данных. Вместо этого входящие данные помещаются в буфер. Этот механизм, известный под названием флуктуационного буфера или буфера воспроизведения, действует по принципу очереди. Получатель устанавливает пороговое значение К, превышающее максимальный ожидаемый уровень флуктуации, и устанавливает паузу, достаточную для накопления в буфере объема данных, необходимого для воспроизведения в течение К единиц времени. После накопления в буфере объема данных, соответствующего К единицам времени, начинается воспроизведение и данные извлекаются из буфера с постоянной скоростью. После поступления нового пакета его данные записываются в буфер. При условии, что ни один пакет не имеет дополнительной задержки, превышающей К единиц времени, в буфере всегда находится достаточный объем данных для того, чтобы можно было обеспечить непрерывное и бесперебойное воспроизведение. Этот принцип проиллюстрирован на рис. 28.2. Воспроизведение продолжается непрерывно путем извлечения данных из буфера с постоянной скоростью. К Следующий пакет заполнит буфер Данные, поступающие в пакетах Рис. 28.2. Флуктуационный буфер, показанный в тот момент времени, когда ожидается поступление очередного входящего пакета Воспроизводимые ' данные 464 Глава 28. Потоковая передача аудио- и видеоинформации...
На этом рисунке показано, как происходит воспроизведение в тот момент времени, когда ожидается поступление пакета. До начала воспроизведения в буфере накапливается объем данных, соответствующий К единицам времени. Поэтому если пакеты будут продолжать поступать точно с такой же скоростью, с какой воспроизводятся данные, каждый очередной пакет будет пополнять буфер до общего объема, соответствующего К единицам. Но если какой-то пакет будет двигаться по сети дольше, чем обычно, то программное обеспечение воспроизведения продолжит выбирать данные из буфера до поступления этого пакета. Если же какой-либо из пакетов пройдет по сети быстрее, чем обычно, то на короткое время буфер будет содержать объем данных, соответствующий более чем К единицам времени. 28.9. Протокол управления RTP (RTCP) Хотя протокол RTP предоставляет получателю информацию, необходимую для воссоздания выходного сигнала в реальном времени, заголовок, предусмотренный этим протоколом, не включает полей, позволяющих участникам соединения управлять передачей или обмениваться метаинформацией (информацией об информации). Метаинформация приобретает особое значение при использовании некоторых адаптивных методов кодирования данных, передаваемых в реальном времени. При таком адаптивном методе применяемая кодировка выбирается с учетом изменения условий в базовой сети (например, при значительном повышении коэффициента потери пакетов в базовой сети происходит переход на другую кодировку). Разработчики протокола RTP решили не занимать место в заголовке пакета, а использовать отдельный, дополнительный протокол для обмена между участниками соединения всей необходимой информацией о сеансе. Этот отдельный протокол получил название RTCP (RTP Control Protocol — Протокол управления RTP). При использовании протокола RTCP получатель контролирует характеристики базовой сети и передает накопленную информацию отправителю. Кроме того, протокол RTCP предоставляет возможность передавать информацию о каждом сеансе. Еще более важно то, что протокол RTCP допускает передачу составных пакетов, в которых объединяются несколько сообщений RTCP. Как показано на рис. 28.3, каждый пакет RTCP начинается с заголовка с постоянным форматом; формат остальной части пакета определяется полем типа в заголовке. Формат области данных указан в поле типа пакета. 0 13 8 16 31 VER Р RC PTYPE Область данных Длина Рис. 28.3. Поля постоянного формата в четырехоктетном заголовке, с которого начинается каждый пакет RTCP Как показано на этом рисунке, первые два поля в заголовке RTCP совпадают с полями в заголовке RTP. Двухбитовый номер версии находится в поле версии; приведенное в нем значение 2 совпадает с текущей версией RTP. Протокол RTCP, как и RTP, позволяет дополнять пакеты нулями в случае необходимости. Однобитовое поле Р указывает, применялось ли заполнение, а последний октет заполнения содержит число, которое указывает общее число октетов заполнения. Область данных пакета содержит ряд записей отчета, пятибитовое поле RC включает число записей отчета, которые следуют за заголовком, а 16-битовое 28.9. Протокол управления RTP (RTCP) 465
поле длины обозначает общую длину пакета, включая заголовок. Длина измеряется в 32-битовых словах, а значение в этом поле заголовка определяет указанную длину, уменьшенную на единицу. Восьмибитовое поле типа пакета PTYPE содержит информацию о типе пакета. Получатель использует тип пакета для определения формата и содержания отчетов, которые следуют за заголовком. В табл. 28.1 показано пять типов отчетов. Таблица 28.1. Пять типов отчетов RTCP и их назначение; поле типа определяет формат отчетов в пакете RTCP Наименование Тип Описание Отчет отправителя 200 Информация о времени для каждого источника синхронизации и число переданных октетов данных Отчет получателя 201 Отчет о потере пакетов и флуктуации, а также информация для синхронизации и оценка времени кругового обращения Описание источника 202 Описание пользователя, которому принадлежит источник Прекращение сеанса 203 Сообщение получателя о том, что он выходит из сеанса Приложение 204 Отчет, относящийся к приложению 28.10. Синхронизация нескольких потоков Одной из основных особенностей протокола RTCP является то, что он позволяет получателю синхронизировать несколько потоков RTP. Например, рассмотрим передачу звукового сигнала наряду с видеосигналом. Поскольку для аудио- и видеоинформации применяются разные кодировки, в протоколе RTP для них используются два отдельных потока с двумя аппаратными часами, функционирующими с разной частотой. Если получатель попытается воспроизводить два взаимосвязанных потока независимо друг от друга, то результат может оказаться бессмысленным (например, звуковое сопровождение не будет соответствовать видеосигналу). Поэтому получатель должен синхронизировать воспроизведение двух потоков для обеспечения точной координации между аудио- и видеоинформацией. К сожалению, ни поле идентификатора источника синхронизации, ни поле отметки времени в заголовке RTP не содержит достаточной информации для синхронизации нескольких потоков. Для получения необходимой информации применяется протокол RTCP. Протокол RTCP требует, чтобы для синхронизации потоков каждый отправитель передавал информацию о каждом активном потоке. Отправитель передает сообщение с описанием источника и периодически формирует сообщения с отчетами отправителя2. Сообщение с описанием источника содержит каноническое имя, которое однозначно обозначает источник (т.е. приложение-отправитель). Хотя для каждого потока, передаваемого из конкретного источника, используется другой идентификатор источника, все они имеют одно и то же каноническое имя. Получатель не может синхронизировать два потока, если они не имеют одинакового канонического имени. Сообщения с отчетами отправителя содержат информацию, применяемую получателем для координации значений отметок времени в двух потоках. Формат отчета отправителя приведен на рис. 28.4. Как показано на этом рисунке, отчет Этот протокол требует также периодического формирования отчетов каждым получателем; отчет получателя информирует отправителя об условиях в сети. 466 Глава 28. Потоковая передача аудио- и видеоинформации...
отправителя начинается с двадцати восьми октетов информации, которые вклю- чают информацию о часах, необходимую получателю для синхронизации потоков. Во-первых, отчет включает абсолютное значение времени, представленное в формате протокола NTP (Network Time Protocol — Синхронизирующий сетевой протокол). Во-вторых, отчет RTCP задает значение отметки времени RTP, полученное от тех же часов, которые применялись для выработки значений в поле отметки времени пакетов RTP. Поскольку каждый конкретный отправитель использует одни и те же часы с абсолютными показаниями времени для отчетов отправителя по всем свои потокам, получатель может сравнить абсолютные значения времени для двух потоков, поступающих из одного источника, и определить, какое соотношение связывает значения в поле отметки времени одного потока со значениями отметки времени для другого потока. Последняя часть пакета содержит последовательность блоков отчета, поступивших от получателей. 0 13 8 16 31 VER Р RC PTYPE Длина Идентификатор источника синхронизации отправителя Отметка времени NTP C2 старших бита) Отметка времени NTP C2 младших бита) Отметка времени RTP Число пакетов, переданных отправителем Число октетов, переданных отправителем Первый источник синхронизации Относительное количество потерянных пакетов Общее количество потерянных пакетов Расширенная информация о максимальном полученном порядковом номере Флуктуация периодичности поступления пакетов Последний отчет отправителя Задержка с момента получения последнего отчета отправителя Рис. 28.4. Структура пакета RTCP, содержащего отчет отправителя 28.11. Транспортный протокол RTP и передача от многих ко многим Протокол RTP предназначен для работы по транспортному протоколу без установления логического соединения; чаще всего в нем применяется протокол UDP. Отправитель инкапсулирует каждый пакет RTP в одну дейтаграмму UDP для передачи, а получатель извлекает из каждой входящей дейтаграммы пакет. Транспортный протокол без установления логического соединения был выбран по многим причинам. Во-первых, как было отмечено ранее, повторная передача в этом случае нежелательна. Во-вторых, поскольку управление потоком данных и упорядочение обеспечиваются протоколом RTP, то не требуется транспортный 28.11. Транспортный протокол RTP и передача от многих ко многим 467
протокол, который выполнял бы эти задачи (к тому же, их невозможно было бы выполнять достаточно эффективно с помощью транспортного протокола). В- третьих, протокол RTP является протоколом с пакетной передачей (в отличие от протокола TCP). В-четвертых, поскольку во многих потоковых приложениях применяется групповая рассылка (для обозначения этого метода доставки информации применяется также термин многоадресная рассылка), проект протокола RTP рассчитан на эксплуатацию в среде групповой рассылки. Поэтому для него требуется такой транспортный протокол без установления логического соединения, как UDP, поскольку транспортные протоколы с установлением логического соединения не применяются для групповой рассылки. Одно из основных преимуществ применения протокола UDP в среде передачи данных в реальном времени вытекает из его способности обеспечить произвольный выбор участников обмена данными. Протокол UDP позволяет не только обеспечить обмен данными между парой приложений, но и выполнять групповую рассылку из одного источника многим получателям. Аналогичным образом, протокол UDP позволяет выполнять передачу от произвольного набора приложений одному получателю. И наконец, протокол UDP позволяет выполнять групповую рассылку от произвольного набора отправителей произвольному набору получателей. Для обозначения этих видов связи, предусмотренных протоколом UDP, применяются термины двухсторонняя связь, передача от многих отправителей к одному получателю, от одного отправителя ко многим получателям или от многих отправителей ко многим получателям. Для обеспечения обмена данными между многими получателями необходимо внести изменения в обычный метод обеспечения взаимодействия типа клиент/сервер. Вместо назначения в качестве оконечной точки связи конкретного хоста и порта протокола на этом хосте, в групповой рассылке в качестве адресата назначается группа (называемая многоадресной группой или группой рассылки). Кроме того, членство в группе рассылки является динамическим — члены группы могут присоединяться к ней или выходить из нее в любое время. В результате этого сложность реализации протокола RTP возрастает. Поскольку протокол RTP может обеспечивать доставку данных по принципу групповой рассылки, на компьютере-получателе должно применяться более сложное программное обеспечение по сравнению с обычным программным обеспечением типа клиент/сервер. 28.12. Сеансы, потоки, порты протокола и демультиплексирование Прежде чем перейти к изучению структуры программного обеспечения протокола передачи данных в реальном времени, необходимо рассмотреть несколько важных понятий. Вначале необходимо определить понятие группы участников обмена данными. Мы определяем сеанс RTP как состоящий из всего трафика, возможно исходящего от многих отправителей, который передается по месту назначения, обозначенному следующей парой: (IP__addressf protocol^port^number) Здесь IP^address обозначает IP-адрес, a protocol_port_number — номер порта протокола. Отметим, что если параметр IP_address представляет собой индивидуальный адрес, то в сеансе RTP принимает участие только один получатель, а если параметр IP_address соответствует групповому адресу, то в сеансе принимает участие группа. Применение групп рассылки является особенно важным в связи с тем, что они позволяют осуществлять произвольный обмен данными между 468 Глава 28. Потоковая передача аудио- и видеоинформации...
членами группы; если группы рассылки не используются, в сеансе может принимать участие только один получатель. Следующее определение в основном касается последовательности пакетов, которые содержат взаимосвязанные данные. Мы определяем поток как последовательность пакетов RTP от одного источника синхронизации. Сеанс RTP может охватывать один или несколько независимых потоков RTP, которые передаются по одному и тому же адресу назначения, с указанием одного и того же порта протокола. Например, при проведении многосторонней аудиоконференции каждый участник передает всем другим независимый поток аудиоинформации. Хотя большинство приложений предусматривает отправку каждым источником в определенном сеансе только одного потока, один источник может в случае необходимости отправлять сразу несколько потоков. Например, может потребоваться, чтобы один источник передавал одновременно данные нескольких типов (допустим, и аудио-, и видеоинформацию). Иным образом, может быть принято решение отправлять из некоторого источника один набор данных с использованием нескольких кодировок (допустим, передавать одну и ту же видеоинформацию и с высоким, и с низким разрешением). Для обеспечения возможности связывать каждое сообщение с тем потоком, к которому оно относится, программное обеспечение компьютера-получателя должно демультиплексировать входящие сообщения RTP. Демультиплексирование происходит на двух уровнях. Первый уровень демультиплексирования (демультиплексирование в сеансе) выполняется на транспортном уровне. Приложение для приема данных RTP должно создать сокет UDP. В этом приложении, как и в обычном сервере, задается конкретный номер порта протокола. В аудиоприложении, ожидающем поступления одноадресного трафика, как и в обычном сервере, может использоваться IP-адрес INADDR_ANY. Но если аудиоинформация передается с применением групповой рассылки, сервер должен указать конкретный групповой IP- адрес. В любом случае этот адрес используется в программном обеспечении транспортного уровня для демультиплексирования на уровне сеанса. Это означает, что программное обеспечение UDP проверяет пару полей (IP_address, protocol_ port_number) в каждом входящем пакете для определения того, какое приложение на компьютере должно получить пакет, передаваемый в данном сеансе. Второй уровень демультиплексирования (демультиплексирование потока) происходит после передачи пакета в программное обеспечение RTP. В этом программном обеспечении используются идентификатор источника синхронизации и тип пакета для объединения очередного пакета с теми, что относятся к тому же потоку. Только после определения принадлежности пакета к тому или иному потоку можно использовать поля порядкового номера и отметки времени для установки его на соответствующее место в потоке и определения времени его воспроизведения. 28.13. Основные принципы выбора кодировки Программное обеспечение RTP является довольно сложным, и это в основном связано с тем, что существует немало способов применения протокола RTP в приложениях. Разные приложения RTP даже могут не иметь между собой почти ничего общего. Например, в них могут применяться два основных принципа кодировки данных, которые резко отличаются друг от друга. ¦ Кодировка выборок. В приложении, предназначенном для передачи аудиоинформации, обычно применяется кодировка потока байтов, которая для представления оцифрованных значений чаще всего предусматривает использование одной из разновидностей импульсно-кодовой модуляции (РСМ — Pulse Code Modulation). В потоке PCM каждый элемент данных 28.13. Основные принципы выбора кодировки 469
соответствует одной выборке. Например, в соответствии со стандартом G.711, применяемом в телефонии, отправитель выполняет выборку входного сигнала 8000 раз в секунду и кодирует каждую выборку в виде одного значения от 0 до 255. Поэтому выходные данные, полученные в результате применения кодировки РСМ, состоят из потока байтов, не имеющего внутренней структуры; получатель может начать воспроизведение с любого места в потоке. Аудиоинформация с более высоким качеством звучания также передается с использованием кодировки выборки, но размеры выборок увеличиваются (например, для представления каждой выборки применяется 16 битов) или используется более высокая частота дискретизации. ¦ Кодировка кадров. При использовании кодировки кадров, в отличие от кодировки выборок, применяется кадрирование, при котором отправитель делит данные на дискретные единицы, называемые кадрами. Одним из самых известных примеров применения кадрирования является передача видеоинформации, при которой результаты каждого сканирования изображения (например, на экране) помещаются в отдельный кадр. В целях уменьшения потребности в пропускной способности, во многих видеокодировках применяется дифференциальная схема кадрирования, при которой отправитель периодически передает полный видеокадр, а затем отправляет относящуюся к последовательным кадрам информацию обновления (которая описывает изменения, происшедшие от момента передачи последнего полного кадра до момента передачи текущего кадра). Для каждого кадра нужен заголовок (например, для того чтобы отправитель мог указать, является ли он полным кадром или содержит только информацию обновления). Кодировка кадров дополнительно усложняется в связи с отсутствием стандарта, поскольку при разработке каждого приложения, основанного на использовании такой кодировки, приходится самостоятельно регламентировать все тонкости формирования кадров, в том числе формат и интерпретацию заголовка кадра. В некоторых кодировках, применяемых на практике, используются кадры постоянного размера; другие допускают изменение размеров. Еще более важно то, что нет общепринятого соглашения об инкапсуляции. Некоторые кодировки предусматривают передачу каждого кадра в отдельном пакете RTP. Но кодировки, в которых используются кадры небольшого размера, часто допускают размещение в одном пакете RTP нескольких кадров, тогда как кодировки, предусматривающие применение кадров больших размеров, допускают разбиение одного кадра на несколько пакетов RTP. 28.14. Концептуальная организация программного обеспечения RTP Многие приложения, в которых используется транспортный протокол передачи данных в реальном времени, включают программное обеспечение RTP непосредственно в само приложение; такие приложения характеризуются отсутствием четкого разграничения между средствами обработки RTP и средствами декодирования или воспроизведения. Еще более важно то, что программное обеспечение RTP в каждом приложении, по-видимому, разрабатывается с нуля. Хотя разработка заказного программного обеспечения, непосредственно учитывающего требования к приложению, позволяет обеспечить наиболее эффективную реализацию, в каждом приложении приходится снова и снова реализовы- вать один и тот же протокол, что приводит к непроизводительным затратам труда разработчика. Почему же программисты по-прежнему реализуют программное обеспечение RTP с нуля, а не разрабатывают библиотеку, которая могла бы использоваться повторно? Если бы была поставлена задача создания библиотеки 470 Глава 28. Потоковая передача аудио- и видеоинформации...
программного обеспечения для решения задач передачи данных в реальном времени, то какие функциональные средства должна была бы предоставить эта библиотека приложениям? Как должна быть организована эта библиотека? Насколько сложнее было бы создание библиотеки общего назначения по сравнению с реализацией поддержки обмена данными в реальном времени непосредственно в самом приложении? Чтобы ответить на эти вопросы, рассмотрим, какие функциональные средства должны быть предусмотрены в библиотеке, а затем сравним их с функциональными средствами, которые требуются для одного конкретного приложения. В настоящей главе описан общий случай, а в следующей главе представлена реализация конкретного приложения. На рис. 28.5 показаны четыре концептуальных уровня, которые должны быть предусмотрены в программном обеспечении для поддержки потоковой передачи данных в реальном времени. На уровне синхронизации не реализуется конкретный протокол. Прикладной уровень Уровень синхронизации Уровень передачи данных в реальном времени (RTP/RTCP) Транспортный уровень (UDP) Рис. 28.5. Концептуальная организация программного обеспечения, применяемого для передачи данных в реальном времени Как показано на этом рисунке, программное обеспечение передачи данных в реальном времени занимает положение между прикладным уровнем и транспортным протоколом. Программное обеспечение передачи данных в реальном времени подразделяется на два концептуальных компонента: нижний уровень, на котором реализован транспортный протокол передачи данных в реальном времени, и верхний уровень, обеспечивающий синхронизацию. Программное обеспечение нижнего уровня взаимодействует с протоколом UDP (через сокет), принимает все входящие пакеты, относящиеся к данному сеансу, демультиплексирует их на отдельные потоки и собирает информацию, которая требуется программному обеспечению RTCP для передачи отправителю отчетов о каждом потоке. Программное обеспечение верхнего уровня предоставляет интерфейс, предназначенный для использования в приложении. Кроме того, ПО верхнего уровня обеспечивает синхронизацию часов; в нем используется информация из отчетов отправителя для установления соответствия между значениями в полях с отметкой времени потоков, исходящих из одного и того же канонического источника. В результате приложения в процессе приема синхронизированных потоков получают данные, выработанные в одно и то же время. Дополнительные сведения об уровне синхронизации приведены на рис. 28.6. Как показывает этот рисунок, приложение должно иметь средства управления не только передачей данных, но также сеансами и потоками. Как показано на этом рисунке, на уровне синхронизации предоставляются две концептуальные интерфейсные функции. Во-первых, программное обеспече- 28.14. Концептуальная организация программного обеспечения RTP 471
ние этого уровня предоставляет интерфейс управления, позволяющий выполнять в приложении административные задачи (например, получать информацию о потоках в сеансе). Во-вторых, это программное обеспечение предоставляет интерфейс передачи данных, позволяющий принимать их в приложении. Хотя приложение внешне имеет доступ к одному интерфейсу передачи данных, неявным образом (т.е. незаметно для приложения) в этом программном обеспечении для выборки данных используется один из трех механизмов. Если в потоке применяется кодировка кадров, то программное обеспечение извлекает и доставляет кадры. Если же в потоке применяется кодировка выборок, то программное обеспечение извлекает и доставляет байты. И наконец, если в потоке не применяется ни одна из обычных кодировок, то программное обеспечение позволяет обеспечить в приложении доступ непосредственно к пакетам RTP. Управление Передача данных Кодировка выборок Кодировка кадров Непосредственный доступ к данным Рис. 28.6. Структура уровня синхронизации, предоставляющего интерфейс для приложения 28.15. Структура процессов/потоков Программное обеспечение RTP должно обеспечивать одновременное выполнение нескольких задач. Например, при поступлении каждого пакета данных RTP проверяется его порядковый номер, извлекается содержимое и помещается во флуктуационный буфер. Кроме того, программное обеспечение должно обрабатывать входящие сообщения RTCP и наряду с этим периодически формировать и отправлять отчеты получателя RTCP. Хотя на рис. 28.6 показана статическая многоуровневая структура программного обеспечения передачи данных в реальном времени, для одновременного осуществления всех этих действий необходимо применять несколько параллельных потоков выполнения. На рис. 28.7 показана одна из возможных структур потоков, которая может применяться для этой цели. Здесь средства передачи данных с одного уровня на другой отделены от средств обработки событий, связанных с управлением и формированием отчетов. Как показано на этом рисунке, в программном обеспечении передачи данных в реальном времени функционируют четыре потока, кроме всех тех потоков, которые связаны с эксплуатацией приложения. Поток RTP обеспечивает передачу данных между двумя уровнями. Он извлекает данные из входящих пакетов и управляет флуктуационным буфером. Отдельный поток RTCP обрабатывает входящие пакеты RTCP. Поток TIMER периодически вырабатывает исходящие отчеты получателя RTCP. И наконец, поток EVENT обрабатывает другие асинхронные события, которые не относятся к обычному потоку передачи данных. В нашей модели организации взаимодействия между потоками могут происходить другие акты координации работы, если потребуется синхронизация потоков во времени. Хотя каждое из приложений может иметь копию "стека" реального времени, потоки должны взаимодействовать между собой для обеспечения синхронизированного воспроизведения. Как показано на рис. 28.8, для обеспечения синхронизации два приложения взаимодействуют между собой, передавая сообщения. Такая координация применяется при возникновении необходимости в синхронизации двух потоков данных. 472 Глава 28. Потоковая передача аудио- и видеоинформации...
[ Приложе-] V ние J Синхронизация Передача данных в реальном времени (RTP/RTCP) Протокол U DP Рис. 28.7. Пример структуры потоков, связанных с программным обеспечением передачи данных в реальном времени 0 Синхронизация Средства передачи в реальном времени Синхронизирующее сообщение / 0 г=* Синхронизация Средства передачи в реальном времени Рис. 28.8. Пример обмена сообщениями между двумя прикладными потоками, каждый из которых обрабатывает поток данных, передаваемых в реальном времени 5.15. Структура процессов/потоков 473
28.16. Назначение API-интерфейса Какие средства должен предоставлять прикладной интерфейс программам, которые обрабатывают данные в реальном времени? Здесь уже рассматривалась разница между кодировками выборок и кадров. Однако возникает еще один важный вопрос: как должно быть представлено время? Для этого может применяться один из двух вариантов. ¦ Явное управление временем. Интерфейс всегда предоставляет вместе с данными отметку времени. В приложении принимается решение о том, когда должны быть воспроизведены данные (т.е. в приложении устанавливается соответствие между отметкой времени, сформированной отправителем, и показаниями часов на локальном компьютере). ¦ Неявное управление временем. После получения от приложения информации о том, что к нему должен поступить определенный поток, программное обеспечение передачи данных в реальном времени устанавливает соответствие между показаниями часов локального компьютера и показаниями часов источника мультимедийного потока. При каждой попытке приложения прочитать дополнительный объем данных программное обеспечение передачи в реальном времени определяет соответствие между текущими показаниями часов локального компьютера и показаниями часов источника мультимедийных данных и возвращает данные, начиная с требуемой точки потока. Основное преимущество применения явного управления временем связано с тем, что приложение сохраняет полный контроль над синхронизацией показаний часов и воспроизведением. Если в работе приложения возникает небольшая пауза, а затем снова начинается чтение, то приложение получает все данные, поступившие с момента выполнения последней операции чтения. Поэтому операция пропуска части входного потока может оказаться неэффективной, поскольку для ее выполнения приложение должно читать и отбрасывать данные. Основное преимущество применения неявного управления временем связано с упрощением программирования; в приложении достаточно открыть поток, прочитать данные и воспроизвести эти данные. В приложении нет необходимости вычислять разницу во времени, поскольку основополагающая система всегда возвращает данные, которые соответствуют текущему времени. Если работа приложения была приостановлена на несколько секунд, а затем в нем было выполнено чтение дополнительного объема данных, то базовая система продвигает вперед указатель в буфере, пропуская данные, соответствующее тому времени, которое было потеряно во время паузы. Один из основных недостатков неявного управления временем (отсутствие возможности осуществлять в приложении абсолютный контроль над передачей данных) становится очевидным при возникновении ошибок или нестандартных ситуаций. Например, предположим, что в приложении, основанном на применении кодировки выбора, была предпринята попытка чтения до поступления данных, соответствующих текущему моменту времени. В программном обеспечении передачи данных в реальном времени предусмотрена возможность заблокировать приложение до поступления данных или возвратить сообщение об ошибке. В качестве другого примера предположим, что приложение, основанное на использовании кодировки кадров, требует выполнить чтение данных, относящихся к определенному моменту времени Т9 и что соответствующие данные находятся в середине кадра. Должен ли интерфейс заблокировать приложение до того момента, когда появится возможность предоставить весь следующий кадр, или просто возвратить сообщение об ошибке и дать возможность приложению повторить попытку чтения позже? Безусловно, что для одних приложений желательно предусмотреть возможность блокировки, тогда как для других целесообразнее предусмотреть получение сообщения об ошибке. 474 Глава 28. Потоковая передача аудио- и видеоинформации...
28.17. Проект флуктуационного буфера и повторная буферизация Реализация флуктуационного буфера также усложняет программное обеспечение передачи данных в реальном времени. Какие структуры данных должны для этого использоваться? В случае применения кодировок выборок, по-видимому, наиболее подходящей для этого структурой данных является циклический буфер, поскольку поступающие данные можно вставлять в его конце одновременно с тем, как приложение извлекает данные с начала буфера. Кроме того, между адресом позиции в буфере и отметкой времени при использовании кодировок выборок можно легко установить соответствие, поскольку позиция в буфере может быть связана со значением отметки времени с помощью линейной функции. Поэтому циклический буфер хорошо приспособлен для неявного управления временем. Безусловно, для обеспечения компенсации флуктуации, воспроизведение не должно начинаться до тех пор, пока буфер не заполнится свыше минимального порогового уровня. Но циклические буферы имеют и свои недостатки. Данные должны копироваться в буфер при поступлении каждого пакета, а затем они должны копироваться вновь для доставки в приложение. Кроме того, циклический буфер не очень подходит для многих кодировок кадров. Такие кодировки часто предусматривают добавление заголовков к данным. Если заголовки хранятся отдельно от данных, то многие преимущества циклического буфера теряются. А если заголовки хранятся в том же циклическом буфере, то поиск данных, соответствующих определенному времени, становится затруднительным. В связи с этим во многих приложениях предусматривается ведение списка пакетов вместо сопровождения циклического буфера. Сложность управления буфером во многом также связана с тем, что протокол RTP позволяет отправителю изменять кодировку или размер пакетов динамически. Например, если передаваемый звуковой сигнал становится неразборчивым, то отправитель может переключиться на кодировку, предусматривающую применение большего числа битов в расчете на каждую выборку. Однако при возникновении такого изменения получатель также должен изменить размер своего буфера. Кроме того, отправитель должен изменить применяемый способ буферизации, если часы на компьютерах отправителя и получателя не идут с абсолютно одинаковой скоростью (т.е. разница между их показаниями постепенно растет). Если часы компьютера получателя идут немного быстрее по сравнению с часами отправителя, такое расхождение в конечном итоге приведет к тому, что поступающие пакеты со временем станут казаться устаревшими. Таким образом будет создаваться впечатление, что время, в течение которого должно начаться воспроизведение одного из пакетов, уже прошло. Хотя такие ситуации, как смена кодировки или постепенное расхождение показаний часов, встречаются редко, их все же надо учитывать, и поэтому код библиотеки общего назначения усложняется. Чтобы иметь возможность справиться с этими проблемами, получатель должен быть готов выполнить повторную буферизацию данных. Для этого он должен переопределить соответствие между показаниями часов локального компьютера и значениями поля для отметок времени, а затем откорректировать положение указателя в буфере воспроизведения в соответствии с новыми данными о соотношении времени. Повторная буферизация может предусматривать уничтожение данных, находящихся в буфере, блокировку приложения- получателя до момента поступления данных или внесение небольшой поправки в значение адреса точки воспроизведения без изменения данных в буфере. 28.17. Проект флуктуационного буфера и повторная буферизация 475
28.18. Обработка событий Кроме передачи данных, программное обеспечение передачи данных в реальном времени должно обрабатывать ряд событий, возникающих асинхронно. Например, к сеансу приема звуковой трансляции в любое время может присоединиться новый слушатель, а один из уже существующих участников сеанса — выйти из него. Кроме того, отправитель может сменить кодировку. Еще более важно то, что не для всех приложений нужна информация обо всех событиях. В одной из возможных реализаций асинхронных событий применяется механизм предоставления интерфейса обратного вызова, при котором каждый прикладной поток предоставляет функцию, вызываемую программным обеспечением передачи данных в реальном времени при возникновении каждого события. В другом варианте реализации используется очередь событий. Для применения механизма очереди событий в приложении или в программном обеспечении уровня синхронизации динамически создается очередь и определяется набор событии, информация о которых требуется приложению. При возникновении события на уровне RTP программное обеспечение определяет тип события Т. Затем программное обеспечение проверяет каждую очередь событий и вводит копию сообщения о событии в каждую очередь, для которой затребовано получение событий типа Т. Реализация, основанная на применении очереди событий, в меньшей степени способствует возникновению нарушений в работе по сравнению с механизмом предоставления интерфейса обратного вызова, поскольку программное обеспечение передачи данных в реальном времени не вызывает прикладной код непосредственно на выполнение. Однако и такая реализация может явиться источником проблем. В частности, если приложение создает очередь событий и не извлекает из нее сообщения о событиях, то в операционной системе в конечном итоге возникнет нехватка оперативной памяти. В процессе работы могут возникать задержки, если событие какого-то определенного типа запрашивается для слишком большого числа потоков, поскольку программное обеспечение нижнего уровня должно поместить копию события в каждую очередь. 28.19. Нарушения воспроизведения и сложности при обработке отметок времени Разработка проекта затрудняется, а сложность результирующего программного обеспечения увеличивается также в связи с тем, что в нем приходится учитывать некоторые другие тонкости передачи данных в реальном времени. При определенном сочетании таких факторов, как скорость передачи, размер базового буфера и режим функционирования устройства, может возникнуть неожиданное нарушение в работе: при увеличении объема данных, записываемых на устройство при каждой передаче, в процессе воспроизведения могут возникать пропуски. Чтобы понять, с чем это связано, рассмотрим звуковое устройство, которое буферизует 16000 выборок аудиоинформации в кодировке РСМ (т.е. звуковой сигнал продолжительностью две секунды). Если приложение пишет блок из 12000 выборок, драйвер копирует данные в буфер устройства и позволяет приложению продолжить выполнение на то время, пока воспроизводятся эти выходные данные. Вывод данных происходит бесперебойно, поскольку в распоряжении рассматриваемого приложения имеются полторы секунды на формирование блока ЛН-1, после того как драйвер принял блок N. Но если в приложении будет предпринята попытка записать 16001 выборок, драйвер скопирует первые 16000 из них в буфер, заблокирует поток и начнет воспроизведение. После того как буфер почти полностью освободится (например, 476 Глава 28. Потоковая передача аудио- и видеоинформации...
в нем останется меньше десятка выборок), устройство активизирует прерывание. Получив сигнал прерывания, драйвер скопирует последнюю выборку в буфер, возобновит работу прикладного потока и даст возможность устройству продолжить воспроизведение. К сожалению, после этого у прикладного потока останется лишь короткое время (которое не превышает времени воспроизведения десятка выборок) для формирования следующего блока данных. Если для работы потока потребуется больше времени по сравнению со временем воспроизведения, то устройство исчерпает весь объем воспроизводимых данных и звучание прекратится. Поэтому на выходе появится пропуск. Особенно сложной может стать задача сравнения отметок времени, поскольку при воспроизведении достаточно продолжительного потока RTP значения отметки времени могут зацикливаться. Например, при использовании кодировки MPEG, при которой выборки формируются с частотой 90 КГц, значение в 32- битовом поле с отметкой времени зацикливается приблизительно через 13,27 часа. При поступлении пакета получатель должен определить, остается ли записанная в нем отметка времени достаточно близкой к значению текущего времени, так как в ином случае пакет будет рассматриваться как недействительный. Эта проверка становится очень сложной, поскольку получатель должен не только учесть разницу между местным временем получателя и временем отправителя мультимедийной информации, но и предусмотреть возможность зацикливания значений отметки времени. Для упрощения такого вычисления при разработке программного обеспечения принимается предположение, что все отметки времени, которые должны в нем обрабатываться, отстоят друг от друга не более чем на половину адресного пространства (т.е. находятся в пределах 231). 28.20. Размер библиотеки функций обработки данных в реальном времени Какой объем кода требуется для создания библиотеки программного обеспечения передачи данных в реальном времени, рассчитанной на применение и кодировки выборок, и кодировки кадров? Чтобы найти ответ на этот вопрос, авторы разработали подобную библиотеку. В ней предусмотрена поддержка не только обработки асинхронных событий, но и синхронизации потоков. Эта библиотека включает также код, обеспечивающий обработку сообщений RTCP отправителя и получателя, и обеспечивает повторную буферизацию. Однако авторы стремились в первую очередь сравнить два подхода: с применением библиотеки и с созданием интегрированного приложения, поэтому ограничили набор функциональных средств библиотеки в двух аспектах. Во-первых, библиотека поддерживает только неявное управление временем, поскольку возможность явного управления исключает и применяемый API- интерфейс, и предусмотренные в ней структуры данных. Во-вторых, код библиотеки не включает таких специальных кодировок, как MPEG. Исходный код библиотеки может быть получен в оперативном режиме по адресу: ftp://ftp.cs.purdue.edu/pub/comer/rtplib.tar Результирующая реализация содержит свыше 6 тыс. строк кода, не включая кода файла Makefile, состоящего из 1600 строк, или кода приложения. В табл. 28.2 показаны ориентировочные размеры основных разделов кода. 28.20. Размер библиотеки функций обработки данных в реальном времени 477
28.21. Пример программы воспроизведения МРЗ В Internet нашел широкое распространение один из форматов кодировки аудиоинформации, известный под названием3 МРЗ. Для проверки своей библиотеки RTP авторы получили реализацию программы воспроизведения МРЗ по адресу: http://www.mp3-tech.org/programmer/sources/distl0.tgz и адаптировали ее для работы со своей библиотекой. В результате было создано приложение, способное принимать и воспроизводить поток аудиоинформации в кодировке МРЗ. Любопытно отметить, что более чем из 12144 строк исходного кода декодер МРЗ занимает примерно две трети общего объема. Кроме самой программы, при декодировании МРЗ применяются динамически загружаемые таблицы значений; объем этих таблиц учтен при определении общего объема исходного кода. Таблица 28.2. Основные разделы и размер кода каждого раздела в примере библиотеки функций обработки данных в реальном времени; процентные соотношения показаны с точностью до десятых долей процента Число строк 1293 1063 990 969 893 578 253 189 6228 Процентное соотношение 20,8 17,1 15,9 15,6 14,3 9,3 4,1 3,0 Описание RTP RTCP Синхронизация потоков Различные сервисные процедуры Объявления и константы в заголовочном файле Обработка данных в виде кадров Обработка данных в виде выборок Чтение пакетов Всего 28.22. Резюме В потоковых приложениях, предусматривающих передачу аудио- или видеоинформации, для обеспечения бесперебойного воспроизведения данных, передаваемых в реальном времени, применяется дополнительная протокольная поддержка. Такие протоколы предусматривают передачу информации о порядковых номерах пакетов для предоставления получателю возможности обнаружить пакеты, поступающие с нарушением порядка следования, и информации отметок времени для повторной сборки данных и их воспроизведения. Основным протоколом, применяемым в Internet для потоковой передачи, является RTP (Real Time Protocol — Протокол реального времени). Протокол RTP не только обеспечивает передачу данных в реальном времени, но и включает дополнительный протокол RTCP, используемый для управления и получения информации о сеансе RTP. Протокол RTP предусматривает возможность применения произвольных кодировок данных и позволяет передавать в одном сеансе несколько независимых потоков. Воспроизведение двух потоков, поступающих из одного нестандарт МРЗ (сокращение от MPEG уровня 3) разработан экспертной группой по вопросам движущегося изображения (Motion Picture Experts Group). 478 Глава 28. Потоковая передача аудио- и видеоинформации...
точника, может быть синхронизировано; синхронизация необходима для тех приложений, которые требуют передачи аудиоинформации наряду с видеоинформацией, поскольку данные каждого типа передаются в виде отдельного потока. Библиотека программного обеспечения поддержки обработки данных в реальном времени общего назначения должна быть приспособлена для всех возможных ситуаций и назначений. Библиотека должна обеспечивать возможность применения не только кодировок выборок или кадров, но и обработку независимых или синхронизированных потоков. Кроме того, библиотека должна обеспечивать координацию доступа к данным нескольких прикладных потоков. Материал для дальнейшего изучения Информация о протоколах RTP и RTCP приведена в документе [143]; в нем также показано, на основе каких соображений были приняты те или иные решения по проектированию этих протоколов. Информация о стандарте MPEG может быть получена по адресу: http://drogo.cselt.stet.it/mpeg/ Упражнения 28.1. Является ли RTP протоколом транспортного уровня? Аргументируйте свой ответ. 28.2. Прочитайте литературу о стандарте G.711, который охватывает одну из разновидностей метода импульсно-кодовой модуляции, применяемую в речевых телефонных системах. Как происходит определение значений данных в форматах ULAW и ALAW? 28.3. Прочитайте стандарт RTP, чтобы узнать, как происходит синхронизация двух потоков. Напишите программу, которая устанавливает соответствие между показаниями часов источника мультимедийной информации для одного потока, показаниями часов источника мультимедийной информации для другого потока и абсолютными показаниями времени, которые были закодированы с использованием протокола NTP (Network Time Protocol — Протокол сетевого времени). 28.4. Прочитайте спецификацию протокола RTCP. Каким образом в этом протоколе предотвращается выделение избыточной пропускной способности для передачи сообщений с отчетами получателя? 28.5. Прочитайте стандарт MPEG-2. Применяется в нем кодировка выборок или кодировка кадров? 28.6. На каком уровне выполняется шифрование (этот уровень находится выше уровня RTP или ниже него)? Объясните ваш ответ. Материал для дальнейшего изучения 479
29 Потоковый транспортный протокол передачи аудио- и видеоинформации (пример реализации RTP) 29.1. Введение В предыдущей главе приведены основные сведения о потоковой передаче данных, описаны протоколы, применяемые для обеспечения доставки данных в реальном времени, показана организация библиотеки RTP и приведены данные о ее размере. В настоящей главе продолжается описание этой темы; проводится сравнение степени сложности библиотеки поддержки средств передачи данных в реальном времени общего назначения и приложения потоковой обработки, разработанного в качестве примера. 29.2. Интегрированная реализация Чтобы сравнить степень сложности кода библиотеки общего назначения, описанной в предыдущей главе, и приложения, которое непосредственно включает все необходимые средства, авторы построили простейшую версию такого интегрированного приложения. Эта версия включает модуль воспроизведения аудиоинформации с использованием метода кодирования выборок по закону "мю", который поддерживается аппаратным обеспечением во многих версиях операционной системы Linux. В приложение встроены необходимые средства поддержки RTP и RTCP. Однако в нем предусмотрены только самые основные функциональные средства; все, что не требуется для поддержки только одного сеанса, одного потока аудиоинформации или одной кодировки, было опущено. После описания общей архитектуры программы перейдем к рассмотрению кода. 29.3. Архитектура программы Хотя рассматриваемый пример программы обеспечивает минимальный набор функций, эта реализация RTP вполне соответствует стандарту. В частности, кроме кода, предназначенного для приема и обработки пакетов данных RTP, в этом примере программы предусмотрен код для передачи и приема минимального набора сообщений RTCP. Как будет показано ниже, даже в этой простейшей
реализации приходится учитывать множество тонкостей. В целях максимального упрощения программы ее структура основана на применении четырех потоков выполнения, как показано на рис. 29.1. Разделяемые данные \ RTP/RTCP Л UDP Рис. 29.1. Структура потоков выполнения для примера программы воспроизведения потоковой аудиоинформации, описанной в этой главе Кроме основного потока, который извлекает и воспроизводит звуковые данные, программа включает три вспомогательных потока. Один из них обрабатывает входящие пакеты RTCP. Второй обеспечивает передачу отчетов RTCP, a третий принимает пакеты RTP. 29.4. Определения протокола RTP Код RTP может подразделяться на три общие категории: код обработки входящих пакетов данных RTP, код, обеспечивающий прием и передачу пакетов RTCP, и вспомогательный код. Определения констант и структур данных, относящихся к программному обеспечению RTP, находятся в файле rtp.h, который приведен ниже. Кроме констант и структуры rtp, которая определяет формат1 пакета RTP, в этом файле находятся определения структур stream и session, содержащих информацию о сеансе приема данных и об одном потоке в составе этого сеанса. Два поля структуры stream содержат ссылки на список входящих пакетов. Поле stm_qhead указывает на первый пакет в очереди, a stm_qtail — на последний. Третье поле, stmjputex, обеспечивает работу механизма взаимного исключения2, который гарантирует, что в любой момент времени к очереди бу- Определения двоичных полей в пакете предназначены для использования в архитектуре со словом, оканчивающимся младшим байтом. 2 В таких системах, как Linux, соответствующих стандарту POSIX, отдельно определены семафоры, которые могут иметь счетное число состояний, и мьютексы, применяемые только для обеспечения взаимно исключающего доступа. 482 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
дет иметь доступ только один поток. И наконец, структура rtpln описывает один элемент в связанном списке пакетов RTP. ./* Файл rtp.h - макросы rtptsgt, rtptsmax, rtphdrlen, rtpdata */ ¦include <common.h> ¦include <util.h> typedef unsigned int ssrc_t; typedef unsigned int mediatimej;; typedef unsigned short seq_t; ¦define RTP TSWINDOW A « 3 ¦define RTP CURRVERS 2 / ¦define RTP MINHDRLEN 12 / ¦define RTP MINSEQUENTIAL 2 / ¦define RTP_JITTHRESH 8000 / ¦define RTP BPBUFSZ ¦define RTP BPBUFCNT ¦define RTP LEEWAY ¦define RTP INACTTHRESH ¦define PCMMUJD ¦define PCMMU CLKRT ¦define PCMMU_BW 2048 / 64 / 400 / 5 / 0 / 8000 / 64000 / 1) * Текущая версия RTP */ * Минимальная длина заголовка RTP */ * Минимальное число последовательных пакетов */ * Коэффициент К флуктуационного буфера */ * (измеряется в 1/8000 с) */ * Размер буфера в буферном пуле */ * Число буферов в буферном пуле */ * Отставание воспроизведения (в 1/8000 с) */ * Число циклов до наступления тайм-аута */ * Идентификатор функции импульсно-кодовой */ * модуляции вида "мю" по спецификации RTP */ * Тактовая частота импульсно-кодовой модуляции */ * Пропускная способность канала передачи */ * РСМ-сигнала (бит/с) */ (х>у?(x-y<RTP_TSWINDOW?TRUE:FALSE): \ ¦define rtptsgt(x, у) (y-x<RTP_TSWINDOW?FALSE:TRUE) ¦define rtptsmax(x, у) (rtptsgt(x, y) ? x : y) ¦define rtphdrlen(prtp) (RTPJ1INHDRLEN + (prtp->rtp_cc * ¦define rtpdata(prtp) ((char *) prtp + rtphdrlen(prtp)) 4)) struct rtp { unsigned int rtp_cc:4; unsigned int rtp_ext:l; unsigned int rtp_pad:l; unsigned int rtp_ver:2; unsigned int rtpj?ayload:7; unsigned int rtp_mark:l; seq_t rtp_seq; mediatimej: rtp_time; ssrc_t rtp_ssrc; char" rtp_data[l]; }; struct rtpln { int rln_len; unsigned int rln_seq; struct rtpln *rln_next; struct rtpln *rln_prev; struct rtp rlnjrtp; }; /* Число источников */ /* Флажок расширения */ /* Флажок заполнения */ /* Версия */ /* Тип данных пакета */ /* Флажок маркера */' /* Порядковый номер */ /* Отметка времени */ /* Идентификатор источника синхронизации */ /* Начало данных RTP */ /* Общая длина пакета */ /* Расширенный порядковый номер */ /* Порядковый номер следующего пакета */ /* Порядковый номер предыдущего пакета */ /* Пакет RTP */ 29.4. Определения протокола RTP 483
struct stream { ssrc_t stm_ssrc; struct rtpln *stm_qhead; struct rtpln *stm_qtail; pthread_mutex_t stm_qmutex; pthread_cond_t stm_rcond; pthread_mutex_t stm_rmutex; pthread_mutex_t stm_smutex; bool stm_buffering; struct timeval stm_clkx; mediatime t stm clky; double int int int seq_t seq_t int int stinjitter; stm_inactive; stmjpackets; stm_probation; stm_firstseq; stmjiiseq; stm_badseq? stm roll; int int stmjrecprior; stm_expprior; mediatime_t stm_lastts; struct timeval stm_lastrec; unsigned int stm_ntp[2]j struct timeval stm lastsr; * Идентификатор источника синхронизации */ * Указатель на пакет с минимальным */ * порядковым номером */ * Указатель на пакет с максимальным */ * порядковым номером */ * Мьютекс, применяемый для блокировки очереди */ * Условная переменная, предназначенная для */ * блокировки операции чтения */ * Мьютекс, связанный с условной переменной */ * блокировки чтения */ * Мьютекс, применяемый для структуры stream */ * Логическая переменная, имеющая истинное */ * значение, если происходит буферизация потока */ * Отметка местного времени */ * Эквивалентная отметка времени */ * мультимедийной информации */ * Показатель флуктуации */ * Число неактивных циклов или информация */ о состоянии */ Накопленное число полученных пакетов */ Число пакетов с последовательными номерами, */ позволяющее проверить устойчивость потока */ Порядковый номер первого полученного пакета */ Наибольший порядковый номер среди */ пакетов, полученных в последнее время */ Неправильный порядковый номер */ Зацикливание порядковых номеров в области */ их определения */ Число пакетов, полученных в последнем */ цикле работы ПО RTCP */ Число пакетов, ожидаемых в последнем */ цикле работы ПО RTCP */ Последняя отметка времени, которая */ применялась для расчета флуктуации */ Время получения последнего пакета, которое */ применялось для расчета флуктуации */ * Отметка времени последнего отчета */ /* отправителя в формате NTP (дробная часть, целая часть) */ }; Местное время получения последнего */ отчета отправителя */ struct session { unsigned int snjssrc; /* Идентификатор источника синхронизации */ /* на локальном компьютере */ int snjrtpfd; /* Дескриптор файла для ПО RTP */ int snjrtcpfd; /* Дескриптор файла для ПО RTCP */ struct sockaddr_in snjrtcpto; /* Адрес назначения пакетов RTCP */ struct stream snjstream; /* Структура данных для приема одного потока */ bool snjietected; /* Логическая переменная, принимающая истинное */ 484 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформац
/* значение при обнаружении потока */ int sn_avgrtcp; /* Длина последовательности пакетов RTCP, */ /* служащая для вычисления продолжительности интервала */ }; void rtpinitfunsigned int, int); void rtprecv(void); int rtpupdate(struct rtp *)? void rtpinitseq(seq_t)? int rtpupdateseq(seq_t); void rtpnewdata(void); bool rtpqinsert(struct rtpln *); struct rtpln *rtpqdequeue(void); void playaudio(void); extern struct session sn; extern struct stream stm; Файл rtp.h содержит операторы включения двух файлов, common.h и util.h, относящихся к RTP. Файл common.h содержит список включаемых файлов с определениями стандартных функций Linux, а также определения констант и типов. /* Файл common.h - макросы min, max */ jfifndef C0MM0N_H jfdefine C0MM0N_H jf include <arpa/inet.h> jf include <fcntl.h> jfinclude <linux/soundcard.h> jf include <math.h> If include <netinet/in.h> jf include <pthread.h> jf include <semaphore.h> jfinclude <stdlib.h> jfinclude <strings.h> jf include <sys/ioctl.h> jfinclude <sys/socket.h> jf include <sys/time.h> jf include <sys/types.h> jf include <time.h> jfinclude <unistd.h> typedef char bool; jfdefine TRUE 1 jfdefine FALSE 0 jfdefine OK 0 jfdefine ERROR -1 jfdefine min(a, b) (a<b?a:b) jfdefine max(a, b) (a>b?a:b) tendif 29.5. Обработка значений времени Файл util.h содержит объявления функций, предназначенных для работы с буферным пулом, а также объявление одной макрокоманды tv21in, которая служит 29.5. Обработка значений времени 485
для преобразования значений времени. Эта макрокоманда необходима в связи с тем, что в операционной системе Linux значения времени хранятся в виде пары величин, представляющих секунды и микросекунды, тогда как в большинстве функций и в программном обеспечении RTP время рассматривается как одно значение. Макрокоманда tv21in принимает два параметра: структуру timeval, которая определена в операционной системе Linux, и переменную со значением частоты аппаратных часов (измеряемой в герцах). В результате вызова макрокоманды возвращается одно значение, которое соответствует отсчету времени с частотой часов, равной второму параметру. Таким образом, вызов функции tv21in(tijn,c) вырабатывает одно значение, полученное после преобразования показаний времени, приведенных в структуре tim, с учетом частоты аппаратных часов, равной с. /* Файл util.h - макрос tv21in */ #include <common.h> ¦define tv21in(t, clkrt) ((t.tvjsec * clkrt) + ((int) ((double) \ t.tvjisec * .000001 * (double) clkrt))) struct bufpool { char *bp_next; /* Указатель на следующий свободный буфер */ sem_t bp_sem; /* Счетный семафор */ pthread mutex_t bpjnutex; /* Мьютекс, применяемый для манипуляции со списком */ }? void bpinit(void); char *bpget(void); void bpfree(void *); extern struct bufpool bp; 29.6. Обработка пропусков в последовательности пакетов RTP Программное обеспечение RTP должно проверять порядковый номер каждого входящего пакета. С начала приема потока программное обеспечение RTP устанавливает период испытания, в течение которого должно поступить в правильном порядке хотя бы минимальное число пакетов (в этом примере кода оно равно RTP_MINSEQUENTIAL). В примере кода, приведенном в процедуре rtpupdateseq, для обратного отсчета пакетов, поступающих по порядку, применяется переменная stm.stmjprobation. Если какой-либо пакет поступает вне очереди, счетчик переустанавливается и период испытания повторяется вновь с самого начала. После прохождения первоначального периода испытания программное обеспечение RTP проверяет последовательность входящих пакетов уже менее строго. Пакет отбрасывается, если только он прибывает со значительным опережением или отставанием от ожидаемого порядкового номера. Однако сравнение двух порядковых номеров усложняется в связи с конечным числом элементов последовательности, поскольку в коде должен учитываться тот случай, когда завершается один полный цикл отсчета порядковых номеров и начинается следующий. Поскольку в этом коде учтено зацикливание порядковых номеров, программное обеспечение RTP проверяет, находится ли порядковый номер входящего пакета в пределах постоянного расстояния от ожидаемой последовательности (в данном примере кода для этой цели служит константа RTP_MAXMISORDER). Рассматриваемый код содержится в файле rtpseq.c. /* Файл rtpseq.c - процедуры rtpinitseq, rtpupdateseq */ tinclude <rtp.h> fdefine RTPJ1AXDR0P0UT 3000 ¦define RTP MAXMISORDER 100 486 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
¦define RTP_SEQMOD A « 16) /* * Процедура rtpinitseq - выполняет инициализацию переменных, предназначенных * для работы с последовательными номерами (адаптирована * из RFC 1889) */ void rtpinitseq(seq J: seq) { stm.stm_firstseq = seq; stm.stmjiiseq = seq; stm.stmjroll = 0; stm.stmjpackets = 0; stm.stmjrecprior =0; stm.stnTexpprior = 0; } /* * Процедура rtpupdateseq - обеспечивает проверку порядковых номеров пакетов и * обновление счетчиков (адаптирована из RFC 1889) */ int rtpupdateseq(seq J: seq) { seq_t udelta = seq - stm.stmjiiseq; if (stm.stmjprobation) { if (seq == stm.stmjiiseq + 1) { stm. stmjprobation—; stm.stmjiiseq = seq; if (stmTstmjprobation == 0) { rtpinitseq(seq); return OK; } } else { stm.stmjprobation = RTPJ1INSEQUENTIAL - 1; stm.stmjiiseq = seq; } return ERROR; } else if (udelta < RTPJiAXDROPOUT) { if (seq < stm.stmjiiseq) stm.stm_roll++; stm.stm hiseq = seq; } else if (udelta <= RTP_SEQMOD - RTPJiAXMISORDER) { if (seq == stm.stm^badseq) rtpinitseq(seq); } else { stm,stm_badseq = (seq + 1) & (RTP_SEQMOD - 1); return ERROR; } 29.6. Обработка пропусков в последовательности пакетов RTP
return OK; } 29.7. Обработка очереди пакетов RTP Основная структура данных в нашем коде RTP представляет собой очередь пакетов, отсортированных по порядковому номеру. Хотя задача управления этой очередью является достаточно простой, код усложняется в связи с тем, что несколько потоков могут одновременно предпринимать попытки обработки этой очереди. Для обеспечения того, чтобы потоки не мешали друг другу, эта очередь защищена мьютексом stm.stm_qmutex. До начала работы с очередью поток вызывает функцию pthread_mutex_lock, которая блокирует мьютекс и исключает для других потоков возможность приобретения блокировки. Таким образом, функция pthread_mutex_lock гарантирует, что только один поток может обратиться к очереди в любое время; другие потоки, которые попытаются обратиться к очереди одновременно с ним, будут заблокированы. Закончив работу с очередью, поток вызывает функцию pthreadjnutexjinlock. Если нет других потоков, заблокированных в ожидании возможности использовать очередь, вызов этой функции приводит лишь к переустановке мьютекса в разблокированное состояние; если же заблокированы другие потоки, этот вызов разблокирует один и только один из них и позволит ему продолжить работу. Самой простой здесь является операция удаления пакета из очереди, поскольку пакет всегда удаляется с начала очереди. После блокировки мьютекса из очереди удаляется первый элемент путем установки указателя начала списка на элемент, следующий за ним. Операция вставки является немного более сложной, чем удаление, поскольку пакеты могут поступать не по порядку. Поэтому должен быть выполнен поиск по списку для определения позиции, в которую необходимо вставить элемент. Как обычно, до начала вставки мьютекс блокируется, а после завершения вставки разблокируется. Рассматриваемый код содержится в файле rtpqueue.c. /* Файл rtpqueue.c - процедуры rtpqdequeue, rtpqinsert */ ¦include <rtp.h> /* * Процедура rtpqdequeue - обеспечивает удаление из очереди пакетов RTP * с учетом времени поступления * */ struct rtpln * rtpqdequeue(void) { struct rtpln *p, *pln; (void) pthreadjnutex_lock(&stm.stmjjmutex); if ((pin = stm.stm_qhead) != NULL) { if ((p = pln->rlnjprev) != NULL) { stm.stm_qhead = p; p->rln_next = NULL; } else stm.stm_qtail = stm.stmjjhead = NULL; } (void) pthreadjnutex_unlock(&stm.stmjjmutex); 488 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
return pin; } /* - * Процедура rtpqinsert - выполняет вставку в очередь пакетов RTP * — */ bool rtpqinsert(struct rtpln *pln) { struct rtpln *p, *q = NULL; bool head = FALSE; (void) pthread_mutex_lock(&stm.stm_qmutex); for (p = stm.stmjjtail; p != NULL && p->rln_seq > pln->rln_seq; 4 = Pr P = p->rln next); if (q !¦ NULL) " / q->rln_next = pin; else stm.stmjjtail = pin; pln->rln prev = q; if (p !="NULL) p->rln_prev = pin; else stm.stm__qhead = pin; pln->rln_next = p; head = pin == stm.stm_qhead; (void) pthreadjnutexjinlock(&stm.stmjjmutex); return head; } 29.8. Обработка входных данных RTP В этом примере кода для приема и обработки пакетов используется отдельный поток. В потоке выполняется процедура rtprecv, представляющая собой бесконечный цикл, в котором повторно вызывается процедура recv для приема и обработки одного пакета. При каждом проходе по циклу процедура rtprecv извлекает один пакет RTP из сокета, проверяет поля заголовка и ставит пакет в очередь. С начала приема потока процедура rtprecv создает поток для подготовки отчета получателя RTCP. Кроме того, процедура rtprecv проверяет, не возникает ли коллизия идентификатора источника синхронизации. Так называется событие, при котором идентификатор источника синхронизации в одном из входящих пакетов совпадает с идентификатором, применяемым локально. В подобных случаях процедура rtprecv выбирает новый идентификатор источника случайным образом. /* Файл rtprecv.с - процедура rtprecv */ ¦include <rtcp.h> /* - - * Процедура rtprecv - обеспечивает получение и обработку пакета RTP * 1 29.8. Обработка входных данных RTP 489
*/ void rtprecv() { struct rtpln *pln; struct rtp *prtp; pthread_t thrid; while(TRUE) { pin = (struct rtpln *) bpget(); prtp = &pln->rln_rtp; pln->rln_len = recv(sn.sn_rtpfd, prtp, RTP_BPBUFSZ - sizeof(struct rtpln), 0); if (pln->rln_len < RTPJ4INHDRLEN || prtp->rtp ver != RTP_CURRVERS) { bpfree(pln); continue; } prtp->rtp_seq = ntohs(prtp->rtp_seq); prtp->rtp_time = ntohl(prtp->rtp_time); prtp->rtp_ssrc = ntohl(prtp->rtp_ssrc); if(sn.sn_ssrc =- prtp->rtp_ssrc) { rtcpsendbye(); sn.sn_ssrc = random(); bpfree(pln); continue; } if (!sn.sn_detected && prtp->rtpj?ayload == PCMMU_ID) { stm.stm^ssrc = prtp->rtp_ssrc; sn.sn^detected = TRUE; (void) pthread create(fcthrid, NULL, (void *(*)(void *)) rtcpcycle, NULL); } if (prtp->rtp_ssrc != stm.stm_ssrc || rtpupdate(prtp) == ERROR) { bpfree(pln); continue; } pln->rln__seq = ((stm.stmjroll) « 16) | prtp->rtp_seq; if (rtpqinsert(pln) || stm.stm__buffering) rtpnewdata(); } } Если поток все еще буферизируется или поступает пакет, который должен быть сразу воспроизведен, программное обеспечение RTP должно повторно вычислить флуктуационную задержку (т.е. должно определить, не превышает ли разница в отметках времени между самым старым пакетом в буфере и новым пакетом порог флуктуации). Вторую ситуацию обнаружить проще, поскольку она может возникать, если входящий пакет ставится в очередь в начале списка. Кроме того, эффективный размер флуктуационного буфера изменяется только после поступления нового пакета. Поэтому каждый раз при постановке нового 490 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
пакета в очередь в начале списка, процедура rtprecv вызывает процедуру rtpnewdata, которая учитывает обе ситуации. Для управления размером флуктуационного буфера процедура rtpnewdata сравнивает разницу в отметках времени между вновь вставленным пакетом (в начале очереди) и самым старым пакетом (в конце очереди). Если разница между ними превышает значение константы RTP_JITTHRESH, процедура rtpnewdata устанавливает значение переменной stm.stm__buffering равным FALSE, указывая на то, что дальнейшая буферизация не требуется. И наконец, процедура rtpnewdata вызывает процедуру pthread_cond_signal, чтобы активизировать ожидающий прикладной поток. Если прикладной поток заблокирован в ожидании данных, он в результате этого вызова активизируется; если же прикладной поток уже выполняется, этот вызов не оказывает никакого воздействия. /* Файл rtpnewdata.с - процедура rtpnewdata */ ¦include <rtp.h> /*- * Процедура rtpnewdata - определяет, закончена ли буферизация, и сигнализирует * о переходе в состояние готовности к чтению * */ void rtpnewdata(void) { mediatime_t begin = 0, end = 0; (void) pthread_mutex_lock(&stm.stmjnnutex); if (stm.stmjDuffering) { (void) pthreadjnutex_lock(&stm.stm_qmutex); if (stm.stmjjhead != NULL) { begin = stm.stm_qhead->rln_rtp.rtp_time; end = stm.stmj3tail->rlnj:tp.rtp_tinie; } (void) pthread mutexjmlock (& stm. stmjjmutex); if (end - begin >= RTP_JITTHRESH) { stm.stm_clky = begin; (void) gettimeofday(&stm.stm_clkx, NULL); stm.stmjDuffering = FALSE; > } (void) pthread_cond_signal(&stm.stmjrcond); (void) pthread_mutex_unlock(& stm.stmjrmutex); } 29.9. Ведение статистики для формирования отчетов RTCP Протокол RTCP предусматривает периодическое формирование отчетов получателя. Для этого программное обеспечение RTCP должно, иметь информацию о том, какой пакет был получен последним по времени и какова при этом была флуктуация. При поступлении каждого пакета RTP вызывается процедура rtpupdate. Она обновляет статистические данные, которые требуются для состав- 29.9. Ведение статистики для формирования отчетов RTCP 491
ления отчета получателя в соответствии со спецификацией, которая определена в стандарте протокола. Рассматриваемый код содержится в файле rtpupdate.c. /* Файл rtpupdate.c - процедура rtpupdate */ #include <rtp.h> /* * Процедура rtpupdate - обновляет статистические данные при получении * каждого пакета RTP * -«— « .— ———™ « - ————.—.—.—. « — */ int rtpupdate(struct rtp *header) { struct timeval now, deltatv; int delta, D; (void) pthread_mutex_lock(&stm.stmjsmutex); if (rtpupdateseq(header->rtp_seq) == ERROR) { (void) pthread_mutex_unlock(&stm.stm_smutex); return ERROR; } (void) gettimeofday(&now, NULL); if (stm.stmj?ackets++ != 0) { timersub(&now, &stm.stm_lastrec, Sdeltatv); delta = tv21in(deltatv, PCMMU_CLKRT); D = delta - (header->rtp_time - stm.stmJLastts); D = (D < 0 ? -D : D); stm.stm_jitter += ((double) D - stm.stm_jitter) / 16.0; } stm.stm_inactive =0; stm.stm_lastts = header->rtp_time; stm.stm_lastrec = now; (void) pthread_mutex_unlock(&stm.stm_smutex); return OK; } 29.10. Инициализация программного обеспечения RTP Теперь, после рассмотрения процедур, которые обрабатывают входящие пакеты RTP, перейдем к описанию процедуры инициализации. Код инициализации, который обеспечивает инициализацию потоков, сокетов и всех структур данных, приведен в файле rtpinit.с. Выполнение процедуры rtpinit начинается с заполнения нулевыми значениями структуры, содержащей информацию о сеансе. Затем эта процедура создает пул буферов пакетов, а также инициализирует условную переменную и мьютекс, которые используются потоками. После этого процедура создает соке- ты для трафика RTP и RTCP и привязывает их к соответствующим (групповым) адресам. И наконец, после подготовки сокетов и всех переменных мьютекса процедура rtpinit запускает второй и третий потоки. Один из них обрабатывает 492 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
входящие пакеты RTP, а другой — входящие пакеты RTCP. Напомним, что четвертый поток, который отправляет отчеты получателя RTCP, создается процедурой rtprecv сразу после установления сеанса. /* Файл rtpinit.с - процедура rtpinit */ ¦include <rtcp.h> struct session sn; struct stream stm; /*- —- * Процедура rtpinit - открывает сеанс RTP и инициализирует структуры stream, * обеспечивает присоединение к группе рассылки * . */ void rtpinit(unsigned int mca, int port) { struct ipjnreq mreq; int reuse = TRUE; unsigned char ttl = RTCPJTTL; pthread_t th; (void) memset(&sn, 0, sizeof(struct session)); (void) bpinit(); sn.sn ssrc = randomf); sn.sn"rtpfd = socket(PF_INET, SOCK DGRAM, 0); sn.snjrtcpfd = socket(PF_INET, SOCKJ)GRAM, 0); (voidj setsockopt(sn.sn_rtpfd, SOLJJOCKET, SOJREUSEADDR, Sreuse, sizeof(reuse)); (void) setsockopt(sn.snjrtcpfd, SOL_SOCKET, SO_REUSEADDR, Sreuse, sizeof(reuse)); sn.snjrtcpto.sinjramily = AF_INET; sn.snjrtcpto.sinjiddr.s_addr = mca; sn.snjrtcpto.sinjport = htons(port); (voidj bind(sn.snj:tpfd, (struct sockaddr *) fcsn.snjrtcpto, sizeof(struct sockaddr_in)); sn.snjrtcpto.sinjport = htonsfport +1); (voidj bind(sn.snjrtcpfd, (struct sockaddr *) ssn.snjrtcpto, sizeof(struct sockaddrj.n)); nireq.imrjnultiaddr.s_addr = mca; mreq.imr interface.s addr = htonl(INADDR ANY); (void) setsockopt(sn7sn_rtpfd, IPPROTOJtt, IPJVDDJIEMBERSHIP, fcmreq, sizeof(mreq)); (void) setsockopt(sn.snjrtcpfd, IPPROTOJP, IPJVDDJIEMBERSHIP, fcmreq, sizeof(mreq)); (void) setsockopt(sn.snjrtcpfd, IPPROTOJCP, IPJ!ULTICASTJTTL, &ttl, sizeof(ttl)); stm.stm_buffering = TRUE; stm.stmj>robation = RTPJ1INSEQUENTIAL; (void) pthread_mutex_init(&stm.stm_qmutex, NULL); (void) pthreadjnutexJLnit(&stm.stm_smutex, NULL); (void) pthread jnutex_init(& stm.stmjrmutex, NULL); (void) pthread_cond_init(&stm.stmj:cond, NULL); 29.10. Инициализация программного обеспечения RTP
(void) pthread_create(&th, NULL, (void*(*)(void*))rtprecv, NULL); (void) pthread_create(&th, NULL, (void*(*)(void*))rtcprecv, NULL); } Последний фрагмент кода находится в главной процедуре. В ней вызывается процедура rtpinit для инициализации кода RTP и playaudio для воспроизведения аудиоинформации. В нашей реализации процедура playaudio никогда не возвращает управление, поскольку по окончании сеанса процедура rtcpcycle вызывает функцию exit и процесс завершается. Однако после вызова процедуры playaudio вставлен оператор return для обеспечения того, чтобы эта главная процедура успешно прошла проверку утилитой lint. Рассматриваемый код содержится в файле main.с. /* Файл main.с - главная процедура */ ¦include <rtcp.h> /* * Главная процедура - выполняет инициализацию и переходит к воспроизведению * аудиоинформации * ; . */ int main(int argc, char **argv) { rtpinit(inet_addr(argv[1]), atoi(argv[2])); playaudio(); return 0; } Процедура playaudio.с выполняет чтение данных из потока RTP и воспроизводит данные, которые соответствуют текущему моменту времени "now". Для этого в указанной процедуре необходимо установить соответствие между временем на часах локального компьютера и временем, отсчитанным по часам источника мультимедийного потока RTP. Это соответствие устанавливается процедурой rtpnewdata; после достижения порогового уровня заполнения флуктуационного буфера процедура rtpnewdata регистрирует текущее местное время в переменной stm.stm_clkx, a мультимедийное время из первого пакета — в переменной stm.stm_clky. Процедура playaudio использует эти два значения для пересчета текущих показаний часов в показания часов источника мультимедийной информации, содержащейся в пакетах RTP. Поэтому после получения доступа к очередному пакету процедура playaudio может еще раз проверить время, в которое должны быть воспроизведены данные из этого пакета. Если это значение соответствует прошедшему моменту времени, процедура playaudio просто пропускает пакет без его воспроизведения, а если же это значение соответствует будущему моменту времени, процедура playaudio вычисляет, сколько пройдет времени до того момента, как должно начаться воспроизведение данных, и блокируется, устанавливая задержку до этого времени. И наконец, даже после обнаружения пакета, который содержит данные, соответствующие текущему времени, процедура playaudio не начинает автоматически воспроизведение данных из этого пакета. Вместо этого процедура playaudio вычисляет смещение в пакете для поиска данных, точно соответствующих текущему времени, и начинает воспроизведение данных с этого места. В этом коде приходится учитывать много тонкостей, что неизбежно приводит к его усложнению. К числу наиболее трудоемких операций относится вычисление 494 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
задержки. Чтобы понять, с чем связаны эти сложности, заметим, что Linux — это операционная система, работающая с разделением времени, а не в режиме реального времени. За доступ к процессору постоянно конкурируют сразу несколько потоков выполнения; даже после возникновения некоторого события может пройти небольшой интервал времени до того, как система Linux передаст процессор в распоряжение потока, ожидающего этого события. Поэтому если поток запрашивает задержку в N миллисекунд (мс), операционная система гарантирует, что предоставленная задержка не будет меньше N мс, но не может гарантировать, что задержка будет точно равна N мс. Также следует учесть, что после того как поток запланирован на выполнение, вычислительные операции и операции ввода/вывода вносят дополнительную задержку до начала воспроизведения. Для большинства программ, связанных с обработкой данных, небольшая задержка не имеет значения. Однако при обработке в реальном времени аудиосигнала задержка при передаче данных на звуковое устройство приведет к возникновению паузы и к искажениям воспроизведения, заметным для слушателя. В целях предотвращения таких проблем в процедуре playaudio применяется эвристический метод: в ней предусмотрено, чтобы поток активизировался незадолго до того момента, когда потребуется воспроизведение. Это означает, что при вычислении задержки в процедуре playaudio используется значение, немного меньшее по сравнению с действительно необходимой точной задержкой. Это означает, что небольшие издержки, связанные с функционированием операционной системы или устройства, не помешают своевременно начать воспроизведение. Обработка еще больше усложняется в связи с тем, как организовано функционирование программного обеспечения RTP. В частности, напомним, что пакеты RTP могут поступать с нарушением исходного порядка. Рассмотрим, что произойдет, если пакет поступит в то время, когда основной поток находится в неактивном состоянии. Например, предположим, что в момент времени, отстоящий от момента начала воспроизведения на 50 единиц времени (назовем его условно моментом времени 50), процедура playaudio просматривает очередь и находит, что первый пакет в этой очереди должен быть воспроизведен в момент времени 60. Основной поток должен установить выдержку времени в течение интервала, немного меньшего 10 единиц времени. Однако в любое время в течение этой задержки может поступить новый пакет, содержащий данные, которые должны быть воспроизведены в интервале между текущим моментом времени и моментом времени 60. Чтобы можно было учесть такие обстоятельства, программное обеспечение должно допускать отмену пауз. В приведенном примере кода для выхода из этой ситуации используется условная переменная. Основной поток переходит на установленное время в состояние ожидания события, связанного с изменением условной переменной stm.stm_rcond. Если установка таймера истекает, а это событие не происходит, операционная система активизирует поток. Если же поступает пакет, то процедура rtpnewdata вызывает функцию pthread_cond_signal для активизации потока. Данный код содержится в файле playaudio.с. /* Файл playaudio.с - процедура playaudio */ ¦include <rtp.h> /* - - - * Процедура playaudio - выбирает из очереди и воспроизводит пакеты * */ void playaudio(void) 29.10. Инициализация программного обеспечения RTP 495
struct rtp *prtp; struct rtpln *pln; struct timespec waketimets; struct timeval now, deltatv, waketimetv; mediatime__t mnow, readfrom, delay; int audiodev, samples, offset, profile = APF_NETWORK; audiodev = openC'/dev/audio", 0_WR0NLY); (void) ioctl(audiodev, SNDCTLJ)SPJ?ROFILE, &profile); (void) pthreadjnutex_lock(&stm.stm_rmutex); while (TRUE) { if (stm.stm__buffering || (pin = rtpqdequeue()) == NULL) { (void) pthread_cond_wait(&stm.stm_rcond, & s tm. s tmjrmut ex); continue; } prtp = &pln->rln_rtp; (void) gettimeofday(&now, NULL); timersub(&now, &stm.stm_clkx, fcdeltatv); mnow = tv21in(deltatv, PCMMU_CLKRT) + stm.stm_clky; samples = pln->rln_len - rtphdrlen(prtp); if (rtptsgt(mnow, prtp->rtp_time + samples - 1)) { bpfree(pln); continue; } readfrom = rtptsmax(prtp->rtp time, mnow); if (rtptsgt(readfrom, mnow + RTP_LEEWAY)) { (void) rtpqinsert(pln); delay = readfrom - mnow - RTP LEEWAY; waketimetv.tv sec = delay / PCMMU_CLKRT; waketimetv.tvjisec = (int) (((delay % PCMMU CLKRT) / (double) PCMMU_CLKRT)~* 1000000); timeradd(&now, &waketimetv, swaketimetv); TIMEVALJTOJTIMESPEC(&waketimetv, fiwaketimets); (void) pthread_cond_timedwait(& stm.stmjrcond, &stm.stmj:mutex, fcwaketimets); continue; } offset = readfrom - prtp->rtp_time; if (prtp->rtp_mark) (void) ioctl(audiodev, SNDCTL_DSP_SYNC); (void) write(audiodev, rtpdataTprtp) + offset, samples - offset); bpfree(pln); } } 29.11. Определения RTCP Каждое сообщение RTCP начинается с заголовка, в котором указан его тип. В рассматриваемом примере кода формат заголовка сообщения определяет структура rtcp. За заголовком следует сообщение RTCP, которое может принадлежать к одному из двух типов: отчет отправителя, определенный структурой sr, или отчет 496 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
получателя, определенный структурой гг. Сообщение с отчетом получателя состоит из последовательности блоков приема, где формат отдельного блока определен структурой rblock. Рассматриваемые определения содержатся в файле rtcp.h. /* Файл rtcp.h - макрос hton24 */ finclude <rtp.h> ¦define RTCP_HEADERLEN ¦define RTCP_BWFRAC ¦define RTCPJTTL fdefine RTCP_MAXPACKETSZ ¦define RTCP_SR ¦define RTCP_RR ¦define RTCP_BYE ¦define hton24(v) ((v&0xff0000)»16)) 4 /* Длина заголовка RTCP (в октетах) */ .05 /* Часть пропускной способности, отведенная */ /* для сообщений RTCP */ 16 /* Время жизни для пакетов RTCP отправителя */ 1024 /* Размер приемного буфера RTCP */ 200 /* Код отчета отправителя в ПО RTCP */ 201 /* Код отчета получателя в ПО RTCP */ 203 /* Код сообщения о разрыве соединения в ПО RTCP */ (((v&Oxff) « 16) | (v&0xff00) | \ struct rtcp { unsigned int rtcp_count:5; unsigned int rtcpjpad-il; unsigned int rtcp_ver:2; unsigned char rtcp_type; unsigned short rtcp_length; char rtcp_data[l]; }; struct rblock- { ssrc_t rb_ssrc; unsigned int rb_fraclost:8; int rb_cumlost:24; unsigned int rbjiiseq? unsigned int rb_jitter; unsigned int rb_lastsrts; unsigned int rb_delay; }? /* Число пакетов */ /* Флажок наличия заполнения */ /* Версия */ /* Тип сообщения */ /* (Длина сообщения) /4-1 */ /* Дата сообщения */ /* Источник синхронизации, к которому */ /* относится данная структура rblock */ /* Относительное число пакетов, потерянных */ /* со времени формирования предыдущего отчета */ /* Накопленное число потерянных пакетов */ /* Расширенное значение максимального */ /* порядкового номера */ /* Показатель флуктуации */ /* Средние 32 бита отметки времени в формате */ /* NTP последнего отчета отправителя */ /* Задержка с момента получения последнего */ /* отчета отправителя */ struct rr { ssrc t rr ssrc; char rr rblock[l]; }; /* Идентификатор источника синхронизации */ /* получателя */ /* Указатель на первый блок отчета */ struct sr { ssrc__t sr_ssrc; unsigned int sr_intts; unsigned int sr fracts; /* Идентификатор источника синхронизации */ /* отправителя */ /* Отметка времени NTP (старшие 32 бита) */ /* Отметка времени NTP (младшие 32 бита) */ 29.11. Определения RTCP 497
unsigned int sr_rtpts; /* Отметка времени потока мультимедийной */ /* информации RTP */ unsigned int srjpackets; /* Число пакетов, переданных отправителем */ unsigned int sr_octets; /* Число октетов, переданных отправителем */ char sr__rblock[l]; /* Указатель на первый блок отчета */ }; void rtcpsendbye(void); void rtcpheader(struct rtcp *, int, unsigned char, int); double rtcpinterval(int, int, double, int, int, int *, int); void rtcpcycle(void); void rtcprecv(void); 29.12. Прием отчетов отправителя RTCP Как было отмечено выше, в рассматриваемом примере кода RTCP функционирование организовано в виде двух отдельных потоков: один из них обеспечивает прием поступающих пакетов RTCP, а другой формирует исходящие пакеты. Процедура rtcprecv обрабатывает входящие сообщения с отчетами отправителя. Для выработки исходящих отчетов получателя требуется два фрагмента информации о каждом отчете отправителя: отметка времени NTP, находящаяся в отчете, и местное время получения этого сообщения. Процедура rtcprecv извлекает из отчета отправителя значение отметки времени и регистрирует это значение в структуре stream наряду со значением местного времени, возвращаемым функцией gettimeofday системы Linux. В процедуре rtcprecv используется также идентификатор источника синхронизации, содержащийся в каждом входящем отчете отправителя, для исключения сообщений, поступивших от других отправителей. Рассматриваемый код приведен в файле rtcprecv.с. /* Файл rtcprecv.с - процедура rtcprecv */ tinclude <rtcp.h> /* - * Процедура rtcprecv - выполняет прием и обработку отчетов отправителя RTCP * */ void rtcprecv(void) { char rtcpbuf[RTCP JiAXPACKETSZ]; struct rtcp *prtcp = (struct rtcp *) rtcpbuf; struct sr *psr = (struct sr *) prtcp->rtcp_data; while(TRUE) { (void) recv(sn.sn_rtcpfd, prtcp, RTCP_MAXPACKETSZr 0)? prtcp->rtcp_length = ntohs(prtcp->rtcp_length); if (prtcp->rtcp_ver == RTP_CURRVERS && prtcp->rtcp_type' == RTCP_SR && ntohl(psr->sr_ssrc) == stm.stm_ssrc) { stm.stm_inactive =0; (void) gettimeofday(&stm.stmJLastsr, NULL); stm.stm_ntp[0] = ntohl(psr->sr__fracts); stm.stm_ntp[l] = ntohl(psr->sr_intts); 498 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
29.13. Выработка отчетов получателя RTCP Второй поток RTCP обеспечивает формирование отчетов получателя; исходный код программного компонента, на котором основано функционирование этого потока, имеет большой объем, и поэтому он разбит на два файла. Процедура rtcpcycle содержит главный цикл, который начинает функционировать с момента установления текущего сеанса. При каждом проходе по циклу устанавливается задержка на число секунд, равное wait, после чего выполняются действия, предусмотренные в данном цикле (значение wait вычисляется, как описано ниже). Во время прохода по циклу процедура rtcpcycle проверяет, остается ли еще поток данных активным. Если во время последнего прохода по циклу поступили данные, процедура rtcpcycle передает отчет получателя. Если данные в течение последнего цикла не поступили, процедура rtcpcycle увеличивает значение переменной stm.stm_inactive, где ведется подсчет подряд идущих циклов, в которых не было получено данных. Если эта ве^ личина не превышает порогового значения RTP_INACTTHRESH, то процедура rtcpcycle не выполняет никаких действий. Если же число холостых циклов достигает порогового значения, процедура rtcpcycle вызывает процедуру rtcpsendbye для выработки сообщения о разрыве сеанса связи RTCP и завершает свою работу. Поля в пакете RTCP имеют нестандартный размер, что приводит к дополнительному усложнению процедуры rtcpcycle. Например, спецификация RTCP предусматривает, что накопленное число потерянных пакетов должно быть представлено в виде 24-битового целого числа. Сразу после вычисления числа потерянных пакетов путем вычитания числа поступивших пакетов из общего числа ожидаемых пакетов, в процедуре rtcpcycle необходимо вызвать макрос hton24 для преобразования полученного значения из 32-битового в 24-битовое целое число. Однако перед выполнением такого преобразования в процедуре rtcpcycle необходимо проверить, что это значение находится в допустимом диапазоне. В ней вначале вызывается макрос min для проверки того, что это значение не превышает максимально возможного положительного 24-битового целого числа. Затем в ней вызывается макрос max для проверки того, что это значение остается выше минимального отрицательного 24-битового целого числа. В том случае, если возникает необходимость отправить отчет получателя, процедура rtcpcycle заполняет блок отчета и заголовок сообщения, а затем передает это сообщение. После отправки отчета процедура rtcpcycle разблокирует структуры данных, повторно вычислит продолжительность своей приостановки и повторит цикл. Рассматриваемый код содержится в файле rtcpcycle.с. /* Файл rtcpcycle.с - процедура rtcpcycle */ tinclude <rtcp.h> /* - * Процедура rtcpcycle - периодически формирует отчеты получателя RTCP * */ void rtcpcycle(void) { int length = RTCP HEADERLEN + sizeof(ssrc_t) + 29.13. Выработка отчетов получателя RTCP
sizeof(struct rblock); char buf[length]; struct rtcp *prtcp = (struct rtcp *) buf; struct rr *prr = (struct rr *) prtcp->rtcp_data; struct rblock *prb = (struct rblock *) prr->rr_rblock; int exthiseq, cumexp, cyclost, cycexp; struct timeval now, delay; double wait = rtcpintervalA, 0, PCMMU_BW * RTCP_BWFRAC, FALSE, 0, &sn.sn_avgrtcp, TRUE); while (TRUE) { (void) usleep((unsigned int) (wait * 1000000)); (void) pthread_mutex_lock(&stm.stm_smutex); if (stm.stmjpackets == stm.stmjrecprior) { if (++stm.stm_inactive == RTP_INACTTHRESH) { rtcpsendbye(); exit@); } (void) pthreadjnutex unlock(&stm.stm_smutex); wait = rtcpintervalB, 0, PCMMU_BW * RTCP_BWFRAC, FALSE, 0, &sn.sn_avgrtcp, FALSE)! continue; } prr->rr_ssrc = htonl(sn.sn_ssrc); exthiseq = (stm.stmjroll « 16) | stm.stmjiiseq; cumexp = exthiseq - stm.stmjEirstseq +1; cycexp = cumexp - stm.stm_expprior; cyclost = max(cycexp - (stm.stmjpackets - stm.stmjrecprior), 0); prb->rb_ssrc = htonl(stm.stm_ssrc); prb->rb_fraclost = cycexp == 0 ? 0 : (cyclost « 8) / cycexp; prb->rb_cumlost = hton24(max(min(cumexp - stm.stm packets, 0x7fffff), -A « 23))); prb->rb_hiseq = htonl( exthiseq); prb->rb_jitter = htonl((unsigned int) stm.stm^jitter); prb->rb lastsrts = htonl(((stm.stm_ntp[l] & OxOOOOffff) « 16) | ((stm.stm jitp[0] & OxffffOOOO) » 16)); (void) gettimeofday(&now, NULL); timersub(&now, &stm.stm_lastsr, fidelay); prb->rb_delay = stm.stm_lastsr.tv_sec != 0 ? htonl( tv21in(delay, 65536)) : 0; rtcpheader(prtcp, 1, RTCP_RR, length); (void) sendto(sn.sn_rtcpfd, prtcp, length, 0, (struct sockaddr *) &sn.sn_rtcpto, sizeof(struct sockaddr_in)); stm.stm_expprior = cumexp; stm.stmjrecprior = stm.stmjpackets; (void) pthreadjnutex unlock(&stm.stm_smutex); wait = rtcpintervalB, 1, PCMMU_BW * RTCP_BWFRAC, FALSE, length + 28, &sn7sn_avgrtcp, FALSE); } } 29.14. Создание заголовка RTCP Для заполнения полей заголовка применяется процедура rtcpheader. 500 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинфо
/* Файл rtcpheader.с - процедура rtcpheader */ If include <rtcp.h> /*-- - * Процедура rtcpheader - заполняет заголовок RTCP в сетевом порядке байтов * */ void rtcpheader(struct rtcp *prtcp, int count, unsigned char type, int length) { prtcp->rtcp_ver = RTP_CURRVERS; prtcp->rtcp_count = count; prtcp->rtcp_type = type? prtcp->rtcp_length = htons((length / 4) - 1); prtcp->rtcp_pad = FALSE; } 29.15. Вычисление продолжительности паузы перед формированием очередного отчета RTCP Частота, с которой программное обеспечение RTCP генерирует отчеты получателя, зависит от числа участников обмена данными (по мере увеличения числа участников частота снижается). Кроме того, стандарт определяет исходное значение частоты, которое используется до того, как программным обеспечением будет получена информация о других участниках обмена данными. Процедура rtcpinterval вычисляет продолжительность паузы до следующего отчета; код, приведенный в файле rtcpinterval.с, взят непосредственно из стандарта RTCP. /* Файл rtcpinterval.с - процедура rtcpinterval */ ((include <rtcp.h> /* - * Процедура rtcpinterval - вычисляет число секунд до следующего цикла RTCP * (адаптирована из RFC 1889) * . ..j. */ double rtcpinterval(int members, int senders, double rtcpjaw, int we_sent, int packet_size, int *avgj:tcp_size, int initial) { double const RTCP_MIN_TIME =5.; double const RTCP_SENDER_BW FRACTION = 0.25; double const RTCP_RCVR BW_FRACTION = A-RTCP_SENDER_BWJ'RACTI0N); double const RTCP_SIZE~GAIN = A./16.); double t; double rtcp_min_time = RTCP_MINJTIME; int n; if (initial) { rtcp_min_time /= 2; *avgj:tcp_size =128; } 29.15. Вычисление продолжительности паузы... 501
n = members; if (senders > 0 && senders < members * RTCPJ3ENDER_BW_FRACTI0N) { if (we_sent) { rtcp_bw *= RTCP_SENDER_BWJ'RACTION; n = senders; } else { rtcp_bw *= RTCPJ*CVR_BW_FRACTION; n -= senders; } } *avg_rtcp_size += (packet size - *avg_rtcp_size) * RTCPJSIZE_GAIN; t = (*avg__rtcp__size) * n 7 rtcp_bw; if (t < rtcp_min_time) t = rtcp min time; return t * (drand48() + 0.5); } 29.16. Выработка сообщения о завершении обмена данными по протоколу RTCP Перед выходом из сеанса RTP участник этого сеанса должен передать сообщение о завершении обмена данными по протоколу RTCP. Код формирования и отправки этого сообщения приведен в файле rtcpsendbye.c. /* Файл rtcpsendbye.c - процедура rtcpsendbye */ tinclude <rtcp.h> /* * Процедура rtcpsendbye - вырабатывает сообщение о прекращении сеанса RTCP * */ void rtcpsendbye(void) { int length = RTCP_HEADERLEN + sizeof(ssrc_t); char buf[length]; struct rtcp *prtcp = (struct rtcp *) buf; *((ssrc_t *) prtcp->rtcp_data) = htonl(sn.sn_ssrc); rtcpheaderfprtcp, 1, RTCP_BYE, length); (void) sendto(sn.sn_rtcpfd, prtcp, length, 0, (struct sockaddr *) &sn.snj:tcpto, sizeof(struct sockaddr_in)); } 29.17. Объем исходного кода программы, созданной в рамках интегрированной версии В предыдущей главе были описаны размеры библиотеки RTP общего назначения. Как показано в табл. 29.1, в отличие от этой библиотеки, которая включает свыше 6 тыс. строк исходного кода, интегрированная версия, рассматриваемая в настоящей главе, состоит только из 700 строк, которые распределяются 502 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
по компонентам; в этой версии достигнуто существенное снижение объема кода за счет того, что вместо программных средств общего назначения применяются специализированные программные средства. Таблица 29.1. Объем исходного кода отдельных компонентов интегрированной версии программного обеспечения RTP Число строк 279 169 56 197 140 841 Процентное соотношение 33,2 20,1 6,7 23,4 16,6 Описание RTP RTCP Вспомогательные процедуры буферного пула Объявления и константы заголовочного файла Главная процедура и прикладной код Всего (включая главную процедуру и все ути- литы) Как показано в этой таблице, интегрированная версия программного обеспечения состоит из 841 строки исходного кода. В их число входят 140 строк главной процедуры и той, что обеспечивает воспроизведение аудиоинформации. Таким образом, все это приложение, включая интегрированную версию программного обеспечения RTP, составляет по объему менее 14% по сравнению с библиотекой, описанной в главе 28. Без учета объема кода главной процедуры, этот исходный код составляет менее 12% по сравнению с библиотекой3. 29.18. Резюме В настоящей главе рассмотрена интегрированная версия приложения для обработки потоковой аудиоинформации, которая обеспечивает прием и воспроизведение аудиоинформации, передаваемой по протоколу RTP. Как и в библиотеке общего назначения, описанной в предыдущей главе, в программном обеспечении, рассматриваемом в настоящей главе, применяется несколько потоков выполнения. Основной поток принимает и воспроизводит аудиоинформацию, а три других потока обрабатывают входящие пакеты RTP, входящие пакеты RTCP и исходящие пакеты RTCP. В отличие от библиотеки, возможности рассматриваемой программы ограничены: она позволяет принимать только один поток аудиоинформации в одном сеансе с использованием одной схемы кодирования, но результирующая программа имеет намного меньший объем по сравнению с библиотекой. Материал для дальнейшего изучения Общие сведения о форматах пакета RTP и RTCP приведены в документе [143]; в нем также определены многие алгоритмы, которые применяются в рассматриваемом примере кода. В частности, в этом стандарте приведена подробная спецификация методов выбора начального значения порядкового номера RTP и вычисления задержки при формировании отчетов получателя. Часть кода, при- Исходный код программ, приведенных в настоящей книге, в том числе и в данной главе, можно получить по адресу ftp://ftp.cs.purdue.edu/pub/Xinu/TCPIP-vol3.linux. dist.tar.Z. 29.18. Резюме 503
веденного в настоящей главе, взята непосредственно из указанного документа. Этот код немного переформатирован и в нем удалена часть комментариев, чтобы он соответствовал стилю, применяемому в данной книге, но во всем остальном он остался неизменным. С информацией о звуковом устройстве Linux (/dev/audio) и звуковых кодировках, предусмотренных для конкретного компьютера, можно ознакомиться в оперативной документации. И наконец, ниже указан Web-узел, на котором приведены данные о комплекте инструментальных средств обработки аудиоинформации RTP Audio Tool: http://www-mice.cs.ucl.ac.uk/multimedia/projects/rat/index.html Упражнения 29.1. В этом программном обеспечении предполагается по умолчанию, что звуковое устройство Linux было инициализировано для воспроизведения аудиоинформации с импульсно-кодовой модуляцией (в кодировке по закону "мю"). Доработайте рассматриваемый пример прикладной программы таким образом, чтобы в нем инициализация звукового устройства выполнялась явно. 29.2. Для предотвращения возникновения пропусков во время воспроизведения в рассматриваемом примере кода предусмотрена заблаговременная активизация приостановленного потока. Проведите эксперименты при разных нагрузках операционной системы Linux и различных размерах буфера устройства, чтобы определить, какая взаимосвязь существует между нагрузкой, размером буфера и моментом заблаговременной активизации потока для предотвращения пропусков. 29.3. Потоки выполнения применяются для достижения следующих двух целей: уменьшение сложности кода и повышение производительности за счет распараллеливания работы. Покажите, в какой степени достигается каждая из этих двух целей в рассматриваемом примере приложения. 29.4. Выполните профилировку приложения, рассматриваемого в данном примере, измеряя продолжительность выполнения отдельных процедур. Зависит ли быстродействие данного приложения в основном от работы процессора или системы ввода/вывода? Какие операции требуют расхода наибольшего количества процессорного времени? 29.5. В рассматриваемом примере программы предусмотрена возможность приема пакетов RTP, передаваемых по методу групповой рассылки. Если соответствующий компонент программы будет заменен компонентом, обеспечивающим только прием данных, передаваемых по принципу индивидуальной передачи, приведет ли это к уменьшению объема исходного кода? 29.6. Доработайте рассматриваемый пример программы таким образом, чтобы в нем для доставки сообщений RTP применялся протокол TCP, а не UDP. В чем состоит основная сложность такой доработки? В чем состоит основное преимущество такой версии программы? 29.7. Разработайте приложение, которое извлекает данные из звукового устройства Linux и передает их с использованием протокола RTP. Для реализации какой функции, передачи или приема, требуется больший объем кода? Почему? 504 Глава 29. Потоковый транспортный протокол передачи аудио- и видеоинформации
30 Практические рекомендации и методы улучшения функционирования серверов Linux 30.1. Введение Пример с серверной программой, который рассматривался на протяжении всей этой книги, был разработан таким образом, чтобы читатель мог понять фундаментальные принципы проектирования сетевого программного обеспечения, пользуясь четкой и наглядной иллюстрацией рассматриваемых концепций. Поэтому в коде этого сервера не реализованы многие функции, необходимые для производственного программного обеспечения. В настоящей главе описаны некоторые широко распространенные методы и приемы, которые обычно применяются при разработке серверных программ производственного назначения. Представленные здесь методы позволяют повысить надежность кода и упростить его отладку. Эти методы позволяют также изолировать сервер от процесса, в котором был выполнен его запуск, и обеспечить его функционирование в соответствии с общепринятыми правилами безопасной работы в среде Linux1. Хотя ни один из этих методов не связан непосредственно с реализацией алгоритмов работы сервера, профессиональные программисты признают необходимость их применения. 30.2. Функционирование в фоновом режиме Операционная система Linux, как и большинство систем UNIX, позволяет эксплуатировать процесс на переднем плане или в фоновом режиме. Проще всего можно понять, в чем состоит разница между этими режимами работы, представив себе, как пользователь вводит команды в командном интерпретаторе. Обычно каждая команда выполняется на переднем плане; это означает, что командный интерпретатор создает для выполнения этой команды соответствующий процесс, а затем переходит в состояние ожидания до того момента, когда командный процесс завершит свою работу, после чего снова выводит приглашение к вводу команд и считывает очередную команду. Но если пользователь указывает, что команда должна выполняться в фоновом режиме, командный интерпре- Хотя при описании методов, рассматриваемых в этой главе, в основном упоминается система Linux, они в равной мере относятся и к другим версиям UNIX.
татор создает процесс для выполнения этой команды и позволяет новому процессу продолжить работу одновременно с тем, как выдается приглашение к вводу следующей команды. Чтобы узнать, завершился ли фоновый процесс, пользователь должен проверить информацию о его состоянии. Выполнение в фоновом режиме может применяться для таких задач, как печать, поскольку это позволяет пользователю одновременно с эксплуатацией подобного процесса продолжать интерактивное выполнение других команд. Большинство серверов функционирует в фоновом режиме, поскольку они должны работать неопределенно долгое время. Серверный процесс выполняется с момента запуска операционной системы и работает в фоновом режиме, ожидая поступления запросов. В системе Linux серверные процессы создаются во время запуска системы. В ходе загрузки операционной системы начальный процесс системы (init) выполняет ряд команд из каталога запуска демонов (/etc/red). Сценарии, находящиеся в каталоге запуска, создают серверные процессы в определенном порядке, чтобы такие системные службы, как служба дистанционного доступа к файловой системе, запускались до того, как прикладные службы попытаются их использовать. Хотя и возможно запрограммировать перевод каждого серверного процесса в фоновый режим в сценарии запуска, большинство производственных серверов самостоятельно переходят в фоновый режим, причем это действие выполняется достаточно быстро. Чтобы понять, почему так важен быстрый переход в фоновый режим, рассмотрим последовательность запуска серверного процесса на выполнение. Предположим, что в сценарии запуска выполняется ряд действий, в каждом из которых стартует один сервер. Если сервер быстро переходит в фоновый'режим, сценарий запуска может сразу же продолжить выполнение и перейти к запуску следующих серверов. Если же сервер задерживается перед переходом в фоновый режим (например, не переходит в фоновый режим до тех пор, пока не закончит выполнение кода инициализации), то процесс, выполняющий сценарий запуска, должен ждать завершения запуска данного сервера, поэтому запуск следующих серверов откладывается. Применение серверов, которые автоматически переходят в фоновый режим, позволяет также уменьшить вероятность возникновения ошибок в ходе запуска операционной системы. Рассмотрим, что произойдет, если конкретный сервер, скажем S, не переходит в фоновый режим автоматически, а системный администратор забыл, что в сценарии запуска сервера S нужно предусмотреть его перевод в фоновый режим. Процесс начальной загрузки выполняет последовательность команд запуска до тех пор, пока не встретит команду запуска сервера S. Сценарий запуска вызывает сервер на выполнение на переднем плане, ожидая, пока сервер не перейдет в фоновый режим, чтобы можно было приступить к запуску других серверов. К сожалению, сервер предназначен для работы в течение неопределенно долгого времени, поэтому весь процесс начальной загрузки заблокируется в ожидании конца выполнения команды, которая так и не будет выполнена. 30.3. Программирование сервера для работы в фоновом режиме В операционной системе Linux для обозначения любого процесса, который предоставляет свои услуги, функционируя в фоновом режиме, применяется термин демон. Можно сказать, что задача преобразования сервера в демон является несложной. Этот метод заключается в том, что в серверной программе необходимо вызвать функцию ветвления fork для создания нового процесса, а затем предусмотреть завершение работы родительского процесса. Для этого в серверной программе выполняется следующий код почти немедленно после начала ее работы. 506 Глава 30. ...методы улучшения функционирования серверов Linux
i=fork(); if (i < 0) { /* Отрицательное значение кода возврата означает, что */ /* возникла ошибка */ fprintf(stderr,"error when forking:%s\n", strerror(errno); exit(l); } if (i) { /* Положительное значение кода возврата показывает, что */ /* выполняется родительский процесс */ exit@); /* Нормальное завершение процесса */ } /* С этого момента продолжает работать дочерний процесс */ /* в качестве сервера */ Вызов функции fork может привести к получению трех возможных результатов: отрицательного значения, которое свидетельствует о том, что возникла ошибка (например, в системе нет достаточного объема памяти для создания нового процесса), положительного значения, по которому можно судить о том, что вызов функции был выполнен успешно и текущий процесс является родительским, и нулевое значение, свидетельствующее о том, что вызов выполнен успешно и текущий процесс является вновь созданным дочерним процессом. В рассматриваемом примере кода в случае возникновения ошибки сервер выводит сообщение в стандартный поток вывода сообщений об ошибках и вызывает функцию exit, чтобы завершить свою работу с кодом завершения 1. В соответствии с общепринятым соглашением, ненулевой код завершения указывает на аварийное окончание процесса. В случае успешного выполнения функции fork в операторе if возвращаемое значение используется для проведения различия между родительским и дочерним процессами. Первоначальный родительский процесс нормально завершает свою работу, а дочерний процесс продолжает выполнение и становится сервером. Проще всего можно наглядно представить себе этот процесс с точки зрения пользователя. Предположим, что приведенный выше код выполняется в некоторой серверной программе Q. Для запуска сервера пользователь вводит в качестве команды имя файла с программой сервера. После ввода этой команды у пользователя создается впечатление, что она выполняется почти мгновенно, и он сразу же получает приглашение к вводу следующей команды. Но если пользователь запросит список процессов, он увидит, что сервер создал свою же копию и что эта копия продолжает выполняться в фоновом режиме. В приведенном выше коде есть еще одна небольшая тонкость: он позволяет полностью отделить демон от его родительского процесса. В любой момент времени каждый процесс UNIX имеет родительский процесс, и в действиях, выполняемых некоторыми системными функциями UNIX, учитываются родительско-дочерние связи. После вызова функции fork завершается родительский процесс сервера, в результате чего вновь созданный дочерний процесс какое-то время находится вне родительско-дочерней связи. В операционной системе UNIX предусмотрено, что если любой процесс лишается связи с родительским процессом, для него родительским процессом должен стать начальный процесс системы (init). Для обозначения такого действия принято говорить, что дочерний процесс, лишенный связи с родительским, был унаследован процессом init2. Хотя сам факт такого наследования может показаться незначительным, описанное действие является очень важным, поскольку оно означает, что завершение работы сервера может быть выполнено корректно, так как процесс init вызывает функцию wait операционной системы Linux для завершения работы каждого из своих дочерних процессов. Описание процесса init приведено в разделе 8 оперативного справочного руководства Linux. 30.3. Программирование сервера для работы в фоновом режиме 507
30.4. Открытые дескрипторы и наследование дескрипторов Любой дочерний процесс Linux наследует копии дескрипторов файлов, или сокетов, открытых родительским процессом к моменту создания этого дочернего процесса. Поэтому набор дескрипторов, наследуемых серверным процессом, зависит от того, как был запущен этот процесс. Поскольку в системе Linux для работы с файлами и другими объектами применяется механизм подсчета ссылок, то сохранение в программе открытых дескрипторов может привести к непроизводительному расходованию ресурсов. Например, предположим, что родительский процесс открыл файл, а затем вызвал на выполнение сервер, который выполнил ветвление и перешел в фоновый режим. Даже если родительский процесс закрыл свою копию дескриптора файла, в сервере все еще остается открытой другая копия, и поэтому файл не будет закрыт. В результате файл не может быть удален с диска до тех пор, пока сервер не завершит свою работу. Сервер должен закрыть все унаследованные им дескрипторы файлов, чтобы исключить непроизводительное потребление ресурсов. 30.5. Программирование сервера для закрытия унаследованных дескрипторов Для закрытия всех унаследованных дескрипторов файлов в сервере выполняется следующий код после его запуска: for(i=getdtablesize()-1;i>=0;—i) (void)close(i); Поскольку число дескрипторов, доступных процессу, зависит от версии операционной системы Linux, в этом коде не предусмотрена фиксированная константа. Вместо этого в нем вызывается функция getdtablesize для определения размера таблицы дескрипторов процесса. Таблица дескрипторов индексируется, начиная с нуля. После определения максимального числа дескрипторов в этом коде выполняется цикл, начиная от уменьшенного на единицу размера таблицы дескрипторов и заканчивая нулем; при этом для каждого дескриптора вызывается функция close. После выполнения этого кода в сервере будут закрыты все унаследованные им открытые дескрипторы; выполнение функции close на неоткрытом дескрипторе равносильно пустой операции. 30.6. Сигналы с управляющего терминала Каждый процесс в системе Linux наследует связь с терминалом, который был назначен в качестве его управляющего терминала. В системе Linux привязка процесса к управляющему терминалу предусмотрена для того, чтобы пользователь, запустивший процесс, мог им управлять (например, мог разорвать связь по коммутируемой линии, передав процессу сигнал отбоя). В отличие от большинства других процессов, серверный процесс не должен получать сигналы от запустившего его процесса. Для обеспечения того, чтобы сигналы с терминала пользователя не воздействовали на работу сервера, который перешел в фоновый режим, сервер должен отключить себя от управляющего терминала. 508 Глава 30. ...методы улучшения функционирования серверов Linux
30.7. Программирование сервера для отключения от управляющего терминала Код, необходимый для отключения процесса от его управляющего терминала состоит всего из трех строк: fd=open( "/dev/tty" ,0 RDWR); (void)ioctl(fd,TIOCNOTTY,0); (void)close(fd); Вызов функции open возвращает дескриптор управляющего терминала, а вызов функции ioctl определяет, что процесс должен быть отключен от этого терминала. И наконец, вызов функции close освобождает дескриптор файла, что дает возможность серверу повторно использовать этот дескриптор для других операций ввода/вывода (например, для приема входящих запросов на установление соединений). 30.8. Переход в безопасный и известный каталог Серверный процесс должен всегда функционировать в известном каталоге. Кроме всего прочего, это позволяет гарантировать, что в случае аварийного завершения работы сервера по любой причине (или при останове системным администратором неправильно функционирующего сервера) администратору будет известно местонахождение создаваемого при этом файла дампа ядра. Кроме того, все серверы должны функционировать в стандартных системных каталогах, а не в тех каталогах, из которых они были запущены. Чтобы понять, с чем это связано, рассмотрим, что произойдет, если системный администратор заметит, что работа сервера закончилась аварийно, и перезапустит его. Если администратор работает с командным интерпретатором в своем начальном каталоге и забудет перейти в системный каталог перед запуском сервера, сервер начнет функционировать в начальном каталоге администратора. Наличие любого процесса, функционирующего в каталоге, исключит возможность выполнения каких-либо действий по управлению системой, которые требуют размонтирования файловой системы. Например, администратор не сможет даже выполнить обычный дамп файловой системы. Сервер должен перейти в известный каталог, где он может работать неопределенно долгое время, не нарушая обычного управления системой, 30.9. Программирование сервера для перехода в другой каталог Чтобы перейти в известный каталог, сервер вызывает системную функцию chdir. Например, в сервере, который должен функционировать в корневом каталоге, выполняется следующий вызов: (void) chdir ('7"); Выбор подходящего каталога может оказаться затруднительным, поскольку ни один каталог не может полностью соответствовать условиям эксплуатации всех серверов. Например, сервер, который отправляет электронную почту, может перейти в каталог, где система хранит исходящую электронную почту (во многих системах UNIX это каталог /usr/spool/mqueue). Однако сервер, который контролирует простаивающие линии связи с терминалами, может перейти в каталог, где находятся файлы всех терминальных устройств (обычно /dev). 30.7. Программирование сервера для отключения от управляющего терминала 509
30.10. Маска umask в системе Linux В операционной системе Linux каждому выполняемому процессу назначена маска umask, которая определяет режим защиты файлов, создаваемых процессом. Маска umask представляет собой целое число, девять младших битов которого представляют собой маску для 9-битового режима защиты файла. При создании каждого файла операционная система Linux вычисляет код режима защиты для этого файла, применяя поразрядную операцию "И" к режиму, указанному в вызове функции open, и к поразрядному дополнению маски umask процесса. Например, предположим, что процессу назначена маска umask с восьмеричным значением 027. Если в процессе предпринимается попытка создать файл с режимом 0777 (предназначенный для чтения, записи и выполнения всеми пользователями), система определяет допустимый режим файла, применяя поразрядную операцию "И" к числу 0750 (дополнению маски umask, равному 027) и к числу 0777 (затребованному режиму). В результате файл будет иметь режим 0750 (предназначенный для чтения и выполнения владельцем и группой файла, для записи только владельцем и не доступный для других пользователей). Маску umask процесса можно рассматривать как защиту от непреднамеренного создания файлов, предоставляющих слишком много прав на чтение, запись или выполнение. Такая защита важна, поскольку серверы часто функционируют с правами суперпользователя, поэтому создаваемые ими файлы принадлежат суперпользователю. Вне зависимости от того, какие режимы защиты файла указаны в вызове функции open, система не предоставит больше прав доступа к файлу, создаваемому процессом, чем указано в маске umask этого процесса. Поэтому установка соответствующего значения umask с начала работы сервера позволяет исключить проблемы, которые могли бы возникнуть при создании файла, для которого неправильно указан режим доступа. 30.11. Программирование сервера для установки маски umask Для управления режимами создания файлов в сервере достаточно предусмотреть простейший код: (void)umask@27); В результате этого вызова маска umask сервера устанавливается в указанное значение. Эта маска umask продолжает действовать до тех пор, пока в процессе не будет выполнен следующий вызов функции umask. 30.12. Группа процессов В системе Linux каждый процесс помещается в группу процессов. Предусмотренное в этой системе понятие группы процессов позволяет определять набор взаимосвязанных процессов как единое целое. Пользователи часто рассматривают группу процессов как одно задание. В частности, если пользователь создает три процесса, обменивающиеся данными с помощью каналов, командный интерпретатор помещает их в группу процессов таким образом, чтобы отправленный им сигнал завершения достиг всех трех процессов. Обычно каждый сервер функционирует независимо от других процессов. Поэтому он не должен входить в состав какой-либо группы и не должен получать сигналов, отправленных в группу, к которой принадлежит его родительский процесс. 510 Глава 30. ...методы улучшения функционирования серверов Linux
Каждый процесс наследует членство в группе процессов. Чтобы исключить возможность получения сигналов, предназначенных для его родительского процесса, сервер должен выйти из группы процессов, к которой принадлежит его родительский процесс. 30.13. Программирование сервера для выхода из группы процессов Код, необходимый для перемещения серверного процесса в его собственную, приватную группу процессов, является простейшим: (void)setpgrp@,getpid()); Вызов функции getpid приводит к получению идентификатора процесса, выполняемого в настоящее время (т.е. сервера), а вызов функции setpgrp представляет собой запрос к операционной системе перевести указанный процесс в новую, приватную группу процессов. 30.14. Дескрипторы для стандартных устройств ввода/вывода Для работы многих библиотечных процедур необходимо, чтобы были открыты и доступны для ввода/вывода три стандартных дескриптора файла: стандартное устройство ввода данных @), стандартное устройство вывода данных A) и стандартное устройство вывода сообщений об ошибках B). В частности, такая стандартная библиотечная процедура, как perror (которая выводит сообщения об ошибках), выполняет запись в стандартный дескриптор устройства вывода сообщений об ошибках, не проверяя его готовности. Если какой-либо из этих дескрипторов открыт и сервер вызывает библиотечную процедуру для чтения или записи в него, ввод/вывод может произойти на терминал или в файл. Чтобы исключить неприятные неожиданности, программисты обычно открывают в серверной программе стандартные дескрипторы и подключают их к безопасным устройствам ввода/вывода. Таким образом, если какая-либо процедура в сервере попытается выполнить ввод/вывод с использованием стандартного дескриптора, в сервере не произойдет непреднамеренная операция ввода/вывода. Поскольку для работы многих библиотечных процедур требуется наличие трех открытых стандартных дескрипторов ввода/вывода, в производственном сервере обычно все эти три дескриптора открываются и подключаются к безопасному устройству ввода/вывода. 30.15. Программирование сервера для открытия стандартных дескрипторов Код, необходимый для открытия стандартных дескрипторов ввода/вывода после закрытия в сервере всех дескрипторов, состоит из трех системных вызовов: fd = open("/dev/null", 0_RDWR); /* Стандартное устройство ввода данных */ (void) dup(fd); /* Стандартное устройство вывода данных */ (void) dup(fd); /* Стандартное устройство вывода сообщений об ошибках */ В вызове функции open указан специальный файл Linux с именем /dev/null, который соответствует устройству. Устройство, связанное с файлом /dev/null, всегда возвращает признак конца файла при вводе и отбрасывает весь вывод. Поэтому чтение или запись на устройство /dev/null представляет собой пустую 30.13. Программирование сервера для выхода из группы процессов 511
операцию, при выполнении которой ни на одном запоминающем устройстве не накапливаются какие-либо данные. Вызовы системной функции dup приводят к дублированию существующего дескриптора файла. В соответствии с общепринятым соглашением система Linux назначает дескрипторы файлов последовательно, начиная с 0. После открытия в сервере файла /dev/null и получения для него дескриптора 0 следующие вызовы функции dup приводят к созданию копии для стандартного устройства вывода (дескриптор 1) и стандартного устройства вывода сообщений об ошибках (дескриптор 2). 30.16. Взаимоисключение для сервера Большинство служб предусматривает применение в любой момент только одной копии ведущего сервера. Если для работы этой службы требуется параллельное выполнение, то распараллеливание всей работы должен обеспечить ведущий сервер. Ограничение числа одновременно функционирующих серверов может стать особенно важным, если право на запуск сервера имеют несколько пользователей системы. Например, предположим, что два системных администратора, работающие за разными терминалами, заметили, что определенная серверная программа завершилась аварийно, и одновременно пытаются ее запустить. Если администраторы не координируют свои действия, то обе копии сервера могут предпринять попытку приступить к выполнению и результаты становятся непредсказуемыми. Только одна копия серверной программы должна функционировать в системе одновременно; все распараллеливание работы должно осуществляться сервером, 30.17. Программирование сервера для предотвращения запуска нескольких копий Можно сформулировать этот вывод так, что дублирующиеся копии сервера являются взаимно исключающими и что сервер уже в начале своего выполнения должен вызывать механизм, который гарантирует такое взаимоисключение. В системе Linux в большинстве процессов используется файл блокировки для обеспечения взаимно исключающего выполнения. Для каждого сервера применяется отдельный файл блокировки. Например, сервер, который предоставляет доступ к службе построчной печати, может использовать файл /usr/spool/lpd.lock, а сервер "белых страниц" может использовать файл /usr/spool/wp.lock. После запуска на выполнение экземпляр сервера пытается создать предназначенный для него файл блокировки. Если этой блокировкой не владеют какие-либо иные процессы, попытка оканчивается успешно, а если файл уже заблокирован другим экземпляром сервера, попытка оканчивается неудачей. Код, необходимый для обеспечения взаимоисключения, состоит из нескольких системных вызовов. /* Приобрести исключительную блокировку или завершить работу. */ /* Предполагается, что имя файла блокировки сервера определено */ /* символической константой L0CKF. Например: */ /* tdefine LOCKF /usr/spool/lpd.lock */ /* */ lf=open(LOCKF,O_RDWR|O_CREAT,0640); if (If < 0) /* Ошибка при открытии файла */ exit(l); if(flock(lffLOCK_EX|LOCKJJB)) exit(O); /* Попытка приобрести блокировку окончилась неудачей */ 512 Глава 30. ...методы улучшения функционирования серверов Linux
Как сказано в комментарии, в этом примере кода подразумевается, что определена символическая константа LOCKF, в которой задано имя файла блокировки сервера. Вызов функции open приводит к получению дескриптора файла блокировки и его созданию в случае необходимости. Следует отметить, что в данном случае файл не содержит каких-либо данных; он просто должен находиться на диске для того, чтобы сервер мог владеть блокировкой. В вызове функции flock содержится требование предоставить файл блокировки в исключительное пользование. Функция flock возвращает нуль в случае успешного выполнения и ненулевое значение, если попытка оказывается неудачной. Если сервер не может получить исключительную блокировку, он просто завершает свою работу, поскольку это означает, что уже работает другая копия сервера. Хотя и существует возможность использовать и другие механизмы обеспечения взаимно исключающего выполнения (например, создание файла блокировки, имеющего режим доступа 000), основное преимущество применения функции flock связано с тем, что она автоматически освобождает блокировку после аварийного завершения работы системы. Фактически процесс владеет блокировкой, установленной функцией flock, только до тех пор, пока он функционирует. В случае аварийного завершения работы сервера или перезагрузки системы блокировка освобождается. В серверах, в которых для обеспечения взаимоисключения применяется создание файла, предусмотрено, чтобы операционная система удаляла этот файл, прежде чем выполнить повторный запуск сервера после перезагрузки системы (так как в ином случае нельзя будет вызвать на выполнение ни один экземпляр сервера). 30.18. Регистрация идентификатора процесса сервера В производственной среде у системных администраторов часто возникает необходимость быстро найти идентификатор процесса сервера в случае нарушения его работы или аварийного завершения. Хотя и есть возможность найти сервер по списку всех процессов в системе, для выполнения такой операции на компьютере с большой нагрузкой может потребоваться значительное время. Чтобы исключить для системного администратора необходимость поиска идентификатора процесса сервера, в большинстве производственных серверов предусмотрена регистрация идентификатора процесса в определенном файле. Бели у системного администратора возникает необходимость найти какой-то процесс сервера, он ищет идентификатор процесса в этом файле. Какой файл должен применяться сервером для хранения его идентификатора процесса? Вполне очевидно, что для этой цели целесообразнее всего использовать файл блокировки сервера. Дело в том, что для каждой службы применяется только один файл блокировки, который обычно не имеет какого-либо иного полезного содержания. 30.19. Программирование сервера для регистрации его идентификатора процесса После открытия сервером файла блокировки и получения дескриптора, применение этого файла блокировки для регистрации идентификатора процесса сервера превращается в очень простую операцию. Ниже приведен соответствующий пример кода: char pbuf[10 ]; /* Массив для хранения идентификатора программы в коде ASCII */ /* Предполагается, что файл блокировки был открыт и его дескриптор */ /* присвоен переменной If, как описано в предыдущем разделе */ (void)sprintf(pbuf,M%6d",getpid()); (void)write(If,pbuf,strlen(pbuf)); 30.18. Регистрация идентификатора процесса сервера 513
Для удобства чтения этого файла в коде вызывается функция sprintf для преобразования идентификатора процесса из двоичного целого числа в печатаемую строку, которая содержит эквивалентное десятичное значение. Затем в нем вызывается функция write для записи строки в файл блокировки. Поскольку файл содержит текст, предназначенный для чтения, администратор может узнать идентификатор процесса путем вывода файла на экран; для его чтения или форматирования данных не требуются специальные инструментальные средства. 30.20. Ожидание завершения работы дочернего процесса Когда происходит завершение работы какого-либо процесса Linux, система не может выполнить операцию завершения до конца, пока не сообщит об этом родительскому процессу. В родительском процессе должна быть вызвана системная функция wait, и только после этого операция завершения может быть закончена. Между тем, завершающийся процесс находится в так называемом состоянии зомби (процесс, находящийся в таком состоянии, иногда называют несуществующим процессом). 30.21. Программирование сервера с учетом необходимости ожидания завершения работы каждого дочернего процесса Как было описано выше, после ветвления серверного процесса и завершения родительского, дочерний процесс приобретает в качестве родительского начальный процесс системы init. Поскольку в процессе init повторно вызывается функция wait, все его дочерние процессы завершают свою работу корректно. Поэтому по окончании работы ведущего сервера не возникает никаких проблем. Однако процессы, вызванные на выполнение ведущим сервером, становятся дочерними процессами именно его, а не начального процесса init. Поэтому любой сервер, в котором создаются процессы, должен вызывать функцию wait после завершения работы дочерних процессов. В главе 11 приведен код, который показывает, как можно обеспечить ожидание завершения работы дочерних процессов с помощью функции wait. В нем показано, что сервер получает сигнал о завершении каждого дочернего процесса, поэтому в обработчике этого сигнала может находиться код для вызова функции wait. 30.22. Посторонние сигналы Даже после того как сервер отключается от управляющего терминала, он по- прежнему может получать сигналы. В частности, передать сигнал серверу может любой системный администратор или привилегированный процесс. Чаще всего, администратор посылает сигнал SIGKILL (сигнал 9) для останова сервера. В процессе проектирования сервера может быть также предусмотрено использование одного или нескольких сигналов для управления его работой. Например, может быть учтена возможность повторной инициализации сервера после получения им сигнала HANGUP (сигнал 1). 514 Глава 30. ...методы улучшения функционирования серверов Linux
30.23. Программирование сервера с учетом предотвращения воздействия посторонних сигналов В большинстве производственных серверов предусмотрено, что они должны игнорировать все сигналы кроме тех из них, которые применяются для управления сервером. Для этого в сервере вызывается либо системная функция signal, либо системная функция sigvec. Например, чтобы игнорировался сигнал HANGUP, в сервере выполняется следующий оператор: (void)signal(SIG_IGN,SIGHUP); В качестве особого случая отметим, что если в процессе Р будет задана опция SIG_IGN для игнорирования сигнала SIG__CHLD, то операционная система не будет создавать процессов зомби после завершения работы дочерних процессов процесса Р. 30.24. Применение средств ведения системного журнала 30.24.1. Формирование сообщений для записи в журнал Серверы и (в меньшей степени) клиенты вырабатывают сообщения, предназначенные для системных программистов или системных администраторов. Безусловно, в процессе разработки основная часть этих сообщений должна помочь программисту выявить ошибки в коде. А после того как сервер становится основой производственной службы, вырабатываемые им сообщения обычно сводятся к сообщениям об ошибках, формируемых при обнаружении сервером необычных обстоятельств или непредвиденных событий. Тем не менее, даже производственное программное обеспечение может регулярно формировать сообщения. Например, сервер может быть запрограммирован на ведение журнала всех входящих запросов на установление соединения или всех транзакций. Для обеспечения контроля за надежностью защиты системы сервер может быть запрограммирован на регистрацию информации о каждом своем действии, когда он отклоняет запрос на установление соединения от клиента, не обладающего соответствующими правами. На первых порах многие серверы направляли вырабатываемые ими сообщения в журнал консоли. Этот принцип применялся в соответствии с традицией, которая возникла в самых первых компьютерных системах, когда весь вывод на консольный терминал печатался на бумаге. Если программа выводила информацию на консоль, эта информация становилась частью постоянного системного журнала, к которому можно было обратиться позже в случае необходимости. В такой системе с началом работы сервера открывался дескриптор вывода на консоль системного терминала и использовался при выводе сообщений в журнал. 30.24.2. Преимущество перенаправления и использование стандартного устройства вывода сообщений об ошибках Основным недостатком применения метода записи сообщений сервера в журнал на терминальную консоль является недостаточная гибкость. При разработке и отладке кода сервера программист должен подходить к системной консоли для просмотра журнала. Кроме того, системные администраторы могут перенести клиентское или серверное программное обеспечение на другой компьютер. Поскольку на некоторых компьютерах отсутствует такое устройство для регистрации твердой копии, как консоль, приходится вносить изменения в сетевое программное обеспечение для записи сообщений в файл журнала и перемещения этого файла на компьютер, гдеу имеется консоль. Раньше в этих случаях для смены места назначения сообщений, выводимых в журнал, приходилось перетранслировать исходный код. 30.23. Программирование сервера с учетом предотвращения воздействия... 515
В системе Linux каждому процессу предоставляется стандартный дескриптор файла для вывода сообщений об ошибках (дескриптор 3). Поэтому при разработке серверного программного обеспечения в этой системе можно не задумываться над тем, будет ли сервер выводит сообщения об ошибках на принтер консоли или в файл. Вместо этого в программе предусматривается вывод всех сообщений об ошибках в стандартный дескриптор устройства вывода сообщений об ошибках, а подключение этого дескриптора к файлу или системной консоли выполняется во время запуска сервера на выполнение. Фактически само понятие стандартного устройства вывода сообщений об ошибках представляет собой своего рода абстракцию, которая позволяет не учитывать в исходном коде конфигурацию среды времени выполнения; в исходном коде просто применяется стандартный дескриптор устройства вывода сообщений об ошибках без учета того, к чему он будет привязан: к файлу или к терминалу. Расширение области применения программы, достигаемое с использованием стандартного дескриптора устройства вывода сообщений об ошибках, становится очевидным при организации работы сетевого узла, на котором установлено несколько компьютеров определенного типа, и на них эксплуатируются отдельные экземпляры определенного сервера. На одном компьютере для регистрации сообщений об ошибках может применяться принтер консоли, а на другом — файл. Системный администратор может вызвать на выполнение одну и ту же серверную программу на обоих компьютерах без ее перетрансляции, поскольку система Linux позволяет выполнить перенаправление ввода/вывода во время запуска сервера. 30.24.3. Недостатки метода перенаправления ввода/вывода Хотя при использовании стандартного дескриптора устройства вывода сообщений об ошибках код сервера становится более гибким по сравнению с кодом вывода сообщений в конкретное устройство или файл, это не решает всех проблем. Программист или системный администратор может принять решение, чтобы сервер отправлял часть сообщений (или все сообщения) об ошибках на терминал пользователя. Может быть также принято решение перенаправлять сообщения об ошибках на компьютер, отличный от того, на котором функционирует сервер (т.е. на тот компьютер, где имеется принтер). Метод с применением стандартного дескриптора устройства вывода сообщений об ошибках системы Linux не обеспечивает достаточной гибкости для того, чтобы можно было учесть все возможные требования к перенаправлению сообщений об ошибках, поскольку эта система не позволяет перенаправлять вывод в произвольную программу или в сетевое соединение. Для этого должен применяться другой, более мощный метод. 30.24.4. Решение на основе взаимодействия типа клиент/сервер Не удивительно, что системные проектировщики пришли к выводу о необходимости использования взаимодействия типа клиент/сервер для создания механизма регистрации сообщений об ошибках, обладающего большей гибкостью по сравнению с перенаправлением ввода/вывода. Фактически на каждом компьютере, участвующем в регистрации сообщений об ошибках, функционирует сервер журнала, который принимает и обрабатывает сообщения, направляемые в системный журнал. Сервер журнала может обеспечивать вывод полученных им сообщений на консольный терминал компьютера, их запись в файл, передачу на терминал системного администратора или даже отправку части сообщений серверу журнала на другом компьютере. 516 Глава 30. ...методы улучшения функционирования серверов Linux
При возникновении необходимости записать сообщение в системный журнал работающая программа становится клиентом сервера журнала. Программа передает сообщение на сервер журнала, а затем продолжает выполнение. Сервер журнала обрабатывает сообщение, записывая его в системный журнал. Если в процессе эксплуатации системы потребуется изменить способ обработки в ней сообщений об ошибках, достаточно изменить конфигурацию сервера журнала. Например, сервер журнала можно настроить таким образом, чтобы он выводил все сообщения журнала на системную консоль, а затем откорректировать эту настройку, чтобы сервер журнала выводил все сообщения в файл. Следует отметить, что сервер журнала обеспечивает перенаправление, причем во многом аналогично дескриптору стандартного устройства вывода сообщений об ошибках, как описано выше. В коде прикладной программы достаточно только предусмотреть вывод сообщений на сервер журнала; в ней не нужно учитывать, как будет обрабатываться каждое сообщение сервером журнала. Кроме того, адрес сервера журнала может быть представлен с помощью специального имени localhost, что позволяет оттранслированной программе найти сервер журнала на своем компьютере, не зная адреса самого компьютера. 30.24.5. Механизм syslog В системе Linux предусмотрен механизм ведения журнала, в котором применяется принцип взаимодействия типа клиент/сервер, описанный выше. Этот механизм, известный под названием syslog, включает код для сервера журнала (syslogd), а также библиотечные процедуры, применяемые в программах для вступление во взаимодействие с сервером и передачи ему сообщений. Механизм syslog отличается двумя важными особенностями. Во-первых, в нем сообщения группируются по классам, и во-вторых, используется файл конфигурации, позволяющий системному администратору определить, как сервер должен обрабатывать сообщения каждого класса. Поскольку механизм syslog предусматривает применение разных методов обработки для каждого класса сообщений, такая система обеспечивает значительную гибкость. Например, она позволяет предусмотреть отправку сообщений о серьезных ошибках системному администратору, и наряду с этим, запись низкоприоритетных (например, информационных) сообщений в файл без информирования кого-либо. В иных обстоятельствах может быть принято решение об использовании других вариантов вывода сообщений. Поскольку в системе syslog предусмотрено применение файла конфигурации, позволяющего регламентировать способ обработки сообщений каждого класса, такая система является удобной в эксплуатации. Для изменения способа обработки сообщений в системе syslog достаточно откорректировать файл конфигурации; клиентское и серверное программное обеспечение остается неизменным. 30.24.6. Классы сообщений syslog В системе syslog предусмотрено разделение сообщений на классы двумя способами. Во-первых, в ней программы подразделяются на группы, которые в этой системе принято называть средствами. Во-вторых, сообщения, поступающие от каждого средства, подразделяются на восемь уровней приоритета. 30.24.7. Средства syslog Средства, которые определены в системе syslog, и их описания приведены в табл. 30.1. Каждое сообщение об ошибке должно быть обозначено как исходящее от одного из этих средств. 30.24. Применение средств ведения системного журнала 517
Таблица 30.1. Классификация средств, которые определены в системе syslog Наименование средства Подсистема, в который оно используется L0G_KERN L0GJJSER L0G_MAIL L0G_DAEM0N L0G_AUTH L0G_LPR L0G_RPC LOG_LOCAL0 Ядро операционной системы Любой пользовательский процесс (т.е. обычная прикладная программа) Система электронной почты Системные демоны, которые функционируют в фоновом режиме Система авторизации (проверки прав доступа) и аутентификации (проверки подлинности) Система буферизации печати Подсистемы RPC и NFS Зарезервировано для локального использования; имена с L0G_L0CAL1 no L0G_L0CAL7 также зарезервированы Как показывает эта таблица, в системе syslog есть средство для каждой крупной подсистемы. Например, программы, относящиеся к электронной почте, принадлежат к средству L0GJMAIL, а большинство серверов, работающих в фоновом режиме, относится к средству L0G_DAEM0N. 30.24.8. Уровни приоритета syslog Как показано в табл. 30.2, в системе syslog определено восемь уровней приоритета. Уровни приоритета отличаются по степени важности, начиная от L0GJ2MERG, который предназначен для сообщений о наиболее серьезных аварийных ситуациях, до L0GJDEBUG, который применяется программистами для регистрации отладочной информации. В каждом сообщении об ошибке должен быть определен один из этих уровней приоритета. Таблица 30.2. Восемь уровней приоритета, которые определены в системе syslog Приоритет Описание L0G_EMERG Аварийная ситуация; должна быть выполнена широковещательная рассылка сообщения всем пользователям L0G_ALERT Ситуация, требующая немедленного вмешательства системного администратора (например, разрушение системной базы данных) L0G_CRIT Опасная ситуация, возникшая, в частности, в результате нарушения работы аппаратных средств (например, отказа диска) L0GJJRR Ошибка, которая требует внимания, но не столь опасна L0G_WARNING Предупреждение о том, что может иметь место сбойная ситуация L0G_N0TICE Ситуация, которая не связана с нарушением в работе, но может потребовать внимания L0G_INF0 Информационное сообщение (например, формируемое сервером при запуске на выполнение) log_debug Сообщение, используемое для отладки 30.24.9. Применение системы syslog При использовании системы syslog для передачи сообщений в журнал в программе необходимо указать средство и уровень приоритета для каждого сообще- 518 Глава 30. ...методы улучшения функционирования серверов Linux
ния. В целях упрощения работы с системой syslog, библиотечные процедуры этой системы позволяют указывать средство с момента запуска программы, после чего система syslog использует это средство для регистрации последующих сообщений. Для указания начального значения средства в программе вызывается процедура openlog. Эта процедура принимает три параметра, которые состоят из идентификационной строки, набора опций обработки и спецификации средства. Система syslog добавляет указанный идентификатор ко всем выводимым программой последующим сообщениям таким образом, чтобы их можно было легко найти в журнале. Обычно в качестве идентификационной строки используется имя самой программы. Процедура openlog инициализирует функции ведения журнала, а также сохраняет в памяти идентификационную строку и спецификацию средства для дальнейшего использования. Например, в отлаживаемой программе может применяться следующий вызов: openlog ("myprog", L0G_PID, LOGJJSER); Здесь указана идентификационная строка myprog, опция регистрации L0G_PID и средство LOGJJSER. Опция LOG_PID требует, чтобы система syslog регистрировала в журнале вместе с каждым сообщением идентификатор процесса данной программы. При возникновении необходимости отправить в журнал сообщение, в программе вызывается процедура syslog. Эта процедура отправляет сообщение на сервер syslogd на локальном компьютере.. Процедура syslog принимает переменное число параметров. В первом из них задается приоритет сообщения, а во втором — строка формата, аналогичная применяемой функции printf. Как и в функции printf, за строкой формата следуют параметры, содержащие значения, на которые имеются ссылки в этой строке. В простейшем случае в программе процедура syslog может быть вызвана с использованием в качестве сообщения строковой константы. Например, в программу для вывода отладочного сообщения в системный журнал может быть включен следующий вызов: syslog(LOG_DEBUG,"my program opened its input file"); После завершения работы с системой syslog в программе может быть вызвана процедура closelog для закрытия файла журнала (т.е. для прекращения взаимодействия с сервером). Процедура closelog закрывает соединение с файлом журнала и освобождает распределенный для него дескриптор ввода/вывода. Поэтому вызов процедуры closelog может потребоваться для освобождения дескриптора файла журнала (например, перед вызовом функции fork). 30.24.10. Пример файла конфигурации системы syslog Гибкость механизма syslog во многом обусловлена тем, что в нем для управления сервером используется файл конфигурации. В файле конфигурации имеются готовые шаблоны, позволяющие системному администратору указывать способ обработки для каждой комбинации средства и приоритета. В большинстве систем файл конфигурации системы syslog носит имя /etc/syslog.conf. Это — текстовый файл, в каждой строке которого находится определение отдельной спецификации. Спецификация состоит из пары полей, разделенных пробелами. Левое поле определяет шаблон, который сопоставляется с некоторым сочетанием средства и приоритета. Правое поле, которое принято называть полем команды, указывает, какие действия должны выполняться с сообщениями, соответствующими этому шаблону. Например, следующая спецификация содержит шаблон, который соответствует всем сообщениям со средством 1рг и приоритетом debug: lpr.debug /usr/adm/printer-errs 30.24. Применение средств ведения системного журнала 519
Поскольку поле команды начинается с косой черты, демон syslogd интерпретирует его как имя файла. Поэтому, встретив приведенную выше спецификацию, демон syslogd будет направлять сообщения с уровнем приоритета debug от средства 1рг в файл /usr/adm/printer-errs. Демон syslogd интерпретирует символ звездочки в поле команды как требование "выполнить широковещательную рассылку этого сообщения для всех пользователей". Он интерпретирует другие поля команд, не начинающиеся с Косой черты, как регистрационные имена пользователей, которые должны получить сообщение. Чтобы проиллюстрировать эти понятия и показать способы применения правил регистрации, рассмотрим приведенный ниже пример файла конфигурации, который используется в одной системе: *.err;kern.debug;auth.notice /dev/console kern.debug ? daemon,auth.notice;auth.info;*.err;mail.crit /usr/adm/messages lpr.debug /usr/adm/printer-errs mail.debug /usr/spool/mqueue/syslog *.alert;kern.err;daemon.err operator *. alert root *.emerg * Сопоставление с шаблонами представляет собой мощный механизм, поскольку он позволяет составлять спецификации, не выписывая отдельно каждое сочетание средства и приоритета. Например, в приведенном выше файле конфигурации шаблон *.егг используется для обозначения "сообщений от любого средства с приоритетом error". 30.25. Резюме В производственных серверах применяется целый ряд методов, которые позволяют упростить управление серверами и их отладку, повысить надежность и уменьшить восприимчивость к ошибкам. В настоящей главе показано, чем обусловлена необходимость применения каждого метода, и приведены примеры кода для каждого из этих методов. Ниже перечислены краткие описания конкретных методов, рассматриваемых в этой главе: ¦ эксплуатация сервера в виде фонового процесса (демона); ¦ закрытие унаследованных дескрипторов файлов; ¦ отключение сервера от его управляющего терминала; ¦ перемещение сервера в безопасный, известный каталог; ¦ установка маски umask системы Linux; ¦ перевод сервера в приватную группу процессов; ¦ открытие трех стандартных дескрипторов ввода/вывода; ¦ обеспечение взаимно исключающего выполнения; ¦ регистрация идентификатора процесса сервера; ¦ ожидание завершения дочерних процессов; ¦ игнорирование посторонних сигналов; ¦ применение системы syslog для обработки сообщений об ошибках. 520 Глава 30. ...методы улучшения функционирования серверов Linux
Материал для дальнейшего изучения Многие правила, применяемые при разработке серверов для системы Linux, основаны из общепринятых рекомендациях и проверены на практике; программисты часто изучают методы на примере существующих программ. В литературе [151] рассматривается разработка программ, функционирующих в качестве демонов, и подробно описаны некоторые методы, представленные в настоящей главе. Упражнения 30.1. Разработайте процедуру daemonize, которая включает код реализации всех методов, рассматриваемых в настоящей главе. 30.2. Прочитайте о сигналах в оперативном руководстве Linux. Какие сигналы не могут быть проигнорированы? Что произойдет, если сервер получит один из них? 30.3. Некоторые программисты предусматривают возможность корректного завершения работы сервера после получения им специального сигнала (например, SIGHUP). Это позволяет системному администратору завершить работу сервера, отправив ему соответствующий сигнал. Как может администратор послать такой сигнал, если сервер отключился от управляющего терминала? 30.4. По условиям предыдущего упражнения ответьте, имеет ли право непривилегированный пользователь завершить работу сервера, передав ему соответствующий сигнал? 30.5. В приведенном здесь примере кода закрываются все дескрипторы файлов, а затем снова открываются стандартные устройства ввода данных, вывода данных и вывода сообщений об ошибках. В некоторых серверных программах эти три дескриптора остаются открытыми со значениями, унаследованными от их родительских процессов. В чем состоят преимущества и недостатки такого подхода? (Подсказка: учтите, что необходимо выполнять регистрацию сообщений об ошибках). 30.6. Большинство серверов функционируют с правами пользователя root (в системе Linux пользователь root обладает неограниченными правами). С чем может быть связана необходимость эксплуатировать сервер с меньшими правами? Как это можно сделать? 30.7. Напишите серверную программу, в которой для обеспечения взаимоисключения используется системная функция creat операционной системы Linux. (Подсказка: этот метод общеизвестен и широко используется, особенно в старых программах UNIX). 30.8. Сравните способы взаимоисключения с применением функции creat (см. предыдущее упражнение), опции 0J2XCL в вызове функции open и функции rename. Какой из этих трех методов является наиболее приемлемым? Почему? 30.9. Изучите исходный код одного из производственных серверов. Какие в нем применяются методы, описанные в настоящей главе? Используются ли в нем другие методы? 30.10. Прочитайте справочное руководство Linux а программе syslogd. Как может системный администратор изменить конфигурацию после запуска сервера? Можете ли вы предложить другой вариант? Материал для дальнейшего изучения 521
31 Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер 31.1. Введение В предыдущих главах в основном рассматривались вопросы проектирования систем клиент/сервер и анализировалась структура отдельных клиентских и серверных программ. В них описаны способы обеспечения параллельного выполнения процессов в клиентах и серверах, рассмотрены такие инструментальные средства, как RPC, позволяющие упростить разработку программного обеспечения клиент/сервер, и приведены соответствующие примеры. В настоящей главе рассматривается ход распределенных вычислений в динамике; описание в ней в основном сосредоточено на некоторых неочевидных на первый взгляд аварийных ситуациях, которые могут возникать в системах клиент/сервер. В этой главе более подробно рассматриваются потенциальные проблемы, отмеченные в предыдущих главах, и подчеркнуты две причины, которые могут привести к замедлению или нарушению обслуживания клиентов: тупиковые ситуации и исчерпание ресурсов. Анализ этих причин становится особенно важным в производственной среде, где какие-либо нарушения в работе являются недопустимыми. В настоящей главе показано, что тупиковые ситуации могут часто возникать не только в связи с тем, что в распределенной программе были реализованы непродуманные спецификации протокола, но и по той причине, что в результате ошибок и недосмотров в программе нарушения в работе одного клиента могут привести к прекращению обслуживания других клиентов. В данной главе показаны некоторые последствия возникновения тупиковых ситуаций в системах клиент/сервер, и описаны методы, предназначенные для предотвращения таких ситуаций. 31.2. Определение тупиковой ситуации В компьютерных системах термин тупиковая ситуация используется для обозначения такого развития событий, при котором вычисления не могут продолжаться, поскольку два или более компонентов системы заблокированы в связи с Для обозначения понятия тупиковой ситуации применяются также термины круговое ожидание, смертельные объятия и одновременная блокировка; в настоящей главе все указанные термины рассматриваются как синонимы.
тем, что каждый компонент ожидает продолжения работы другого. Обычно каждый компонент представляет собой процесс, заблокированный в ожидании освобождения ресурса, захваченного другим процессом из числа заблокированных. Тупиковая ситуация — это постоянный отказ, который не следует путать с временной блокировкой. Для определения наличия тупиковой ситуации может применяться простая проверка: если поступающая извне информация позволяет продолжить вычисление, то данная ситуация не является тупиковой. Например, рассмотрим набор из трех процессов. Предположим, что два процесса заблокиро- вались на то время, как третий вступил во взаимодействие с пользователем. После получения ответа от пользователя третий процесс передает двум остальным необходимую информацию и обработка продолжается. Хотя эти три процесса могут оставаться заблокированными на неопределенно долгое время, ожидая ответа от пользователя, такой ряд процессов не находится в тупиковой ситуации, поскольку после получения данных от пользователя обработка может быть продолжена. В отличие от этого, набор процессов оказывается в тупиковой ситуации, если каждый процесс в этом наборе ожидает поступления данных от других членов набора. Поскольку каждый процесс заблокирован, ни один из них не выдаст необходимых данных. Поэтому ни один из процессов не получит входных данных и для них нет способа выйти из тупиковой ситуации. Согласно описанному выше методу проверки, ситуация является тупиковой, поскольку ни один из процессов в наборе не ожидает поступления информации извне. 31.3. Трудность обнаружения тупиковой ситуации Обнаружить тупиковую ситуацию во время выполнения очень трудно, а в распределенной системе обнаружить ее обычно почти невозможно. Это связано со следующими двумя причинами. Во-первых, чтобы отличить тупиковую ситуацию от временной блокировки, необходимо предусмотреть механизм, позволяющий определить, какие ресурсы захватила каждая программа и почему она заблокировалась. Для получения такой информации в среде клиент/сервер необходимо обращаться к нескольким операционным системам, но в каждой из них может быть предусмотрен уникальный набор операций. Во-вторых, поскольку в программе может быть предусмотрено собственное определение ресурсов, то операционная система не имеет возможности установить, какая программа владеет такими ресурсами, поскольку состояние использования ресурсов известно только в программах, в которых создаются и применяются такие ресурсы. Любопытно отметить, что даже при наличии исходного кода каждого компонента системы задача определения того, может ли система попасть в тупиковую ситуацию, является такой же сложной, как доказательство математической теоремы. Предпосылки возникновения тупиковой ситуации могут стать очевидными только после перехода к рассмотрению поведения данной системы в динамике. Это означает, что вероятность возникновения тупиковой ситуации может зависеть от порядка выполнения отдельных действий, однако если клиенты и серверы функционируют на разных компьютерах, то выполняемые ими действия могут происходить в самом различном порядке. Еще более важно то, что при самом продуманном проекте и тщательной реализации каждого компонента распределенной системы внезапно могут возникать тупиковые ситуации. Должно быть очевидно, насколько неприятными становятся их последствия. Поскольку обнаружить тупиковую ситуация чрезвычайно трудно или почти невозможно, нельзя разработать практически применимую компьютерную программу, которая позволяла бы определить, что ряд клиентов или серверов оказался в тупиковой ситуации. 524 Глава 31. Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер
31.4. Предотвращение тупиковой ситуации А Что можно сделать, чтобы исключить предпосылки нарушения работы сете- jBtfft службы? Общий ответ на этот вопрос состоит в использовании тщательного планирования. При проектировании протоколов, разработке программного обеспечения, инсталляции и настройке систем клиент/сервер следует всегда учитывать возможность возникновения тупиковой ситуации и стремиться исключить предпосылки создания системы, восприимчивой к такой ситуации. Чтобы предотвратить тупиковую ситуацию, необходимо знать, вследствие чего она может возникнуть. В следующих разделах описаны три варианта возникновения подобных проблем в системе клиент/сервер: при взаимодействии между одной парой клиент/сервер, между несколькими клиентами и одним сервером, и наконец, между несколькими клиентами и серверами. 31.5. Тупиковая ситуация, возникающая при взаимодействии между клиентом и сервером Простейшая форма тупиковой ситуации при взаимодействии между клиентом и сервером возникает, когда в ней участвует один клиент и один сервер. Если клиент блокируется в ожидании сообщения от сервера, а сервер блокируется, ожидая сообщения от клиента, эта пара программ оказывается в безвыходной тупиковой ситуации. Для предотвращения подобных тупиковых ситуаций большинство прикладных протоколов спроектировано с учетом взаимодействия по принципу запрос/ответ. Это означает, что один из участников обмена данными (обычно клиент) передает запрос, на который отвечает другой участник соединения. Протокол должен указывать, какой из участников вырабатывает запросы и какой отправляет ответы. При организации взаимодействия между одним клиентом и одним сервером могут быть допущены ошибки двух типов, которые могут привести к возникновению тупиковой ситуации. Во-первых, если в проекте протокола не определены все возможные варианты развития событий при обмене данными между клиентом и сервером, то в одном из вариантов взаимодействие этих распределенных компонентов может быть нарушено. Во-вторых, если в проекте протокола предусмотрено использование надежных средств доставки данных, то работа программного обеспечения такого протокола может быть нарушена при его использовании с ненадежным транспортным механизмом. Чтобы понять, почему отсутствие полной спецификации всех вариантов взаимодействия может привести к возникновению проблем, рассмотрим следующий прикладной протокол. 1. Вначале клиент устанавливает соединение с сервером. 2. Сразу после установления соединения либо клиент, либо сервер должен отправить вступительное сообщение; другой участник соединения ожидает такое сообщение и отправляет ответ на вступительное сообщение. 3. После того как произойдет отправка вступительного сообщения и получение ответа, клиент может отправлять запросы; сервер отправляет ответ на каждый запрос. 4. После получения ответа на свой заключительный запрос клиент закрывает соединение. Такая неопределенная спецификация протокола может быть составлена проектировщиком в целях предоставления разработчику права самостоятельно выбирать способы ее реализации. В данном примере проектировщик мог не иметь 31.4. Предотвращение тупиковой ситуации 525
возможности определить, на какую программу должна быть возложена обязанность отправить первое сообщение: клиентскую или серверную. Поэтому протокол был намеренно определен как неоднозначный для обеспечения максимальной свободы выбора. К сожалению, при вступлении во взаимодействие двух реализаций, соответствующих такому протоколу, может возникнуть тупиковая ситуация, если программист, разрабатывающий клиентскую часть, будет предполагать, что вступительное сообщение поступит с сервера, а программист, реализующий серверную часть, будет ожидать вступительного сообщения от клиента. После того как такой клиент и сервер вступят во взаимодействие, они забло- кируются, ожидая друг от друга получения начального сообщения. Чтобы понять, как могут возникать ошибки, связанные с применением ненадежного транспортного протокола, рассмотрим протокол запрос/ответ, разработанный для надежной транспортной службы (например, TCP). Этот протокол может определять, что клиент должен отправить запрос, а затем ожидать ответ. Если подобный протокол будет применяться в ненадежной транспортной службе (например, UDP), сообщение может быть потеряно. К сожалению, потеря запроса или ответа приведет к возникновению тупиковой ситуации, в которой клиент остается заблокированным, ожидая ответа, а сервер блокируется в ожидании следующего запроса. 31.6. Предотвращение тупиковых ситуаций при взаимодействии между одной парой приложений Существует два способа предотвращения тупиковой ситуации, возникающей при взаимодействии между одной парой программ, состоящей из клиента и сервера. Во-первых, проект прикладного протокола должен четко регламентировать правила синхронизации обмена данными. На одного из участников соединения должна быть возложена обязанность инициировать взаимодействие (т.е. отправлять запрос), а определение порядка взаимодействия не должно допускать никаких противоречивых толкований. Во-вторых, в реализации должен использоваться надежный транспортный протокол или предусматриваться механизм таймера, который устанавливает максимальную продолжительность времени ожидания отправителем ответа перед повторной передачей запроса. Хотя возникновение тупиковой ситуации в ходе взаимодействия между клиентом и сервером является нежелательным, эта проблема затрагивает только пару программ. В отличие от этого, на разработчика разделяемого сервера возлагается дополнительная ответственность, поскольку любая проблема, вызывающая нарушение работоспособности сервера, исключает возможность доступа других клиентов к службе, предоставляемой сервером. Еще более важно то, что сервер, восприимчивый к таким нарушениям, может подвергнуться атакам злонамеренных пользователей, которые смогут нарушить обслуживание других клиентов, воспользовавшись своей клиентской программой. 31.7. Исчерпание ресурсов сервера, обслуживающего ряд клиентов Термин исчерпание ресурсов применяется для описания ситуации, в которой одни клиенты могут получить доступ к службе, а другие клиенты — нет. При исчерпании ресурсов нарушается принцип равнодоступности, согласно которому сервер должен предоставлять свои услуги всем клиентам в равной степени. Последовательный сервер, который допускает поддержание сеансов взаимодействия с неопределенно долгой продолжительностью, является по своей сути нарушающим принцип равнодоступности, поскольку позволяет одному клиенту неограничен- 526 Глава 31. Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер
РрКШьэоваться его услугами, в то время как другие клиенты должны ожидать сво- № очереди. Поэтому большинство последовательных серверов не позволяет одному ¦шроту приобретать исключительное право на продолжительное использование пре- рставляемой ими службы. Для обеспечения равнодоступности последовательный Кедр может ограничить число запросов, которые разрешено отправлять конкретно- щ клиенту. Например, последовательный сервер с установлением логического соединения может закрывать соединение после обработки одного запроса клиента. Еще един вариант организации взаимодействия может предусматривать закрытие соединения сервером по истечении установленного интервала времени. В качестве примера того, как злонамеренный пользователь клиентской программы может исключить возможность использования службы другими клиентами, рассмотрим один из вариантов развития событий в процессе взаимодействия между Клиентом и последовательным сервером с установлением логического соединения. Предположим, что согласно спецификации протокола клиент должен передавать запросы, на которые отвечает сервер. Чтобы добиться исчерпания ресурсов и исключить их предоставление другим клиентам, злонамеренный пользователь клиентской программы может открыть соединение с сервером, а затем не отправить запрос. Сервер заблокируется в ожидании запроса клиента, который никогда не поступит. Между тем, все остальные клиенты не смогут получить доступ к серверу. Безусловно, в сервере можно предотвратить возникновение таких проблем, предусмотрев в нем использование механизма таймера, который автоматически закрывает простаивающее соединение по истечении установленного тайм-аута Т. Сразу после установления клиентом соединения сервер запускает таймер с нуля. После получения запроса сервер останавливает таймер, формирует ответ и перезапускает таймер с нуля. По достижении таймером значения Т сервер закрывает соединение. 31.8. Активные соединения и исчерпание ресурсов Хотя метод закрытия по таймеру простаивающих соединений, описанный в Предыдущем разделе, позволяет предотвратить ситуацию, в которой клиент не передает запросы, исчерпание ресурсов все еще остается возможным. Чтобы понять, с чем это связано, необходимо отметить два факта. Во-первых, таймер, применяемый для обнаружения простаивающих соединений, измеряет время между передачей ответа и получением следующего запроса. Во-вторых, транспортный протокол предусматривает использование буферов и средств управления потоком данных. От выбора способа буферизации зависит многое, поскольку транспортный протокол предусматривает применение буферов на каждом конце соединения. Со стороны отправителя передающее приложение помещает исходящие данные в буфер передачи для отправки их по протоколу TCP; со стороны получателя программное обеспечение протокола TCP помещает входящие данные в буфер приема, откуда их извлекает принимающее приложение. Программное обеспечение TCP продолжает передавать данные из буфера отправителя до тех пор, пока остается свободное место в буфере получателя. Как может клиент захватить большую часть времени сервера, чем ему положено? Клиент может задержать передачу данных или вообще исключить возможность ее осуществления. В частности, клиент может: ¦ определить слишком малый размер приемного буфера TCP по сравнению с объемом ожидаемых данных ; Одна из опций сокета позволяет определить в приложении размер, используемый для буфера TCP. 31.8. Активные соединения и исчерпание ресурсов 527
¦ отправить запрос, который требует от сервера передать данные; ¦ затем нарушить дальнейшую передачу, не считывая поступающие данные (что приводит к заполнению приемного буфера), или задержать передачу, считывая данные слишком медленно. В подобных ситуациях передающая часть программного обеспечения TCP будет передавать данные до тех пор, пока не заполнится буфер приемной части программного обеспечения TCP, и в этот момент получатель объявит нулевое окно. Отправитель не сможет передавать дополнительные данные до тех пор, пока не появится свободное место в буфере получателя. Поэтому передача остановится до того времени, как клиент прочитает данные. Со стороны отправителя сервер продолжает записывать данные в буфер для исходящих данных. Поскольку передача задержана или остановлена, этот выходной буфер в конечном итоге заполнится. При очередной попытке сервера записать данные в заполненный буфер серверный процесс будет заблокирован. Подобную ситуацию нельзя исключить таким же способом, как при использовании таймера для закрытия простаивающего соединения, поскольку сервер заблокировался не в связи с ожиданием запроса. В данном случае сервер заблокировался при отправке ответа. 31.9. Отказ от применения блокирующих операций Как можно предотвратить блокировку сервера во время передачи? Для этого могут применяться два основных варианта: организовать параллельную работу сервера или исключить использование в сервере вызовов, которые могут забло- кироваться. В первом варианте для обслуживания каждого клиента применяется отдельный процесс, поэтому задержка, созданная одним из клиентов, не отражается на работе других клиентов. В последнем случае в сервере может применяться механизм закрытия по тайм-ауту активных соединений. Перед выполнением каждого вызова такой блокирующей операции, как send, в сервере необходимо проверять, не будет ли заблокирован данный вызов. Если система сообщает, что сокет не готов к приему данных, сервер устанавливает таймер, а затем снова пытается проверить готовность. Если сокет не переходит в состояние готовности в течение установленного тайм-аута, сервер может закрыть это соединение. 31.10. Процессы, соединения и связанные с ними ограничения Хотя параллельная организация работы позволяет решить многие проблемы тупиковых ситуаций, распараллеливание также имеет свои пределы. Как было указано в главе 16, необходимо управлять распараллеливанием работы, поскольку при использовании неограниченного числа параллельных процессов вполне могут возникнуть проблемы. В частности, функционирование сервера, создающего новый процесс для каждого клиентского соединения, может быть нарушено при взаимодействии с неправильно разработанными клиентскими программами, поскольку операционная система не допускает применения произвольной степени распараллеливания. В конечном итоге такой параллельный сервер исчерпает все ресурсы системы. Например, в любой операционной системе ограничено число процессов, активных сокетов, суммарное число доступных дескрипторов, а также число блоков управления передачей (ТСВ — Transmission Control Block), которые могут быть распределены программным обеспечением TCP. В частности, в каждом открытом соединении TCP используется буферное пространство, поэтому для создания каждого дополнительного соединения требуется соответствующий объем памяти. Чтобы уменьшить восприимчивость сервера к нарушениям в работе, связанным с исчерпанием ресурсов, в серверной программе необходимо контролировать 528 Глава 31. Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер
Использование всех ресурсов, в том числе памяти, открытых соединений и степям распараллеливания. К сожалению, задача прогнозирования или управления ^пользованием ресурсов может оказаться трудной. Поскольку серверные программы почти никогда не создаются только для конкретного компьютера, в процессе их разработки обычно не известно о том, какие пределы устанавливает операционная система. Кроме того, серверы часто функционируют в сложных операционных системах с разделением времени, которые распределяют ресурсы "между всеми процессами. Поэтому в любой момент времени объем ресурсов, 1федоставляемый сервером, зависит от того, какие ресурсы используются в на- ;бтоящее время другими приложениями. i ¦. В тех случаях, когда нельзя прогнозировать или управлять степенью распараллеливания и использованием ресурсов, в серверной программе можно, по меньшей мере, предусмотреть, чтобы сервер сообщал о возникших проблемах. Например, сервер может проверять возвращаемое значение каждого системного вызова и передавать в журнал сообщения об ошибках, чтобы системный администратор имел возможность периодически проверять этот журнал для определе- ния того, не возникают ли нарушения в процессе работы сервера. При появлении ошибок администратор может предпринять дополнительные действия для определения причины. Хотя такие отчеты не позволяют предотвратить возникновение проблем, они предоставляют механизм, с помощью которого администратор может следить за работой сервера. 31.11. Циклические зависимости между клиентами и серверами Одна из наиболее опасных форм тупиковой ситуации и исчерпания ресурсов возникает при наличии взаимозависимостей между несколькими службами. Чтобы понять, в чем состоит проблема, напомним, что сервер одной службы может стать клиентом другой. Например, рассмотрим файловый сервер. Если в файловой системе необходимо зарегистрировать время последнего внесения изменений в файл, то для сервера может потребоваться определять время суток при обработке каждого запроса на запись. В большинстве операционных систем прикладная программа для получения текущего значения времени вызывает системную функцию. Эта функция получает текущее значение времени от аппаратных часов, а затем возвращает это значение приложению. Однако в среде клиент/сервер может быть предусмотрено Получение значений времени от удаленного компьютера. Фактически функция, Которая обеспечивает получение текущего значения времени, содержит клиентский код. Приложение, которое вызывает эту функцию, становится клиентом сервера службы времени. Клиент передает запрос, ожидает ответ, который содержит значение времени, а затем возвращает значение этого времени вызывающему приложению. В такой среде может возникнуть тупиковая ситуация, если между клиентами и серверами непреднамеренно сформирован цикл. По условиям приведенного выше примера предположим, что программисту поручено задание модернизировать сервер службы времени. В целях упрощения отладки программист может предусмотреть вывод в журнал информации обо всех запросах, поступивших на сервер службы времени. К сожалению, если этот журнал записывается в файл, возникает циклическая зависимость: файловый сервер вызывает сервер службы времени, а тот, в свою очередь, вызывает файловый сервер для регистрации полученного запроса в журнал. Если какой-либо из этих серверов не является параллельным, сразу же возникает тупиковая ситуация. А если оба сервера явля- 31.11. Циклические зависимости между клиентами и серверами 529
ются параллельными, то файловый сервер снова вызывает сервер службы времени, который вызывает файловый сервер, и таким образом возникает цикл, продолжающий действовать до полного исчерпания ресурсов. Приведенный выше пример может служить иллюстрацией проблемы, известной под названием активный тупик. Активный тупик, как и обычная тупиковая ситуация, возникает в связи с наличием циклических зависимостей. Однако в отличие от тупиковой ситуации, процессы, которые попали в активный тупик, интенсивно используют ресурсы процессора и продолжают обмениваться сообщениями. В данном примере активный тупик возникает, если в клиентах и серверах для связи применяется протокол без установления логического соединения. Файловый сервер передает сообщение серверу службы времени и ожидает ответа. В ходе обработки полученного сообщения сервер службы времени посылает запрос на файловый сервер. После получения запроса файловый сервер формирует второе сообщение для сервера службы времени, что приводит к формированию сервером службы времени третьего запроса, и т.д. Хотя оба сервера активно передают и принимают сообщения, они находятся в цикле, который не может быть разорван. Если один из серверов работает медленнее, чем другой, на нем очередь входящих сообщений заполнится, что приведет к потере одного или нескольких сообщений. Но как только сервер прочитает из очереди одно сообщение, сразу же поступит другое и займет его место. Серверы не могут выполнять продуктивную работу, поскольку они заблокированы, выполняя обработку бесконечного цикла сообщений. 31.12. Изучение зависимостей Для предотвращения возникновения циклов, которые могут привести к созданию тупиковых ситуаций или активных тупиков, программисты и администраторы должны исключить возможность появления циклических зависимостей между серверами. По мере дальнейшего перемещения компьютерных систем в среду клиент/сервер диагностирование таких зависимостей становится все более затруднительным. Например, если файловая система Linux настроена на применение системы NFS для доступа к удаленным файлам, то на основе только имен файлов пользователи или программисты не могут отличить удаленный файл от локального. Аналогичным образом, не всегда можно определить, что в программу, реализующую какую- либо из системных команд (например, применяемых для определения текущего времени дня), может быть встроено клиентское программное обеспечение. Чтобы иметь возможность исключить непреднамеренное создание зависимостей, программисты, работающие с программным обеспечением типа клиент/сервер, должны иметь информацию о том, предусматривает ли каждая конкретная библиотечная процедура или функция операционной системы доступ к удаленному серверу. Для предоставления такой информации на предприятии необходимо вести подробный учет характеристик каждого сервера с указанием серверов, от которых он зависит. При разработке или установке любого программного обеспечения необходимо вносить изменения в этот список зависимостей. Для учета информации о зависимостях могут применяться два подхода: с низкой степенью детализации и с высокой степенью детализации. В методе предотвращения зависимостей с низкой степенью детализации каждая служба рассматривается как отдельный программный объект и контролируется отсутствие циклических зависимостей между службами. Например, если служба дистанционного доступа к файлам зависит от службы времени, то служба времени не может быть запрограммирована на использование службы дистанционного доступа к файлам. В методе предотвращения зависимостей с высокой степенью детализации в качестве отдельного объекта рассматривается каждый сервер и контролируется отсутствие циклических зависимостей между серверами. Например, 530 Глава 31. Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер
при использовании этого метода допускается, что файловый сервер X может вызвать сервер службы времени У, а сервер службы времени У может вызвать файловый сервер Z (но не файловый сервер X), Основным преимуществом подхода с низкой степенью детализации является упрощение контроля: обычно на предприятии эксплуатируется не более десятка служб и между ними существует небольшое число зависимостей. Основным недостатком подхода с низкой степенью детализации являются ненужные ограничения на взаимодействие. В отличие от этого, подход с высокой степенью детализации обеспечивает более подробный анализ зависимостей; он допускает, что между службами может существовать циклическая зависимость, при условии, что нет цикла, который связывает отдельные серверы. Основной недостаток подхода с высокой степенью детализации обусловлен тем, что при его использовании приходится учитывать больший объем информации. Сведения о зависимостях необходимо обновлять при добавлении к набору серверов каждого нового сервера или при внесении каждого изменения в конфигурацию сервера. 31.13. Резюме Проблемы тупиковых ситуаций и исчерпания ресурсов в среде клиент/сервер требуют особого внимания. Тупиковой ситуацией называется такое развитие событий, при котором два или несколько системных компонентов взаимно блокируются, ожидая действий друг друга. Исчерпание ресурсов является более общим понятием, которое относится к любой ситуации, когда доступ к службе предоставляется неравноправно: одна группа клиентов получает доступ на лучших условиях, чем другая. Тупиковая ситуация может возникнуть в процессе взаимодействия между одной парой клиентских и серверных программ. Такие тупиковые ситуации обычно являются следствием применения неоднозначной спецификации протокола или использования ненадежного транспорта в сочетании с протоколом, который спроектирован для работы с надежной транспортной службой. Если к одному серверу обращается несколько клиентов, то при попадании в тупиковую ситуацию или активный тупик вместе с этим сервером только одного клиента может произойти исчерпание ресурсов, а это означает, что другие клиенты теряют возможность доступа к службе. Исчерпание ресурсов может возникнуть, если клиент открывает соединение с последовательным сервером, но не передает запросы, или если клиент отправляет запросы, но не читает ответы. Метод закрытия по тайм-ауту простаивающих соединений позволяет исключить первый вариант исчерпания ресурсов, но не второй. Поскольку любой сервер может сам стать на время клиентом, то в ходе взаимодействия между двумя или несколькими серверами может возникнуть тупиковая ситуация, если каждый сервер по очереди пытается стать клиентом другого сервера, принадлежащего к числу рассматриваемых серверов. Чтобы исключить такие тупиковые ситуации, необходимо исключить возможность создания циклических зависимостей между серверами. Упражнения 31.1. Составьте схему зависимостей между службами на вашем предприятии. 31.2. Составьте схему зависимостей между отдельными серверами на вашем предприятии. 31.3. Можно ли использовать в файловой системе на компьютере А службу NFS для доступа к файлам на компьютере В и одновременно с этим 31.13. Резюме 531
использовать службу NFS в файловой системе на компьютере В для доступа к файлам на компьютере А? Объясните ваш ответ. 31.4. Может ли возникнуть тупиковая ситуация из-за циклических зависимостей между тремя серверами на одном компьютере? Объясните ваш ответ. 31.5. Проведите эксперименты с серверами на вашем предприятии, чтобы определить, какое число одновременных соединений они разрешают. 31.6. Исследуйте конфигурацию вашей локальной операционной системы. Будет ли в этой системе в первую очередь исчерпано число блоков управления передачей, буферов или сокетов? 31.7. Если вам предложат на выбор j^umt'ir проблему обычной тупиковой ситуации или активного тупика, то какую из них вы выберете? Почему? Каковы будут ваши действия? 31.8. Можно ли в клиентской программе, работающей по протоколу TCP, различить между собой отказ, возникший в результате переполнения на сервере очереди запросов, и отказ, возникший в результате останова сети? 31.9. В одной из версий NFS программное обеспечение, которое выполняло монтирование удаленной файловой системы, часто блокировалось; это было связано с тем, что клиентское программное обеспечение NSF должно было ожидать ответа удаленной системы. Для предотвращения тупиковой ситуации программист использовал программу ping (которая предусматривает передачу сообщения эхо-повтора ICMP) для определения того, доступен ли удаленный компьютер, прежде чем предпринять попытку смонтировать файловую систему. При каких обстоятельствах все равно может возникнуть тупиковая ситуация? 31.10. Студенты при изучений курса по работе с сетями разработали устройство для контроля за работой сети, позволяющее анализировать трафик в сети. Двое из них решили использовать систему X Window для отображения полученных результатов на цветном экране. Другая группа студентов до этого могла отдельно эксплуатировать свой вариант программы, но как только оба студента запустили свои программы, в сети возникла перегрузка. Объясните причину. 532 Глава 31. Тупиковые ситуации и исчерпание ресурсов в системах клиент/сервер
Приложение Системные вызовы и библиотечные процедуры, применяемые с сокетами Введение В системе Linux взаимодействие по сети основано на понятии сокета. В приложениях для взаимодействия с программным обеспечением TCP/IP, входящим в состав операционной системы, используется ряд системных вызовов сокетов. В клиентском приложении создается сокет, который подключается к серверу на удаленном компьютере, а затем применяется для обмена данными с удаленным компьютером. И наконец, по завершении использования сокета клиентское приложение его закрывает. Сервер создает сокет, привязывает его к общепринятому порту протокола на локальном компьютере, а затем использует этот сокет для приема запроса на установление соединения от клиента. На каждой странице данного приложения описан один из системных вызовов или одна из библиотечных функций, которые применяются при разработке клиентских или серверных приложений. Функции представлены в алфавитном порядке, и каждая страница посвящена отдельной функции. К числу таких функций относятся следующие: accept, bind, close, connect, fork, gethostbyaddr, gethostbyname, gethostid, gethostname, getpeername, getprotobyname, getservbyname, getsockname, getsockopt, gettimeofday, listen, read, recv, recvfrom, recvmsg, select, send, sendmsg, sendto, sethostid, setsockopt, shutdown, socket и write. Многие из функций, описанных в настоящем приложении, предусмотрены и в других версиях UNIX. Однако в этих версиях они могут немного отличаться. Например, в первых версиях UNIX для обозначения ошибки, которая возникает, если сокет отмечен как неблокирующий, а вызов должен заблокироваться, использовалась символическая константа EW0ULDBL0CK. В системе Linux для обозначения той же ошибки применяется символическое имя EAGAIN (так как это позволяет подчеркнуть, что тот же вызов, выполненный позднее, может завершиться успешно). Для обеспечения обратной совместимости в системе Linux определена константа EWOULDBLOCK со значением, совпадающим с EAGAIN. 1
Системный вызов accept Пример применения retcode = accept(socket, addr, addrlen); Описание Функция accept используется в серверах для приема очередного входящего запроса на установление соединения через пассивный сокет. В сервере вначале вызывается функция socket для создания сокета, функция bind для его привязки к локальному IP-адресу и номеру порта протокола, функция listen для перевода сокета в пассивный режим и установки длины очереди запросов на установление соединения, и только после этого вызывается функция accept. Функция accept извлекает из очереди следующий запрос на установление соединения (или переходит в состояние ожидания до тех пор, пока не поступит запрос на соединение), создает для выполнения запроса новый сокет и возвращает дескриптор нового сокета. Функция accept может применяться только к потоковым сокетам (например, таким, которые предназначены для передачи данных по протоколу TCP). Параметры Параметр Тип Описание socket int Дескриптор сокета, создаваемый функцией socket addr &sockaddr Указатель на структуру, в которой хранится значение адреса. Функция accept записывает в эту структуру IP-адрес и номер порта протокола удаленного компьютера addrlen &int Указатель на целое число, в котором перед вызовом функции записывается размер параметра sockaddr, а после возврата функцией управления содержится число байтов, хранящихся в параметре addr Код возврата Функция accept в случае успешного выполнения возвращает неотрицательный дескриптор сокета, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной errno EBADF В первом параметре не указан допустимый дескриптор ENOTSOCK В первом параметре не указан дескриптор сокета EOPNOTSUPP Сокет не принадлежит к типу SOCKJSTREAM EFAULT Во втором параметре содержится недействительное значение указателя EAGAIN Сокет является неблокирующим, а в очереди отсутствуют запросы от клиентов, ожидающих установления соединения (т.е. вызов был бы заблокирован; в вызывающей программе необходимо снова повторить этот вызов через некоторое время) EPERM Правила брандмауэра запрещают установление соединения ENOBUFS Недостаточный объем доступных буферов ЕДОМЕМ Недостаточный объем доступной памяти 534 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов bind Пример применения retcode = bind(socket, localaddr, addrlen); Описание Функция bind позволяет указать локальный IP-адрес и номер порта протокола, применяемые для сокета. Функция bind в основном используется в серверах, для которых должен быть указан общепринятый порт протокола. Параметры Параметр Тип Описание socket int Дескриптор сокета, созданный в результате вызова функции socket localaddr &sockaddr Адрес структуры, в которой указаны IP-адрес и номер порта протокола addrlen int Размер в байтах структуры с определением адреса Описание структуры sockaddr приведено в главе 5. Код возврата Функция bind возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная еггпо содержит код, который указывает причину ошибки. Возможные ошибки перечислены ниже. Значение, содержащееся в переменной еггпо Причина ошибки EBADF EN0TS0CK EADDRNOTAVAIL EADDRINUSE EINVAL EACCES EFAULT ER0FS ENAMETOOLONG EN0ENT EN0MEM EN0TDIR EL00P В параметре socket не указан допустимый дескриптор В параметре socket не указан дескриптор сокета Указанный адрес недоступен (например, IP-адрес не соответствует локальному сетевому интерфейсу) Указанный адрес находится в использовании (например, порт протокола распределен для другого процесса) К сокету уже привязан адрес Прикладная программа не имеет права использовать указанный адрес Указатель, используемый в качестве параметра localaddr, является недействительным Индексный узел сокета будет находиться в файловой системе, допускающей только чтение Параметр localaddr имеет слишком длинное имя Файл не существует Недостаточный объем доступной памяти в области ядра Компонент префикса составного имени файла не является каталогом Обнаружено слишком много символических ссылок Системный вызов bind 535
Системный вызов close Пример применения retcode ¦ close(socket)? Описание В приложении функция close вызывается после завершения использования сокета. Функция close корректно разрывает связь и удаляет сокет. Любые непрочитанные данные, ожидающие чтения в сокете, отбрасываются. На практике в системе Linux применяется механизм подсчета ссылок, позволяющий нескольким процессам совместно использовать сокет. Если сокет совместно используется п процессами, то число ссылок на него также равно п. При каждом вызове в процессе функция close уменьшает число ссылок на сокет на единицу. После достижения нулевого значения числа ссылок (иными словами, после вызова функции close всеми процессами), занимаемые сокетом ресурсы освобождаются и сокет уничтожается. Параметры Параметр Тип Описание socket int Дескриптор сокета, который должен быть закрыт Код возврата Функция close возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная errno содержит следующее значение. Значение, содержащееся е переменной errno Причина ошибки EBADF В параметре не указан допустимый дескриптор 536 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов connect Пример применения retcode = connect(socket, addr, addrlen)? Описание Функция connect позволяет в вызывающей программе указывать адрес удален- кЬй оконечйой точки дли ранее созданного сокета. Если Сокет предназначен для работы по протоколу TCP, то функция connect устанавливает соединение с помощью трехстороннего квитирования; если же сокет предназначен для работы по протоколу UDP, то функция connect записывает в память указанный адрес удалённой оконечной точки, но не передает по этому адресу никаких дейтаграмм. Параметры Параметр Тип Описание socket int Дескриптор сокета addr &sockaddr_in Адрес оконечной точки, соответствующей удаленному компьютеру addrlen int Длина второго параметра Описание структуры sockaddr_in приведено в главе 5. Код возврата Функция connect возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная еггпо Содержит одно из следующих значений. Значение, содержащееся е переменной еггпо Причина ошибки EBADF EN0TS0CK EAFN0SUPP0RT EADDRNOTAVAIL EISC0NN ETIMEDOUT ECONNREFUSED ENETUNREACH EADDRINUSE EINPROGRESS EALREADY EFAULT EACCES В первом параметре не указан допустимый дескриптор В первом параметре не указан дескриптор сокета С сокетом этого типа не может использоваться семейство адресов, указанное в удаленной оконечной точке Указанный адрес оконечной точки недоступен Сокет уже подключен (Только TCP) Программное обеспечение протокола достигло тайм- аута без успешного установления соединения (Только TCP) В соединении отказано удаленным компьютером (Только TCP) В настоящее время сеть недостижима Указанный адрес уже используется (Только TCP) Сокет является неблокирующим, и попытка соединения была бы заблокирована (Только TCP) Сокет является неблокирующим, и попытка вызова привела бы к переходу в состояние ожидания завершения предыдущего соединения Структура адреса в сокете является недопустимой Попытка подключиться к широковещательному адресу с помощью сокета, в котором не установлен флажок широковещательной рассылки Системный вызов connect 537
Системный вызов fork Пример применения retcode = fork()? Описание Хотя функция fork непосредственно не относится к числу функций, предназначенных для связи через сокет, она имеет важное значение, поскольку используется в серверах для создания параллельных процессов. Функция fork создает новый процесс, в котором выполняется тот же код, что и в первоначальном процессе. Оба процесса, родительский и дочерний, имеют совместный доступ ко всем дескрипторам сокетов и файлов, открытым ко времени вызова функции fork. Эти процессы имеют разные идентификаторы процессов и разные идентификаторы родительских процессов. Параметры Функция fork не принимает параметров. Код возврата В случае успешного выполнения функция fork возвращает 0 в дочернем процессе и идентификатор вновь созданного процесса (отличный от нуля) в первоначальном процессе. Функция возвращает -1 в качестве указания на то, что возникла ошибка. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащее- Причина ошибки ся в переменной errno EAGAIN Достигнут установленный операционной системой предел общего числа процессов или предел числа пользовательских процессов EN0MEM В системе недостаточно памяти для нового процесса 538 Приложение 1. Системные вызовы и библиотечные процедуры...
Библиотечная функция gethostbyaddr Пример применения retcode = gethostbyaddr(addr, alen, atype); Описание Функция gethostbyaddr выполняет поиск информации об указанном хосте по его IP-адресу. Параметры Параметр Тип Описание addr &char Указатель на массив, который содержит адрес хоста (например, IP-адрес) alen int Целое число, которое задает длину адреса (для IP-адреса равно 4) Atype int Целое число, которое задает тип адреса (для IP-адреса применяется значение AF_INET) Код возврата Функция gethostbyaddr в случае успешного выполнения возвращает указатель на структуру hostent, а при возникновении ошибки возвращает значение 0. Структура hostent определена следующим образом: struct hostent { /* Информация о хосте */ char *h_name; /* Доменное имя хоста */ char *h_aliases[]; /* Список других псевдонимов */ int h_addrtype; /* Тип адреса хоста */ int hJLength; /* Длина адреса хоста */ char **h_addr_list; /* Список адресов хоста */ }? При возникновении ошибки глобальная переменная h_errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной herrno HOST_NOT_FOUND Указанное имя неизвестно TRY_AGAIN Временное состояние ошибки: локальный сервер не может в настоящее время получить окончательный ответ на запрос о преобразовании адреса от сетевой службы NO_RECOVERY Возникла неисправимая ошибка NO_ADDRESS Указанное имя имеет допустимый формат, но не соответствует ни одному IP-адресу NOJ)ATA Указанное имя имеет допустимый формат, но не соответствует ни одному IP-адресу Библиотечная функция gethostbyaddr 539
Библиотечная функция gethostbyname Пример применения retcode = gethostbyname(name); Описание Функция gethostbyname преобразует имя хоста в IP-адрес. Параметры Параметр Тип Описание name &char Адрес символьной строки, которая содержит имя хоста Код возврата Функция gethostbyname в случае успешного выполнения возвращает указатель на структуру hostent, а при возникновении ошибки возвращает значение 0. Структура hostent определена следующим образом: struct hostent { /* Информация о хосте */ char *h_name; /* Доменное имя хоста */ char *h~aliases[]; /* Список других псевдонимов */ int h_addrtype; /* Тип адреса хоста */ int h_length; /* Длина адреса хоста */ char **h addr list; /* Список адресов хоста */ }? При возникновении ошибки глобальная переменная h_errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки е переменной herrno HOSTjraT_FOUND Указанное имя неизвестно TRY_AGAIN Временное состояние ошибки: локальный сервер не может в настоящее время получить окончательный ответ на запрос о преобразовании адреса от сетевой службы NO_RECOVERY Возникла неисправимая ошибка NO_ADDRESS Указанное имя имеет допустимый формат, но не соответствует ни одному IP-адресу N0JJATA Указанное имя имеет допустимый формат, но не соответствует ни одному IP-адресу 540 Приложение 1. Системные вызовы и библиотечные процедуры...
I Системный вызов gethostid I Пример применения I hostid ¦ gethostid(); 1 Описание [ В приложении функция gethostid может быть вызвана для определения уникального 32-битового идентификатора хоста, присвоенного локальному компьютеру. 1 Обычно идентификатор хоста представляет собой основной IP-адрес компьютера. Параметры I Функция gethostid не принимает параметров. Возвращаемое значение Функция gethostid возвращает длинное целое число, содержащее идентификатор хоста. Системный вызов gethostid 541
Системный вызов gethostname Пример применения retcode = gethostname(name, namelen); Описание Функция gethostname возвращает основное имя локального компьютера в форме текстовой строки. Параметры Параметр Тип Описание name &char Адрес символьного массива, в который должно быть помещено имя namelen int Длина массива name (не должна быть меньше 65) Код возврата Функция gethostname возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная errno содержит следующее значение. Значение, содержащееся Причина ошибки в переменной errno EFAULT Параметр name имеет неправильное значение EINVAL Параметр namelen является отрицательным или (в системе Linux/i386) указывает размер, меньший фактического 542 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов getpeername Пример применения retcode = getpeername(socket, remaddr, addrlen); Описание В приложении функция getpeername используется для получения адреса удаленной оконечной точки подключенного сокета. Обычно в клиентской программе известен адрес удаленной оконечной точки, поскольку его значение задается при вызове функции connect. Однако в серверной программе, в которой используется функция accept для получения поступившего запроса на установление соединения, может возникнуть необходимость опросить сокет для определения адреса удаленного участника соединения. Параметры Параметр Тип Описание socket int Дескриптор сокета, создаваемый функцией socket remaddr &sockaddr Указатель на структуру sockaddr, в которую должен быть записан адрес оконечной точки addrlen &int Указатель на целое число, в котором перед вызовом функции записывается размер второго параметра, а после возврата функцией управления содержится фактическая длина адреса оконечной точки Описание структуры sockaddr приведено в главе 5. Код возврата Функция getpeername возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная err по содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной егто EBADF ENOTSOCK ENOTCONN EN0BUFS EFAULT В первом параметре не указан допустимый дескриптор В первом параметре не указан дескриптор сокета Сокет не является подключенным сокетом В системе нет достаточных ресурсов для выполнения операции Указатель, заданный параметром remaddr, является недействительным Системный вызов getpeername 543
Библиотечная функция getprotobyname Пример применения retcode = getprotobyname(name); Описание В приложении функция getprotobyname применяется для определения стандартного целочисленного номера протокола исходя только из его имени. Параметры Параметр Тип Описание name &char Адрес строки, которая содержит имя протокола Код возврата Функция getprotobyname в случае успешного выполнения возвращает указатель на структуру protoent, а при возникновении ошибки возвращает значение 0. Структура protoent определена следующим образом: struct protoent { /* Запись с описанием протокола */ char *pjiamej /* Стандартное имя протокола */ char **p_aliases? /* Список псевдонимов для протокола */ int pjproto; /* Стандартный номер протокола */ }? 544 Приложение 1. Системные вызовы и библиотечные процедуры...
Библиотечная функция getservbyname Пример применения retcode * getservbyname(name, proto); Описание Функция getservbyname позволяет получить из сетевой базы данных служб запись, соответствующую,искомой службе, указав ее имя. И в клиентах, и в серверах вызов функции getservbyname применяется для определения номера порта протокола, соответствующего имени службы. Параметры Параметр Тип Описание name &char Указатель на строку символов, которая содержит имя службы proto ichar Указатель на строку символов, которая содержит имя применяемого протокола (например, tcp) Код возврата Функция getservbyname в случае успешного выполнения возвращает указатель на структуру servent, а при возникновении ошибки возвращает нулевой указатель @). Структура servent определена следующим образом: struct servent { /* Запись с информацией об одной службе */ char *s_name ; /* Стандартное имя службы */ char **s_aliases? /* Список других псевдонимов */ int sjport; /* Порт, применяемый для данной службы */ char *s proto; /* Протокол, применяемый для данной службы */ }; Библиотечная функция getservbyname 545
Системный вызов getsockname Пример применения retcode = getsockname(socket, name, namelen); Описание Функция getsockname позволяет подучить локальный адрес указанного сокета. Параметры Параметр Тип Описание socket int Дескриптор сокета, создаваемый функцией socket name fcsockaddr Адрес структуры, которая будет содержать IP-адрес и номер порта протокола сокета namelen &int Число позиций в структуре name; в этом параметре возвращается значение, которое указывает размер структуры Код возврата Функция getsockname возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная еггпо содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной еггпо EBADF В первом параметре не указан допустимый дескриптор enotsock В первом параметре не указан дескриптор сокета enobufs В системе недостаточно свободного места для размещения буферов EFAULT Адрес, заданный параметром name, или параметр namelen является недопустимым 546 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов getsockopt Пример применения retcode = getsockopt(socket, level, opt, optval, optlen); Описание Функция getsockopt позволяет получить в приложении значение параметра (опции) для сокета или протокола, используемого сокетом. Параметры Параметр Тип Описание socket int Дескриптор сокета level int Целое число, которое обозначает уровень протокола opt int Целое число, которое обозначает опцию optval &char Адрес буфера, в котором будет возвращено требуемое значение optlen &int Размер буфера; после возврата управления из функции содержит длину искомого значения Ниже перечислены опции уровня сокета, которые относятся ко всем сокетам. SO_DEBUG Состояние информации об отладке SOJIEUSEADDR Допускается ли многократное использование локального адреса? SOJSEEPALIVE Режим поддержания соединения S0JD0NTR0UTE Должен ли быть предусмотрен отказ от использования средств маршрутизации исходящих сообщений? SO_LINGER Должна ли быть предусмотрена задержка или сокет необходимо закрывать сразу же, даже если в нем имеются данные? SO_BROADCAST Разрешена ли передача широковещательных сообщений? S0_00BINLINE Должны ли внеочередные данные приниматься вместе с остальными данными? SO_RCVLOWAT Минимальный объем данных, который должен быть получен, прежде чем уровень сокета предоставит к ним доступ приложению (в системе Linux — 1 байт) S0_RCVTIME0 Тайм-аут приема (в системе Linux не применяется) S0JPRI0RITY Установить приоритет для всех передаваемых пакетов S0J3NDBUF Размер буфера для вывода so_rcvbuf Размер буфера для ввода SOJTYPE Тип сокета S0_ERR0R Получить и очистить информацию о последней ошибке, относящейся к данному сокету Код возврата Функция getsockopt возвращает 0 в случае успешного выполнения и -1, если] (возникла ошибка. При возникновении ошибки глобальная переменная errno со держит одно из следующих значений. Значение, содержащееся в переменной errno Причина ошибки EBADF EN0TS0CK EN0PR0T00PT EFAULT В первом параметре не указан допустимый дескриптор В первом параметре не указан дескриптор сокета Параметр opt является недопустимым Адрес, заданный параметром optval или optlen, является неправильным Системный вызов getsockopt 547
Системный вызов gettimeofday Пример применения retcode = gettimeofday(tm, tmzone); Описание Функция gettimeofday позволяет получить из операционной системы значение текущего времени и даты, наряду с информацией о местном часовом поясе. Параметры Параметр Тип Описание tm ^struct timeval Адрес структуры timeval tmzone istruct timezone Адрес структуры tiraezone Структуры, значения которым присваиваются функцией gettimeofday, имеют следующие объявления: struct timeval { /* Структура для хранения значений времени */ long tv_sec; /* Число сехунд с начала эпохи A января 1970 года) */ long tv usee; /* Число микросекунд, не учтенных в значении tv sec */ }? struct timezone { int tzjitinuteswest; int tz dsttime; }? Код возврата Функция gettimeofday возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная err по содержит следующее значение. Значение, содержащееся Причина ошибки в переменной errno EFAULT Параметр tm или tmzone содержит неправильный адрес /* Струхтура для хранения информации о часовом поясе */ /* Корректировка поясного времени в направлении */ /* к западу от Гринвича, в минутах */ /* Тип применяемой корректировки */ 548 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов listen Пример применения retcode = listen(socket, queuelen); Описание Функция listen используется в серверах для перевода сокета в пассивный режим (т.е., подготовки сокета к приему входящих запросов на установление соединения). Кроме того, функция listen устанавливает число входящих запросов на установление соединения, которые должны быть поставлены программным обеспечением протокола в очередь к данному сокету на время обработки сервером другого запроса. Функция listen применяется только к сонетам, предназначенным для работы по протоколу TCP. Параметры Параметр Тип Описание socket int Дескриптор сокета, созданный в результате вызова функции socket queuelen int Размер очереди запросов входящих соединений (обычно может иметь значение вплоть до 5) Код возврата Функция listen возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная еггпо содержит одно из следующих значений. Значение, содержащееся Причина ошибки е переменной еггпо EBADF В первом параметре не указан допустимый дескриптор EN0TS0CK В первом параметре не указан дескриптор сокета E0PN0TSUPP Сокет данного типа не поддерживает функцию listen Системный вызов listen 549
Системный вызов read Пример применения retcode = read(socket, buff, buflen)? Описание Функция read используется в клиентах или серверах для получения входных данных из сокета. Параметры Параметр Тип Описание * socket int Дескриптор сокета, создаваемый функцией socket buff &char Указатель на массив символов, в который должны быть записаны входные данные buflen int Целое число, которое указывает число байтов в массиве buff Код возврата Функция read возвращает 0, если при вводе из сокета был обнаружен признак конца файла, число считанных байтов в случае успешного ввода и -1, если возникла ошибка. При возникновений ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной errno EBADF В первом параметре не указан допустимый дескриптор EFAULT Указатель buff имеет недопустимое значение ЕЮ При чтении данных произошла ошибка ввода/вывода EINTR Операция прервана по сигналу EAGAIN Указан неблокирующий ввод/вывод, но в сокете отсутствуют данные EINVAL Дескриптор файла не может применяться для чтения EISDIR Дескриптор файла ссылается на каталог 550 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов recv Пример применения retcode = recv(socket, buffer, length, flags); Описание Функция recv позволяет получить из сокета следующее входящее сообщение. Параметры Параметр Тип Описание socket int Дескриптор сокета, создаваемый функцией socket buffer &char Адрес буфера, предназначенного для хранения сообщения length int Длина буфера flags int Управляющие биты, которые указывают, должны ли быть приняты внеочередные данные или выполнена подготовка к приему сообщений Код возврата Функция recv в случае успешного ввода возвращает число считанных байтов в сообщении, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной errno EBADF В первом параметре не указан допустимый дескриптор EN0TS0CK В первом параметре не указан дескриптор сокета EAGAIN В сокете отсутствуют данные, но был указан неблокирующий ввод/вывод EINTR Прежде чем операция чтения могла доставить данные, был получен сигнал EFAULT Параметр buffer имеет недопустимое значение EN0TC0NN Сокет еще не подключен EINVAL Передан недопустимый параметр Системный вызов recv 551
Системный вызов recvfrom Пример применения retcode « recvfrom(socket, buffer, buflen, flags, from, fromlen); Описание Функция recvfrom извлекает очередное сообщение, поступившее в сокет, и записывает адрес отправителя (что позволяет в вызывающей программе отправить по этому адресу ответ). Параметры Параметр Тип Описание socket int Дескриптор сокета, создаваемый функцией socket buffer &char Адрес буфера, предназначенного для хранения сообщения buflen int Длина буфера flags int Управляющие биты, которые указывают, что должны быть приняты внеочередные данные или выполнена подготовка к приему сообщения from fisockadd Адрес структуры, в которую должен быть записан адрес отправителя г fromlen &int Длина буфера from, возвращаемая как размер адреса отправителя Описание структуры sockaddr приведено в главе 5. Код возврата Функция recvfrom в случае успешного выполнения возвращает число байтов в сообщении, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной errno EBADF В первом параметре не указан допустимый дескриптор EN0TS0CK В первом параметре не указан дескриптор сокета EAGAIN В сокете отсутствуют данные, но был указан неблокирующий ввод/вывод EINTR Прежде чем операция чтения могла доставить данные, был получен сигнал EFAULT Параметр buffer имеет недопустимое значение ENOTC0NN Сокет еще не подключен EINVAL Передан недопустимый параметр 552 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов recvmsg Пример применения retcode « recvmsg(socket, msg, flags)? Описание Функция recvmsg возвращает очередное сообщение, поступившее в сокет, и помещает его в структуру, которая включает заголовок наряду с данными. Параметры Параметр Тип Описание socket int Дескриптор со кета, создаваемый функцией socket msg fcstruct msghdr Адрес структуры msghdr flags int Управляющие биты, которые указывают, что должны быть приняты внеочередные данные или выполнена подготовка к приему сообщения Сообщение доставляется в структуре msghdr, которая имеет следующий формат: struct msghdr { caddrj: msgjiame; /* Необязательный адрес */ . int msgjiamelen? /* Размер адреса */ struct iovec *msg_iov; /* Массив для разборки/сборки компонентов сообщения*/ int msg_iovlen; /* Число элементов в массиве msg_iov */ caddr_t msg_accrights; /* Права доступа, связанные с выполнением */ /* операций передачи/приема */ int msg accrghtslen; /* Длина предыдущего поля */ }? Код возврата Функция recvmsg в случае успешного выполнения возвращает число байтов в сообщении, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки е переменной errno EBADF В первом параметре не указан допустимый дескриптор ENOTSOCK В первом параметре не указан дескриптор сокета EAGAIN В сокете отсутствуют данные, но был указан неблокирующий ввод/вывод EINTR Прежде чем операция чтения могла доставить данные, был получен сигнал EFAULT Параметр buffer имеет недопустимое значение EN0TC0NN Сокет еще не подключен EINVAL Передан недопустимый параметр Системный вызов recvmsg 553
Системный вызов select Пример применения retcode = select(numfds, refds, wrfds, exfds, time); Описание * Функция select обеспечивает асинхронный ввод/вывод, позволяя перевести отдельный процесс в состояние ожидания готовности к вводу/выводу первого из дескрипторов файла в указанном наборе. В вызывающем операторе может быть также указана максимальная продолжительность ожидания. Параметры Параметр Тип Описание numfds int Число дескрипторов файлов в наборе Refds &fd_set Указатель на область двоичных данных, в которой отмечены дескрипторы файлов, предназначенные для ввода данных wrfds &fd_set Указатель на область двоичных данных, в которой отмечены дескрипторы файлов, предназначенные для вывода данных exfds &fd_set Указатель на область двоичных данных, где отмечены дескрипторы файлов, предназначенные для вывода информации об ошибках Ti»e istruct Максимальная продолжительность ожидания или нуль timeval Параметры, которые указывают на дескрипторы, состоят из целых чисел, в которых 1-й бит соответствует дескриптору i. Для очистки или установки отдельных битов применяются макрокоманды FD_CLR и FD_SET. Описание структуры timeval содержится в справочном руководстве Linux, в котором рассматривается функция gettimeofday. Код возврата Функция select в случае успешного выполнения возвращает число готовых дескрипторов файлов, по истечении установленного тайм-аута возвращает 0, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной errno EBADF В одном из наборов дескрипторов указан недопустимый дескриптор EINTR Прежде чем истек тайм-аут или перешел в состояние готовности к вводу/выводу любой из выбранных дескрипторов, был получен сигнал ENOMEM Не удалось распределить память для внутренних таблиц 554 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов send Пример применения retcode = send(socket, msg, msglen, flags); Описание В приложении функция send применяется для передачи сообщения на другой компьютер. Параметры Параметр Тип socket int msg &char Msglen int Flags int Код возврата Описание Дескриптор со кета, создаваемый функцией socket Указатель на сообщение Длина сообщения в байтах Управляющие биты, которые указывают, что должны быть приняты внеочередные данные или выполнена подготовка к приему сообщения Функция send в случае успешного выполнения возвращает число переданных символов, а если возникла ошибка» возвращает -1. При возникновении ошибки глобальная переменная еггпо содержит одно из следующих значений. Значение, содержащееся в переменной еггпо EBADF EN0TS0CK EFAULT EMSGSIZE EAGAIN ENOBUFS EINTR ENOMEN EINVAL EPIPE Причина ошибки В первом параметре не указан допустимый дескриптор В первом параметре не указан дескриптор сокета Параметр buffer имеет недопустимое значение Сообщение слишком велико для данного сокета В сокете отсутствуют данные, но был указан неблокирующий ввод/вывод В системе нет достаточных ресурсов для выполнения операции Получен сигнал Недостаточный объем доступной памяти Передан недопустимый параметр Второй участник логического соединения закрыл для ввода свой сокет Системный вызов send 555
Системный вызов sendmsg Пример применения retcode = sendmsg(socket, msg, flags); Описание | Функция sendmsg передает сообщение, извлекая его из структуры msghdr. Параметры Параметр Тип socket int Описание Дескриптор со кета, создаваемый функцией socket msg fcstruct msghdr Указатель на структуру msghdr flags int Управляющие биты, которые указывают, что должны быть приняты внеочередные данные или выполнена подготовка к приему сообщения Определение структуры msghdr содержится в справочном руководстве с описанием функции recvmsg. Код возврата Функция sendmsg в случае успешного выполнения возвращает число переданных байтов, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся в переменной errno 1 EBADF EN0TS0CK EFAULT EMSGSIZE EAGAIN EN0BUFS EINTR EN0MEM EINVAL EPIPE Причина ошибки В первом параметре не указан допустимый дескриптор В первом параметре не указан дескриптор сокета Параметр buffer имеет недопустимое значение Сообщение слишком велико для данного сокета В сокете отсутствуют данные, но был указан неблокирующий ввод/вывод В системе нет достаточных ресурсов для выполнения операции Получен сигнал Недостаточный объем доступной памяти Передан недопустимый параметр Второй участник логического соединения закрыл для ввода свой сокет 556 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов sendto Пример применения retcode = sendto(socket, msg, msglen, flags, to, tolen); Описание Функция sendto позволяет передать сообщение, получив адрес назначения из соответствующей структуры. Параметры Параметр Тип Описание socket int Дескриптор со кета, создаваемый функцией socket msg &char Указатель на сообщение msglen int Длина сообщения в байтах flags int Управляющие биты, которые указывают, что должны быть приняты внеочередные данные или выполнена подготовка к приему сообщения to fcsockaddr Указатель на структуру, в которой хранится значение адреса tolen int Длина адреса в байтах Описание структуры sockaddr приведено в главе 5. Код возврата Функция sendto в случае успешного выполнения возвращает число переданных байтов, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная еггпо содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной еггпо EBADF В первом параметре не указан допустимый дескриптор EN0TS0CK В первом параметре не указан дескриптор сокета EFAULT Параметр buffer имеет недопустимое значение EMSGSIZE Сообщение слишком велико для данного сокета EAGAIN В сокете отсутствуют данные, но был указан неблокирующий ввод/вывод EN0BUFS В системе нет достаточных ресурсов для выполнения операции EINTR Получен сигнал EN0MEM Недостаточный объем доступной памяти EINVAL Передан недопустимый параметр EPIPE Второй участник логического соединения закрыл для ввода свой сокет Системный вызов sendto 557
Системный вызов sethostid Пример применения (void)sethostid(hostid); Описание Системный администратор может выполнить при запуске системы привилегированную программу, в которой вызывается функция sethostid для присвоения локальному компьютеру уникального 32-битового идентификатора хоста. Обычно идентификатор хоста представляет собой основной IP-адрес компьютера. Параметры Параметр Тип Описание hostid int Значение, которое должно быть записано в память как идентификатор хоста Ошибки Приложение должно выполняться от имени привилегированного пользователя root, так как в противном случае функция sethostid не сменит идентификатор хоста. 558 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов setsockopt Пример применения retcode = setsockopt(socket, level, opt, optval, optlen); Описание Функция setsockopt позволяет изменить в приложении значение параметра (опции) для сокета или протокола, используемого сокетом:. Параметры Параметр Тип Описание socket int Дескриптор сокета level int Целое число, которое обозначает протокол (например, TCP) opt int Целое число, которое обозначает опцию optval &char Адрес буфера, который содержит соответствующее значение (обычно 1 применяется для установки опции, а 0 для ее отмены) optlen int Длина параметра optval Ниже перечислены опции уровня сокета, которые относятся ко всем сокетам. S0_DEBUG Установить/отменить режим вывода информации об отладке SO_REUSEADDR Установить/отменить возможность многократного использования локального адреса SOJCEEPALIVE Установить/отменить режим поддержания соединения SO_DONTROUTE Установить/отменить режим отказа от использования средств маршрутизации исходящих сообщений S0_LINGER Установить задержку при закрытии сокета, если сокет содержит данные S0_BR0ADCAST Установить/отменить разрешение на рассылку широковещательных сообщений SOJDOBINLINE Установить/отменить прием внеочередных данных вместе с остальными данными S0_SNDBUF Установить размер буфера для вывода SO_RCVBUF Установить размер буфера для ввода SO_SNDLOWAT Установить минимальный объем данных, который должен быть накоплен в буфере, прежде чем данные будут переданы в программное обеспечение транспортного протокола S0_BINDT0DEVICE Привязать сокет к интерфейсному устройству SOJPRIORITY Установить приоритет для пакетов, передаваемых через сокет Код возврата Функция setsockopt возвращает 0 в случае успешного выполнения и -1, если возникла ошибка. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной errno EBADF В первом параметре не указан допустимый дескриптор EN0TS0CK В первом параметре не указан дескриптор сокета EN0PR0T00PT Целочисленное значение опции opt является недопустимым EFAULT Адрес, заданный параметром optval или optlen, является неправильным Системный вызов setsockopt 559
Системный вызов shutdown Пример применения retcode = shutdown(socket, direction); Описание * Функция shutdown применяется к дуплексным сокетам (т.е., к подключенным сокетам TCP) и служит для частичного закрытия соединения. Параметры Параметр Тип Описание socket int Дескриптор со кета, созданный в результате вызова функции socket direction int Направление, в котором должно быть выполнено закрытие сокета: О означает, что должен быть прекращен дальнейший ввод, 1 —дальнейший вывод, а 2 — и ввод, и вывод Код возврата Функция shutdown в случае успешного выполнения операции возвращает О, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная еггпо содержит код, который указывает причину ошибки. Возможные ошибки перечислены ниже. Значение, содержащееся Причина ошибки в переменной еггпо EBADF В первом параметре не указан допустимый дескриптор enotsock В первом параметре не указан дескриптор сокета ENOTCONN Указанный сокет в настоящее время не подключен 560 Приложение 1. Системные вызовы и библиотечные процедуры...
Системный вызов socket Пример применения retcode = socket(family, type, protocol); Описание Функция socket создает сокет, применяемый для обмена данными по сети, и возвращает целочисленный дескриптор для этого сокета. Параметры Параметр Тип Описание family int Семейство протоколов или адресов (PF_T.NET обозначает семейство протоколов TCP/IP; вместо этой константы может также применяться AF_INET) Type int Тип обслуживания (например, SOCK_STREAM соответствует TCP, a S0CKJ)GRAM применяется для UDP) " protocol int Номер применяемого протокола или 0 Нулевое значение параметра указывает, что должен применяться протокол, предусмотренный по умолчанию для данного семейства протоколов и типа обслуживания Код возврата Функция socket в случае успешного выполнения возвращает дескриптор, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная err по содержит код, который указывает причину ошибки. Возможные ошибки перечислены ниже. Значение, содержащееся Причина ошибии в переменной егто EPR0T0N0SUPP0RT EMFILE ENFILE EACCES EN0BUFS EN0MEM EINVAL Ошибка в параметрах: недействительное значение параметра с указанием службы или протокола Таблица дескрипторов приложения заполнена Внутренняя системная таблица файлов заполнена Отказано в разрешении создать сокет В системе отсутствует доступное пространство для размещения буферов В системе отсутствует доступная память Протокол или семейство протоколов неизвестны или недоступны Системный вызов socket 561
Системный вызов write Пример применения retcode = write(socket, buf, buflen); Описание Функция write позволяет передавать в приложении данные на удаленный компьютер. Параметры Параметр Тип Описание socket int Дескриптор со кета, созданный в результате вызова функции socket buf &char Адрес буфера, содержащего данные buflen int Число байтов в буфере buf Код возврата Функция write в случае успешного выполнения возвращает число переданных байтов, а если возникла ошибка, возвращает -1. При возникновении ошибки глобальная переменная errno содержит одно из следующих значений. Значение, содержащееся Причина ошибки в переменной errno EBADF В первом параметре не указан допустимый дескриптор EPIPE Попытка записи в неподключенный потоковый сокет EFAULT Параметр buf содержит неправильный адрес EINVAL Указатель на сокет является недействительным ЕЮ Произошла ошибка ввода/вывода EAGAIN Сокет не может принять без блокировки все записанные данные, но указан неблокирующий ввод/вывод EINTR Вызов был прерван сигналом до завершения вывода данных EN0SPC На устройстве, где находится файл, нет места для выводимых данных 562 Приложение 1. Системные вызовы и библиотечные процедуры...
Приложение Операции с дескрипторами файлов и сокетов в системе Linux Введение В системе Linux во всех операциях ввода и вывода используется структура данных, называемая дескриптором файла. В программе применяется системный вызов open для получения доступа к файлу или системный вызов socket для получения дескриптора, предназначенного для сетевой связи. Интерфейс сокетов описан в главах 4 и 5, а дескрипторы Linux и операции ввода/вывода подробно описаны в главе 24. В этих главах внимание акцентируется на том, что вновь созданный процесс наследует копии всех дескрипторов файлов, открытых родительским процессом ко времени создания. И наконец, в главе 30 указано, что в производственных серверах должны быть закрыты лишние дескрипторы файлов и открыты стандартные дескрипторы ввода/вывода. В настоящем приложении описан способ использования в программах стандартных дескрипторов ввода/вывода в виде параметров и показано, как выполнить в родительском процессе переупорядочение существующих дескрипторов для обеспечения их соответствия стандартным дескрипторам ввода/вывода перед порождением дочернего процесса. Этот метод особенно удобен для мультисервис- ных серверов, в которых вызываются отдельные программы для выполнения запросов к каждой службе. Передача дескрипторов в виде неявных параметров Новый дочерний процесс, созданный функцией fork, наследует копию всех дескрипторов файлов, открытых родительским процессом ко времени вызова этой функции. Кроме того, в дочернем процессе дескриптор каждого конкретного файла или сокета находится в точно такой же позиции в таблице дескрипторов, как и в родительском процессе. Это означает, что если дескриптор 5 в родительском процессе соответствует сокету TCP, то дескриптор 5 во вновь созданном дочернем процессе соответствует точно тому же сокету. Дескрипторы остаются открытыми и после вызова функции execve. Для создания нового процесса, который выполняет код из файла JP, в родительском процессе вызывается функция fork, а в дочернем процессе предусматривается вызов функции execve с именем файла F в качестве параметра. Дескрипторы файлов фактически становятся своего рода неявными параметрами, передаваемыми вновь созданным процессам, а также тем процессам которые перекрывают код работающей программы кодом, считанным из файла. В 2
родительском процессе можно выбрать, какие дескрипторы должны остаться открытыми, а какие должны быть закрыты перед вызовом функции fork, а также определить точный перечень дескрипторов, которые останутся открытыми в процессе передачи параметров при вызове функции execve. В программах Linux для управления обработкой вместо явных параметров часто используются дескрипторы. В частности, в любой программе Linux предусмотрено наличие трех открытых стандартных дескрипторов ввода/вывода с началом ее выполнения: дескрипторов стандартного устройства ввода данных (дескриптор 0), стандартного устройства вывода данных (дескриптор 1) и стандартного устройства вывода сообщений об ошибках (дескриптор 2). Программа читает входные данные из дескриптора 0, пишет выходные данные в дескриптор 1 и передает сообщения об ошибках в дескриптор 2. Преимущества применения постоянных дескрипторов В сервере, в котором предусмотрен вызов отдельной программы для обработки каждого конкретного запроса, также можно использовать дескрипторы в качестве неявных параметров. Например, ведомая программа, которая входит в состав сервера с установлением логического соединения, может быть разработана таким образом, чтобы в ней предусматривалось применение дескриптора 0 в качестве устройства ввода данных из соединения TCP. В этом случае в ведущем сервере устанавливается соединение через дескриптор 0, а затем для вызова на выполнение ведомой программы вызывается функция execve. Ведущая программа могла быть также запрограммирована на использование произвольного дескриптора и передачу дочернему процессу параметра с указанием того, какой именно дескриптор соответствует соединению. Однако применение постоянного дескриптора позволяет упростить код без потери функциональных возможностей. В процессах дескрипторы являются неявными параметрами вызова функции execve. Ведущие серверы часто запрограммированы на использование одного определенного дескриптора в качестве неявного параметра, который передается вызываемым ведомым программам. Необходимость переупорядочения дескрипторов Если в ведущем сервере вызывается системная функция socket для создания сокета, предназначенного для связи без установления логического соединения, или функция accept для получения сокета для связи с установлением логического соединения, в сервере невозможно определить, какой дескриптор будет возвращен этим вызовом, поскольку дескриптор выбирается операционной системой. Если ведомая программа разработана с учетом того, что дескриптор 0 будет соответствовать сокету, применяемому для связи, то в ведущей программе нельзя просто создать сокет со случайно выбранным дескриптором, а затем вызвать на выполнение ведомую программу. Вместо этого в ведущей программе необходимо переупорядочить ее дескрипторы перед выполнением вызова. Переупорядочение дескрипторов В любом процессе для переупорядочения дескрипторов тмогут применяться системные вызовы dup2 и close системы Linux. Закрытие дескриптора с помощью функции close позволяет отключить его от файла или сокета, которому он соответствует, освободить все связанные с ним ресурсы и обеспечить возможность его 564 Приложение 2. Операции с дескрипторами файлов и сокетов в системе Linux
дальнейшего использования. Например, если в процессе должен применяться дескриптор 0, но он уже находится в использовании, то в процессе вызывается функция close для его освобождения. В приложении 1 описано, как можно закрыть в сервере ненужные дескрипторы файлов с началом его выполнения. Для создания полной копии одного дескриптора файла в другом вызывается функция dup2. Этот вызов принимает два параметра: (void)dup2(oiddesc, newdesc); Здесь olddesc ^- целое число, которое обозначает существующий дескриптор, a newdesc — целое число, обозначающее дескриптор, в котором должна появиться копия. Если дескриптор newdesc в настоящее время уже используется, система освобождает связанные с ним ресурсы так, как если бы пользователь вызвал функцию close перед созданием копии дескриптора olddesc. После вызова функции dup2 дескрипторы olddesc и newdesc ссылаются на один и тот же объект (например, на одно и то же соединение TCP). Чтобы "переместить" сокет из одного дескриптора в другой, в программе вначале вызывается функция dup2, а затем функция close для уничтожения первоначального дескриптора. Например, предположим, что в ведущем сервере вызвана функция accept для получения входящего запроса на установление соединения, а операционная система при выполнении вызова функции accept выбрала для использования в новом соединении дескриптор 5. В ведущем сервере можно выполнить следующие два вызова для перемещения сокета в дескриптор 0: (void)dup2E,0)? (void)closeE); Флажок close-on-exec В сервере можно предусмотреть использование метода явного закрытия ненужных дескрипторов файлов перед вызовом на выполнение другой программы. Для этого могут применяться два подхода. В одном из них в сервере необходимо явно вызвать функцию close для закрытия каждого ненужного дескриптора файла, а в другом применяется определенное системное средство для автоматического закрытия дескрипторов. Для использования этого автоматического средства в сервере необходимо установить флажок close-on-exec (закрытие после вызова на выполнение) в каждом дескрипторе, который должен быть закрыт системой. После вызова в сервере функции execve система проверяет каждый дескриптор для определения того, установлен ли в нем этот флажок, и в случае положительного ответа, автоматически вызывает функцию close. Хотя на первый взгляд может показаться, что автоматическое закрытие не упрощает работу по программированию, следует помнить, что при разработке большой, сложной программы программист может уже не помнить о назначении каждого дескриптора к тому моменту, когда в программе должна быть вызвана функция execve. Применение флажка close-on- exec позволяет программисту определить, должен ли дескриптор оставаться открытым, в ходе разработки той части программы, где создается сам дескриптор. Поэтому такое системное средство может стать исключительно удобным. Резюме В процессах дескрипторы служат в качестве неявных параметров при создании нового процесса или при перекрытии кода- работающей программы кодом из файла. Дескрипторы часто используются для передачи соединения TCP или сокета UDP в серверах, которые вызывают на выполнение отдельно оттранслированные программы. Системный вызов socket 565
Для упрощения разработки и сопровождения программ принято выбирать для каждого неявного параметра постоянный дескриптор. При этом в вызывающей программе перед вызовом функции execve необходимо переупорядочить дескрипторы таким образом, чтобы сокет, применяемый для ввода/вывода, соответствовал дескриптору, выбранному в качестве неявного параметра. Для переупорядочения дескрипторов в процессе применяются системные вызовы dup2 и close. Функция dup2 копирует существующий дескриптор в указанное место, а функция close удаляет исходный дескриптор. Чтобы переместить дескриптор А в дескриптор В, в процессе вначале выполняется оператор dup2(A,?), а затем оператор close(A). Материал для дальнейшего изучения Дополнительная информация о системных вызовах dup2 и close приведена в оперативной документации Linux. Упражнения П2.1. Прочитайте литературу о командном интерпретаторе Linux. Какие дескрипторы он присваивает перед вызовом команды на выполнение? П2.2. Вкратце опишите алгоритм, применяемый в командном интерпретаторе для выполнения операции перенаправления ввода/вывода в файл. Покажите, как в нем перемещаются дескрипторы файлов перед вызовом функции execve. П2.3. Доработайте мультисервисный сервер, описанный в главе 15 таким образом, чтобы в нем дескриптор 0 использовался в качестве неявного параметра, а для вызова ведомой программы на выполнение применялась функция execve. П2.4. Прочитайте литературу о программе inetd. Как в ней используются дескрипторы в качестве неявных параметров? П2.5. Какие действия выполняет вызов dup2(fl,jn), если п уже равен т? Напишите версию функции dup2, которая работает с произвольными значениями л и in. 566 Приложение 2. Операции с дескрипторами файлов и сокетов в системе Linux
Список литературы 1. ABRAMSON N. The ALOHA System — Another Alternative for Computer Communications. Proceedings of the Fall Joint Computer Conference, 1970. 2. AHO A., KERNIGHAN В., WEINBERGER P. The AWK Programming Language. Addison-Wesley, Reading, Massachusetts, 1988. 3. ANDREWS D. W., SHULTZ G. D. A Token-Ring Architecture for Local Area Networks: An Update, Proceedings of Fall 82 COMPCON. IEEE, 1982. 4. AT&T. System VRelease 4 Programmers Reference Manual. Englewood Cliffs, Prentice-Hall, 1989. 5. ATALLAH M., COMER D. E. Algorithms for Variable Length Subnet Address Assignment. IEEE Transactions on Computers, vol. 47:6, 693-699, 1998. 6. BACH M. J. The Design of the Unix Operating System. Englewood Cliffs, Prentice-Hall, 1986. 7. BBN. A History of the ARPANET: The First Decade, Technical Report. Bolt, Beranek, and Newman, Inc., 1981. 8. BBN. Specification for the Interconnection of a Host and an IMP (revised), Technical Report 1822. Bolt, Beranek, and Newman, Inc., 1981. 9. BERTSEKAS D., GALLAGER R. Data Networks, 2nd edition. Prentice-Hall, Upper Saddle River, New Jersey, 1991. 10. BIAGIONI E., COOPER E., SANSOM R. Designing a Practical ATM LAN. IEEE Network, 32-39, 1993. 11. BIRRELL A., NELSON B. Implementing Remote Procedure Calls. ACM Transactions on Computer Systems, 2A), 39-59, 1984. 12. BLACK U. ATM: Foundation for Broadband Networks. Prentice-Hall, JJpper Saddle River, New Jersey, 1995. 13. BOLSKY M. I., KORN D. G. The Kornshell Command and Programming Language. Prentice-Hall, Upper Saddle River, New Jersey, 1989. 14. BOGGS D., SHOCH J., TAFT E., METCALFE R. Pup: An Internetwork Architecture. IEEE Transactions on Communications, Prentice Hall, 1980. 15. BORMAN D. Implementing TCP/IP on a Cray Computer. Computer Communication Review, 19B), 11-15, 1989. 16. BROWNBRIDGE D., MARSHALL L., RANDELL B. The Newcastle Connections or UNIXes of the World Unite!, Software — Practice and Experience, 12A2), 1147-1162, 1982. 17. CALLAGHAN В., PAWLOWSKI В., STAUBACH P. NFS Version 3 Protocol Specification, RFC 1813, Sun Microsystems, Inc., 1995. 18. CALLAGHAN B. WebNFS Client Specification, RFC 2054. Sun Microsystems, Inc., 1996. 19. CALLAGHAN B. WebNFS Server Specification, RFC 2055. Sun Microsystems, Inc., 1996. 20. CALLAGHAN B. NFS URL Scheme, RFC 2224. Sun Microsystems, Inc., 1997.
21. CASNER S., DEERING S. First IETF Internet Audiocast, Computer Communications Review, 22C), 92-97, 1992. 22. CERF V., CAIN E. The DOD Internet Architecture Model. Computer Networks, 1983. 23. CERF V., KAHN R. A Protocol for Packet Network Interconnection. IEEE Transactions of Communications, Com-22E), 1974. 24. CERF V. A History of the ARPANET, Connexions, The Interoperability Report. Mountain View, California, 1989. 25. CHERITON D. R. Local Networking and Internetworking in the V-System, Proceedings of the Eighth Data Communications Symposium, 1983. 26. CHERITON D. VMTP: A Transport Protocol for the Next Generation of Communication Systems, Proceedings of ACM SIGCOMM '86, 406-415, 1986. 27. CHESSON G. Protocol Engine Design, Proceedings of the 1987 Summer USENIX Conference. Phoenix, AZ, 1987. 28. CHESWICK W., BELLOVIN S. Firewalls And Internet Security: Repelling the Wiley Hacker, 2nd edition. Addison-Wesley, Reading, Massachusetts, 1998. 29. CLARK D., FANG W. Explicit Allocation Of Best-Effort Packet Delivery Service. IEEE/ACM Transactions On Networking, 6D), 1998. 30. CLARK D., LAMBERT M., ZHANG L. NETBLT: A High Throughput Transport Protocol, Proceedings of ACM SIGCOMM '87, 1987. 31. CLARK D., JACOBSON V., ROMKEY J., SALWEN H. An Analysis of TCP Processing Overhead. IEEE Communications, 23-29, 1989. 32. COHEN D. On Holy Wars and a Plea for Peace. IEEE Computer, 48-54, 1981. 33. COMER D. E. Operating System Design — The XINU Approach. Prentice-Hall, Englewood Cliffs, New Jersey, 1984. 34. COMER D. E. Computer Networks And Internets, 2nd edition. Prentice-Hall, Upper Saddle River, New Jersey, 1999. 35. COMER D. E., KORB J. T. CSNET Protocol Software: The IP-to-X25 Interface. Computer Communications Review, 13B), 1983. 36. COMER D. E., NARTEN Т., YAVATKAR R. The Cypress Network: A Low Cost Internet Connection Technology, Technical Report TR-653. Purdue University, ^ West Lafayette, IN, 1987. 37. COMER D. E., NARTEN Т., YAVATKAR R. The Cypress Coaxial Packet Switch. Computer Networks and ISDN Systems, vol. 14:2-5, 383-388, 1987. 38. COMER D. E., STEVENS D. L. Internetworking With TCP/IP: Volume II: Design, Implementation, and Internals, 3rd edition. Prentice-Hall, Upper Saddle River, New Jersey, 1999. 39. COMER D. E., STEVENS D. L. Internetworking With TCP/IP Volume III — Client-Server Programming And Applications, BSD socket version, 2nd edition. Prentice-Hall, Upper Saddle River, New Jersey, 1996. 40. COMER D. E., STEVENS D. L. Internetworking With TCP/IP Volume III — Client-Server Programming And Applications, AT&T ТЫ version. Prentice-Hall, Upper Saddle River, New Jersey, 1994. 568 Приложение 2. Список литературы
41. COMER D. E., STEVENS D. L. Internetworking With TCP/IP Volume III — Client-Server Programming And Applications, Windows Sockets version. Prentice-Hall, Upper Saddle River, New Jersey, 1997. 42. COTTON I. Technologies for Local Area Computer Networks, Proceedings of the Local Area Communications Network Symposium, 1979. 43. DALAL Y. K., PRINTIS R. S. 48-Bit Absolute Internet and Ethernet Host Numbers, Proceedings of the Seventh Data Communications Symposium, 1981. 44. DEERING S. E., CHERITON D. R. Multicast Routing in Datagram Internetworks and Extended LANs. ACM Transactions on Computer Systems, 8B), 85-110, 1990. 45. DEERING S., ESTRIN D., FARINACCI D., JACOBSON V., LIU C-G., WEI L. An Architecture for Wide-Area Multicasting Routing, Proceedings of ACM SIGCOMM '94, 126-135, 1994. 46. DENNING P. J. The Science of Computing: Worldnet, in American Scientist, 432-434, 1989. 47. DENNING P. J. The Science of Computing: The ARPANET After Twenty Years, in American Scientist, 530-534, 1989. 48. DE PRYCKER M. Asynchronous Transfer Mode Solution for Broadband ISDN, 3rd edition. Prentice-Hall, Upper Saddle River, New Jersey, 1995. 49. DIGITAL EQUIPMENT CORPORATION, INTEL CORPORATION, XEROX CORPORATION. The Ethernet: A Local Area Network Data Link Layer and Physical Layer Specification, 1980. 50. DION J. The Cambridge File Server. Operating Systems Review, 14D), 26-35, 1980. 51. DRIVER H., HOPEWELL H., IAQUINTO J. How the Gateway Regulates Information Control. Data Communications, 1979. 52. EDGE S. W. Comparison of the Hop-by-Hop and Endpoint Approaches to Network Interconnection, in Flow Control in Computer Networks. GRANGE J- L., GIEN M. (EDS.), North-Holland, Amsterdam, 359-373, 1983. 53. EDGE S. An Adaptive Timeout Algorithm for Retransmission Across a Packet Switching Network, Proceedings of ACM SIGCOMM '83, 1983. 54. ERIKSSON H. MBONE: The Multicast Backbone. Communications of the ACM, 37(8), 54-60, 1994. 55. FALK G. The Structure and Function of Network Protocols, in Computer Communications, Volume I: Principles, CHOU W. (ED.). Prentice-Hall, Upper Saddle River, New Jersey, 1983. 56. FARMER W. D., NEWHALL E. E. An Experimental Distributed Switching System to Handle Bursty Computer Traffic, Proceedings of the ACM Symposium on Probabilistic Optimization of Data Communication Systems, 1-33, 1969. 57. FEDOR M. GATED: A Multi-Routing Protocol Daemon for UNIX, Proceedings of the 1988 Summer USENIX conference. San Francisco, California, 1988. 58. FLOYD S., JACOBSON V. Random Early Detection Gateways for Congestion Avoidance. IEEE /ACM Transactions on Networking, 1D), 1993. 59. FRANK H., CHOU W. Routing in Computer Networks. Networks, 1A), 99-112, 1971. Системный вызов socket 569
60. FULTZ G. L., KLEINROCK L. Adaptive Routing Techniques for Store-and- Forward Computer Communication Networks, presented at IEEE International Conferenceon Communications. Montreal, Canada, 1971. 61. GALVIN P. В., SILBERSCHATZ A. Operating System Concepts. John Wiley & V Sons, New York, 1999. 62. GERLA M., KLEINROCK L. Flow Control: A Comparative Survey. IEEE Transactions on Communications, 1980. 63. HARRENSTIEN K. Time Server, RFC 738. Information Sciences Institute, 1977. 64. HINDEN R., HAVERTY J., SHELTZER A. The DARPA Internet: Interconnecting Heterogeneous Computer Networks with Gateways. Computer, 1983. 65. IETF. An Agreement Between the Internet Society, the IETF, and Sun Microsystems, Inc. in the matter of NFS V.4 Protocols, RFC 2339. Sun Microsystems, Inc., 1998. 66. INTERNATIONAL ORGANIZATION FOR STANDARDIZATION. Information processing systems — Open Systems Interconnection — Transport Service Definition, International Standard number 8072. ISO, Switzerland, 1986. 67. INTERNATIONAL ORGANIZATION FOR STANDARDIZATION. Information processing systems — Open Systems Interconnection — Connection Oriented Transport Protocol Specification, International Standard number 8073. ISO, Switzerland, 1986. 68. INTERNATIONAL ORGANIZATION FOR STANDARDIZATION. Information processing systems — Open Systems Interconnection — Specification of Basic Specification of Abstract Syntax Notation One (ASN.l), International Standard number 8824, ISO, Switzerland, 1987. 69. INTERNATIONAL ORGANIZATION FOR STANDARDIZATION. Information processing systems — Open Systems Interconnection — Specification of Basic Encoding Rules for Abstract Syntax Notation One (ASN.l), International Standard number 8825. ISO, Switzerland, 1987. 70. INTERNATIONAL ORGANIZATION FOR STANDARDIZATION. Information processing systems — Open Systems Interconnection — Management Information Service Definition, Part 2: Common Management Information \ Service, Draft International Standard number 9595-2. ISO, Switzerland, 1988. 71. INTERNATIONAL ORGANIZATION FOR STANDARDIZATION. Information processing systems — Open Systems Interconnection — Management Information Protocol Definition, Part 2: Common Management Information Protocol. Draft International Standard number 9596-2, 1988. 72. JACOBSON V. Congestion Avoidance and Control, Proceedings ACM SIGCOMM '88, 1988. 73. JAIN R. On Caching Out-of-Order Packets in Window Flow Controlled Networks, Technical Report, DEC-TR-342. Digital Equipment Corporation, 1985. 74. JAIN R. Divergence of Timeout Algorithms for Packet Retransmissions, Proceedings Fifth Annual International Phoenix Conference on Computers and Communications. Scottsdale, AZ, 1986. 75. JAIN R. A Timeout-Based Congestion Control Scheme for Window Flow- Controlled Networks. IEEE Journal on Selected Areas in Communications, Vol. SAC-4, no. 7, 1986. 570 Приложение 2. Список литературы
76. JAIN R., RAMAKRISHNAN K., CHIU D-M. Congestion Avoidance in Computer Networks With a Connectionless Network Layer. Technical Report, DEC-TR- 506. Digital Equipment Corporation, 1987. 77. JAIN R. The Art of Computer Systems Performance Analysis. John Wiley & Sons, New York, 1991. 78. JAIN R. Myths About Congestion Management in High-speed Networks. Internetworking: Research and Experience, 3C), 101-113, 1992. 79. JAIN R. FDDI Handbook; High-Speed Networking Using Fiber and Other Media. Addison Wesley, Reading, Massachusetts, 1994. 80. JENNINGS D. M„ LANDWEBER L. H., FUCHS I. H. Computer Networking for Scientists and Engineers. Science, vol. 231, 941-950, 1986. 81. JOUVELAS I., HARDMAN V., WATSON A. Lip Synchronisation for use over the Internet: Analysis and implementation. Proceedings IEEE '96, 1996. 82. JUBIN J., TORNOW J. The DARPA Packet Radio Network Protocols. IEEE Proceedings, 1987. 83. KAHN R. Resource-Sharing Computer Communications Networks. Proceedings of the IEEE, 60A1), 1397-1407, 1972. 84. KARN P., PRICE H., DIERSING R. Packet Radio in the Amateur Service. IEEE Journal on Selected Areas in Communications, 1985. 85. KARN P., PARTRIDGE С Improving Round-Trip Time Estimates in Reliable Transport Protocols. Proceedings of ACM SIGCOMM 487, 1987. 86. KORB J. Standard for the Transmission of IP datagrams Over Public Data Networks. RFC 877, Purdue University, 1983. 87. KAUFMAN C, PERLMAN, R., SPECINER, M. Network Security: Private Communication in a Public World. Prentice-Hall, Upper Saddle River, I^ew Jersey, 1995. 88. KENT C, MOGUL J. Fragmentation Considered Harmful. Proceedings of ACM SIGCOMM '87, 1987. 89. LAMPSON B. W., PAUL M., SIEGERT H. J. (EDS.). Distributed Systems — Architecture and Implementation (An Advanced Course). Springer-Verlag, Berlin, 1981. 90. LAMPSON B. W., SRINIVASAN V., VARGHESE G. IP Lookups Using Multiway and Multicolumn Search. IEEE/ACM Transactions on Networking, vol 7, 324-334, 1999. 91. LANZILLO A. L., PARTRIDGE С Implementation of Dial-up IP for UNIX Systems, Proceedings 1989 Winter USENIX Technical Conference. San Diego, CA, 1989. 92. LEFFLER S., M. McKUSICK, KARELS M., QUARTERMAN J. The Design and Implementation of the 4.3BSD UNIX Operating System. Addison Wesley, Reading, Massachusetts, 1989. 93. LEFFLER S., M. McKUSICK, KARELS M., QUARTERMAN J. The Design and Implementation of the 4.4BSD UNIX Operating System. Addison Wesley, Reading, Massachusetts, 1996. 94. MARZULLO K., OWICKI S. Maintaining the time in a distributed system. ACM Operating Systems Review, 19, 3, 44-54, 1985. Системный вызов socket 571
95. MCKUSICK M. К., JOY W. N., LEFFLER S. J., FABRY R. S. A Fast File System for UNIX. ACM Transactions on Computer Systems, Vol. 2C), pp. 181- 197, 1984. 96. MCNAMARA J. Technical Aspects of Data Communications, 2nd edition. Digital Press, Digital Equipment Corporation, Bedford, Massachusetts, 1998. 97. MCQUILLAN J. M., RICHER I., ROSEN E. The New Routing Algorithm for the ARPANET. IEEE Transactions on Communications, (COM-28), 711-719, 1980. 98. METCALFE R. M., BOGGS D. R. Ethernet: Distributed Packet Switching for Local Computer Networks. Communications of the ACM, 19G), 395-404, 1976. 99. MILLS D. L., BRAUN H-W. The NSFNET Backbone Network. Proceedings of ACM SIGCOMM '87, 1987. 100. MILLS D. L. Network Time Protocol, RFC 1305. University of Delaware, 1992. 101. MILLS D.L. On the chronology and metrology of computer network timescales and their application to the Network Time Protocol. ACM Computer Communications Review, 21, 5, 8-17, 1991. 102. MILLS D. L. Simple Network Time Protocol (SNTP), RFC 1361. University of Delaware, 1992. 103. MORRIS R. Fixing Timeout Intervals for Lost Packet Detection in Computer Communication Networks, Proceedings AFIPS National Computer Conference. AFIPS Press, Montvale, New Jersey, 1979. 104. MYERS J., ROSE M. Post Office Protocol - Version 3, RFC 1725. Carnegie Mellon, 1994. 105. NAGLE J. On Packet Switches With Infinite Storage. IEEE Transactions on Communications, Vol. COM-35:4, 1987. 106. NARTEN T. Internet Routing. Proceedings ACM SIGCOMM '89, 1989. 107. NEEDHAM R. M. System Aspects of the Cambridge Ring, Proceedings of the ACM Seventh Symposium on Operating System Principles, 82-85, 1979. 108. NEWMAN P., MINSHALL G., LYON T. L. IP Switching — ATM Under IP. IEEE Transactions on Networking, Vol. 6:2, 117-129, 1998. 109. OPPEN D., DALAL Y. The Clearinghouse: A Decentralized Agent for Locating Named Objects, Office Products Division. XEROX Corporation, 1981. 110. PARTRIDGE С Mail Routing Using Domain Names: An Informal Tour, Proceedings of the 1986 Summer USE NIX Conference. Atlanta, GA, 1986. 111. PARTRIDGE С Implementing the Reliable Data Protocol (RDP), Proceedings of the 1987 Summer USENIX Conference. Phoenix, Arizona, 1987. 112. PARTRIDGE С Gigabit Networking. Addison-Wesley, Reading, Massachusetts, 1994. 113. PARTRIDGE С, ROSE M.T. A Comparison of External Data Formats, IFIP International Working Conference on Message Handling Systems and Distributed Applications. Costa-Mesa, California, 1988. 114. PELTON J. Wireless and Satellite Telecommunications. Prentice-Hall, Upper Saddle River, New Jersey, 1995. 115. PERLMAN R. Interconnections: Bridges and Routers, 2nd edition. Addison- Wesley, Reading, Massachusetts, 2000. 572 Приложение 2. Список литературы
116. PETERSON L. Defining and Naming the Fundamental Objects in a Distributed Message System, Ph.D. Dissertation. Purdue University, West Lafayette, Indiana, 1985. 117. PETERSON L., DAVIE B. Computer Networks: A Systems Approach, 2nd edition. Morgan Kaufmann, San Francisco, CA, 1999. 118. PIERCE J. R. Networks for Block Switching of Data. Bell System Technical Journal, 51, 1972. 119. POSTEL J. B. Internetwork Protocol Approaches. IEEE Transactions on Communications, COM-28, 604-611, 1980. 120. POSTEL J. B. REYNOLDS J., Telnet Protocol Specification, STD 8, RFC 854, ISI, 1983. 121. POSTEL J. В., REYNOLDS J. Telnet Option Specification, STD 8, RFC 855, ISI, 1983. 122. POSTEL J. В., REYNOLDS J. Telnet Binary Transmission, STD 27, RFC 856. USC/Information Sciences Institute, 1983. \ 123. POSTEL J. В., REYNOLDS J. Telnet Echo Option, STD 28, RFC 857. USC/Information Sciences Institute, 1983. 124. POSTEL J. В., REYNOLDS J. Telnet Suppress Go Ahead Option, STD 29, RFC 858, USC/Information Sciences Institute, 1983. 125. POSTEL J. B. Echo Protocol, RFC 862. Network Information Center, SRI International, Menlo Park, CA, 1983. 126. POSTEL J. B. Daytime Protocol, RFC 867. Network Information Center, SRI International, Menlo Park, CA, 1983. 127. POSTEL J. В., HARRENSTIEN K. Time Protocol, RFC 868. Network Information Center, SRI International, Menlo Park, CA, 1983. 128. POSTEL J. В., SUNSHINE C. A., CHEN D. The ARPA Internet Protocol. Computer Networks, 1981. 129. PRESOTTO D. L., RITCHIE D. M. Interprocess Communication in the Ninth Edition SCO UNIX System, Software — Practice and Experience, Vol.'20:81, S1-3-S1-17, 1990. 130. QUARTERMAN J. S., HOSKINS J. C. Notable Computer Networks. Communications of the ACM, 29A0), 1986. 131. RAMAKRISHNAN K., JAIN R. A Binary Feedback Scheme For Congestion Avoidance In Computer Networks. ACM Transactions on Computer Systems, 8B), 158-181, 1990. 132. REYNOLDS J., POSTEL J., KATZ A. R., FINN G. G„ DESCHON A. L. The DARPA Experimental Multimedia Mail System. IEEE Computer, 1985. 133. REYNOLDS J., POSTEL J. Assigned Numbers, STD 2, RFC 1700. USC/Information Sciences Institute, 1994. 134. RITCHIE D. M. A Stream Input-Output System. AT&T Bell Laboratories Technical Journal, 63(8), 1987-1910, 1984. 135. RITCHIE D. M„ THOMPSON K. The UNIX Time-Sharing System, Communications of the ACM, 17G), 365-375, 1974; revised and reprinted in Bell System Technical Journal, 57F), 1905-1929, 1978. Системный вызов socket 573
136. ROSE M. The Internet Message: Closing The Book with Electronic Mail. Prentice-Hall, Upper Saddle River, New Jersey, 1993. 137. SALTZER J. Naming and Binding of Objects, Operating Systems, An Advanced? Course. Springer-Verlag, 99-208, 1978. 138. SALTZER J. Naming and Binding of Network Destinations, International Symposium on Local Computer Networks. IFIP/T.C.6, 311-317, 1982. 139. SALTZER J., REED D., CLARK D. End-to-End Arguments in System Design. ACM Transactions on Computer Systems, 2D), 277-288, 1984. 140. SHEPLER S. NFS Version 4 Design Considerations, RFC 2624. Sun Microsystems, Inc., 1999. 141. SHOCH J. F. Internetwork Naming, Addressing, and Routing. Proceedings of COMPCON, 1978. 142. SHOCH J. F., DALAL Y., REDELL D. Evolution of the Ethernet Local Computer Network. Computer, 1982. 143. SCHULZRINNE H., CASNER S., FREDERICK R. JACOBSON V. RTP: A Transport Protocol for real-time applications, RFC 1889, 1996. 144. SOLOMON J. Mobile IP: The Internet Unplugged. Prentice-Hall, Upper Saddle River, New Jersey, 1997. 145. SOLOMON M., LANDWEBER L., NEUHEGEN D. The CSNET Name Server. Computer Networks, F), 161-172, 1982. 146. SRINIVASAN R. RPC: Remote Procedure Call Protocol Specification Version 2, RFC 1831. Sun Microsystems, Inc., 1995. 147. SRINIVASAN R. XDR: External Data Representation Standard, RFC 1832. Sun Microsystems, Inc., 1995. 148. SRINIVASAN V., VARGHESE G. Fast Address Lookups Using Controlled Prefix Expansion. ACM Transactions on Computer Systems, vol. 17, 1-40, 1999. 149. STALLINGS W. Local and Metropolitan Area Networks. Prentice-Hall, Upper Saddle River, New Jersey, 1997. 150. STALLINGS W. High-Speed Networks: TCP/IP and ATM Design Principles. Prentice-Hall, Upper Saddle River, New Jersey, 1998. 151. STEVENS W. R. UNIX Network Programming, 2nd edition. Prentice-Hall, Upper Saddle River, New Jersey, 1998. 152. SUN MICROSYSTEMS. XDR: External Data Representation Standard, RFC 1014. Sun Microsystems, Inc., 1987. 153. SUN MICROSYSTEMS. RPC: Remote Procedure Call Protocol Specification, Version 2, RFC 1057. Sun Microsystems, Inc., 1988. 154. SWINEHART D., MCDANIEL G., BOGGS D. R. WFS: A Simple Shared File System for a Distributed Environment, Proceedings of the Seventh Symposium on Operating System Principles, 9-17, 1979. 155. TICHY W., RUAN Z. Towards a Distributed File System, Proceedings of Summer 84 USENIX Conference. Salt Lake City, Utah, 87-97, 1984. 156. TOMLINSON R. S. Selecting Sequence Numbers, Proceedings ACM SIGOPS/SIGCOMM Interprocess Communication Workshop, 11-23, 1975. 574 Приложение 2. Список литературы
157. VANBOKKELEN J. Telnet Terminal Type Option, RFC 1091. FTP Software, Inc., 1989. 158. WATSON R. Timer-Based Mechanisms in Reliable Transport Protocol Connection Management. Computer Networks, North-Holland Publishing Company, 1981. 159. WEINBERGER P. J. The UNIX Eighth Edition Network File System, Proceedings 1985 ACM Computer Science Conference, 299-301, 1985. 160. WELCH В., OSTERHAUT J. Prefix Tables: A Simple Mechanism for Locating Files in a Distributed System, Proceedings IEEE Sixth International Conference on Distributed Computing Systems, 1845-189, 1986. 161. WILKES M. V., WHEELER D. J. The Cambridge Digital Communication Ring, Proceedings Local Area Computer Network Symposium, 1979. 162. XEROX. Internet Transport Protocols, Report XSIS 028112, Xerox Corporation, California, 1981. 163. ZHANG, L. [August 1986], Why TCP Timers Don't Work Well, Proceedings of ACM SIGCOMM '86. Системный вызов socket 575
Предметный указатель А API-интерфейс, 35 ТЫ, 66 Windows Sockets, 66 сокетов, 66; 73 I IP-адрес, 45; 77 IP-маршрутизатор, 259; 261 W Web-броузер, 39; 40 Web-вещание, 29 А Автомат конечный, 399; 433 конечный для вывода в сокет, 419; 420 конечный для уточнения опции, 422 Авторизация, 39 Агентство АКРА, 73 Администратор сетевой, 89 системный, 40; 152; 161; 169; 352 Адрес INADDR_ANY, 469 локальной оконечной точки, 95 оконечной точки, 151 сервера, 88; 108 удаленной оконечной точки конкретный, 79 удаленной оконечной точки неопределенный, 79 Адресация косвенная, 338 Алгоритм LRU, 129 замены страниц, 129 параллельного сервера, 53; 133 параллельного сервера без установления логического соединения, 134 параллельного сервера с установлением логического соединения, 135 последовательного сервера, 130 последовательного сервера без установления логического соединения, 132 последовательного сервера с установлением логического соединения, 130 сервера концептуальный, 121 удаления записей с наиболее давним использованием, 129 Анализ синтаксический параметра адреса, 90 Архитектура CORBA, 303 однопроцессорная, 54 Атрибут файла NFS, 368 Аутентификация, 39; 103; 299 Б База данных распределенная, 40 Библиотека RTP общего назначения, 502 termcap, 448 termlib, 448 процедур, 104 эмуляции сокетов, 246 Бит М, 463 Р, 463 Блок управления передачей, 528; 532 Блокировка временная, 524 одновременная, 523 Брандмауэр, 259 Брокер объектных запросов, 303 Буфер buf, 237
воспроизведения, 464 флуктуационный, 464; 490 циклический, 475 в Ввод/вывод асинхронный, 62; 122; 179; 187; 188; 232 Версия распределенная прикладной программы, 311 Взаимодействие типа клиент/сервер, 25 Взаимозаменяемость функций read и recv, 81 функций write и send, 81 Видеокамера, 459 Владелец файла, 354 Воспроизведение отсроченное, 464 Время жизни сегмента максимальное, 158 координированное всемирное, 111 кругового обращения, 112 обработки запроса сервером, 130 отклика наблюдаемое, 130; 162 Вызов процедуры вложенный, 288 процедуры дистанционный, 283; 284; 286; 287 процедуры локальный, 288 системный, 67 системный accept, 533 системный bind, 535 системный close, 536 системный connect, 536 системный execve, 61 системный fork, 63; 537 системный gethostid, 540 системный gethostname, 540 системный get peer name, 541 системный getsockname, 543 системный getsockopt, 544 системный gettimeofday, 545 системный listen, 546 системный read, 70; 546 системный recv, 547 системный recvfrom, 548 системный recvmsg, 549 системный select, 62; 550 системный send, 551 системный sendmsg, 552 системный sendto, 553 системный sethostid, 554 системный setsockopt, 554 системный shutdown, 97; 556 системный socket, 556 системный wait3, 166 системный write, 70; 557 функции accept блокирующий, 162 функции recvfrom, 133 функции select, 143 Выполнение параллельное, 51; 55 программы параллельное, 170 процессов параллельное, 51 Вычисления распределенные, 51 г Генератор заглушек, 307 программ, 307 Глагол DO, 410 DONT, 410 TTJ3END, 423 WILL, 410 WONT, 410 Год високосный, 152 Группа IETF, 378 инженерная проектирования Internet, 378 многоадресная, 468 рассылки, 468 Д Дейтаграмма UDP, 70; 467 Демаршалинг, 299 Демон, 345; 506 Демультиплексирование, 469 Предметный указатель 57
Дескриптор ведомого сокета, 184 ведущего сокета, 184 свежий, 372 сокета, 75; 232 устаревший, 372 файла, 75; 232; 372 файла разделяемый, 170 Диспетчер блокировки сетевой, 396 Дистрибутив программный BSD, 73 Доставка надежная, 123 ненадежная, 124 Доступ к данным дистанционный, 30 к устройству дистанционный, 367 к файлам дистанционный, 50 з Завершение дочернего процесса полное, 166 Заглушка клиентская, 333 связи, 312 связи клиентская, 338 серверная, 335 Загрузка системы начальная, 161; 169 Задача проектирования параллельного сервера, 53 Задержка флуктуационная, 490 Закрытие сокета явное, 157 Запись активизации процедуры, 56 десятичная точечная, 90 учетная root, 208 учетная пользователя, 354 Запуск ведущего серверного процесса автоматический, 161 Защита, 39; 103 данных, 39 и Идемпотентный, 47 Идентификатор группы числовой, 354 пользователя, 54 пользователя числовой, 354 Идентификация клиента, 45 по оконечной точке, 45 Имя доменное, 88 файла составное полное, 360 Инициатор установления соединения, 48 Инкапсуляция, 245 Интерпретатор командный, 60 Интерфейс ТЫ, 26 ТЫ компании AT&T, 71 прикладного программирования, 35 системного вызова, 67 сокетов, 26; 66; 71; 73; 246 Информация о состоянии, 43; 127 Исчерпание ресурсов, 526 к Канал виртуальный TCP, 242 виртуальный Х.25, 242 именованный, 78 коммутируемый, 247 Каталог /etc/rc.d, 506 запуска, 506 корневой, 360 Квантование времени, 58; 179 Квитирование трехэтапное, 101; 125; 157 Клиент, 39 DAYTIME, 159 TCP для службы DAYTIME, 109 TCP для службы ECHO, 114 578 Предметный указате
TELNET, 41; 399 UDP для службы ECHO, 116 UDP для службы TIME, 112 UDPtime, 152 без установления логического соединения, 98 параллельный, 230 параллельный однопотоковый, 233 с установлением логического соединения, 93 службы дистанционного доступа, 40 службы передачи файлов, 40 электронной почты, 40 Ключ магический, 375 Код ошибки NFS3ERRJSDIR, 384 NFS3ERR_NOTDIR, 384 Кодировка NVT, 438; 442; 445; 448 РСМ, 470 выборок, 469 кадров, 470 Команда DATA MARK, 448 DO, 436 DONT, 436 finger, 36 stty, 431 telnet, 30 TELNET SYNCH, 448 Комната чата, 29 Компания AT&T, 26; 66 Digital Equipment Corporation, 73 Microsoft, 26; 66; 74 Sun Microsystems, 73; 274; 289; 291; 302; 352; 377; 378 Tektronix, 73 Комплект инструментальных средств RTP Audio Tool, 504 Компьютер многопроцессорный, 55; 170 однопроцессорный, 55; 170 Константа CCOUNT, 237 INADD$_ANY, 131; 147 INTERVAL, 177 LJSET, 357 0_CREAT, 356 0_RDWR, 356 PTHREADJV[UTEX_INITIALIZER, 172 QLEN, 191 RTP_JITTHRESH, 491 RTPJV1AXMISORDER, 486 SOMAXCONN, 165 UNIXEPOCH, 151 XDRJDECODE, 279 символическая AF_INET, 77 символическая DICTSIZE, 329 символическая LOCKF, 513 символическая MAXWORD, 328; 329 символическая QLEN, 165 символическая RMACHINE, 343 символическая SOCKJDGRAM, 84 символическая SOCKJSTREAM, 84 Конструкция case, 323 ifdef препроцессора С, 273 Конфигурация динамическая, 207 статическая, 207 Курс учебный по программированию сокетов, 26 учебный по распределенным вычислениям, 26 л Линеаризация, 299 Линия телефонная коммутируемая, 247 м Макрокоманда FD_CLR, 184 FDJSSET, 201 FDJSET, 184; 191; 237 FD_ZERO, 184 tv21in, 485; 486 Маршалинг, 299 Маршрутизатор, 243 Маска umask, 510 Предметный указатель 579
Массив fd2sv, 201 hname, 237 sostab, 422 svent, 201 ttstab, 418 двухмерный, 322 структур fsm__trans одномерный, 416 Матрица битов защиты, 355 переходов, 430 Метод воспроизведения с пропусками, 461 доступа без установления логического соединения, 122 доступа с установлением логического соединения, 122 исправления ошибок опережающего, 461 многопроцессорной обработки, 51 оптимизации работы сервера, 227 отсроченного создания ведомых процессов, 226 предварительного создания ведомых процессов, 226 разделения времени, 51 Микрофон, 459 Минимизация переключения контекста, 62 Модель RPC, 283 взаимодействия типа клиент/сервер, 35; 37; 38 дистанционного вызова процедур, 283 клиент/сервер, 37 процедурная выполнения программы, 286 Монитор, 177 сетевой, 102 Мьютекс, 172; 227; 482 stats.st_mutex, 178 stm.stm__qmutex, 488 н Набор протоколов TCP/IP, 30; 122 Направление активизации взаимодействия, 39 Номер порта протокола, 77; 145 порта протокола клиента, 45 Носитель физический, 358 о Обеспечение программное промежуточное, 283 Область применения сервера, 138 функций close и shutdown, 102 Обработка параллельная, 34; 53; 122 Обработчик сигнала revurg, 448 Объявление extern, 330 Ограничение на число возможных системных вызовов, 71 Ожидание круговое, 523 Оператор define, 329 if, 61; 108 include, 84 return, 286; 494 switch, 316; 322 условный, 60 Операция блокирующая, 528 завершения работы, 45 чтения каталога READDIR, 385 Опция TELNET, 410 termtype, 440 типа терминала termtype, 442 Организация NBS, 299 обмена данными дейтаграммная, 124 обмена данными потоковая, 123 работы параллельная, 142 работы псевдопараллельная, 137 Отключение от управляющего терминала, 509 Отличия процесса от программы, 54 580 Предметный указате.
Отсутствие управления потоком данных, 124 Отчет RTCP, 467 \ получателя, 491; 499 получателя RTCP, 489 Очередь событий, 476 п Пакет RTCP, 465 RTP, 462; 489 Память разделяемая, 170 Параметр dgram, 208 inproc, 280 internal, 209 localhost, 238 npwait, 208 outproc, 280 stream, 208 tcp, 208 udp, 208 wait, 208 WNOHANG, 167 заглушки связи, 338 локальной процедуры, 338 Параметризация клиентской программы полная, 41 Пароль магический, 375 Перевод сокета в пассивный режим, 132 Передача аудиоинформации по протоколу RTP, в прямом эфире, 460 дуплексная, 123 от многих отправителей к одному получателю, 468 от многих отправителей ко многим получателям, 468 от одного отправителя ко многим получателям, 468 повторная, 125 повторная автоматическая, 42 с управлением потоком данных, 123 стереосигнала, 461 файлов, 30 Перезапуск компьютерной системы, 46 Переключение контекста, 62; 170 процессора, 51 Переменная ccount, 237 doecho, 436 maxfd, 237 retaddr, 265 stm.stm_clky, 494 stm.stm_inactive, 499 stm.stm__probation, 486 глобальная, 55 глобальная last_char, 448 глобальная noga, 437 глобальная nwords, 322 глобальная oldtty, 405; 406 глобальная option_cmd, 438; 442 глобальная rcvbinary, 438 глобальная scrindex, 453 глобальная synching, 428 глобальная term, 444 глобальная разделяемая, 170 индексная, 55 локальная tntty, 405 локальная приватная, 170 условная, 173 условная stm.stm_rcond, 495 целочисленная глобальная portbase, 146 Подавление символов GA, 437 Поле From:, 264 PTYPE, 462 stm__qhead, 482 stm_qmutex, 482 stm_qtail, 482 Subject:, 264 sv_func, 201 To:, 266 VER, 462 Пользователь root, 521 Помехи фоновые, 462 Порт протокола 13, 109 протокола 37, 112 Предметный указатель 581
протокола привилегированный, 88 службы ECHO, 102 Порядок байтов локального хоста, 83 сетевой, 82 стандартный сетевой, 273 Поток EVENT, 472 TIMER, 472 ведомый, 134; 180 ведущий, 134; 161 выполнения, 54; 122 данных, 459 Почта электронная, 30; 40 Право на выполнение поиска в каталоге, 357 на выполнение файла, 355 на запись файла, 355 на чтение файла, 355 Предотвращение запуска нескольких копий сервера, 512 Представление данных ASN.1, 282 данных XDR, 274; 281 данных внешнее, 274 данных внутреннее, 65; 272 данных симметричное, 400 компактное, 422 символьное NVT, 400 целых чисел со старшим байтом в начале, 82 Преобразование данных асимметричное, 272 данных симметричное ,274 Привязка портов динамическая, 295 Приложение gnutella, 24 infrasearch, 24 napster, 24 slirp, 25 заказное, 40 нестандартное, 40 потоковое, 29 типа клиент/сервер, 24; 28; 244 Примитив потока POSIX, 64 Принцип организации взаимодействия типа клиент/сервер, 38 параллельной работы, 51 передачи сообщений, 71 Приоритет debug, 519 Проблема быстрого роста числа вариантов преобразования, 273 дополнительного участка маршрута, 259 не полностью завершившихся процессов, 166 ненадежной доставки сообщений, 46 согласования условий соединения, 38 Проверка данных, 42 Программа /bin/mail, 265 bash, 270 daytimed, 191 dbms, 292 DBXprog, 292 diet, 323 FTP, 36; 263 klmprog, 292 mount, 292 netdbms, 292 nlmprog, 292 perfmeter, 292 ping, 532 quota, 292 remote__exec, 292 rex, 292 RPC, 213 rpegen, 313; 317 rquota, 292 rquotaprog, 292 rstat, 292 rup, 292 rwall, 292 sendmail, 270 showmount, 292 shutdown, 292 slocal, 266 spray, 292 TCPtecho, 237 UDPtime, 114 UDPtimed, 150 unify, 292 582 Предметный указатель
yppasswd, 292 вспомогательная rpcinfо, 303 клиентская, 39; 433 клиентская TELNET, 41 нераспределенная, 284 прикладная параллельная, 34 распределенная, 523 серверная, 39 управления параллельным вводом/выводом, 62 Пространство адресное, 53 Протокол FTP, 49; 261; 267; 269 HTTP, 40 ICMP, 49 NFS, 25; 35; 49; 382; 396 NLM, 396 NTP, 118; 151; 467 POP, 40; 258 PPP, 247 RLOGIN, 430 RTCP, 465; 479; 491 RTP, 25; 35; 462; 468; 478; 479 SLIP, 247 SMTP, 40; 49; 92; 258 SNA, 241 TCP, 41; 88; 101; 103; 123; 154; 187; 189; 468; 526 TCP/IP, 24 TELNET, 26; 30; 35; 40; 41; 49; 267; 399; 433 TIME, 111 UDP, 41; 88; 103; 123; 151; 187; 189; 467; 526 X.25, 241 идемпотентный, 50 компьютерной связи, 37 монтирования, 376; 391; 396 объединенной сети, 24 передачи данных в реальном времени, 461 передачи файлов, 261 пользовательских дейтаграмм, 41 потоковой передачи, 459 прикладной нестандартный, 30 прикладной стандартный, 30; 40 сетевой синхронизирующий, 118; 467 транспортный, 124 транспортный RTP, 467 транспортный без установления логического соединения, 26; 41; 42 транспортный передачи в реальном времени, 25 транспортный передачи данных в реальном времени, 462 транспортный с установлением логического соединения, 26; 41; 126; 139; 154; 161 управления RTP, 465 управления передачей, 41 Прототип процедуры, 108 Процедура bind, 131 callrpc, 307 cerrexit, 408 clnt_create, 307 closelog, 519 connectsock, 105; 106; 108 connectTCP, 105; 237 connectUDP, 105 dcon, 407; 451 deletew, 323; 330 deletew_l, 330 do_echo, 434 do_noga, 437 do_notsup, 436 do_txbinary, 438 doTCP,201 fsmbuild, 423 fsminit, 419; 423 getportbyname, 131 getprotobyname, 107 insertw, 323 insertw_l, 330 listen, 132 lookupw, 323; 324 memcpy, 108 memset, 108 MOUNTPROC3JDUMP, 393 MOUNTPROC3_EXPORT, 393; 394 MOUNTPROC3JV1NT, 393; 394 MOUNTPROC3_NULL, 393 MOUNTPROC3JJMNT, 393 MOUNTPROC3_UMNTALL, 394 nextin, 322 NFS, 386 - NFSPROC3_ACCESS, 388 NFSPROC3_COMMIT, 390 NFSPROC3_CREATE, 388 Предметный указатель 583
NFSPR0C3_FSINF0, 390 NFSPROC3_FSSTAT, 390 NFSPROC3_GETATTR, 387 NFSPROC3_LINK, 389 NFSPROC3_LOOKUP, 388 NFSPROC3_MKDIR, 388 NFSPROC3_MKNOD, 389 NFSPROC3JNrULL, 387 NFSPROC3_PATHCONF, 390 NFSPROC3JREAD, 388 NFSPROC3_READDIR, 389 NFSPROC3JREADDIRPLUS, 390 NFSPROC3_READLINK, 388 NFSPROC3JREMOVE, 389 NFSPROC3_RENAME, 389 NFSPROC3_RMDIR, 389 NFSPROC3_JSETATTR, 387 NFSPROC3_SYMLINK, 388 NFSPROC3_WRITE, 388 no__op, 434 openlog, 519 passivesock, 145 passiveTCP, 145; 153; 165; 191 passiveUDP, 145; 191 playaudio, 494 pmap_unset, 315 prstats, 177 pthread_cond_signal, 491 rcvurg, 427 reader, 238 READUNK, 386 recopt, 434 rtcpcycle, 494; 499 rtcpheader, 500 rtcpinterval, 501 rtcprecv, 498 rtcpsendbye, 499 rtpinit, 492; 494 rtpnewdata, 490; 491; 494; 495 rtprecv, 489; 490; 492 rtpupdate, 491 rtpupdateseq, 486 scrgetc, 452 scrinit, 452; 453 scrwrap, 452; 454; 455 SEND, 67 shutdown, 429 soputc, 444 sowrite, 449 status, 457 subend, 444 subopt, 442 subtermtype, 423; 443 syslog, 519 tcdm, 427 tcout, 448 TCPdaytimed, 157 TCPechod, 165; 168; 174 TCPtecho, 237 tcsetattr, 436 telnet, 427; 449 tgetstr, 448 ttputc, 447; 448 ttwrite, 428; 431 ttysetup, 404; 427 unscript, 456 will_notsup, 439 will_termtype, 440 writer, 238 xdr_example, 333 xdr_int, 277 xdrmem__create, 277; 280 xdrrec_create, 281 xdrstdio_create, 280 xputc, 447; 448 библиотечная clnt_create, 343 библиотечная ctime, 114 библиотечная gethostbyname, 91 библиотечная getprotobyname, 92 библиотечная getservbyname, 92 библиотечная inet__addr, 91 библиотечная XDR, 276 интерфейсная клиентская, 337 интерфейсная серверная, 339 исполнительная, 433 клиентская, 311 локальная, 308 пустая NULL, 386 серверная, 311 Удаленная, 308 Процедура-заглушка, 305 Процесс, 53; 54 init, 507 ведомый, 166 дочерний, 166; 358 многопотоковый, 62; 170 однопотоковый, 60; 62; 137; 162; 163; 171 родительский, 358; 514 серверный, 506 системный init, 221 Процесс-зомби, 166 584 Предметный указатель
Процессор, 286 Пул буферный, 485 р Различие между активным и пассивным сокетами, 77 Разупорядочение, 299 Распределение пассивного сокета TCP, 153 Рассылка групповая, 42; 88; 126; 468 широковещательная, 42; 88; 126 Режим доступа к файлу, 355 защиты, 355 многозадачный с вытеснением, 170 файла, 355 фоновый, 506 Резервирование оперативное, 40 Реконфигурация динамическая, 207 Руководство по операционной системе, 36 с Сбор данных в реальном времени дистанционный, 40 Связь двухсторонняя, 468 двухточечная, 123; 126 одноранговая, 37 прямая, 37 речевая, 40 Сеанс RTP, 468 дистанционного доступа, 30 приема звуковой трансляции, 476 Сегмент SYN, 268 Секретность, 39 Секунда високосная, 152 Семафор, 173; 482 Семейство адресов, 77 адресов AF_UNIX, 78 протоколов, 74 протоколов Internet, 74 протоколов PF_INET, 76; 93 Сервер, 39 DAYTIME, 154; 159 ECHO параллельный, 163 NFS, 368 syslogd, 519 TIME, 149 UDPtimed, 152 без установления логического соединения, 122; 125 ведущий, 161 действующий в качестве клиента, 47 монтирования, 391 мультипротокольный, 103; 141; 188 мультипротокольный последовательный, 188 мультисервисный, 103; 141 мультисервисный без установления логического соединения, 196 мультисервисный однопотоковый, 198 не поддерживающий состояние, 43; 126; 374 однопотоковый, 181 параллельный, 122 параллельный без установления логического соединения, 135; 139 параллельный с установлением логического соединения, 135; 138; 140; 179 параллельный службы ECHO, 179 поддерживающий состояние, 43; 126 последовательный, 122; 130 последовательный без установления логического соединения, 132; 139; 156 последовательный с установлением логического соединения, 131; 139; 154; 156 почтовый SMTP, 92 с установлением логического соединения, 122; 124; 135; 142 службы DAYTIME, 154 службы времени, 529 файловый, 39; 43; 44; 529 Сериализация, 299 Предметный указатель 585
Сеть ATM, 242 Frame Relay, 242 X.25, 243; 256 локальная, 36; 42; 100; 126; 139; 231 локальная Ethernet, 242; 292 объединенная, 29; 42; 126 объединенная TCP/IP» 241 распределенная X.25, 243 Сигнал HANGUP, 514 SIGCHLD, 167 SIGHUP, 208 SIGKILL, 514 аналоговый непрерывный, 459 Символ KCDCON, 451 TCDO, 434 TCDONT, 434 TCIAC, 413; 433 TCWILL, 433 TCWONT, 433 возврата каретки, 159 зарезервированный IAC, 409 перевода строки, 159 Синхронизация потоков, 466 Система NFS, 224; 292; 352; 377; 381; 530; 532 NIS, 292 syslog, 517 X Window, 532 ур, 292 базы данных, 40 виртуальной памяти, 129 доменных имен, 89 компьютерная многопользовательская, 51 операционная, 38; 39; 49; 51; 53; 56; 62; 63 операционная Berkeley UNIX, 71 операционная BSD UNIX версии 4.3, 63 операционная Linux, 25; 74; 166; 220; 262; 277; 397; 402; 485 операционная UNIX, 49; 56; 262; 397 операционная UNIX версии System V, 66 операционная Windows, 26; 368 операционная Xinu, 63 операционная многозадачная, 52 параллельного программирования, 56 поддержки выполнения программ, 56 с открытым исходным кодом, 24 файловая UNIX, 354; 378 файловая локальная, 368 файловая сетевая, 49; 228; 289; 292; 352; 381 электронной почты, 253 Ситуация тупиковая, 140; 233; 523 г Слово ключевое string, 384 Служба CHARGEN, 204 DAYTIME, 109; 154; 185; 187; 192; 195;204 ECHO, 114; 131; 161; 182; 195; 204; 231 FINGER, 32; 213 SOCKJSTREAM, 93; 94 TELNET, 41 TIME, 111; 149; 195; 204; 231 генератора символов, 103 дейтаграммная, 84 динамической привязки портов, 295 дневного времени, 32 передачи файлов, 103; 352 потоковая, 84; 111; 459 привязки портов, 292; 296; 297; 303 привязки портов RPC, 296 прикладная локально определенная, 40 прикладная нестандартная, 40 прикладного шлюза, 258 стандартная, 40 Соблюдение границ между записями при передаче, 111 Согласование опции, 410 Соединение телефонное коммутируемое, 25 Создание активного сокета, 145 ведомых процессов отсроченное» 224 параллельных потоков предварительное, 219 параллельных процессов предварительное, 219 пассивного сокета, 145 потока динамическое, 169 Сокет, 66; 75; 85 UDP неподключенный, 98 UDP подключенный, 98 586 Предметный указатель
активный, 77 ведущий, 157 дейтаграммный, 147 пассивный, 77; 184 потоковый, 147 разделяемый, 220 Сокеты, 66 Сокращение TCANY, 412 Сообщение WILL, 436 WONT, 436 типа CALL, 298 типа REPLY, 298 Состояние KSLOCAL, 451 KSREMOTE, 451 SSJTERMTYPE, 423 TSDATA, 413; 444 TSIAC, 413; 433 TSSUBIAC, 413 TSSUBNEG, 413; 442 TSWOPT, 433 Спецификация API-интерфейса неформальная, 66 DCE RPC, 303 NFS, 353; 377; 383 ONC RPC, 289; 302 RTCP, 499 интерфейса концептуальная, 67 конечного автомата, 409 протокола RTP, 462 Способ обеспечения параллельного доступа, 51 обеспечения параллельной работы сервера, 136 представления данных, 272 структуризации программ связи, 88 Среда мультипротокольная, 241 разнородная, 252 Средства компенсации неравномерной задержки, 460 контроля доступа, 39 Ссылка, 361 жесткая, 361 Стандарт 1003.1с, 169 DES, 299 G.711,470 MPEG, 479 MPEG-2, 479 POSIX, 482 SMTP, 253 X.400, 253 внешнего представления данных, 271 внешнего представления данных XDR, 302 потоков POSIX, 169 протокола DAYTIME, 118 протокола ECHO, 118; 167 протокола NFS, 383 протокола TIME, 118; 151 протокола монтирования, 391 языка ANSI С, 108 Стек, 56 Степень распараллеливания допустимая, 216 Структура diropargs3, 385 fattr3, 368; 384 fsm_trans, 415; 418 hostent, 91 ¦{ nfstime3, 384 protoent, 92 rblock, 496 READUNK3resok, 386 rr, 496 rtcp, 496 rtp, 482 session, 482 sockaddr, 78; 79 sockaddr_in, 78; 79; 90; 108; 131; 147 sr, 496 stat, 384 stats, 177 status, 167 stream, 482 termios, 404; 406 tim, 486 timeval, 486 WRITE3args, 386 имени файла иерархическая, 359 Сумма контрольная данных, 42 Суперсервер, 200 inetd, 208; 212 Предметный указатель 587
Сценарий /usr/local/bin/rfc, 265 rfc, 263 командного интерпретатора, 71; 264 т Таблица дескрипторов файлов, 75 маршрутизации, 243; 259 Терминал виртуальный сетевой, 399 локальный, 433 Технология ONC RPC, 381 сетевая, 24 Тип fhandle3, 384 Тип службы SOCKJSTREAM, 76 Точка связи оконечная, 77 оконечная локальная, 66 оконечная удаленная, 66 Трансляция условная, 273 Туннелирование, 25; 245; 255; 268 на прикладном уровне, 246 Туннель, 255 транспортного уровня, 256 Тупик активный, 530 У Узел индексный, 360 Указатель команд, 54 Университет Калифорнийский, 66 Упорядочение, 299 Управление временем неявное, 474 временем явное, 474 оборудованием дистанционное, 40 потоком данных, 42 устройствами дистанционное, 40 Уровень приоритета LOGJDEBUG, 518 LOG_EMERG, 518 Установление соединения надежное, 123 Устройство /dev/audio, 504 /dev/tcp, 71 блочное, 366 ввода/вывода, 366 звуковое Linux, 504 символьное, 366 Утилита awk, 264 lint, 494 make, 346 Ф Файл .forward, 266 /dev/null, 511 /etc/inetd.conf, 208 /etc/services, 210 /etc/syslog.conf, 519 /pub/inetd_daytimed ,211 Makefile, 346 rfcd, 264 блокировки, 512 блочный специальный, 367 включаемый, 85 конфигурации, 207 конфигурации системы syslog, 519 почтового ящика, 258 символьный специальный, 367 специальный системы UNIX, 366 спецификации .maildelivery, 266 спецификации rpcgen, 328 Флуктуация, 460; 464 Формат ALAW, 479 МРЗ, 25 rply_body, 298 ULAW, 479 XDR, 289 обобщенный адреса оконечной точки, 77 Формат слова оканчивающийся младшим байтом, 271 оканчивающийся старшим байтом, 271 588 Предметный указатель
Формирование адреса ответа сервером, 132 Функционирование параллельное клиентских программ, 52 Функция accept, 81; 157; 165; 174; 182; 197; 221; 222 AUTH_NONE протокола RPC, 394 bind, 81 close, 69; 73; 80; 157; 166; 192; 508 connect, 79; 95; 133 connectTCP, 427 errexit, 108; 166; 406 execve, 137; 200 exit, 166; 171; 494; 507 fdopen, 280; 455 fflush, 280; 429 flock, 228; 359; 513 fork, 165; 169; 200; 201; 220; 226; 358; 506 fputs, 444 fsmbuild, 406 getdtablesize, 508 getenv, 448 gethostbyname, 101; 102 getservent, 102 gettimeofday, 498 htonl, 83; 151 htons, 83; 279 ioctl, 73; 509 listen, 81; 152; 159 lockf, 359 lseek, 73; 357 mount, 363 ntohl, 83; 114 ntohs, 83; 279 open, 69; 73; 75; 356; 509; 513; 521 printf, 108; 519 pthread_attr__init, 178 pthread_cond_broadcast, 174 pthread__cond_jsignal, 174; 495 pthread_cond_wait, 173 pthread_create, 169 pthread_exit, 172 pthread_mutex_init, 172 pthread_mutex_lock, 172; 222; 488 pthread_mutex_unlock, 172; 222; 488 putc, 444; 445 read, 73; 111; 114; 141; 166; 180; 198; 280; 356; 428 reaper, 167 recv, 80; 111; 141; 246 recvfrom, 150; 152; 188; 223 rename, 521 sched_yield, 170 select, 181; 191; 197; 198; 201; 232; 402 sem_init, 173 sem_post, 173 sem_wait, 173 send, 80; 133; 138; 141; 246 sendto, 151; 188; 223 shutdown, 238 signal, 167 socket, 79; 95; 246 sowrite, 406 sprintf, 514 suspend, 408 svc_sendreply, 315 time, 157 ttwrite, 406 vfprintf, 108 wait, 507 write, 73; 133; 138; 141; 166; 180; 198; 238; 280; 514 библиотечная ctime, 157 библиотечная getenv, 440 библиотечная gethostbyaddr, 538 библиотечная gethostbyname, 171; 539 библиотечная getprotobyname, 542 библиотечная getservbyname, 153; 542 ввода/вывода Linux, 69 ввода/вывода UNIX, 73 координации, 170 обработчика службы sv_func, 201 синхронизации, 170 системная chdir, 509 системная creat, 521 системная dup, 512 системная fork, 57; 137 системная fstat, 456 системная getdtablesize, 184 системная Linux, 485 системная signal, 427; 515 системная sigvec, 515 системная socket, 75 системная stat, 361 системная tcsetattr, 403 системная time, 151 системная wait, 514 системная wait3, 221 системная синхронная read, 142 системная синхронная recv, 142 Предметный указатель 589
системная синхронная send, 142 системная синхронная write, 142 х Характеристика потока данных, 460 Хост почтовый, 89 ч Часы аппаратные, 86 Число ссылок на сокет, 80 ш Шаблон INADDR_ANY, 131 Шлюз IP, 242 почтовый, 252 прикладного уровня, 25 прикладной, 252; 269 прикладной slirp, 266 Шум белый, 462 Э Эмуляция нулевых байтов, 358 Эпоха Internet, 114; 151 Linux, 114; 151 Эхо-повтор, 434 я Язык XDR, 297 программирования процедурно- ориентированный, 56 сценариев Perl, 71; 270 Ящик почтовый, 258 590 Предметный ук