Тем не менее последнее слово в вопросе транспортных интерфейсов, скорее всего, останется не за сокетами. Довольно часто приложениям приходится работать с группой связанных потоков — так, например, браузер может одновременно запрашивать у сервера несколько объектов. В таком случае применение сокетов обычно означает, что для каждого объекта будет использоваться один поток, и в результате управление перегрузкой будет выполняться отдельно для каждого потока (а не для всей группы), что, безусловно, является далеко не оптимальным вариантом, поскольку управление набором потоков ложится на плечи приложения. Чтобы более эффективно обрабатывать группы связанных потоков и уменьшить роль приложения в этом процессе, были созданы новые протоколы и интерфейсы. В качестве примеров приведем SCTP (Stream Control Transmission Protocol — протокол передачи с управлением потоками), описанный в RFC 4960, и SST (Structured Stream Transport — иерархическая поточная транспортировка данных) (Ford, 2007). Эти протоколы слегка изменяют сокет-API для удобства работы с группами потоков, обеспечивая ряд новых возможностей, таких как работа со смешанным трафиком (с установлением соединения и без) и даже поддержка множественных сетевых путей. О том, насколько они успешны, мы узнаем по прошествии некоторого времени.
6.1.4. Пример программирования сокета: файл-сервер для Интернета
Чтобы узнать, как выполняются настоящие вызовы для сокета, рассмотрим программу, демонстрирующую работу клиента и сервера, представленную в листинге 6.1. Имеется примитивный файл-сервер, работающий в Интернете и использующий его клиент.
У программы много ограничений (о которых еще будет сказано), но в принципе данный код, описывающий сервер, может быть скомпилирован и запущен на любой UNIX-системе, подключенной к Интернету. Код, описывающий клиента, может быть запущен с определенными параметрами. Это позволит ему получить любой файл, к которому у сервера есть доступ. Файл отображается на стандартном устройстве вывода, но, разумеется, может быть перенаправлен на диск или какому-либо процессу.
Листинг 6.1. Программы использования сокетов для клиента и сервера
Рассмотрим сперва ту часть программы, которая описывает сервер. Она начинается с включения некоторых стандартных заголовков, последние три из которых содержат основные структуры и определения, связанные с Интернетом. Затем SERVER_PORT определяется как 12345. Значение выбрано случайным образом. Любое число от 1024 до 65535 подойдет с не меньшим успехом, если только оно не используется каким-либо другим процессом; порты с номерами 1023 и ниже зарезервированы для привилегированных пользователей.
В последующих двух строках определяются две необходимые серверу константы. Первая из них задает размер блока данных для передачи файлов (в байтах). Вторая определяет максимальное количество незавершенных соединений, после установки которых новые соединения будут отвергаться.
После объявления локальных переменных начинается сама программа сервера. Вначале она инициализирует структуру данных, которая будет содержать IP-адрес сервера. Эта структура будет связана с серверным сокетом. Вызов memset полностью обнуляет структуру данных. Последующие три присваивания заполняют три поля этой структуры. Последнее из них содержит порт сервера. Функции htonl и htons занимаются преобразованием значений в стандартный формат, что позволяет программе нормально выполняться как на машинах с представлением числовых разрядов little-endian (например, Intel x86), так и с представлением big-endian (например, SPARC). Детали их семантики здесь роли не играют.
После этого сервером создается и проверяется на ошибки (определяется по s < 0) сокет. В окончательной версии программы сообщение об ошибке может быть чуть более понятным. Вызов setsockopt нужен для того, чтобы порт мог использоваться несколько раз, а сервер — бесконечно, обрабатывая запрос за запросом. Теперь IP-адрес привязывается к сокету и выполняется проверка успешного завершения вызова bind. Конечным этапом инициализации является вызов listen, свидетельствующий о готовности сервера к приему входящих вызовов и сообщающий системе о том, что нужно ставить в очередь до QUEUE_SIZE вызовов, пока сервер обрабатывает текущий вызов. При заполнении очереди прибытие новых запросов спокойно игнорируется.
В этом месте начинается основной цикл программы, который никогда не покидается. Его можно остановить только извне. Вызов accept блокирует сервер на то время, пока клиент пытается установить соединение. Если вызов завершается успешно, accept возвращает дескриптор (описатель) сокета, который можно использовать для чтения и записи, аналогично тому, как файловые дескрипторы (описатели) могут применяться для чтения и записи в каналы. Однако, в отличие от однонаправленных каналов, сокеты двунаправлены, поэтому для чтения (и записи) данных из соединения можно использовать sa (принятый сокет). Файловые дескрипторы канала могут использоваться для чтения или записи, но не для того и другого одновременно.
После установления соединения сервер считывает имя файла. Если оно пока недоступно, сервер блокируется, ожидая его. Получив имя файла, сервер открывает файл и входит в цикл, который читает блоки данных из файла и записывает их в сокет. Это продолжается до тех пор, пока не будут скопированы все запрошенные данные. Затем файл закрывается, соединение разрывается и начинается ожидание нового вызова. Данный цикл повторяется бесконечно.
Теперь рассмотрим часть кода, описывающую клиента. Чтобы понять, как работает программа, необходимо вначале разобраться, как она запускается. Если она называется client, ее типичный вызов будет выглядеть так:
client flits.csvu.nl /usr/tom/filename >f
Этот вызов сработает только в том случае, если сервер расположен по адресу flits. cs.vu.nl, файл /usr/tom/filename существует и у сервера есть доступ по чтению для этого файла. Если вызов произведен успешно, файл передается по Интернету и записывается на место f, после чего клиентская программа заканчивает свою работу. Поскольку серверная программа продолжает работать, клиент может быть запущен снова с новыми запросами на получение файлов.
Клиентская программа начинается с подключения файлов и объявлений. Работа начинается с проверки корректности числа аргументов (argc = 3 означает, что в строке запуска содержалось имя программы и два аргумента). Обратите внимание на то, что argv[1] содержит имя сервера (например, flits.cs.vu.nl) и переводится в IP-адрес с помощью gethostbyname. Для поиска имени функция использует DNS. Мы будем изучать технологию DNS в главе 7.
Затем создается и инициализируется сокет, после чего клиент пытается установить TCP-соединение с сервером посредством connect. Если сервер включен, работает на указанной машине, соединен с SERVER_PORT и либо простаивает, либо имеет достаточно места в очереди listen (очереди ожидания), то соединение с клиентом будет рано или поздно установлено. По данному соединению клиент передает имя файла, записывая его в сокет. Количество отправленных байтов на единицу превышает требуемое для передачи имени, поскольку нужен еще нулевой байт-ограничитель, с помощью которого сервер может понять, где кончается имя файла.
Теперь клиентская программа входит в цикл, читает файл блок за блоком из сокета и копирует на стандартное устройство вывода. По окончании этого процесса она просто завершается.
Процедура fatal выводит сообщение об ошибке и завершается. Серверу также требуется эта процедура, и она пропущена в листинге только из соображений экономии места. Поскольку программы клиента и сервера компилируются отдельно и в обычной ситуации запускаются на разных машинах, код процедуры fatal не может быть разделяемым.