• Предыдущий экземпляр сервера создал дочерний процесс для обработки клиентского запроса. Позже сервер завершил работу, тогда как его потомок продолжил обслуживать клиента. В итоге его конец соединения остался привязанным к общеизвестному порту сервера.
В обоих случаях оставшийся конец соединения не может принимать новые запросы. Хотя большинство реализаций протокола TCP не дали бы новому слушающему сокету привязаться к уже занятому серверному порту.
Ошибка EADDRINUSE обычно не встречается на клиентской стороне, где, как правило, используются динамические порты, которые никогда не совпадают с теми, что уже заняты соединением в состоянии TIME_WAIT. Однако клиенты, привязанные к порту с определенным номером, не защищены от этой ошибки.
Чтобы понять принцип работы параметра SO_REUSEADDR, вернемся к нашей аналогии с телефонами, которую мы приводили ранее в разделе 52.5 при знакомстве с потоковыми сокетами. Как и любой телефонный вызов (кроме, разве что, телеконференций), TCP-соединение идентифицируется с помощью сочетания из двух конечных точек, подключенных друг к другу. Вызов accept() аналогичен процедуре, выполняемой внутренним коммутатором («сервером»). Обнаружив внешний вызов, коммутатор направляет его к какому-то телефону внутри организации («новому сокету»). Внешний наблюдатель не имеет возможности определить внутренний телефон. Единственный способ различить несколько звонков, выполненных извне, — использовать комбинацию внешнего номера вызывающего абонента и номера коммутатора (последний необходим, поскольку таких коммутаторов в телефонной сети может быть сколько угодно). Аналогично мы создаем новый сокет каждый раз, когда принимаем соединение с помощью слушающего сокета. Все эти сокеты (включая слушающий) связаны с одним и тем же локальным адресом. Различить их можно только по тому, с какими удаленными сокетами они соединены.
Иными словами, подключенный TCP-сокет идентифицируется путем комбинации из четырех значений следующего вида:
{ локальный-IP-адрес, локальный-порт, удаленный-IP-адрес, удаленный-порт }
Спецификация протокола TCP требует, чтобы каждый такой набор был уникальным; то есть у соответствующего соединения может быть только один экземпляр («телефонный звонок»). Проблема вот в чем: в большинстве реализаций (включая Linux) действует более строгое ограничение: локальный порт нельзя использовать повторно (передавая его вызову bind()), если в системе существует экземпляр TCP-соединения с тем же портом. Это правило действует даже в том случае, если сокет не принимает новые соединения (как в сценарии, описанном в начале данного раздела).
Применение параметра SO_REUSEADDR смягчает указанное ограничение, делая его более близким к требованиям протокола TCP. По умолчанию этот параметр равен 0 (то есть выключен). Чтобы его включить, перед привязкой сокета ему нужно установить ненулевое значение, как показано в листинге 57.4.
Установка параметра SO_REUSEADDR позволяет привязывать сокет к локальному порту, даже если он занят другим TCP-соединением (в любом из двух сценариев, описанных в начале этого раздела). Данный параметр следует включать в большинстве TCP-серверов. Мы уже сталкивались с его применением в листингах 55.6 и 55.9.
Листинг 57.4. Установка параметра сокета SO_REUSEADDR
int sockfd, optval;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
errExit("socket");
optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval,
sizeof(optval)) == -1)
errExit("socket");
if (bind(sockfd, &addr, addrlen) == -1)
errExit("bind");
if (listen(sockfd, backlog) == -1)
errExit("listen");
С дескрипторами и описаниями открытого файла могут быть связаны различные флаги и настройки (см. раздел 5.4). Кроме того, как было показано в разделе 57.9, сокету можно устанавливать разные параметры. Если речь идет о слушающем сокете, то наследуются ли все эти метаданные новым сокетом, который возвращается вызовом accept()? В данном разделе мы дадим подробный ответ.
В Linux новый файловый дескриптор, полученный из вызова accept(), не наследует следующие атрибуты.
• Флаги состояния, связанные с описанием открытого файла, например O_NONBLOCK или O_ASYNC. Для их включения или выключения можно использовать операцию F_SETFL вызова fcntl() (см. раздел 5.3).
• Флаги файлового дескриптора. В эту категорию входит лишь один флаг, FD_CLOEXEC (описанный в разделе 27.4), и для его изменения тоже можно применять операцию F_SETFL вызова fcntl().
• Атрибуты файлового дескриптора F_SETOWN (идентификатор процесса-владельца) и F_SETSIG (сгенерированный сигнал), связанные с вводом/выводом на основе сигналов (см. раздел 59.3).
С другой стороны, новый дескриптор, возвращаемый вызовом accept(), наследует копии большинства параметров сокета, которые можно установить с помощью setsockopt() (см. раздел 57.9).