Число отправленных байтов превышает количество, необходимое для передачи имени, на единицу, поэтому нужен еще нулевой байт-ограничитель, с помощью которого сервер определяет, где кончается имя файла.
Теперь клиентская программа входит в цикл, читает файл блок за блоком из сокета и копирует на стандартное устройство вывода. По окончании этого процесса она просто завершается.
Процедура fatal выводит сообщение об ошибке и завершается. Серверу также требуется эта процедура, и она пропущена в листинге только из соображений экономии места. Поскольку программы клиента и сервера компилируются отдельно и в обычной ситуации выполняются на разных устройствах, они не могут одновременно использовать код процедуры fatal.
Стоит заметить, что такой сервер построен далеко не по последнему слову техники. Осуществляемая проверка ошибок минимальна, а сообщения об ошибках реализованы весьма посредственно. Система будет обладать низкой производительностью, поскольку все запросы обрабатываются только последовательно (используется один поток запросов). Понятно, что ни о какой защите информации здесь говорить не приходится, а применение аскетичных системных вызовов UNIX не лучшее решение для достижения независимости от платформы. При этом делаются некоторые некорректные с технической точки зрения допущения. Например, что имя файла всегда поместится в буфер и будет передано без ошибок. Несмотря на эти недостатки, с помощью данной программы можно организовать полноценный работающий файл-сервер для интернета. Более подробную информацию вы найдете в работах Донаху и Калверта (Donahoo and Calvert; 2008, 2009), а также Стивенса и др. (Stevens et al., 2004).
6.2. Элементы транспортных протоколов
Транспортные службы реализуются транспортным протоколом, который используется между двумя транспортными подсистемами. Транспортные протоколы несколько напоминают протоколы канального уровня, которые мы подробно рассмотрели в главе 3. И те и другие протоколы, помимо прочего, занимаются обработкой ошибок, управлением очередями и потоками.
Однако у этих протоколов также имеется множество существенных различий, из-за разных условий, в которых они работают (илл. 6.7). На канальном уровне два маршрутизатора общаются напрямую по физическому каналу (проводному или беспроводному), тогда как на транспортном уровне физический канал заменен целой сетью. Это различие сильно влияет на протоколы.
Илл. 6.7. Окружение: (а) канального уровня; (б) транспортного уровня
Во-первых, при двухточечном соединении по проводу или оптоволоконной линии маршрутизатор обычно не должен указывать, с каким маршрутизатором он хочет обменяться данными, — каждая выходная линия ведет к конкретному маршрутизатору. На транспортном уровне требуется явно указывать адрес получателя.
Во-вторых, процесс установки соединения по проводу (см. илл. 6.7 (а)) прост: противоположная сторона всегда присутствует (если только она не вышла из строя). В любом случае работы не очень много. Даже в случае беспроводного соединения ситуация не слишком отличается. Простой отправки сообщения достаточно, чтобы оно дошло до всех адресатов. Если подтверждение о получении не приходит (вследствие ошибок), сообщение может быть отправлено повторно. На транспортном уровне начальная установка соединения, как будет показано ниже, происходит достаточно сложно.
Еще одно весьма досадное различие между канальным и транспортным уровнями состоит в том, что сеть потенциально обладает возможностями хранения информации. Когда маршрутизатор отправляет пакет по каналу связи, пакет может прийти или потеряться, но он не может побродить где-то какое-то время, спрятаться на краю света, а затем внезапно появиться и прийти в место назначения после пакетов, отправленных гораздо позже него. Если же сеть использует дейтаграммы, которые маршрутизируются в ней независимо, то всегда есть ненулевая вероятность, что пакет пройдет по какому-то странному пути и придет к получателю позже, чем нужно, и в неправильном порядке; возможно также, что при этом будут получены копии пакета. Последствия способности сети задерживать и копировать пакеты порой катастрофичны и требуют применения специальных протоколов, обеспечивающих правильную передачу данных.
Последнее различие между канальным и транспортным уровнями является скорее количественным, чем качественным. Буферизация и управление потоком необходимы в обоих случаях. Однако наличие на транспортном уровне большого и непостоянного числа соединений с пропускной способностью, колеблющейся из-за их конкуренции, может потребовать принципиально другого подхода. Некоторые из рассмотренных в главе 3 протоколов выделяют фиксированное количество буферов для каждой линии, поэтому для входящего фрейма всегда имеется свободный буфер. На транспортном уровне из-за множества управляемых соединений и колебаний пропускной способности выделение нескольких буферов каждому соединению не слишком хорошая идея. В следующих разделах мы изучим эти и другие важные вопросы.
6.2.1. Адресация
Когда один прикладной процесс желает установить соединение с другим прикладным процессом, он должен его конкретно указать. Обычно для этого задаются транспортные адреса, куда процессы отправляют запросы на установление соединения. В интернете такие конечные точки на транспортном уровне называются портами. Для их обозначения мы будем пользоваться нейтральным термином точка доступа к службам транспортного уровня (Transport Service Access Point, TSAP). Аналогичные конечные точки сетевого уровня (то есть адреса сетевого уровня) называются точками доступа к сетевым службам (Network Service Access Point, NSAP). Примерами NSAP являются IP-адреса.
На илл. 6.8 показаны взаимоотношения между NSAP, TSAP и транспортным соединением. Прикладные процессы клиента и сервера могут связываться с локальной TSAP для установления соединения с удаленной TSAP. Такие соединения проходят через NSAP на каждом хосте, как показано на рисунке. TSAP нужны для того, чтобы различать конечные точки, совместно использующие NSAP, в сетях, где у каждого компьютера есть своя NSAP.
Возможный сценарий для транспортного соединения выглядит следующим образом.
1. Процесс почтового сервера подсоединяется к точке доступа TSAP 1522 на хосте 2 и ожидает входящего вызова. Вопрос о том, как процесс соединяется с TSAP, лежит за пределами сетевой модели и полностью зависит от локальной операционной системы. Например, может вызываться примитив типа LISTEN.
2. Прикладной процесс хоста 1 хочет отправить почтовое сообщение, поэтому он подключается к TSAP 1208 и обращается к сети с запросом CONNECT, указывая TSAP 1208 на хосте 1 в качестве адреса отправителя и TSAP 1522 на хосте 2 в качестве адреса получателя. Это действие в результате приводит к установке транспортного соединения между прикладным процессом и сервером.
3. Прикладной процесс отправляет почтовое сообщение.
4. Почтовый сервер отвечает, что сообщение будет доставлено.
5. Транспортное соединение разрывается.
Илл. 6.8. Точки доступа к службам транспортного и сетевого уровня и транспортные соединения
Обратите внимание, что на хосте 2 могут располагаться и другие серверы, соединенные со своими TSAP и ожидающие входящих запросов на соединение, приходящих с той же NSAP.
Это хорошая схема, только мы обошли стороной один маленький вопрос: как пользовательский процесс хоста 1 узнает, что почтовый сервер соединен с TSAP 1522? Возможно, почтовый сервер подключается к TSAP 1522 уже долгие годы, и постепенно об этом узнали все пользователи сети. В этом случае службы имеют постоянные TSAP-адреса, хранящиеся в файлах, которые расположены в известных местах. Так, например, в /etc/services UNIX-систем перечисляются серверы, за которыми закреплены конкретные порты, в частности, там указано, что почтовый сервер использует TCP порт 25.