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

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

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


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


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

Жанр:

   

ОС и Сети


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

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

4.5. Функция listen

Функция listenвызывается только сервером TCP и выполняет два действия.

1. Когда сокет создается с помощью функции socket, считается, что это активный сокет, то есть клиентский сокет, который запустит функцию connect. Функция listenпреобразует неприсоединенный сокет в пассивный сокет, запросы на подключение к которому начинают приниматься ядром. В терминах диаграммы перехода между состояниями TCP (см. рис. 2.4) вызов функции listenпереводит сокет из состояния CLOSED в состояние LISTEN.

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

#include

int listen(int sockfd, int backlog);

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

Эта функция обычно вызывается после функций socketи bind. Она должна вызываться перед вызовом функции accept.

Чтобы уяснить смысл аргумента backlog, необходимо понять, что для данного прослушиваемого сокета ядро поддерживает две очереди:

1.  Очередь не полностью установленных соединений( incomplete connection queue), содержащую запись для каждого сегмента SYN, пришедшего от клиента, для которого сервер ждет завершения трехэтапного рукопожатия TCP. Эти сокеты находятся в состоянии SYN_RCVD (см. рис. 2.4).

2.  Очередь полностью установленных соединений( complete connection queue), содержащую запись для каждого клиента, с которым завершилось трехэтапное рукопожатие TCP. Эти сокеты находятся в состоянии ESTABLISHED (см. рис. 2.4).

На рис. 4.2 представлены обе эти очереди для прослушиваемого сокета.

Рис. 4.2. Две очереди, поддерживаемые прослушиваемым сокетом TCP

Когда в очередь не полностью установленных соединений добавляется новая запись, параметры прослушиваемого сокета копируются на создаваемое соединение. Механизм создания соединения полностью автоматизирован, и процесс сервера в нем не участвует. На рис. 4.3 показан обмен пакетами во время установления соединения с использованием этих очередей.

Рис. 4.3. Обмен пакетами в процессе установления соединения с применением очередей

Когда от клиента приходит сегмент SYN, TCP создает новую запись в очереди не полностью установленных соединений, а затем отвечает вторым сегментом трехэтапного рукопожатия, посылая сегмент SYN вместе с сегментом ACK, подтверждающим прием клиентского сегмента SYN (см. раздел 2.6). Эта запись останется в очереди не полностью установленных соединений, пока не придет третий сегмент трехэтапного рукопожатия (клиентский сегмент ACK для сегмента сервера SYN) или пока не истечет время жизни этой записи. (В реализациях, происходящих от Беркли, время ожидания (тайм-аут) для элементов очереди не полностью установленных соединений равно 75 с.) Если трехэтапное рукопожатие завершается нормально, запись переходит из очереди не полностью установленных соединений в конец очереди полностью установленных соединений. Когда процесс вызывает функцию accept(о которой мы поговорим в следующем разделе), ему возвращается первая запись из очереди полностью установленных соединений, а если очередь пуста, процесс переходит в состояние ожидания до появления записи в ней.

Есть несколько важных моментов, которые нужно учитывать при работе с этими очередями.

■ Аргумент backlogфункции listenисторически задавал максимальное суммарное значение для обеих очередей.

■ Беркли-реализации включают поправочный множитель для аргумента backlog, равный 1,5 [111, с. 257], [128, с. 462]. Например, при типичном значении аргумента backlog= 5 в таких системах допускается до восьми записей в очередях, как показано в табл. 4.6.

ПРИМЕЧАНИЕ

Формального определения аргумента backlog никогда не существовало. В руководстве 4.2BSD сказано, что «он определяет максимальную длину, до которой может вырасти очередь не полностью установленных соединений». Многие руководства и даже POSIX копируют это определение дословно, но в нем не говорится, в каком состоянии должно находится соединение – в состоянии SYN_RCVD, ESTABLISHED (до вызова accept), или же в любом из них. Определение, приведенное выше, относится к реализации Беркли 4.2BSD, и копируется многими другими реализациями.

