Книга: UNIX: взаимодействие процессов
4.10. Потоки и сообщения
Разделы на этой странице:
- Листинг 4.12. Структура mymesg и сопутствующие определения
- Листинг 4.13. Функция mesg_send
- Листинг 4.14. Функция mesg_recv
- Листинг 4.15. Функция client с использованием сообщений
- Считывание имени файла и отправка его серверу
- Считывание содержимого файла или сообщения об ошибке от сервера
- Листинг 4.16. Функция server, использующая сообщения
- Считывание имени файла из канала IPC, открытие файла
- Отправка файла клиенту
4.10. Потоки и сообщения
Приведенные примеры пpoгрaммныx каналов и каналов FIFO использовали потоковую модель ввода-вывода, что естественно для Unix. При этом отсутствуют грaницы записей — данные при операциях чтения и записи не проверяются вовсе. Процесс, считывающий 100 байт из FIFO, не может определить, записал ли другой процесс в FIFO все 100 байт за 1 раз, или за 5 раз по 20 байт, или в любой другой комбинации общим объемом 100 байт. Возможно, один процесс записал в FIFO 55 байт, а потом другой — 45. Данные представляют собой просто поток байтов, никак не интерпретируемых системой. Если же требуется какая-либо интерпретация данных, пишущий и читающий процессы должны заранее «договориться» о ее правилах и выполнять всю работу самостоятельно.
Иногда приложению может потребоваться передавать данные, обладающие некоторой внутренней структурой. Это могут быть, например, сообщения переменной длины: в этом случае читающий процесс должен знать, где заканчивается одно сообщение и начинается следующее. Для разграничения сообщений широко используются три метода:
1. Специальная внутриполосная завершающая последовательность: множество приложений под Unix используют в качестве разделителя сообщений символ перевода строки. Пишущий процесс добавляет к каждому сообщению этот символ, а считывающий процесс производит построчное считывание. Так работают клиент и сервер из листингов 4.10 и 4.11, чтобы разделить запросы клиентов. Этот метод требует исключения символа-разделителя из самих передаваемых данных (в случае необходимости его передать он должен предваряться другим специальным символом).
2. Явное указание длины: каждой записи предшествует информация об ее длине. Мы вскоре воспользуемся этим методом. Он также применяется в Sun RPC при использовании совместно с TCP. Одним из преимуществ этого метода является отсутствие необходимости исключать разделитель из передаваемых данных, поскольку получатель не проверяет все данные, а переходит сразу к концу очередной записи, чтобы узнать длину следующей.
3. Одна запись за подключение: приложение закрывает подключение к партнеру (подключение TCP для сетевых приложений либо просто подключение IPC), обозначая конец записи. Это требует повторного подключения для передачи следующей записи, однако используется в стандарте HTTP 1.0.
Стандартная библиотека ввода-вывода также может использоваться для считывания и записи данных в пpoгрaммный канал или FIFO. Поскольку канал может быть открыт только функцией piре, возвращающей открытый дескриптор, для создания нового стандартного потока, связанного с этим дескриптором, можно использовать стандартную функцию fdopen. Канал FIFO обладает именем, поэтому он может быть открыт с помощью функции fopen.
Можно создавать и более структурированные сообщения — эта возможность предоставляется очередями сообщений и в Posix, и в System V. Мы вскоре узнаем, что каждое сообщение обладает длиной и приоритетом (типом в System V). Длина и приоритет указываются отправителем и возвращаются получателю после считывания сообщения. Каждое сообщение представляет собой запись, аналогично дeйтaгрaммaм UDP ([24]).
Мы можем структурировать данные, передаваемые по программному каналу или FIFO, самостоятельно. Определим сообщение в нашем заголовочном файле mesg.h, как показано в листинге 4.12.
Листинг 4.12. Структура mymesg и сопутствующие определения
//pipemesg/mesg.h
1 #include "unpipc.h"
2 /* Наши собственные "сообщения", которые могут использоваться с каналами, FIFO и очередями сообщений */
3 /* Мы хотим, чтобы sizeof(struct mymesg) <= PIPE_BUF */
4 #define MAXMESGDATA (PIPE_BUF – 2*sizeof(long))
5 /* Длина mesg_len и mesg_type */
6 #define MESGHDRSIZE (sizeof(struct mymesg) – MAXMESGDATA)
7 struct mymesg {
8 long mesg_len; //количество байтов в mesg_data, может быть О
9 long mesg_type;//тип сообщения, должен быть > 0
10 char mesg_data[MAXMESGDATA];
11 };
12 ssize_t mesg_send(int, struct mymesg *);
13 void Mesg_send(int, struct mymesg *);
14 ssize_t mesg_recv(int, struct mymesg *);
15 ssize_t Mesg_recv(int, struct mymesg *);
Каждое сообщение содержит в себе информацию о своем типе (mesg_type), причем значение этой переменной должно быть больше нуля. Пока мы будем игнорировать это поле в записи, но вернемся к нему в главе 6, где описываются очереди сообщений System V. Каждое сообщение также обладает длиной, кoтopая может быть и нулевой. Структура mymesg позволяет предварить каждое сообщение информацией о его типе и длине вместо использования символа перевода строки для сигнализации конца сообщения. Ранее мы отметили два преимущества этого подхода: получатель не должен сканировать все принятые байты в поисках конца сообщения и отсутствует необходимость исключать появление разделителя в самих данных.
На рис. 4.13 изображен вид структуры mymesg и ее использование с каналами, FIFO и очередями сообщений System V.
Рис. 4.13. Структура mymesg
Мы определяем две функции для отправки и приема сообщений. В листинге 4.13 приведен текст функции mesg_send, а в листинге 4.14 — функции mesg_recv.
Листинг 4.13. Функция mesg_send
//pipemesg/mesg_send.c
1 #include "mesg.h"
2 ssize_t
3 mesg_send(int fd, struct mymesg *mptr)
4 {
5 return(write(fd, mptr, MESGHDRSIZE + mptr->mesg_len));
6 }
Листинг 4.14. Функция mesg_recv
//pipemesg/mesg_recv.c
1 #include "mesg.h"
2 ssize_t
3 mesg_recv(int fd, struct mymesg *mptr)
4 {
5 size_t len;
6 ssize_t n;
8 /* считывание заголовка сообщения для определения его длины */
9 if ((n = Read(fd, mptr, MESGHDRSIZE)) == 0)
10 return(0); /* end of file */
11 else if (n != MESGHDRSIZE)
12 err_quit("message header: expected %d, got %d". MESGHDRSIZE, n);
13 if ((len = mptr->mesg_len) > 0)
14 if ((n = Read(fd, mptr->mesg_data, len)) != len)
15 err_quit("message data: expected %d, got %d", len, n);
16 return(len);
17 }
Теперь для каждого сообщения функция read вызывается дважды: один раз для считывания длины, а другой — для считывания самого сообщения (если его длина больше 0).
ПРИМЕЧАНИЕ
Внимательные читатели могли заметить, что функция mesg_recv проверяет наличие всех возможных ошибок и прекращает работу при их обнаружении. Однако мы все же определили функцию-обертку Mesg_recv и вызываем из наших программ именно ее — для единообразия.
Изменим теперь функции client и server, чтобы воспользоваться новыми функциями mesg_send и mesg_recv. В листинге 4.15 приведен текст функции-клиента.
Листинг 4.15. Функция client с использованием сообщений
//pipemesg/client.c
1 #include "mesg.h"
2 void
3 client(int readfd, int writefd)
4 {
5 size_t len;
6 ssize_t n;
7 struct mymesg mesg;
8 /* считывание полного имени */
9 Fgets(mesg.mesg_data, MAXMESGDATA, stdin);
10 len = strlen(mesg.mesg_data);
11 if (mesg.mesg_data[len-1] == 'n')
12 len--; /* удаление перевода строки из fgets() */
13 mesg.mesg_len = len;
14 mesg.mesg_type = 1;
15 /* запись полного имени в канал IPC */
16 Mesg_send(writefd, &mesg);
17 /* считывание из канала IPC. запись в stdout */
18 while ( (n = Mesg_recv(readfd, &mesg)) > 0)
19 Write(STDOUT_FILENO, mesg.mesg_data, n);
20 }
Считывание имени файла и отправка его серверу
8-16 Полное имя считывается из стандартного потока ввода и затем отправляется на сервер с помощью функции mesg_send.
Считывание содержимого файла или сообщения об ошибке от сервера
17-19 Клиент вызывает функцию mesg_recv в цикле, считывая все приходящие от сервера сообщения. По соглашению, когда mesg_recv возвращает нулевую длину сообщения, это означает конец передаваемых сервером данных. Мы увидим, что сервер добавляет символ перевода строки к каждому сообщению, отправляемому клиенту, поэтому пустая строка будет иметь длину сообщения 1. В листинге 4.16 приведен текст функции-сервера.
Листинг 4.16. Функция server, использующая сообщения
//pipemesg/server.c
1 #include "mesg.h"
2 void
3 server(int readfd, int writefd)
4 {
5 FILE *fp;
6 ssize_t n;
7 struct mymesg mesg;
8 /* считывание полного имени из канала */
9 mesg.mesg_type = 1;
10 if ((n = Mesg_recv(readfd, &mesg)) == 0)
11 err_quit("pathname missing");
12 mesg.mesg_data[n] = ''; /* полное имя, завершающееся 0 */
13 if ((fp = fopen(mesg.mesg_data, "r")) == NULL) {
14 /* ошибка, нужно сообщить клиенту */
15 snprintf(mesg.mesg_data + n, sizeof(mesg.mesg_data) – n,
16 ": can't open, %sn", strerror(errno));
17 mesg.mesg_len = strlen(mesg.mesg_data);
18 Mesg_send(writefd, &mesg);
19 } else {
20 /* файл успешно открыт, передача данных */
21 while (Fgets(mesg.mesg_data, MAXMESGDATA, fp) != NULL) {
22 mesg.mesg_len = strlen(mesg.mesg_data);
23 Mesg_send(writefd, &mesg);
24 }
25 Fclose(fp);
26 }
27 /* отправка сообщения нулевой длины для обозначения конца связи */
28 mesg.mesg_len = 0;
29 Mesg_send(writefd, &mesg);
30 }
Считывание имени файла из канала IPC, открытие файла
8-18 Сервер принимает от клиента имя файла. Хотя значение mesg_type, равное 1, нигде не используется (оно затирается функцией mesg_recv из листинга 4.14), мы будем использовать ту же функцию при работе с очередями сообщений System V (листинг 6.8), а в данном случае в этом значении уже возникает потребность (см., например, листинг 6.11). Стандартная функция ввода-вывода fopen открывает файл, что отличается от листинга 4.3, где вызывалась функция open для получения дескриптора файла. Причина, по которой мы воспользовались fopen, заключается в том, что в этой пpoгрaммe мы пользуемся библиотечной функцией fgets для считывания содержимого файла построчно и затем отправляем клиенту строку за строкой.
Отправка файла клиенту
19-26 Если вызов fopen оказывается успешным, содержимое файла считывается с помощью функции fgets и затем отправляется клиенту построчно. Сообщение с нулевой длиной означает конец файла.
При использовании пpoгрaммныx каналов или FIFO мы могли бы также закрыть канал IPC, чтобы дать клиенту знать о том, что передача файла завершена. Однако мы используем передачу сообщения нулевой длины, потому что другие типы IPC не поддерживают концепцию конца файла.
Функции main, вызывающие новые функции client и server, вообще не претерпели никаких изменений. Мы можем использовать либо версию для работы с каналами (листинг 4.1), либо версию для работы с FIFO (листинг 4.6).
- 4.1. Введение
- 4.2. Приложение типа клиент-сервер
- 4.3. Программные каналы
- 4.4. Двусторонние каналы
- 4.5. Функции popen и pclose
- 4.6. Именованные каналы (FIFO)
- 4.7. Некоторые свойства именованных и неименованных каналов
- 4.8. Один сервер, несколько клиентов
- 4.9. Последовательные и параллельные серверы
- 4.10. Потоки и сообщения
- 4.11. Ограничения программных каналов и FIFO
- 4.12. Резюме
- Упражнения
- Обмен сообщениями и многопоточность
- Процессы и потоки
- Сообщения об ошибках gsec
- Стандартные потоки: stdin, stdout, stdeir, stdaux, stdprn.
- 1.2 Процесс, контекст процесса и потоки
- ЧАСТЬ 2 ОБМЕН СООБЩЕНИЯМИ
- Текстовые сообщения процедуры POST
- Обмен сообщениями в сети
- Распределенный обмен сообщениями
- Сокращения в SMS-сообщениях
- Пример неудачного электронного сообщения
- 10.1.2. Сообщения сервера