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

13.2. Приложение типа клиент-сервер

Пример приложения модели клиент-сервер приведен на рис. 13.1. Именно на него мы будем ссылаться в тексте этой главы и главы 17 при необходимости проиллюстрировать использование программных каналов, FIFO и очередей сообщений System V.

Клиент считывает полное имя файла из стандартного потока ввода и записывает его в канал IPC. Сервер считывает это имя из канала IPC и производит попытку открытия файла на чтение. Если попытка оказывается успешной, сервер считывает файл и записывает его в канал IPC. В противном случае сервер возвращает клиенту сообщение об ошибке. Клиент считывает данные из канала IPC и записывает их в стандартный поток вывода. Если сервер не может считать файл, из канала будет считано сообщение об ошибке. В противном случае будет принято содержимое файла. Две штриховые линии между клиентом и сервером на рис. 13.1 представляют собой канал IPC.

рис. 13.1

13.3. Программные каналы

Программные каналы имеются во всех существующих реализациях и версиях Unix. Канал создается вызовом pipe и предоставляет возможность однонаправленной (односторонней) передачи данных.

#include <unistd.h>

int pipe (int fd[2]);

/* функция возвращает 0 в случае успешного завершения, -1 – в случае ошибки */

Функция возвращает два файловых дескриптора: fd[0] и fd[1], причем первый открыт для чтения, а второй – для записи.

Для определения типа дескриптора (файла, программного канала или FIFO) можно использовать макрос S_ISFIFO. Он принимает единственный аргумент: поле st_mode структуры stat и возвращает значение “истина” (ненулевое значение) или ложь (ноль). Структуру stat для канала возвращает функция fstat. Для FIFO структура возвращается функциями fstat, lstat и stat.

На рис. 13.2 изображен канал при использовании его единственным процессом.

рис. 13.2

Хотя канал создается одним процессом, он редко используется только этим процессом. Каналы обычно используются для связи между двумя процессами (родительским и дочерним) следующим образом: процесс создает канал, а затем вызывает fork, создавая свою копию – дочерний процесс (рис. 13.3). Затем родительский процесс закрывает открытый для чтения конец канала, а дочерний, в свою очередь, – открытый на запись конец канала. Это обеспечивает одностороннюю передачу данных между процессами, как показано на рис. 13.4.

При вводе команды наподобие

who | sort | lp

в интерпретаторе команд Unix интерпретатор выполняет вышеописанные действия для создания трех процессов с двумя каналами между ними. Интерпретатор также подключает открытый для чтения конец каждого канала к стандартному потоку ввода, а открытый на запись – к стандартному потоку вывода. Созданный таким образом канал показан на рис. 13.5.

рис. 13.3

рис. 13.4

рис. 13.5

Все рассмотренные выше каналы были однонаправленными (односторонними), то есть позволяли передавать данные только в одну сторону. При необходимости передачи данных в обе стороны нужно создавать пару каналов и использовать каждый из них для передачи данных в одну сторону. Этапы создания двунаправленного канала IPC следующие:

  1. Создаются каналы 1 (fd1[0] и fd1[1]) и 2 (fd2[0] и fd2[1]).

  2. Вызов fork.

  3. Родительский процесс закрывает доступный для чтения конец канала 1 (fd1[0]).

  4. Родительский процесс закрывает доступный для записи конец канала 2 (fd2[1]).

  5. Дочерний процесс закрывает доступный для записи конец канала 1 (fd1[1]).

  6. Дочерний процесс закрывает доступный для чтения конец канала 2 (fd2[0]).

Текст программы, выполняющей эти действия, приведен в листинге 13.1. При этом создается структура каналов, изображенная на рис. 13.6.

рис. 13.6

Пример. Давайте напишем программу, описанную в разделе 13.2, с использованием каналов. Функция main создает два канала и вызывает fork для создания копии процесса. Родительский процесс становится клиентом, а дочерний – сервером. Первый канал (pipe1) используется для передачи полного имени файла от клиента серверу, а второй (pipe2) – для передачи содержимого файла (или сообщения об ошибке) от сервера клиенту.

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

В листинге 13.1 приведена программа для данного примера.

Листинг 13.1. Приложение клиент-сервер, использующее два канала

#include <errno.h>

#include <fcntl.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

#define MAXLINE 4096

