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

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.

Данный текст является ознакомительным фрагментом.