Системы 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 <sys/socket.h>
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 <time.h>
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); /* сервер времени и даты */
, которая будет переменной типа «значение-результат», и
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.
Если мы запустим наш новый сервер, а затем запустим клиент на том же узле, то дважды соединившись с сервером, мы получим от клиента следующий вывод: