UNIX: разработка сетевых приложений - Уильям Стивенс
Шрифт:
Интервал:
Закладка:
Традиционно System V возвращала для неблокируемой операции ввода-вывода, которую невозможно выполнить, ошибку EAGAIN, в то время как Беркли-реализации возвращали ошибку EWOULDBLOCK. Еще больше дело запутывается тем, что согласно POSIX.1 используется EAGAIN, в то время как в POSIX.1g определено, что используется EWOULDBLOCK. К счастью, большинство систем (включая SVR4 и 4.4BSD) определяют один и тот же код для этих двух ошибок (проверьте свой системный заголовочный файл <sys/errno.h>), поэтому не важно, какой из них использовать. В нашем тексте мы используем ошибку EWOULDBLOCK, как определяется в POSIX.
В разделе 6.2 мы представили различные модели ввода-вывода и сравнили неблокируемый ввод-вывод с другими моделями. В этой главе мы покажем примеры четырех типов операций и разработаем новый тип клиента, аналогичный веб-клиенту, инициирующий одновременно множество соединений TCP при помощи неблокируемой функции connect.
16.2. Неблокируемые чтение и запись: функция str_cli (продолжение)
Мы снова возвращаемся к нашей функции str_cli, которую мы обсуждали в разделах 5.5 и 6.4. Последняя ее версия, задействующая функцию select, продолжает использовать блокируемый ввод-вывод. Например, если в стандартном устройстве ввода имеется некоторая строка, мы читаем ее с помощью функции fgets и затем отправляем серверу с помощью функции writen. Но вызов функции writen может вызвать блокирование процесса, если буфер отправки сокета полон. В то время как мы заблокированы в вызове функции writen, данные могут быть доступны для чтения из приемного буфера сокета. Аналогично, когда строка ввода доступна из сокета, мы можем заблокироваться в последующем вызове функции fputs, если стандартный поток вывода работает медленнее, чем сеть. Наша цель в данном разделе — создать версию этой функции, использующую неблокируемый ввод-вывод. Блокирование будет предотвращено, благодаря чему в это время мы сможем сделать еще что-то полезное.
К сожалению, добавление неблокируемого ввода-вывода значительно усложняет управление буфером функции, поэтому мы будем представлять функцию частями. Мы также заменим вызовы функций из стандартной библиотеки ввода-вывода на обычные read и write. Это даст возможность отказаться от функций стандартной библиотеки ввода-вывода с неблокируемыми дескрипторами, так как их применение может привести к катастрофическим последствиям.
Мы работаем с двумя буферами: буфер to содержит данные, направляющиеся из стандартного потока ввода к серверу, а буфер fr — данные, приходящие от сервера в стандартный поток вывода. На рис. 16.1 представлена организация буфера to и указателей в буфере.
Рис. 16.1. Буфер, содержащий данные из стандартного потока ввода, идущие к сокету
Указатель toiptr указывает на следующий байт, в который данные могут быть считаны из стандартного потока ввода. Указатель tooptr указывает на следующий байт, который должен быть записан в сокет. Число байтов, которое может быть считано из стандартного потока ввода, равно &to[MAXLINE] минус toiptr. Как только значение tooptr достигает toiptr, оба указателя переустанавливаются на начало буфера.
На рис. 16.2 показана соответствующая организация буфера fr. В листинге 16.1[1] представлена первая часть функции.
Рис. 16.2. Буфер, содержащий данные из сокета, идущие к стандартному устройству вывода
Листинг 16.1. Функция str_cli: первая часть, инициализация и вызов функции
//nonblock/strclinonb.c
1 #include "unp.h"
2 void
3 str_cli(FILE *fp, int sockfd)
4 {
5 int maxfdp1, val, stdineof;
6 ssize_t n, nwritten;
7 fd_set rset, wset;
8 char to[MAXLINE], fr[MAXLINE];
9 char *toiptr, *tooptr, *friptr, *froptr;
10 val = Fcntl(sockfd, F_GETFL, 0);
11 Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);
12 val = Fcntl(STDIN_FILENO, F_SETFL, 0);
13 Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);
14 val = Fcntl(STDOUT_FILENO, F_SETFL, 0);
15 Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);
16 toiptr = tooptr = to; /* инициализация указателей буфера */
17 friptr = froptr = fr;
18 stdineof = 0;
19 maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
20 for (;;) {
21 FD_ZERO(&rset);
22 FD_ZERO(&wset);
23 if (stdineof == 0 && toiptr < &to[MAXLINE])
24 FD_SET(STDIN_FILENO, &rset); /* чтение из стандартного потока
ввода */
25 if (friptr < &fr[MAXLINE])
26 FD_SET(sockfd, &rset); /* чтение из сокета */
27 if (tooptr != toiptr)
28 FD_SET(sockfd, &wset); /* данные для записи в сокет */
29 if (froptr != friptr)
30 FD_SET(STDOUT_FILENO, &wset); /* данные для записи в стандартный
поток вывода */
31 Select(maxfdp1, &rset, &wset, NULL, NULL);
Установка неблокируемых дескрипторов10-15 Все три дескриптора делаются неблокируемыми при помощи функции fcntl: сокет в направлении к серверу и от сервера, стандартный поток ввода и стандартный поток вывода.
Инициализация указателей буфера16-19 Инициализируются указатели в двух буферах и вычисляется максимальный дескриптор. Это значение, увеличенное на единицу, будет использоваться в качестве первого аргумента функции select.
Основной цикл: подготовка к вызову функции select20 Как и в случае первой версии этой функции, показанной в листинге 6.2, основной цикл функции содержит вызов функции select, за которой следуют отдельные проверки различных интересующих нас условий.
Подготовка интересующих нас дескрипторов21-30 Оба набора дескрипторов обнуляются и затем в каждом наборе включается не более двух битов. Если мы еще не прочитали конец файла из стандартного потока ввода и есть место как минимум для 1 байта данных в буфере to, то в наборе флагов чтения включается бит, соответствующий стандартному потоку ввода. Если есть место как минимум для 1 байта данных в буфере fr, то в наборе флагов чтения включается бит, соответствующий сокету. Если есть данные для записи в сокет в буфере to, то в наборе флагов записи включается бит, соответствующий сокету. Наконец если в буфере fr есть данные для отправки в стандартный поток вывода, то в наборе флагов записи включается бит, соответствующий этому стандартному потоку.
Вызов функции select31 Вызывается функция select, ожидающая, когда одно из четырех условий станет истинным. Для этой функции мы не задаем тайм-аута.
Следующая часть нашей функции показана в листинге 16.2. Этот код содержит первые две проверки (из четырех возможных), выполняемые после завершения функции select.
Листинг 16.2. Функция str_cli: вторая часть, чтение из стандартного потока ввода или сокета
//nonblock/strclinonb.c
32 if (FD_ISSET(STDIN_FILENO, &rset)) {
33 if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
34 if (errno != EWOULDBLOCK)
35 err_sys("read error on stdin");
36 } else if (n == 0) {
37 fprintf(stderr, "%s: EOF on stdinn", gf_time());
38 stdineof = 1; /* с stdin все сделано */
39 if (tooptr == toiptr)
40 Shutdown(sockfd, SHUT_WR); /* отсылаем FIN */
41 } else {
42 fprintf(stderr, "%s: read %d bytes from stdinn", gf_time(),
43 n);
44 toiptr += n; /* только что полученное из функции read число */
45 FD_SET(sockfd, &wset); /* включаем бит в наборе чтения */
46 }
47 }
48 if (FD_ISSET(sockfd, &rset)) {
49 if ((n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
50 if (errno != EWOULDBLOCK)
51 err_sys("read error on socket");
52 } else if (n == 0) {
53 fprintf(stderr, "%s: EOF on socketn", gf_time());
54 if (stdineof)
55 return; /* нормальное завершение */
56 else
57 err_quit("str_cli: server terminated prematurely");
58 } else {
59 fprintf(stderr, "%s: read %d bytes from socketn",
60 gf_time(), n);
61 friptr += n; /* только что полученное из функции read число */
62 FD_SЕТ(STDOUT_FILЕNO, &wset); /* включаем бит в наборе
чтения */
63 }
64 }
Чтение из стандартного потока ввода с помощью функции read32-33 Если стандартный поток ввода готов для чтения, мы вызываем функцию read. Третий ее аргумент — это количество свободного места в буфере to.