ПРИМЕЧАНИЕ

Причина возникновения этого множителя теряется в истории [57]. Но если мы рассматриваем backlog как способ задания максимального числа установленных соединений, которые ядро помещает в очередь прослушиваемого сокета (об этом вскоре будет рассказано), этот множитель нужен для учета не полностью установленных соединений, находящихся в очереди [8].

■ Не следует задавать нулевое значение аргументу backlog, поскольку различные реализации интерпретируют это по-разному (см. табл. 4.6). Некоторые реализации допускают помещение в очередь одного соединения, в то время как в других вообще невозможно помещать соединения в очередь. Если вы не хотите, чтобы клиенты соединялись с вашим прослушиваемым сокетом, просто закройте прослушиваемый сокет.

■ Если трехэтапное рукопожатие завершается нормально (то есть без потерянных сегментов и повторных передач), запись остается в очереди не полностью установленных соединений на время одного периода обращения (round-trip time, RTT), какое бы значение ни имел этот параметр для конкретного соединения между клиентом и сервером. В разделе 14.4 [112] показано, что для одного веб-сервера средний период RTT оказался равен 187 мс. (Чтобы редкие большие числа не искажали картину, здесь использована медиана, а не обычное среднее арифметическое по всем клиентам.)

■ Традиционно в примерах кода всегда используется значение backlog, равное 5, поскольку это было максимальное значение, которое поддерживалось в системе 4.2BSD. Это было актуально в 80-х, когда загруженные серверы могли обрабатывать только несколько сотен соединений в день. Но с ростом Сети (WWW), когда серверы обрабатывают миллионы соединений в день, столь малое число стало абсолютно неприемлемым [112, с. 187–192]. Серверам HTTP необходимо намного большее значение аргумента backlog, и новые ядра должны поддерживать такие значения.

ПРИМЕЧАНИЕ

В настоящее время многие системы позволяют администраторам изменять максимальное значение аргумента backlog.

■ Возникает вопрос: какое значение аргумента backlogдолжно задавать приложение, если значение 5 часто является неадекватным? На этот вопрос нет простого ответа. Серверы HTTP сейчас задают большее значение, но если заданное значение является в исходном коде константой, то для увеличения константы требуется перекомпиляция сервера. Другой способ – принять некоторое значение по умолчанию и предоставить возможность изменять его с помощью параметра командной строки или переменной окружения. Всегда можно задавать значение больше того, которое поддерживается ядром, так как ядро должно обрезать значение до максимального, не возвращая при этом ошибку [128, с. 456].

Мы приводим простое решение этой проблемы, изменив нашу функцию-обертку для функции listen. В листинге 4.1 [1]1
  Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http://www.piter.com.


[Закрыть]
представлен действующий код. Переменная окружения LISTENQпозволяет переопределить значение по умолчанию.

Листинг 4.1. Функция-обертка для функции listen, позволяющая переменной окружения переопределить аргумент backlog

//lib/wrapsock.c

137 void

138 Listen(int fd, int backlog)

139 {

140  char *ptr;

141  /* может заменить второй аргумент на переменную окружения */

142  if ((ptr = getenv("LISTENQ")) != NULL)

143   backlog = atoi(ptr);

144  if (listen(fd, backlog) < 0)

145   err_sys("listen error");

146 }

■ Традиционно в руководствах и книгах утверждалось, что помещение фиксированного числа соединений в очередь позволяет обрабатывать случай загруженного серверного процесса между последовательными вызовами функции accept. При этом подразумевается, что из двух очередей больше записей будет содержаться, вероятнее всего, в очереди полностью установленных соединений. Но оказалось, что для действительно загруженных веб-серверов это не так. Причина задания большего значения backlogв том, что очередь не полностью установленных соединений растет по мере поступления сегментов SYN от клиентов; элементы очереди находятся в состоянии ожидания завершения трехэтапного рукопожатия.

