В этот момент сервер входит в основной цикл программы и уже из него не выходит. Этот цикл можно остановить только извне. Вызов accept блокирует сервер на то время, пока клиент пытается установить соединение. В случае успеха accept возвращает дескриптор сокета, который можно использовать для чтения и записи, аналогично тому, как файловые дескрипторы применяются для чтения и записи в каналы. Однако, в отличие от однонаправленных каналов, сокеты двунаправлены, поэтому для чтения (и записи) данных из соединения можно использовать sa (принятый сокет). Файловые дескрипторы канала могут применяться для чтения или записи, но не одновременно.
После установления соединения сервер считывает имя файла. Если оно еще недоступно, сервер блокируется, ожидая его. Получив имя файла, сервер открывает файл и входит в цикл, который читает блоки данных из файла и записывает их в сокет. Это продолжается до тех пор, пока не будут скопированы все запрошенные данные. Затем файл закрывается, соединение разрывается и начинается ожидание нового вызова. Данный цикл повторяется бесконечно.
Теперь рассмотрим часть кода, описывающую клиента. Чтобы понять, как работает программа, сначала необходимо разобраться, как она запускается. Если она называется client, ее типичный вызов будет выглядеть так:
client flits.cs.vu.nl /usr/tom/filename >f
Этот вызов сработает, только если сервер расположен по адресу flits.cs.vu.nl, файл /usr/tom/filename существует и у сервера есть доступ по чтению для этого файла. Если вызов произведен успешно, файл передается по интернету и записывается в f, после чего клиентская программа заканчивает свою работу. Поскольку серверная программа продолжает работать, клиент может быть запущен снова с новыми запросами на получение файлов.
Клиентская программа начинается с подключения файлов и объявлений. Прежде всего проверяется корректность числа аргументов (где argc = 3 означает, что была вызвана программа с указанием ее имени и двух аргументов). Обратите внимание на то, что argv[1] содержит имя сервера (например, flits.cs.vu.nl) и переводится в IP-адрес с помощью функции gethostbyname. Для поиска имени эта функция использует DNS. Межсетевые экраны мы изучим отдельно в главе 7.
Затем создается и инициализируется сокет, после чего клиент пытается установить TCP-соединение с сервером посредством connect. Если сервер включен, работает на указанном компьютере, соединен с SERVER_PORT и либо простаивает, либо имеет достаточно места в очереди listen (очереди ожидания), то соединение с клиентом будет рано или поздно установлено. По данному соединению клиент передает имя файла, записывая его в сокет.
Число отправленных байтов превышает количество, необходимое для передачи имени, на единицу, поэтому нужен еще нулевой байт-ограничитель, с помощью которого сервер определяет, где кончается имя файла.
Теперь клиентская программа входит в цикл, читает файл блок за блоком из сокета и копирует на стандартное устройство вывода. По окончании этого процесса она просто завершается.
Процедура fatal выводит сообщение об ошибке и завершается. Серверу также требуется эта процедура, и она пропущена в листинге только из соображений экономии места. Поскольку программы клиента и сервера компилируются отдельно и в обычной ситуации выполняются на разных устройствах, они не могут одновременно использовать код процедуры fatal.
Стоит заметить, что такой сервер построен далеко не по последнему слову техники. Осуществляемая проверка ошибок минимальна, а сообщения об ошибках реализованы весьма посредственно. Система будет обладать низкой производительностью, поскольку все запросы обрабатываются только последовательно (используется один поток запросов). Понятно, что ни о какой защите информации здесь говорить не приходится, а применение аскетичных системных вызовов UNIX не лучшее решение для достижения независимости от платформы. При этом делаются некоторые некорректные с технической точки зрения допущения. Например, что имя файла всегда поместится в буфер и будет передано без ошибок. Несмотря на эти недостатки, с помощью данной программы можно организовать полноценный работающий файл-сервер для интернета. Более подробную информацию вы найдете в работах Донаху и Калверта (Donahoo and Calvert; 2008, 2009), а также Стивенса и др. (Stevens et al., 2004).
6.2. Элементы транспортных протоколов
Транспортные службы реализуются транспортным протоколом, который используется между двумя транспортными подсистемами. Транспортные протоколы несколько напоминают протоколы канального уровня, которые мы подробно рассмотрели в главе 3. И те и другие протоколы, помимо прочего, занимаются обработкой ошибок, управлением очередями и потоками.