Уильям Стивенс - UNIX: разработка сетевых приложений
Нам нужен способ закрыть одну половину соединения TCP. Другими словами, мы хотим отправить серверу сегмент FIN, тем самым сообщая ему, что закончили отправку данных, но оставляем дескриптор сокета открытым для чтения. Это делается с помощью функции shutdown, которая описывается в следующем разделе.
Вообще говоря, буферизация ввода-вывода для повышения производительности приводит к усложнению сетевых приложений (от чего пострадала и программа в листинге 6.1). Рассмотрим пример, в котором из стандартного потока ввода считывается несколько строк текста. Функция select передаст управление строке 20, в которой функция fgets считает доступные данные в буфер библиотеки stdio. Однако эта функция возвратит приложению только одну строку, а все остальные так и останутся в буфере. Считанная строка будет отправлена серверу, после чего будет снова вызвана функция select, которая будет ждать появления новых данных в стандартном потоке ввода несмотря на наличие еще не обработанных строк в буфере stdio. Причина в том, что select ничего не знает о буферах stdio и сообщает о доступности дескриптора для чтения с точки зрения системного вызова read, а не библиотечного вызова fgets. По этой причине использование fgets и select в одной программе считается опасным и требует особой осторожности.
Та же проблема связана с вызовом readline в листинге 6.1. Теперь данные скрываются от функции select уже не в буфере stdio, а в буфере readline. Вспомните, что в разделе 3.9 мы создали функцию, проверявшую состояние буфера readline. Мы могли бы воспользоваться ею перед вызовом select, чтобы проверить, нет ли в буфере readline данных, дожидающихся обработки. Наша программа усложнится еще больше, если мы допустим, что буфер readline может содержать лишь часть строки (то есть нам придется дожидаться считывания этой строки целиком).
Проблемы буферизации мы постараемся решить в усовершенствованной версии str_cli в разделе 6.7.
6.6. Функция shutdown
Обычный способ завершить сетевое соединение — вызвать функцию close. Но у функции close есть два ограничения, которых лишена функция shutdown:
1. Функция close последовательно уменьшает счетчик ссылок дескриптора и закрывает сокет, только если счетчик доходит до нуля. Мы рассматривали это в разделе 4.8. Используя функцию shutdown, мы можем инициировать обычную последовательность завершения соединения TCP (четыре сегмента, начинающихся с FIN, на рис. 2.5) независимо от значения счетчика ссылок.
2. Функция close завершает оба направления передачи данных — и чтение, и запись. Поскольку соединение TCP является двусторонним, возможны ситуации, когда нам понадобится сообщить другому концу соединения, что мы закончили отправку, даже если на том конце соединения имеются данные для отправки нам. Это случай, рассмотренный в предыдущем разделе при описании работы нашей функции str_cli в пакетном режиме. На рис. 6.10 показаны типичные вызовы функций в этом сценарии.
Рис. 6.10. Вызов функции shutdown для закрытия половины соединения TCP
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
Возвращает: 0 в случае успешного выполнения, -1 в случае ошибки
Действие функции зависит от значения аргумента howto.
■ SHUT_RD. Закрывается считывающая половина соединения: из сокета больше нельзя считывать данные, и все данные, находящиеся в данный момент в буфере приема сокета, сбрасываются. Процесс больше не может выполнять функции чтения из сокета. Любые данные для сокета TCP, полученные после вызова функции shutdown с этим аргументом, подтверждаются и «молча» игнорируются.
ПРИМЕЧАНИЕПо умолчанию все, что записывается в маршрутизирующий сокет (см. главу 17), возвращается как возможный ввод на все маршрутизирующие сокеты узла. Некоторые программы вызывают функцию shutdown со вторым аргументом SHUT_RD, чтобы предотвратить получение подобной копии. Другой способ избежать копирования — отключить параметр сокета SO_USELOOPBACK.
■ SHUT_WR. Закрывается записывающая половина соединения. В случае TCP это называется половинным закрытием (см. раздел 18.5 [111]). Все данные, находящиеся в данный момент в буфере отправки сокета, будут отправлены, а затем будет выполнена обычная последовательность действий по завершению соединения TCP. Как мы отмечали ранее, закрытие записывающей половины соединения выполняется независимо от того, является ли значение в счетчике ссылок дескриптора сокета положительным или нет. Процесс теряет возможность записывать данные в сокет.
■ SHUT_RDWR. Закрываются и читающая, и записывающая половины соединения. Это эквивалентно двум вызовам функции shutdown: сначала с аргументом SHUT_RD, затем — с аргументом SHUT_WR.
В табл. 7.4 приведены все возможные сценарии, доступные процессу при вызове функций shutdown и close. Действие функции close зависит от значения параметра сокета SO_LINGER.
ПРИМЕЧАНИЕТри константы SHUT_xxx определяются в спецификации POSIX. Типичные значения аргумента howto, с которыми вы встретитесь, — это 0 (закрытие читающей половины), 1 (закрытие записывающей половины) и 2 (закрытие обеих половин).
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 показано состояние сервера до того, как первый клиент установил соединение.