■ Если очереди заполнены, когда приходит клиентский сегмент SYN, то TCP игнорирует приходящий сегмент SYN [128, с. 930–931] и не посылает RST. Это происходит потому, что состояние считается временным, и TCP клиента должен еще раз передать свой сегмент SYN, для которого в ближайшее время, вероятно, найдется место в очереди. Если бы TCP сервера послал RST, функция connectклиента сразу же возвратила бы ошибку, заставив приложение обработать это условие, вместо того чтобы позволить TCP выполнить повторную передачу. Кроме того, клиент не может увидеть разницу между сегментами RST в ответе на сегмент SYN, означающими, что на данном порте нет сервера либо на данном порте есть сервер, но его очереди заполнены.

ПРИМЕЧАНИЕ

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

■ Данные, которые приходят после завершения трехэтапного рукопожатия, но до того, как сервер вызывает функцию accept, должны помещаться в очередь TCP-сервера, пока не будет заполнен приемный буфер.

В табл. 4.6 показано действительное число установленных в очередь соединений для различных значений аргумента backlogв операционных системах, показанных на рис. 1.7. Семь операционных систем помещены в пять колонок, что иллюстрирует многообразие значений аргумента backlog.

Таблица 4.6. Действительное количество соединений в очереди для различных значений аргумента backlog


013111
124122
245334
356445
477656
588768
61099710
7И1010811
8131112913
91412131014
101613151116
111714161217
121915181319
132016191420
142217211522

Системы AIX, BSD/ОХ и SunOS реализуют традиционный алгоритм Беркли, хотя последний не допускает значения аргумента backlogбольше пяти. В системах HP-UX и Solaris 2.6 используется другой поправочный множитель к аргументу backlog. Системы Digital Unix, Linux и UnixWare воспринимают этот аргумент буквально, то есть не используют поправочный множитель, а в Solaris 2.5.1 к аргументу backlogпросто добавляется единица.

ПРИМЕЧАНИЕ

Программа для измерения этих значений представлена в решении упражнения 15.4.

Как мы отмечали, традиционно аргумент backlog задавал максимальное значение для суммы обеих очередей. В 1996 году была предпринята новая атака через Интернет, названная SYN flooding (лавинная адресация сегмента SYN). Написанная хакером программа отправляет жертве сегменты SYN с высокой частотой, заполняя очередь не полностью установленных соединений для одного или нескольких портов TCP. (Хакером мы называем атакующего, как сказано в предисловии к [20].) Кроме того, IP-адрес отправителя каждого сегмента SYN задается случайным числом – формируются вымышленные IP-адреса (IP spoofing), что ведет к получению доступа обманным путем. Таким образом, сегмент сервера SYN/ACK уходит в никуда. Это не позволяет серверу узнать реальный IP-адрес хакера. Очередь не полностью установленных соединений заполняется ложными сегментами SYN, в результате чего для подлинных сегментов SYN в ней не хватает места – происходит отказ в обслуживании (denial of service) нормальных клиентов. Существует два типичных способа противостояния этим атакам [8]. Но самое интересное в этом примечании – это еще одно обращение к вопросу о том, что на самом деле означает аргумент backlog функции listen. Он должен задавать максимальное число установленных соединений для данного сокета, которые ядро помещает в очередь. Ограничение количества установленных соединений имеет целью приостановить получение ядром новых запросов на соединение для данного сокета, когда их не принимает приложение (по любой причине). Если система реализует именно такую интерпретацию, как, например, BSD/OS 3.0, то приложению не нужно задавать большие значения аргумента backlog только потому, что сервер обрабатывает множество клиентских запросов (например, занятый веб-сервер), или для защиты от «наводнения» SYN (лавинной адресации сегмента SYN). Ядро обрабатывает множество не полностью установленных соединений вне зависимости от того, являются ли они законными или приходят от хакера. Но даже в такой интерпретации мы видим (см. табл. 4.6), что значения 5 тут явно недостаточно.

4.6. Функция accept

Функция acceptвызывается сервером TCP для возвращения следующего установленного соединения из начала очереди полностью установленных соединений (см. рис. 4.2). Если очередь полностью установленных соединений пуста, процесс переходит в состояние ожидания (по умолчанию предполагается блокируемый сокет).

#include

int accept(int sockfd, struct sockaddr * cliaddr, socklen_t * addrlen);

Возвращает: неотрицательный дескриптор в случае успешного выполнения функции, -1 в случае ошибки

Аргументы cliaddrи addrlenиспользуются для возвращения адреса протокола подключившегося процесса (клиента). Аргумент addrlen– это аргумент типа «значение-результат» (см. раздел 3.3). Перед вызовом мы присваиваем целому числу, на которое указывает *addrlen, размер структуры адреса сокета, на которую указывает аргумент cliaddr, и по завершении функции это целое число содержит действительное число байтов, помещенных ядром в структуру адреса сокета.

Если выполнение функции acceptпрошло успешно, она возвращает новый дескриптор, автоматически созданный ядром. Этот дескриптор используется для обращения к соединению TCP с конкретным клиентом. При описании функции acceptмы называем ее первый аргумент прослушиваемым сокетом( listening socket) (дескриптор, созданный функцией socketи затем используемый в качестве аргумента для функций bindи listen), а значение, возвращаемое этой функцией, мы называем присоединенным сокетом( connected socket). Сервер обычно создает только один прослушиваемый сокет, который существует в течение всего времени жизни сервера. Затем ядро создает по одному присоединенному сокету для каждого клиентского соединения, принятого с помощью функции accept(для которого завершено трехэтапное рукопожатие TCP). Когда сервер заканчивает предоставление сервиса данному клиенту, сокет закрывается.

Эта функция возвращает до трех значений: целое число, которое является либо дескриптором сокета, либо кодом ошибки, а также адрес протокола клиентского процесса (через указатель cliaddr) и размер адреса (через указатель addrlen). Если нам не нужно, чтобы был возвращен адрес протокола клиента, следует сделать указатели cliaddrи addrlenпустыми указателями.

В листинге 1.5 показаны эти моменты. Присоединенный сокет закрывается при каждом прохождении цикла, но прослушиваемый сокет остается открытым в течение времени жизни сервера. Мы также видим, что второй и третий аргументы функции acceptявляются пустыми указателями, поскольку нам не нужно идентифицировать клиент.

Пример: аргументы типа «значение-результат»

В листинге 4.2 представлен измененный код из листинга 1.5 (вывод IP-адреса и номера порта клиента), обрабатывающий аргумент типа «значение-результат» функции accept.

Листинг 4.2. Сервер определения времени и даты, сообщающий IP-адрес и номер порта клиента

//intro/daytimetcpsrv1.c

 1 #include "unp.h"

 2 #include

 3 int

 4 main(int argc, char **argv)

 5 {

 6  int listenfd, connfd;

 7  socklen_t len;

 8  struct sockaddr_in servaddr, cliaddr;

 9  char buff[MAXLINE];

10  time_t ticks;

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

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

13  servaddr.sin_family = AF_INET;

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

15  servaddr.sin_port = htons(13); /* сервер времени и даты */

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

17  Listen(listenfd, LISTENQ);

18  for (;;) {

19   len = sizeof(cliaddr);

20   connfd = Accept(listenfd, (SA*)&cliaddr, &len);

21   printf("connection from %s, port %dn",

22    Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff));

23   ntohs(cliaddr.sin_port));

24   ticks = time(NULL);

25   snprintf(buff, sizeof(buff), "% 24srn", ctime(&ticks));

26   Write(connfd, buff, strlen(buff));

27   Close(connfd);

28  }

29 }

Новые объявления

7-8 Мы определяем две новых переменных: len, которая будет переменной типа «значение-результат», и cliaddr, которая будет содержать адрес протокола клиента.

Принятие соединения и вывод адреса клиента

