Скачиваний:
90
Добавлен:
12.05.2015
Размер:
913.92 Кб
Скачать

13.7. Один сервер, несколько клиентов

Преимущества канала FIFO проявляются более явно в том случае, когда сервер представляет собой некоторый долго функционирующий процесс (например, демон syslogd, описанный в разделе 11.4), не являющийся родственным клиенту. Демон создает именованный канал с вполне определенным известным именем, открывает его на чтение, а запускаемые впоследствии клиенты открывают его на запись и отправляют демону команды и необходимые данные. Односторонняя связь в этом направлении (от клиента к серверу) легко реализуется с помощью FIFO, однако необходимость отправки данных в обратную сторону (от сервера к клиенту) усложняет задачу. Рис. 13.8 иллюстрирует прием, применяемый в этом случае.

рис. 13.8

Сервер создает канал с известным полным именем, в данном случае /tmp/fifo.serv. Из этого канала он считывает запросы клиентов. Каждый клиент при запуске создает свой собственный канал, полное имя которого определяется его идентификатором процесса. Клиент отправляет свой запрос в канал сервера с известным именем, причем этот запрос содержит идентификатор процесса клиента и имя файла, отправку которого клиент запрашивает у сервера.

В листинге 13.6 приведен текст программы сервера.

Листинг 13.6. Сервер, обслуживающий нескольких клиентов с помощью канала FIFO

#include <errno.h>

#include <fcntl.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#define MAXLINE 4096

#define SERV_FIFO "/tmp/fifo.serv"

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

void delete_fifo(void)

{

unlink(SERV_FIFO);

}

int main(int argc, char **argv)