void server(int readfd, int writefd)

{

int fd;

ssize_t n;

char buff[MAXLINE+1], msg[MAXLINE+1];

/* чтение полного имени файла из канала IPC */

if ((n = read(readfd, buff, MAXLINE)) < 0)

{

snprintf(msg, sizeof(msg), "Сервер: ошибка чтения полного имени файла из

канала IPC: %s\n", strerror(errno));

write(writefd, msg, strlen(msg));

exit(1);

}

if (n == 0)

{

snprintf(msg, sizeof(msg), "Сервер: считано пустое имя файла, завершение

работы\n");

write(writefd, msg, strlen(msg));

exit(1);

}

buff[n] = '\0'; /* добавляем признак конца строки к полному имени */

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

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

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

strerror(errno));

write(writefd, msg, strlen(msg));

exit(1);

}

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

{

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

if (write(writefd, msg, n) != n)

{

snprintf(msg, sizeof(msg), "Сервер: ошибка записи содержимого файла %s в

канал IPC: %s\n", buff, strerror(errno));

write(writefd, msg, strlen(msg));

exit(1);

}

if (n < 0)

{

snprintf(msg, sizeof(msg), "Сервер: ошибка чтения содержимого файла %s:

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

write(writefd, msg, strlen(msg));

exit(1);

}

close(fd);

}

}

void client(int readfd, int writefd)

{

size_t len;

ssize_t n;

char buff[MAXLINE];

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

if (fgets(buff, MAXLINE, stdin) == NULL)

{

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

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

exit(1);

}

len = strlen(buff); /* fgets() гарантирует завершающий нулевой байт */

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

len--; /* удаление символа перевода строки (если есть) */

if (len == 0)

{

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

exit(1);

}

/* запись полного имени файла в канал IPC */

if (write(writefd, buff, len) < 0)

{

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

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

exit(1);

}

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

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

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

{

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

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

exit(1);

}

if (n < 0)

{

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

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

exit(1);

}

}

int main(int argc, char **argv)

{

int pipe1[2], pipe2[2], status;

pid_t childpid;

if (pipe(pipe1) == -1) /* создание двух каналов */

{

fprintf(stderr, "Невозможно создать канал pipe1: %s\n", strerror(errno));

exit(1);

}

if (pipe(pipe2) == -1)

{

fprintf(stderr, "Невозможно создать канал pipe2: %s\n", strerror(errno));

exit(1);

}

if ((childpid = fork()) == -1)

{

fprintf(stderr, "Ошибка вызова функции fork: %s\n", strerror(errno));

exit(1);

}

if (childpid == 0) /* дочерний процесс */

{

close(pipe1[1]);

close(pipe2[0]);

server(pipe1[0], pipe2[1]);

exit(0);

}

close(pipe1[0]); /* родительский процесс */

close(pipe2[1]);

client(pipe2[0], pipe1[1]);

if (waitpid(childpid, &status, 0) == -1)

{ /* ожидание завершения дочернего процесса */

fprintf(stderr, "Ошибка вызова функции waitpid: %s\n", strerror(errno));

exit(1);

}

if (status == 0)

fprintf(stderr, "Копирование файла успешно завершено\n");

exit(0);

}

Внутри функции main мы создаем два канала и выполняем шесть шагов, которые упоминались в отношении рис. 13.6. Родительский процесс вызывает функцию client, а дочерний – функцию server.

Дочерний процесс (сервер) завершает свою работу первым, вызывая функцию exit после завершения записи данных в канал. После этого функция client родительского процесса возвращает управление функции main, закончив считывание данных из канала. Затем родительский процесс вызывает waitpid для получения информации о статусе завершения дочернего процесса. Только в случае корректного завершения обоих процессов копирование файла считается проведенным успешно.

Полное имя файла клиент считывает из стандартного потока ввода и записывает в канал, удалив при необходимости завершающий символ перевода строки. Сервер считывает записанное в канал клиентом имя файла и дополняет его нулевым байтом. Далее сервер открывает указанный файл для чтения, а при возникновении ошибки извещает о ней клиента с помощью канала. Затем клиент считывает все, что сервер направляет в канал, и записывает эти данные в стандартный поток вывода. Ожидается, что это будет содержимое файла, но в случае возникновения ошибки это будет сообщение о причине, которая привела к ней.

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