19-23 Мы инициализируем переменную len, присвоив ей значение, равное размеру структуры адреса сокета, и передаем указатель на структуру cliaddrи указатель на lenв качестве второго и третьего аргументов функции accept. Мы вызываем функцию inet_ntop(см. раздел 3.7) для преобразования 32-битового IP-адреса в структуре адреса сокета в строку ASCII (точечно-десятичную запись), а затем вызываем функцию ntohs(см. раздел 3.4) для преобразования сетевого порядка байтов в 16-битовом номере порта в порядок байтов узла.

ПРИМЕЧАНИЕ

При вызове функции sock_ntop вместо inet_ntop наш сервер станет меньше зависеть от протокола, однако он все равно зависит от IPv4. Мы покажем версию этого сервера, не зависящего от протокола, в листинге 11.7.

Если мы запустим наш новый сервер, а затем запустим клиент на том же узле, то дважды соединившись с сервером, мы получим от клиента следующий вывод:

solaris % daytimetcpcli 127.0.0.1

Thu Sep 11 12:44:00 2003

solaris % daytimetcpcli 192.168.1.20

Thu Sep 11 12:44:09 2003

Сначала мы задаем IP-адрес сервера как адрес закольцовки на себя (loopback address) (127.0.0.1), а затем как его собственный IP-адрес (192.168.1.20). Вот соответствующий вывод сервера:

solaris # daytimetcpsrv1

connection from 127.0.0.1, port 43388

connection from 192.168.1.20, port 43389

Обратите внимание на то, что происходит с IP-адресом клиента. Поскольку наш клиент времени и даты (см. листинг 1.1) не вызывает функцию bind, как сказано в разделе 4.4, ядро выбирает IP-адрес отправителя, основанный на используемом исходящем интерфейсе. В первом случае ядро задает IP-адрес равным адресу закольцовки, во втором случае – равным IP-адресу интерфейса Ethernet. Кроме того, мы видим, что динамически назначаемый порт, выбранный ядром Solaris, – это 33 188, а затем 33 189 (см. рис. 2.10).

Наконец, заметьте, что приглашение интерпретатора команд изменилось на знак # – это приглашение к вводу команды для привилегированного пользователя. Наш сервер должен обладать правами привилегированного пользователя, чтобы с помощью функции bindсвязать зарезервированный порт 13. Если у нас нет прав привилегированного пользователя, вызов функции bindоказывается неудачным:

solaris % daytimetcpsrv1

bind error: Permission denied

4.7. Функции fork и exec

Прежде чем рассматривать создание параллельного сервера (что мы сделаем в следующем разделе), необходимо описать функцию Unix fork. Эта функция является единственным способом создания нового процесса в Unix.

#include

pid_t fork(void);

Возвращает: 0 в дочернем процессе, идентификатор дочернего процесса в родительском процессе, -1 в случае ошибки

Если вы никогда не встречались с этой функцией, трудным для понимания может оказаться то, что она вызывается один раз, а возвращает два значения. Одно значение эта функция возвращает в вызывающем процессе (который называется родительским процессом) – этим значением является идентификатор созданного процесса (который называется дочерним процессом). Второе значение (нуль) она возвращает в дочернем процессе. Следовательно, по возвращаемому значению можно определить, является ли данный процесс родительским или дочерним.

Причина того, что функция forkвозвращает в дочернем процессе нуль, а не идентификатор родительского процесса, заключается в том, что у дочернего процесса есть только один родитель, и дочерний процесс всегда может получить идентификатор родительского, вызвав функцию getppid. У родителя же может быть любое количество дочерних процессов, и способа получить их идентификаторы не существует. Если родительскому процессу требуется отслеживать идентификаторы своих дочерних процессов, он должен записывать возвращаемые значения функции fork.

Все дескрипторы, открытые в родительском процессе перед вызовом функции fork, становятся доступными дочерним процессам. Вы увидите, как это свойство используется сетевыми серверами: родительский процесс вызывает функцию accept, а затем функцию fork. Затем присоединенный сокет совместно используется родительским и дочерним процессами. Обычно дочерний процесс использует присоединенный сокет для чтения и записи, а родительский процесс только закрывает присоединенный сокет.