{

int readfifo, writefifo, dummyfd, fd;

char *ptr, buff[MAXLINE], fifoname[MAXLINE];

ssize_t n;

FILE *readfp;

/* создание FIFO сервера с известным именем; если уже существует - OK */

if ((mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST))

{

fprintf(stderr, "Сервер: невозможно создать %s: %s\n", SERV_FIFO,

strerror(errno));

exit(1);

}

if (atexit(delete_fifo))

{

unlink(SERV_FIFO);

fprintf(stderr, "Сервер: невозможно зарегистрировать delete_fifo: %s\n",

strerror(errno));

exit(1);

}

/* открытие FIFO сервера для чтения */

if ((readfifo = open(SERV_FIFO, O_RDONLY, 0)) < 0)

{

fprintf(stderr, "Сервер: невозможно открыть %s для чтения: %s\n", SERV_FIFO,

strerror(errno));

exit(1);

} /* открытие FIFO сервера для записи. Этот дескриптор не используется */

if ((dummyfd = open(SERV_FIFO, O_WRONLY, 0)) < 0)

{

fprintf(stderr, "Сервер: невозможно открыть %s для записи: %s\n", SERV_FIFO,

strerror(errno));

exit(1);

}

if ((readfp = fdopen(readfifo, "r")) == NULL) /* открываем поток для fgets */

{

fprintf(stderr, "Сервер: невозможно переоткрыть %s для чтения: %s\n",

SERV_FIFO, strerror(errno));

exit(1);

}

setbuf(readfp, NULL); /* отключаем буферизацию */

/* цикл приема запросов от клиентов */

while (fgets(buff, MAXLINE, readfp) != NULL)

{ /* fgets() гарантирует завершающий нулевой байт */

if (buff[strlen(buff)-1] == '\n') /* удаление символа перевода строки */

buff[strlen(buff)-1] = '\0'; /* (если есть) */

if ((ptr = strchr(buff, ' ')) == NULL)

{

fprintf(stderr, "Сервер: неправильный запрос: %s\n", buff);

continue;

}

*ptr++ = '\0'; /* нулевой байт вставлен в buff на место пробела */

/* ptr после увеличения (++) указывает на имя запрошенного файла */

snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%s", buff);

if ((writefifo = open(fifoname, O_WRONLY, 0)) < 0)

{

fprintf(stderr, "Сервер: невозможно открыть %s для записи: %s\n",

fifoname, strerror(errno));

continue;

}

if ((fd = open(ptr, O_RDONLY)) < 0)

{ /* сообщаем клиенту об ошибке открытия файла */

snprintf(fifoname, sizeof(fifoname), "Сервер: ошибка открытия файла %s:

%s\n", ptr, strerror(errno));

write(writefifo, fifoname, strlen(fifoname));

close(writefifo);

}

else /* файл успешно открыт; копируем его в FIFO */

{

while ((n = read(fd, fifoname, MAXLINE)) > 0)

if (write(writefifo, fifoname, n) != n)

{

snprintf(fifoname, sizeof(fifoname), "Сервер: ошибка записи содержимого

файла %s в FIFO: %s\n", ptr, strerror(errno));

write(writefifo, fifoname, strlen(fifoname));

exit(1);

}

if (n < 0)

{

snprintf(fifoname, sizeof(fifoname), "Сервер: ошибка чтения содержимого

файла %s: %s\n", ptr, strerror(errno));

write(writefifo, fifoname, strlen(fifoname));

exit(1);

}

close(fd);

close(writefifo);

}

}

}

Сервер создает канал FIFO с известным именем, обрабатывая ситуацию, когда такой канал уже существует. Затем этот канал открывается дважды: один раз только для чтения, а второй – только для записи. Дескриптор readfifo используется для приема запросов от клиентов, а дескриптор dummyfd не используется вовсе. Причина, по которой нужно открывать канал для записи, видна из табл. 13.1. Если канал не открыть на запись, то при завершении работы очередного клиента этот канал будет опустошаться, вследствие чего сервер будет считывать 0, означающий конец файла. В этом случае пришлось бы каждый раз закрывать канал вызовом close, а затем заново открывать его с флагом O_RDONLY, что приводило бы к блокированию сервера до подключения следующего клиента. Мы же всегда будем иметь дескриптор, открытый для записи, поэтому функция read не будет возвращать 0, означающий конец файла, при отсутствии клиентов. Вместо этого сервер просто будет блокироваться при вызове функции fgets, ожидая подключения следующего клиента. Этот трюк упрощает код программы-сервера и уменьшает количество вызовов функции open для канала FIFO сервера.

При запуске сервера первый вызов open (с флагом O_RDONLY) приводит к блокированию процесса до появления первого клиента, открывающего канал FIFO сервера для записи (см. табл. 13.1). Второй вызов open (с флагом O_WRONLY) не приводит к блокированию, поскольку канал уже открыт для чтения.

Каждый запрос, принимаемый от клиента, представляет собой одну строку, содержащую идентификатор процесса, пробел и полное имя требуемого файла. Эта строка считывается функцией fgets. Символ перевода строки, возвращаемый функцией fgets, удаляется. Этот символ может отсутствовать только в том случае, если буфер был заполнен, прежде чем был обнаружен символ перевода строки, либо если принятая от клиента строка не была завершена этим символом. Функция strchr возвращает указатель на первый пробел в этой строке, который затем увеличивается на единицу, чтобы он указывал на первый символ полного имени файла, следующего после пробела. Полное имя канала FIFO клиента содержит идентификатор его процесса, и этот канал открывается сервером для записи.

Оставшаяся часть кода программы-сервера аналогична функции server из листинга 13.1. Программа открывает файл, указанный клиентом; если при этом возникает ошибка, то клиенту отсылается сообщение о ней. Если открытие файла завершается успешно, то его содержимое копируется в канал FIFO клиента. После завершения копирования открытый сервером “конец” (дескриптор) канала FIFO клиента должен быть закрыт с помощью функции close, чтобы функция read вернула программе-клиенту значение 0 (конец файла). Сервер не удаляет канал FIFO клиента; клиент должен самостоятельно позаботиться об этом после приема от сервера символа конца файла.

Текст программы-клиента приведен в листинге 13.7.

Листинг 13.7. Клиент, взаимодействующий с сервером через канал FIFO

#include <errno.h>

#include <fcntl.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#define MAXLINE 4096

#define SERV_FIFO "/tmp/fifo.serv"

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

char fifoname[MAXLINE];

void delete_fifo(void)

{

unlink(fifoname);

}

int main(int argc, char **argv)

{

int readfifo, writefifo;

size_t len;

ssize_t n;

char *ptr, buff[MAXLINE];

pid_t pid;

/* создание FIFO с именем, включающим PID; если уже существует - OK */

pid = getpid();

snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long) pid);

if ((mkfifo(fifoname, FILE_MODE) < 0) && (errno != EEXIST))

{

fprintf(stderr, "Клиент: невозможно создать %s: %s\n", fifoname,

strerror(errno));

exit(1);

}

if (atexit(delete_fifo))

{

unlink(fifoname);

fprintf(stderr, "Клиент: невозможно зарегистрировать delete_fifo: %s\n",

strerror(errno));

exit(1);

}

/* записываем в буфер PID и пробел */

snprintf(buff, sizeof(buff), "%ld ", (long) pid);

len = strlen(buff);

ptr = buff + len;

/* чтение полного имени файла из стандартного потока ввода */

if (fgets(ptr, MAXLINE - len, stdin) == NULL)

{ /* fgets() гарантирует завершающий нулевой байт */

fprintf(stderr, "Клиент: ошибка чтения полного имени файла из стандартного

потока ввода\n");

exit(1);

}

if (buff[len] == '\n')

{ /* при вводе имени файла был введен только перевод строки */

fprintf(stderr, "Клиент: введено пустое имя файла, завершение работы\n");

exit(1);

}

/* открытие FIFO сервера и запись в него PID и полного имени файла */

if ((writefifo = open(SERV_FIFO, O_WRONLY, 0)) < 0)

{

fprintf(stderr, "Клиент: невозможно открыть %s: %s\n", SERV_FIFO,

strerror(errno));

exit(1);

}

if ((n=write(writefifo, buff, strlen(buff))) < 0)

{

fprintf(stderr, "Клиент: ошибка записи полного имени файла в канал %s:

%s\n", SERV_FIFO, strerror(errno));

exit(1);

}

/* открытие своего FIFO; блокирование до его открытия сервером */

if ((readfifo = open(fifoname, O_RDONLY, 0)) < 0)

{

fprintf(stderr, "Клиент: невозможно открыть %s: %s\n", fifoname,

strerror(errno));

exit(1);

}

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

while ((n = read(readfifo, buff, MAXLINE)) > 0)

if (write(STDOUT_FILENO, buff, n) != n)

{

fprintf(stderr, "Клиент: ошибка записи содержимого файла в стандартный

поток вывода: %s\n", strerror(errno));

exit(1);

}

if (n < 0)

{

fprintf(stderr, "Клиент: ошибка чтения содержимого файла из канала FIFO:

%s\n", strerror(errno));

exit(1);

}

close(readfifo);

exit(0);

}

Клиент создает свой канал FIFO с использованием идентификатора процесса. Строка запроса клиента содержит идентификатор процесса, один пробел, полное имя запрашиваемого файла и символ перевода строки. Строка запроса записывается в массив buff, причем имя файла считывается из стандартного потока ввода. Клиент открывает канал FIFO сервера для записи и записывает в него строку запроса. Если клиент окажется первым с момента запуска сервера, первый вызов функции open (с флагом O_RDONLY) вернет управление и разблокирует сервер.

Ответ сервера клиент считывает из своего канала FIFO и записывает его в стандартный поток вывода, после чего канал FIFO клиента закрывается и удаляется.

Для корректной работы клиента и сервера сначала необходимо запустить программу-сервер в фоновом режиме или на отдельной консоли, а затем запустить программу-клиента.

Мы можем также связаться с сервером из интерпретатора команд, поскольку каналы FIFO обладают именами в файловой системе.

$ PID=$$

$ mkfifo /tmp/fifo.$PID

$ echo "$PID /etc/resolv.conf" >/tmp/fifo.serv

$ cat </tmp/fifo.$PID

nameserver 192.168.1.1

search apeps.kiev.ua ntu-kpi.kiev.ua

$ rm /tmp/fifo.$PID

Мы отсылаем серверу идентификатор процесса текущей копии интерпретатора и полное имя файла с помощью одной команды (echo) и считываем результат из канала FIFO сервера с помощью другой команды (cat). Между выполнением этих двух команд может пройти произвольный промежуток времени. Таким образом, сервер помещает содержимое файла в канал, а клиент затем запускает команду cat, чтобы считать оттуда данные. Может показаться, что данные каким-то образом хранятся в канале, хотя он не открыт ни одним процессом. На самом деле все не так. После закрытия программного канала или FIFO последним процессом с помощью функции close все находящиеся в нем данные теряются. В нашем примере сервер, считав строку запроса от клиента из своего FIFO, блокируется при попытке открыть канал FIFO клиента, потому что клиент (в данном случае это наша копия интерпретатора) еще не открыл его на чтение (см. табл. 13.1). Только после вызова cat некоторое время спустя канал FIFO клиента будет открыт для чтения, и тогда сервер разблокируется. Кстати, подобным образом осуществляется атака типа “отказ в обслуживании” (denial-of-service attack), которую мы обсудим в следующем разделе.

Атомарность записи в FIFO. Наша простейшая пара клиент-сервер позволяет наглядно показать важность наличия свойства атомарности записи в программные каналы и FIFO. Предположим, что два клиента посылают серверу запрос приблизительно в один и тот же момент. Первый клиент отправляет следующую строку:

1234 /etc/resolv.conf

а второй:

9876 /etc/passwd

Предполагая, что каждый клиент помещает данные в канал FIFO с помощью одного вызова функции write и каждая строка имеет размер, не превышающий величины PIPE_BUF (что заведомо выполняется в ОС Linux, поскольку величина PIPE_BUF обычно равна 4096 байтам, а длина полного имени файла обычно ограничена 1024 байтами), мы можем гарантировать, что в канале FIFO сервера запросы клиентов не смешаются в “кашу”.

Соседние файлы в папке Chapter.4