Книга: UNIX: разработка сетевых приложений

3.9. Функции readn, writen и readline

3.9. Функции readn, writen и readline

Потоковые сокеты (например, сокеты TCP) демонстрируют с функциями read и write поведение, отличное от обычного ввода-вывода файлов. Функция read или write на потоковом сокете может ввести или вывести немного меньше байтов, чем запрашивалось, но это не будет ошибкой. Причиной может быть достижение границ буфера для сокета в ядре. Все, что требуется в этой ситуации — чтобы процесс повторил вызов функции read или write для ввода или вывода оставшихся байтов. (Некоторые версии Unix ведут себя аналогично при записи в канал (pipe) более 4096 байт.) Этот сценарий всегда возможен на потоковом сокете при выполнении функции read, но с функцией write он обычно наблюдается, только если сокет неблокируемый. Тем не менее вместо write мы всегда вызываем функцию writen на тот случай, если в данной реализации возможно возвращение меньшего количества данных, чем мы запрашиваем.

Введем три функции для чтения и записи в потоковый сокет.

#include "unp.h"
ssize_t readn(int filedes, void *buff, size_t nbytes);
ssize_t writen(int filedes, const void *buff, size_t nbytes);
ssize_t readline(int filedes, void *buff, size_t maxlen);
Все функции возвращают: количество считанных или записанных байтов, -1 в случае ошибки

В листинге 3.9 представлена функция readn, в листинге 3.10 — функция writen, а в листинге 3.11 — функция readline.

Листинг 3.9. Функция readn: считывание n байт из дескриптора

//lib/readn.c
 1 #include "unp.h"
 2 ssize_t /* Считывает n байт из дескриптора */
 3 readn(int fd, void *vptr, size_t n)
 4 {
 5  size_t nleft;
 6  ssize_t nread;
 7  char *ptr;
 8  ptr = vptr;
 9  nleft = n;
10  while (nleft > 0) {
11   if ((nread = read(fd, ptr, nleft)) < 0) {
12    if (errno == EINTR)
13     nread = 0; /* и вызывает снова функцию read() */
14    else
15     return (-1);
16   } else if (nread == 0)
17   break; /* EOF */
18   nleft -= nread;
19   ptr += nread;
20  }
21  return (n - nleft); /* возвращает значение >= 0 */
22 }

Листинг 3.10. Функция writen: запись n байт в дескриптор

//lib/writen.c
 1 #include "unp.h"
 2 ssize_t /* Записывает n байт в дескриптор */
 3 writen(int fd, const void *vptr, size_t n)
 4 {
 5  size_t nleft;
 6  ssize_t nwritten;
 7  const char *ptr;
 8  ptr = vptr;
 9  nleft = n;
10  while (nleft > 0) {
11   if ((nwritten = write(fd, ptr, nleft)) <= 0) {
12    if (errno == EINTR)
13     nwritten = 0; /* и снова вызывает функцию write() */
14    else
15     return (-1); /* ошибка */
16   }
17   nleft -= nwritten;
18   ptr += nwritten;
19  }
20  return (n);
21 }

Листинг 3.11. Функция readline: считывание следующей строки из дескриптора, по одному байту за один раз

//test/readline1.с
 1 #include "unp.h"
   /* Ужасно медленная версия, приводится только для примера */
 2 ssize_t
 3 readline(int fd, void *vptr, size_t maxlen)
 4 {
 5  ssize_t n, rc;
 6  char c, *ptr;
 7  ptr = vptr;
 8  for (n = 1; n < maxlen; n++) {
 9   again:
10   if ((rc = read(fd, &c, 1)) == 1) {
11    *ptr++ = c;
12    if (c == 'n')
13     break; /* записан символ новой строки, как в fgets() */
14   } else if (rc == 0) {
15    if (n == 1)
16     return (0); /* EOF, данные не считаны */
17    else
18     break; /* EOF, некоторые данные были считаны */
19   } else {
20    if (errno == EINTR)
21     goto again;
22    return (-1); /* ошибка, errno задается функцией read() */
23   }
24  }
25  *ptr = 0; /* завершаем нулем, как в fgets() */
26  return (n);
27 }

Если функция чтения или записи (read или write) возвращает ошибку, то наши функции проверяют, не совпадает ли код ошибки с EINTR (прерывание системного вызова сигналом, см. раздел 5.9). В этом случае прерванная функция вызывается повторно. Мы обрабатываем ошибку в этой функции, чтобы не заставлять процесс снова вызвать read или write, поскольку целью наших функций является предотвращение обработки нехватки данных вызывающим процессом.

В разделе 14.3 мы покажем, что вызов функции recv с флагом MSG_WAITALL позволяет обойтись без использования отдельной функции readn.