Существует два типичных случая применения функции fork:

1. Процесс создает свои копии таким образом, что каждая из них может обрабатывать одно задание. Это типичная ситуация для сетевых серверов. Далее в тексте вы увидите множество подобных примеров.

2. Процесс хочет запустить другую программу. Поскольку единственный способ создать новый процесс – это вызвать функцию fork, процесс сначала вызывает функцию fork, чтобы создать свою копию, а затем одна из копий (обычно дочерний процесс) вызывает функцию exec(ее описание следует за описанием функции fork), чтобы заменить себя новой программой. Этот сценарий типичен для таких программ, как интерпретаторы командной строки.

Единственный способ запустить в Unix на выполнение какой-либо файл – вызвать функцию exec. (Мы будем часто использовать общее выражение «функция exec», когда неважно, какая из шести функций семейства execвызывается.) Функция execзаменяет копию текущего процесса новым программным файлом, причем в новой программе обычно запускается функция main. Идентификатор процесса при этом не изменяется. Процесс, вызывающий функцию exec, мы будем называть вызывающим процессом, а выполняемую при этом программу – новой программой.

ПРИМЕЧАНИЕ

В старых описаниях и книгах новая программа ошибочно называется «новым процессом». Это неверно, поскольку новый процесс не создается.

Различие между шестью функциями execзаключается в том, что они допускают различные способы задания аргументов:

■ выполняемый программный файл может быть задан или именем файла( filename), или полным именем( pathname);

■ аргументы новой программы либо перечисляются один за другим, либо на них имеется ссылка через массив указателей;

■ новой программе либо передается окружение вызывающего процесса, либо задается новое окружение.

#include

int execl(const char * pathname, const char * arg0, ... /* (char*)0 */ );

int execv(const char * pathname, char *const argv[]);

int execle(const char * pathname, const char * arg0... /* (char*)0,

 char *const envp[] */ );

int execve(const char * pathname, char *const argv[], char *const envp[]);

int execlp(const char * filename, const char * arg0, .... /* (char*)0 */ );

int execvp(const char * filename, char *const argv[]);

Все шесть функций возвращают: -1 в случае ошибки, если же функция выполнена успешно, то ничего не возвращается

Эти функции возвращают вызывающему процессу значение -1, только если происходит ошибка. Иначе управление передается в начало новой программы, обычно функции main.

Отношения между этими шестью функциями показаны на рис. 4.4. Обычно только функция execveявляется системным вызовом внутри ядра, а остальные представляют собой библиотечные функции, вызывающие execve.

Рис. 4.4. Отношения между шестью функциями exec

Отметим различия между этими функциями:

1. Три верхних функции (см. рис. 4.4) принимают каждую строку как отдельный аргумент, причем перечень аргументов завершается пустым указателем (так как их количество может быть различным). У трех нижних функций имеется массив argv, содержащий указатели на строки. Этот массив должен содержать пустой указатель, определяющий конец массива, поскольку размер массива не задается.

2. Две функции в левой колонке получают аргумент filename. Он преобразуется в pathnameс использованием текущей переменной окружения PATH. Если аргумент filenameфункций execlpили execvpсодержит косую черту ( /) в любом месте строки, переменная PATHне используется. Четыре функции в двух правых колонках получают полностью определенный аргумент pathname.

3. Четыре функции в двух левых колонках не получают явного списка переменных окружения. Вместо этого с помощью текущего значения внешней переменной environсоздается список переменных окружения, который передается новой программе. Две функции в правой колонке получают точный список переменных окружения. Массив указателей envpдолжен быть завершен пустым указателем.

Дескрипторы, открытые в процессе перед вызовом функции exec, обычно остаются открытыми во время ее выполнения. Мы говорим «обычно», поскольку это свойство может быть отключено при использовании функции fcntlдля установки флага дескриптора FD_CLOEXEC. Это нужно серверу inetd, о котором пойдет речь в разделе 13.5.


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

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