Книга: Основы программирования в Linux

Более сложная тема: применение каналов FIFO в клиент-серверных приложениях

Более сложная тема: применение каналов FIFO в клиент-серверных приложениях

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

Вам нужно разрешить множественным клиентским процессам отправлять данные серверу. Для простоты предположим, что данные, которые нужно обработать, можно разбить на блоки, каждый из которых меньше PIPE_BUF байтов. Конечно, реализовать такую систему можно разными способами, но мы рассмотрим только один, как иллюстрацию применения именованных каналов.

Поскольку сервер будет обрабатывать только один блок данных в каждый момент времени, кажется логичным создать один канал FIFO, который читается сервером и в который записывают всё клиенты. Если открыть FIFO в блокирующем режиме, сервер и клиенты будут при необходимости блокироваться.

Возвращать обработанные данные клиентам немного сложнее. Вам придется организовать второй канал для возвращаемых данных, один для каждого клиента. Если передавать идентификатор (PID) процесса-клиента в исходных данных, отправляемых на сервер, обе стороны смогут использовать его для генерации уникального имени канала с возвращаемыми данными.

Выполните упражнение 13.13.

Упражнение 13.13. Пример клиент-серверного приложения

1. Прежде всего, вам нужен заголовочный файл client.h, в котором определены данные, общие для серверных и клиентских программ. В приложение также для удобства включены требуемые системные заголовочные файлы.

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define SERVER_FIFO_NAME "/tmp/serv_fifo"
#define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"
#define BUFFER_SIZE 20
struct data_to_pass_st {
 pid_t client_pid;
 char some_data[BUFFER_SIZE - 1];
};

2. Теперь займемся серверной программой server.c. В этом разделе вы создаете и затем открываете канал сервера. Он задается в режиме "только для чтения" и с блокировкой. После засыпания (из демонстрационных соображений) сервер читает данные от клиента, у которого есть структура типа data_to_pass_st.

#include "client.h"
#include <ctype.h>
int main() {
 int server_fifo_fd, client fifo_fd;
 struct data_to_pass_st my_data;
 int read_res;
 char client_fifo[256];
 char *tmp_char_ptr;
 mkfifo(SERVER_FIFO_NAME, 0777);
 server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);
 if (server_fifo_fd == -1) {
  fprintf(stderr, "Server fifo failuren");
  exit(EXIT_FAILURE);
 }
 sleep(10); /* для целей демонстрации разрешает клиентам создать очередь */
 do {
  read_res = read(server_fifo_fd, &my_data, sizeof(my_data));
  if (read res > 0) {

3. На следующем этапе вы выполняете некоторую обработку данных, только что полученных от клиента: преобразуете все символы в некоторых данных в прописные и соединяете CLIENT_FIFO_NAME с полученным идентификатором client_pid.

   tmp_char_ptr = my_data.some_data;
   while (*tmp_char_ptr) {
    *tmp_char_ptr = toupper(* tmp_char_ptr);
    tmp_char_ptr++;
   }
   sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);

4. Далее отправьте обработанные данные назад, открыв канал клиентской программы в режиме "только для записи" и с блокировкой. В заключение закройте серверный FIFO с помощью закрытия файла и отсоединения FIFO.

   client_fifo_fd = open(client_fifo, O_WRONLY);
   if (client_fifo_fd ! = -1) {
    write(client_fifo_fd, &my_data, sizeof(my_data));
    close(client_fifo_fd);
   }
  }
 } while (read_res > 0);
 close(server_fifo_fd);
 unlink(SERVER_FIFO_NAME);
 exit(EXIT_SUCCESS);
}

5. Далее приведена клиентская программа client.с. В первой части этой программы FIFO сервера, если он уже существует, открывается как файл. Далее программа получает идентификатор собственного процесса, который формирует некие данные, которые будут отправляться на сервер. Создается FIFO клиента, подготовленный для следующего раздела.

