Листинг 1.3. Файл header.pas, используемый клиентом и сервером

Примеры клиента и сервера

Чтобы внести большую ясность в то, как работают клиент и сервер, в этом пункте мы рассмотрим описание клиента и файлового сервера на языке Pascal. И клиент, и сервер должны совместно использовать некоторые определения, которые мы соберем вместе в файл под названием header.pas, текст которого приведен в лис­тинге 1.3. Эти определения затем включаются в тексты программ клиента и сер­вера следующей строкой:

 

uses header.pas;

 

/* Определения, необходимые и клиентам, и серверам */

Const

/* Максимальная длина имени файла */

МАХ_РАТН = 255;

/* Максимальное количество данных, передаваемое за один раз */

BUF_SIZE = 1024;

/* Сетевой адрес файлового сервера */

FILE_SERVER = 243;

/* Определения разрешенных операций */

/* создать новый файл */

OP_CREATE = 1;

/* считать данные из файла и вернуть их */

OP_READ = 2;

/* записать данные в файл */

OP_WRITE = 3;

/* удалить существующий файл */

OP_DELETE = 4;

/* Коды ошибок */

/* операция прошла успешно */

OK = 0;

/* запрос неизвестной операции */

E_BAD_OPER = -1;

/* ошибка в параметре */

E_BAD_PARAM = -2;

/* ошибка диска или другая ошибка чтения-записи */

E_IO = –3;

 

/* Определение формата сообщения */

type

TMessage = record

source: longint; /* идентификатор источника */

dest: longint; /* идентификатор приемника */

opcode: longint; /* запрашиваемая операция */

count: longint; /* число передаваемых байт */

offset: longint; /* позиция в файле, с которой начинается ввод-вывод */

result: longint; /* результат операции */

name: string[MAX_PATH]; /* имя файла, с которым производятся операции */

data: string [BUF_SIZE]; /* данные, которые будут считаны или записаны */

end;

 

Итак, перед нами текст файла header.pas. Он начинается с определения двух констант, МАХ_РАТН и BUF_SIZE, которые определяют размер двух массивов, исполь­зуемых в сообщении. Первая задает число символов, которое может содержаться в имени файла (то есть в строке с путем типа /usr/ast/books/opsys/chapter1.t). Вторая задает размер блока данных, который может быть прочитан или записан за одну операцию путем установки размера буфера. Следующая константа, FILE_ SERVER, задает сетевой адрес файлового сервера, на который клиенты могут посы­лать сообщения.

Вторая группа констант задает номера операций. Они необходимы для того, чтобы и клиент, и сервер знали, какой код представляет чтение, какой код — за­пись и т. д. Мы приводим здесь только четыре константы, в реальных системах их обычно больше.

Каждый ответ содержит код результата. Если операция завершена успешно, код результата обычно содержит полезную информацию (например, реальное число считанных байтов). Если нет необходимости возвращать значение (напри­мер, при создании файла), используется значение ОК. Если операция по каким-либо причинам окончилась неудачей, код результата (E_BAD_OPER, E_BAD_PARAM и др.) сообщает нам, почему это произошло.

Наконец, мы добрались до наиболее важной части файла header.pas — определе­ния формата сообщения. В нашем примере это структура с 8 полями. Этот фор­мат используется во всех запросах клиентов к серверу и ответах сервера. В ре­альных системах, вероятно, фиксированного формата у сообщений не будет (по­скольку не во всех случаях нужны все поля), но здесь это упростит объяснение. Поля source и dest определяют, соответственно, отправителя и получателя. Поле opcode — это одна из ранее определенных операций, то есть create, read, write или delete. Поля count и offset требуются для передачи параметров. Поле result в за­просах от клиента к серверу не используется, а при ответах сервера клиенту со­держит значение результата. В конце структуры имеются два массива. Первый из них, name, содержит имя файла, к которому мы хотим получить доступ. Во вто­ром, data, находятся возвращаемые сервером при чтении или передаваемые на сервер при записи данные.

Давайте теперь рассмотрим код в листингах 1.4 и 1.5. Листинг 1.4 — это про­грамма сервера, листинг 1.5 — программа клиента. Программа сервера достаточно элементарна. Основной цикл начинается вызовом receive в ответ на сообщение с запросом. Первый параметр определяет отправителя запроса, поскольку в нем указан его адрес, а второй указывает на буфер сообщений, идентифицируя, где должно быть сохранено пришедшее сообщение. Процедура receive блокирует сервер, пока не будет получено сообщение. Когда оно наконец приходит, сервер продолжает свою работу и определяет тип кода операции. Для каждого кода опе­рации вызывается своя процедура. Входящее сообщение и буфер для исходящих сообщений заданы в параметрах. Процедура проверяет входящее сообщение в параметре m1 и строит исходящее в параметре m2. Она также возвращает значе­ние функции, которое передается через поле result. После посылки ответа сер­вер возвращается к началу цикла, выполняет вызов receive и ожидает следующе­го сообщения.

В листинге 1.5 находится процедура, копирующая файлы с использованием сервера. Тело процедуры содержит цикл чтения блока из исходного файла и за­писи его в файл-приемник. Цикл повторяется до тех пор, пока исходный файл не будет полностью скопирован, что определяется по коду возврата операции чте­ния — должен быть ноль или отрицательное число.

Первая часть цикла состоит из создания сообщения для операции чтения и пе­ресылки его на сервер. После получения ответа запускается вторая часть цикла, в ходе выполнения которой полученные данные посылаются обратно на сервер для записи в файл-приемник. Программы из листингов 1.4 и 1.5 — это только на­бросок кода. В них опущено множество деталей. Так, например, не приводятся процедуры do_xxx (которые на самом деле выполняют работу), отсутствует также обработка ошибок. Однако общая идея взаимодействия клиента и сервера вполне понятна. В следующих пунктах мы ближе рассмотрим некоторые дополнитель­ные аспекты модели клиент-сервер.