Заметим, что наша функция readline вызывает системную функцию read один раз для каждого байта данных. Это очень неэффективно, поэтому мы и написали в примечании «Ужасно медленно!». Возникает соблазн обратиться к стандартной библиотеке ввода-вывода (stdio). Об этом мы поговорим через некоторое время в разделе 14.8, но учтите, что это может привести к определенным проблемам. Буферизация, предоставляемая stdio, решает проблемы с производительностью, но при этом создает множество логистических сложностей, которые в свою очередь порождают скрытые ошибки в приложении. Дело в том, что состояние буферов stdio недоступно процессу. Рассмотрим, например, строчный протокол взаимодействия клиента и сервера, причем такой, что могут существовать разные независимые реализации клиентов и серверов (достаточно типичное явление; например, множество веб-браузеров и веб-серверов были разработаны независимо в соответствии со спецификацией HTTP). Хороший стиль программирования заключается в том, что эти программы должны не только ожидать от своих собеседников соблюдения того же протокола, но и контролировать трафик на возможность получения непредвиденного трафика. Подобные нарушения протокола должны рассматриваться как ошибки, чтобы программисты имели возможность находить и устранять неполадки в коде, а также обнаруживать попытки взлома систем. Обработка некорректного трафика должна давать приложению возможность продолжать работу. Буферизация stdio мешает достижению перечисленных целей, поскольку приложение не может проверить наличие непредвиденных (некорректных) данных в буферах stdio в любой конкретный момент.

Существует множество сетевых протоколов, основанных на использовании строк текста: SMTP, HTTP, FTP, finger. Поэтому соблазн работать со строками будет терзать вас достаточно часто. Наш совет: мыслить в терминах буферов, а не строк. Пишите код таким образом, чтобы считывать содержимое буфера, а не отдельные строки. Если же ожидается получение строки, ее всегда можно поискать в считанном буфере.

В листинге 3.12 приведена более быстрая версия функции readline, использующая свой собственный буфер (а не буферизацию stdio). Основное достоинство этого буфера состоит в его открытости, благодаря чему вызывающий процесс всегда знает, какие именно данные уже приняты. Несмотря на это, использование readline все равно может вызвать проблемы, как мы увидим в разделе 6.3. Системные функции типа select ничего не знают о внутреннем буфере readline, поэтому неаккуратно написанная программа с легкостью может очутиться в состоянии ожидания в вызове select, при том, что данные уже будут находиться в буферах readline. По этой причине сочетание вызовов readn и readline не будет работать так, как этого хотелось бы, пока функция readn не будет модифицирована с учетом наличия внутреннего буфера.

Листинг 3.12. Улучшенная версия функции readline

//lib/readline.c
 1 #include "unp.h"
 2 static int read_cnt;
 3 static char *read_ptr;
 4 static char read_buf[MAXLINE];
 5 static ssize_t
 6 my_read(int fd, char *ptr)
 7 {
 8  if (read_cnt <= 0) {
 9   again:
10   if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
11    if (errno == EINTR)
12     goto again;
13    return(-1);
14   } else if (read_cnt == 0)
15   return(0);
16   read_ptr = read_buf;
17  }
18  read_cnt--;
19  *ptr = *read_ptr++;
20  return(1);
21 }
22 ssize_t
23 readline(int fd, void *vptr, size_t maxlen)
24 {
25  ssize_t n, rc;
26  char c, *ptr;
27  ptr = vptr;
28  for (n = 1; n < maxlen; n++) {
29   if ((rc = my_read(fd, &c)) == 1) {
30    *ptr++ = c;
31    if (c== 'n')
32     break; /* Записан символ новой строки, как в fgets() */
33   } else if (rc == 0) {
34    *ptr = 0;
35    return(n - 1); /* EOF, считано n-1 байт данных */
36   } else
37    return(-1); /* ошибка, read() задает значение errno */
38  }
39  *ptr = 0; /* завершающий нуль, как в fgets() */
40  return(n);
41 }
42 ssize_t
43 readlinebuf(void **vptrptr)
44 {
45  if (read_cnt)
46   *vptrptr = read_ptr;
47  return(read_cnt);
48 }
2-21
 Внутренняя функция my_read считывает до MAXLINE символов за один вызов и затем возвращает их по одному.

29 Единственное изменение самой функции readline заключается в том, что теперь она вызывает функцию my_read вместо read.

42-48 Новая функция readlinebuf выдает сведения о состоянии внутреннего буфера, что позволяет вызывающим функциям проверить, нет ли в нем других данных, помимо уже принятой строки.

ПРИМЕЧАНИЕ

К сожалению, использование переменных типа static в коде readline.c для поддержки информации о состоянии при последовательных вызовах приводит к тому, что функция больше не является безопасной в многопоточной системе (thread-safe) и повторно входимой (reentrant). Мы обсуждаем это в разделах 11.18 и 26.5. Мы предлагаем версию, безопасную в многопоточной системе, основанную на собственных данных программных потоков, в листинге 26.5.

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


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