#include "client.h"
#include <ctype.h>
int main() {
 int server_fifo_fd, client_fifo_fd;
 struct data_to_pass_st my_data;
 int times_to_send;
 char client_fifo[256];
 server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);
 if (server_fifo_fd == -1) {
  fprintf (stderr, "Sorry, no servern");
  exit(EXIT_FAILURE);
 }
 my_data.client_pid = getpid();
 sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);
 if (mkfifo(client_fifo, 0777) == -1) {
  fprintf(stderr, "Sorry, can't make %sn", client_fifo);
  exit(EXIT_FAILURE);
 }

6. В каждом из пяти проходов цикла клиентские данные отправляются на сервер. Далее клиентский FIFO открывается (в режиме "только для чтения" с блокировкой) и данные считываются обратно. В конце серверный FIFO закрывается, а клиентский FIFO удаляется из файловой системы.

 for (times_to_send = 0; times_to_send < 5; times_to_send++) {
  sprintf(my_data.some_data, "Hello from %d", my_data.client_pid);
  printf("%d sent %s, ", my_data.client_pid, my_data.some_data);
  write(server_fifo_fd, &my_data, sizeof(my_data));
  client_fifo_fd = open(client_fifo, O_RDONLY);
  if (client_fifo_fd != -1) {
   if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0) {
    printf("received: %sn", my_data.some_data);
   }
   close(client_fifo_fd);
  }
 }
 close(server_fifo_fd);
 unlink(client_fifo);
 exit(EXIT_SUCCESS);
}

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

$ ./server &
$ for i in 1 2 3 4 5
do
./client &
done
$

Они запускают один серверный процесс и пять клиентских. Вывод клиентских программ, отредактированный для краткости, выглядит следующим образом:

531 sent Hello from 531, received: HELLO FROM 531
532 sent Hello from 532, received: HELLO FROM 532
529 sent Hello from 529, received: HELLO FROM 529
530 sent Hello from 530, received: HELLO FROM 530
531 sent Hello from 531, received: HELLO FROM 531
532 sent Hello from 532, received: HELLO FROM 532

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

Как это работает

Теперь мы обсудим последовательность клиентских и серверных операций во взаимодействии, чего не делали до сих пор.

Сервер создает свой канал FIFO в режиме "только чтение" и блокируется. Он делает это до тех пор, пока первый клиентский процесс не подсоединится, открыв тот же FIFO для записи. В этот момент серверный процесс разблокируется и выполняется вызов sleep, поэтому вызовы write клиентов образуют очередь. (В реальном приложении вызов sleep может быть удален, мы применяем его только чтобы продемонстрировать корректное функционирование программы с множественными одновременно действующими клиентами.)

Между тем, после того как клиентский процесс открыл серверный канал FIFO, он создает собственный FIFO с уникальным именем для считывания данных с сервера. Только после этого клиент записывает данные на сервер (причем, если канал полон или сервер все еще спит, клиентская программа блокируется) и затем блокирует для вызова read свой собственный канал FIFO, ожидая ответа.

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

Процесс повторяется полностью до тех пор, пока последний клиент не закроет канал сервера, вызывая аварийное завершение серверного вызова read (возвращение 0), поскольку ни у одного процесса нет серверного канала, открытого для записи. Если бы это был реальный серверный процесс, вынужденный ожидать будущих клиентов, возможно, вам пришлось бы изменить его, выбрав одно из двух:

? открыть файловый дескриптор собственного серверного канала, чтобы вызов read всегда его блокировал, а не возвращал 0;

? закрыть и повторно открыть серверный канал, когда read вернет 0 байтов, чтобы серверный процесс блокировался вызовом open, ожидая клиента, так, как он это делал, стартуя первый раз.

Оба эти метода проиллюстрированы в новом варианте приложения для работы с базой данных компакт-дисков, использующем именованные каналы.

Оглавление книги


Генерация: 0.098. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз