355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Уильям Ричард Стивенс » UNIX: разработка сетевых приложений » Текст книги (страница 18)
UNIX: разработка сетевых приложений
  • Текст добавлен: 17 сентября 2016, 20:42

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


Автор книги: Уильям Ричард Стивенс


Соавторы: Эндрю М. Рудофф,Билл Феннер

Жанр:

   

ОС и Сети


сообщить о нарушении

Текущая страница: 18 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]

6.7. Функция str_cli (еще раз)

В листинге 6.2 представлена наша обновленная (и корректная) функция str_cli. В этой версии используются функции selectи shutdown. Первая уведомляет нас о том, когда сервер закрывает свой конец соединения, а вторая позволяет корректно обрабатывать пакетный ввод. Эта версия избавлена от ориентации на строки. Вместо этого она работает с буферами, что позволяет полностью избавиться от проблем, описанных в конце раздела 6.5.

Листинг 6.2. функция str_cli, использующая функцию select, которая корректно обрабатывает конец файла

//select/strcliselect02.c

 1 #include "unp.h"

 2 void

 3 str_cli(FILE *fp, int sockfd)

 4 {

 5  int maxfdp1, stdineof;

 6  fd_set rset;

 7  char buf[MAXLINE];

 8  int n;

 9  stdineof = 0;

10  FD_ZERO(&rset);

11  for (;;) {

12   if (stdineof == 0)

13    FD_SET(fileno(fp), &rset);

14   FD_SET(sockfd, &rset);

15   maxfdp1 = max(fileno(fp), sockfd) + 1;

16   Select(maxfdp1, &rset, NULL, NULL, NULL);

17   if (FD_ISSET(sockfd, &rset)) { /* сокет готов для чтения */

18    if ((n = Read(sockfd, buf, MAXLINE)) == 0) {

19     if (stdineof == 1)

20      return; /* нормальное завершение */

21     else

22      err_quit("str_cli: server terminated prematurely");

23    }

24    Write(fileno(stdout), buf, n);

25   }

26   if (FD_ISSET(fileno(fp), &rset)) { /* есть данные на входе */

27    if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) {

28     stdineof = 1;

29     Shutdown(sockfd, SHUT_WR); /* отправка сегмента FIN */

30     FD_CLR(fileno(fp), &rset);

31     continue;

32    }

33    Writen(sockfd, buf, n);

34   }

35  }

36 }

5-8  stdineof– это новый флаг, инициализируемый нулем. Пока этот флаг равен нулю, мы будем проверять готовность стандартного потока ввода к чтению с помощью функции select.

16-24 Если мы считываем на сокете признак конца файла, когда нам уже встретился ранее признак конца файла в стандартном потоке ввода, это является нормальным завершением и функция возвращает управление. Но если конец файла в стандартном потоке ввода еще не встречался, это означает, что процесс сервера завершился преждевременно. В новой версии мы вызываем функции readи writeи работаем с буферами, а не со строками, благодаря чему функция selectдействует именно так, как мы рассчитывали.

25-33 Когда нам встречается признак конца файла на стандартном устройстве ввода, наш новый флаг stdineofустанавливается в единицу и мы вызываем функцию shutdownсо вторым аргументом SHUT_WRдля отправки сегмента FIN.

Если мы измерим время работы нашего клиента TCP, использующего функцию str_cli, показанную в листинге 6.2, с тем же файлом из 2000 строк, это время составит 12,3 с, что почти в 30 раз быстрее, чем при использовании версии этой функции, работающей в режиме остановки и ожидания.

Мы еще не завершили написание нашей функции str_cli: в разделе 15.2 мы разработаем ее версию с использованием неблокируемого ввода-вывода, а в разделе 23.3 – версию, работающую с программными потоками.

6.8. Эхо-сервер TCP (продолжение)

Вернемся к нашему эхо-серверу TCP из разделов 5.2 и 5.3. Перепишем сервер как одиночный процесс, который будет использовать функцию selectдля обработки любого числа клиентов, вместо того чтобы порождать с помощью функции forkпо одному дочернему процессу для каждого клиента. Перед тем как представить этот код, взглянем на структуры данных, используемые для отслеживания клиентов. На рис. 6.11 показано состояние сервера до того, как первый клиент установил соединение.

Рис. 6.11. Сервер TCP до того, как первый клиент установил соединение

У сервера имеется одиночный прослушиваемый дескриптор, показанный на рисунке точкой.

Сервер обслуживает только набор дескрипторов для чтения, который мы показываем на рис. 6.12. Предполагается, что сервер запускается в приоритетном (foreground) режиме, а дескрипторы 0, 1 и 2 соответствуют стандартным потокам ввода, вывода и ошибок. Следовательно, первым доступным для прослушиваемого сокета дескриптором является дескриптор 3. Массив целых чисел clientсодержит дескрипторы присоединенного сокета для каждого клиента. Все элементы этого массива инициализированы значением -1.

Рис. 6.12. Структуры данных для сервера TCP с одним прослушиваемым сокетом

Единственная ненулевая запись в наборе дескрипторов – это запись для прослушиваемого сокета, и поэтому первый аргумент функции selectбудет равен 4.

Когда первый клиент устанавливает соединение с нашим сервером, прослушиваемый дескриптор становится доступным для чтения и сервер вызывает функцию accept. Новый присоединенный дескриптор, возвращаемый функцией accept, будет иметь номер 4, если выполняются приведенные выше предположения. На рис. 6.13 показано соединение клиента с сервером.

Рис. 6.13. Сервер TCP после того как первый клиент устанавливает соединение

Теперь наш сервер должен запомнить новый присоединенный сокет в своем массиве client, и присоединенный сокет должен быть добавлен в набор дескрипторов. Изменившиеся структуры данных показаны на рис. 6.14.

Рис. 6.14. Структуры данных после того как установлено соединение с первым клиентом

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

Рис. 6.15. Сервер TCP после того как установлено соединение со вторым клиентом

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

Рис. 6.16. Структуры данных после того как установлено соединение со вторым клиентом

Далее мы предположим, что первый клиент завершает свое соединение. TCP-клиент отправляет сегмент FIN, превращая тем самым дескриптор номер 4 на стороне сервера в готовый для чтения. Когда наш сервер считывает этот присоединенный сокет, функция readlineвозвращает нуль. Затем мы закрываем сокет, и соответственно изменяются наши структуры данных. Значение client[0]устанавливается в -1, а дескриптор 4 в наборе дескрипторов устанавливается в нуль. Это показано на рис. 6.17. Обратите внимание, что значение переменной maxfdне изменяется.

Рис. 6.17. Структуры данных после того как первый клиент разрывает соединение

Итак, по мере того как приходят клиенты, мы записываем дескриптор их присоединенного сокета в первый свободный элемент массива client(то есть в первый элемент со значением -1). Следует также добавить присоединенный сокет в набор дескрипторов для чтения. Переменная maxi– это наибольший используемый в данный момент индекс в массиве client, а переменная maxfd(плюс один) – это текущее значение первого аргумента функции select. Единственным ограничением на количество обслуживаемых сервером клиентов является минимальное из двух значений: FD_SETSIZEи максимального числа дескрипторов, которое допускается для данного процесса ядром (о чем мы говорили в конце раздела 6.3).

В листинге 6.3 показана первая половина этой версии сервера.

Листинг 6.3. Сервер TCP, использующий одиночный процесс и функцию select: инициализация

//tcpcliserv/tcpservselect01.c

 1 #include "unp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  int i, maxi, maxfd, listenfd, connfd, sockfd;

 6  int nready, client[FD_SETSIZE],

 7  ssize_t n;

 8  fd_set rset, allset;

 9  char buf[MAXLINE];

10  socklen_t clilen;

11  struct sockaddr_in cliaddr, servaddr;

12  listenfd = Socket(AF_INET, SOCK_STREAM, 0);

13  bzero(&servaddr, sizeof(servaddr));

14  servaddr.sin_family = AF_INET;

15  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

16  servaddr.sin_port = htons(SERV_PORT);

17  Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));

18  Listen(listenfd, LISTENQ);

19  maxfd = listenfd; /* инициализация */

20  maxi = -1; /* индекс в массиве client[] */

21  for (i = 0; i < FD_SETSIZE; i++)

22   client[i] = -1; /* -1 означает свободный элемент */

23  FD_ZERO(&allset);

24  FD_SET(listenfd, &allset);

Создание прослушиваемого сокета и инициализация функции select

12-24 Этапы создания прослушиваемого сокета те же, что и раньше: вызов функций socket, bindи listen. Мы инициализируем структуры данных при том условии, что единственный дескриптор, который мы с помощью функции selectвыберем, изначально является прослушиваемым сокетом.

Вторая половина функции mainпоказана в листинге 6.4.

Листинг 6.4. Сервер TCP, использующей одиночный процесс и функцию select: цикл

//tcpcliserv/tcpservselect01.c

25  for (;;) {

26   rset = allset; /* присваивание значения структуре */

27   nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);

28   if (FD_ISSET(listenfd, &rset)) { /* соединение с новым клиентом */

29    clilen = sizeof(cliaddr);

30    connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);

31    for (i = 0; i < FD_SETSIZE; i++)

32     if (client[i] < 0) {

33      client[i] = connfd; /* сохраняем дескриптор */

34      break;

35     }

36    if (i == FD_SETSIZE)

37     err_quit("too many clients");

38    FD_SET(connfd, &allset); /* добавление нового дескриптора */

39    if (connfd > maxfd)

40     maxfd = connfd; /* для функции select */

41    if (i > maxi)

42     maxi = i; /* максимальный индекс в массиве clientf[] */

43    if (–nready <= 0)

44     continue; /* больше нет дескрипторов, готовых для чтения */

45   }

46   for (i = 0; i <= maxi; i++) { /* проверяем все клиенты на наличие

                                      данных */

47    if ((sockfd – client[i]) < 0)

48     continue;

49    if (FD_ISSET(sockfd, &rset)) {

50     if ((n = Read(sockfd, buf, MAXLINE)) == 0) {

51      /* соединение закрыто клиентом */

52      Close(sockfd);

53      FD_CLR(sockfd, &allset);

54      client[i] = -1;

55     } else

56      Writen(sockfd, line, n);

57     if (–nready <= 0)

58      break; /* больше нет дескрипторов, готовых для чтения */

59    }

60   }

61  }

62 }

Блокирование в функции select

26-27 Функция selectждет, пока не будет установлено новое клиентское соединение или на существующем соединении не прибудут данные, сегмент FIN или сегмент RST.

Принятие новых соединений с помощью функции accept

28-45 Если прослушиваемый сокет готов для чтения, новое соединение установлено. Мы вызываем функцию acceptи соответствующим образом обновляем наши структуры данных. Для записи присоединенного сокета мы используем первый незадействованный элемент массива client. Число готовых дескрипторов уменьшается, и если оно равно нулю, мы можем не выполнять следующий цикл for. Это позволяет нам использовать значение, возвращаемое функцией select, чтобы избежать проверки не готовых дескрипторов.

Проверка существующих соединений

46-60 Каждое существующее клиентское соединение проверяется на предмет того, содержится ли его дескриптор в наборе дескрипторов, возвращаемом функцией select. Если да, то из этого дескриптора считывается строка, присланная клиентом, и отражается обратно клиенту. Если клиент закрывает соединение, функция read возвращает нуль и мы обновляем структуры соответствующим образом.

Мы не уменьшаем значение переменной maxi, но могли бы проверять возможность сделать это каждый раз, когда клиент закрывает свое соединение.

Этот сервер сложнее, чем сервер, показанный в листингах 5.1 и 5.2, но он позволяет избежать затрат на создание нового процесса для каждого клиента, что является хорошим примером использования функции select. Тем не менее в разделе 15.6 мы опишем проблему, связанную с этим сервером, которая, однако, легко устраняется, если сделать прослушиваемый сокет неблокируемым, а затем проверить и проигнорировать несколько ошибок из функции accept.

Атака типа «отказ в обслуживании»

К сожалению, функционирование только что описанного сервера вызывает проблемы. Посмотрим, что произойдет, если некий клиент-злоумышленник соединится с сервером, отправит 1 байт данных (отличный от разделителя строк) и войдет в состояние ожидания. Сервер вызовет функцию readline, которая прочитает одиночный байт данных от клиента и заблокируется в следующем вызове функции read, ожидая следующих данных от клиента. Сервер блокируется (вернее, «подвешивается») этим клиентом и не может предоставить обслуживание никаким другим клиентам (ни новым клиентским соединениям, ни данным существующих клиентов), пока упомянутый клиент-злоумышленник не отправит символ перевода строки или не завершит свой процесс.

Дело в том, что обрабатывая множество клиентов, сервер никогдане должен блокироваться в вызове функции, относящейся к одному клиенту. В противном можно «подвесить» сервер, что приведет к отказу в обслуживании для всех остальных клиентов. Это называется атакой типа «отказ в обслуживании» (DoS attack – Denial of Service). Такая атака воздействует на сервер, делая невозможным обслуживание нормальных клиентов. Обезопасить себя от подобных атак позволяют следующие решения: использовать неблокируемый ввод-вывод (см. главу 16), предоставлять каждому клиенту обслуживание отдельным потоком (например, для каждого клиента порождать процесс или поток) или установить тайм-аут для ввода-вывода (см. раздел 14.2).

6.9. Функция pselect

Функция pselectбыла введена в POSIX и в настоящий момент поддерживается множеством версий Unix.

#include

#include

#include

int pselect(int maxfdp1, fd_set * readset, fd_set * writeset, fd_set * exceptset,

 const struct timespec * timeout, const sigset_t * sigmask);

Возвращает: количество готовых дескрипторов, 0 в случае тайм-аута, -1 в случае ошибки

Функция pselectимеет два отличия от обычной функции select:

1. Функция pselectиспользует структуру timespec, нововведение стандарта реального времени POSIX, вместо структуры timeval.

struct timespec {

 time_t tv_sec; /* секунды */

 long tv_nsec;  /* наносекунды */

};

Эти структуры отличаются вторыми элементами: элемент tv_nsecновой структуры задает наносекунды, в то время как элемент tv_usecпрежней структуры задает микросекунды.

2. В функции pselectдобавляется шестой аргумент – указатель на маску сигналов. Это позволяет программе отключить доставку ряда сигналов, проверить какие-либо глобальные переменные, установленные обработчиками этих отключенных сигналов, а затем вызвать функцию pselect, сообщив ей, что нужно переустановить маску сигналов.

В отношении второго пункта рассмотрим следующий пример (описанный на с. 308–309 [110]). Обработчик сигнала нашей программы для сигнала SIGINTпросто устанавливает глобальную переменную intr_flagи возвращает управление. Если наш процесс блокирован в вызове функции select, возвращение из обработчика сигнала заставляет функцию завершить работу, присвоив errnoзначение EINTR. Код вызова selectвыглядит следующим образом:

if (intr_flag)

 handle_intr(); /* обработка этого сигнала */

if ((nready = select(...)) < 0) {

 if (errno == EINTR) {

  if (intr_flag)

   handle_intr();

 }

 ...

}

Проблема заключается в том, что если сигнал придет в промежутке между проверкой переменной intr_flagи вызовом функции select, он будет потерян в том случае, если функция selectзаблокирует процесс навсегда. С помощью функции pselectмы можем переписать этот пример так, чтобы он работал более надежно:

sigset_t newmask, oldmask, zeromask;

sigemptyset(&zeromask);

sigemptyset(&newmask);

sigaddset(&newmask, SIGINT);

sigprocmask(SIG_BLOCK, &newmask, &oldmask); /* блокирование сигнала SIGINT */

if (intr_flag)

 handle_intr(); /* обработка этого сигнала */

if ((nready = pselect(..., &zeromask)) < 0) {

 if (errno == EINTR) {

  if (intr_flag)

  handle_intr();

 }

 ...

}

Перед проверкой переменной intr_flagмы блокируем сигнал SIGINT. Когда вызывается функция pselect, она заменяет маску сигналов процесса пустым набором ( zeromask), а затем проверяет дескрипторы, возможно, переходя в состояние ожидания. Но когда функция pselectвозвращает управление, маске сигналов процесса присваивается то значение, которое предшествовало вызову функции pselect(то есть сигнал SIGINTблокируется).

Мы поговорим о функции pselectболее подробно и приведем ее пример в разделе 20.5. Функцию pselectмы используем в листинге 20.3, а в листинге 20.4 показываем простую, хотя и не вполне корректную реализацию этой функции.

ПРИМЕЧАНИЕ

Есть одно незначительное различие между функциями select и pselect. Первый элемент структуры timeval является целым числом типа long со знаком, в то время как первый элемент структуры timspec имеет тип time_t. Число типа long со знаком в первой функции также должно было относиться к типу time_t, но мы не меняли его тип, чтобы не разрушать существующего кода. Однако в новой функции это можно было бы сделать.

6.10. Функция poll

Функция pollпоявилась впервые в SVR3, и изначально ее применение ограничивалось потоковыми устройствами (STREAMS devices) (см. главу 31). В SVR4 это ограничение было снято, что позволило функции pollработать с любыми дескрипторами. Функция pollпредоставляет функциональность, аналогичную функции select, но позволяет получать дополнительную информацию при работе с потоковыми устройствами.

#include

int poll(struct pollfd * fdarray, unsigned long nfds, int timeout);

Возвращает: количество готовых дескрипторов, 0 в случае тайм-аута, -1 в случае ошибки

Первый аргумент – это указатель на первый элемент массива структур. Каждый элемент массива – это структура pollfd, задающая условия, проверяемые для данного дескриптора fd.

struct pollfd {

 int fd;        /* дескриптор, который нужно проверить */

 short events;  /* события на дескрипторе, которые нас интересуют */

 short revents; /* события, произошедшие на дескрипторе fd */

};

Проверяемые условия задаются элементом events, и состояние этого дескриптора функция возвращает в соответствующем элементе revents. (Наличие двух переменных для каждого дескриптора, одна из которых – значение, а вторая – результат, дает возможность обойтись без аргументов типа «значение-результат». Вспомните, что три средних аргумента функции selectимеют тип «значение-результат».) Каждый из двух элементов состоит из одного или более битов, задающих определенное условие. В табл. 6.2 перечислены константы, используемые для задания флага eventsи для проверки флага revents.

Таблица 6.2. Различные значения флагов events и revents для функции poll


POLLINМожно считывать обычные или приоритетные данные
POLLRDNORMМожно считывать обычные данные
POLLRDBANDМожно считывать приоритетные данные
POLLPRIМожно считывать данные с высоким приоритетом
POLLOUTМожно записывать обычные данные
POLLWRNORMМожно записывать обычные данные
POLLWRBANDМожно записывать приоритетные данные
POLLERRПроизошла ошибка
POLLHUPПроизошел разрыв соединения
POLLNVALДескриптор не соответствует открытому файлу

Мы разделили эту таблицу на три части: первые четыре константы относятся ко вводу, следующие три – к выводу, а последние три – к ошибкам. Обратите внимание, что последние три константы не могут устанавливаться в элементе events, но всегда возвращаются в revents, когда выполняется соответствующее условие.

Существует три класса данных, различаемых функцией poll: обычные, приоритетныеи данные с высоким приоритетом. Эти термины берут начало в реализациях, основанных на потоках (см. рис. 31.5).

ПРИМЕЧАНИЕ

Константа POLLIN может быть задана путем логического сложения констант POLLRDNORM и POLLRDBAND. Константа POLLIN существовала еще в реализациях SVR3, которые предшествовали полосам приоритета в SVR4, то есть эта константа существует в целях обратной совместимости. Аналогично, константа POLLOUT эквивалентна POLLWRNORM, и первая из них предшествовала второй.

Для сокетов TCP и UDP при описанных условиях функция pollвозвращает указанный флаг revent. К сожалению, в определении функции pollстандарта POSIX имеется множество слабых мест (неоднозначностей):

■ Все регулярные данные TCP и все данные UDP считаются обычными.

■ Внеполосные данные TCP (см. главу 24) считаются приоритетными.

■ Когда считывающая половина соединения TCP закрывается (например, если получен сегмент FIN), это также считается равнозначным обычным данным, и последующая операция чтения возвратит нуль.

■ Наличие ошибки для соединения TCP может расцениваться либо как обычные данные, либо как ошибка ( POLLERR). В любом случае последующая функция read возвращает -1, что сопровождается установкой переменной errnoв соответствующее значение. Это происходит при получении RST или истечении таймера.

■ Информация о доступности нового соединения на прослушиваемом сокете может считаться либо обычными, либо приоритетными данными. В большинстве реализаций эта информация рассматривается как обычные данные.

Число элементов в массиве структур задается аргументом nfds.

ПРИМЕЧАНИЕ

Исторически этот аргумент имел тип long без знака, что является некоторым излишеством. Достаточно будет типа int без знака. В Unix 98 для этого аргумента определяется новый тип – nfds_t.

Аргумент timeoutопределяет, как долго функция находится в ожидании перед завершением. Положительным значением задается количество миллисекунд – время ожидания. В табл. 6.3 показаны возможные значения аргумента timeout.

Таблица 6.3. Значения аргумента timeout для функции poll


INFTIMЖдать вечно
0Возвращать управление немедленно, без блокирования
>0Ждать в течение указанного числа миллисекунд

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

ПРИМЕЧАНИЕ

POSIX требует, чтобы константа INFTIM была определена в заголовочном файле , но многие системы все еще определяют ее в заголовочном файле .

Как и в случае функции select, любой тайм-аут, установленный для функции poll, ограничивается снизу разрешающей способностью часов в конкретной реализации (обычно 10 мс).

Функция pollвозвращает -1, если произошла ошибка, 0 – если нет готовых дескрипторов до истечения времени таймера, иначе возвращается число дескрипторов с ненулевым элементом revents.

Если нас больше не интересует конкретный дескриптор, достаточно установить элемент fdструктуры pollfdравным отрицательному значению. В этом случае элемент eventsбудет проигнорирован, а элемент reventsпри возвращении функции будет сброшен в нуль.

Вспомните наши рассуждения в конце раздела 6.3 относительно константы FD_SETSIZEи максимального числа дескрипторов в наборе в сравнении с максимальным числом дескрипторов для процесса. У нас не возникает подобных проблем с функцией poll, поскольку вызывающий процесс отвечает за размещение массива структур pollfdв памяти и за последующее сообщение ядру числа элементов в массиве. Не существует типа данных фиксированного размера, аналогичного fd_set, о котором знает ядро.

ПРИМЕЧАНИЕ

POSIX требует наличия и функции select, и функции poll. Но если сравнивать их с точки зрения переносимости, то функцию select в настоящее время поддерживает больше систем, чем функцию poll. POSIX определяет также функцию pselect – усовершенствованную версию функции select, которая обеспечивает возможность блокирования сигналов и предоставляет лучшую разрешающую способность по времени, а для функции poll ничего подобного в POSIX нет.


    Ваша оценка произведения:

Популярные книги за неделю