Как мы уже говорили, вызов функции bind позволяет нам задать IP-адрес и порт (вместе или по отдельности) либо не задавать никаких аргументов. В табл. 4.5 приведены все возможные значения, которые присваиваются аргументам sin_addr и sin_port либо sin6_addr и sin6_port в зависимости от желаемого результата.
Таблица 4.5. Результаты задания IP-адреса и (или) номера порта в функции bind
Процесс задает Результат IP-адрес Порт Универсальный 0 Ядро выбирает IP-адрес и порт Универсальный Ненулевое значение Ядро выбирает IP-адрес, процесс задает порт Локальный 0 Процесс задает IP-адрес, ядро выбирает порт Локальный Ненулевое значение Процесс задает IP-адрес и портЕсли мы зададим нулевой номер порта, то при вызове функции bind ядро выберет динамически назначаемый порт. Но если мы зададим IP-адрес с помощью символов подстановки, ядро не выберет локальный IP-адрес, пока к сокету не присоединится клиент (TCP) либо на сокет не будет отправлена дейтаграмма (UDP).
В случае IPv4 универсальный адрес, состоящий из символов подстановки (wildcard), задается константой INADDR_ANY, значение которой обычно нулевое. Это указывает ядру на необходимость выбора IP-адреса. Пример вы видели в листинге 1.5:
struct sockaddr_in servaddr;
servaddr sin_addr s_addr = htonl(INADDR_ANY); /* универсальный */
Этот прием работает с IPv4, где IP-адрес является 32-разрядным значением, которое можно представить как простую численную константу (в данном случае 0), но воспользоваться им при работе с IPv6 мы не можем, поскольку 128-разрядный адрес IPv6 хранится в структуре. (В языке С мы не можем поместить структуру в правой части оператора присваивания.) Эта проблема решается следующим образом:
struct sockaddr_in6 serv;
serv sin6_addr = in6addr_any; /* универсальный */
Система выделяет место в памяти и инициализирует переменную in6addr_any, присваивая ей значение константы IN6ADDR_ANY_INIT. Объявление внешней константы in6addr_any содержится в заголовочном файле <netinet/in.h>.
Значение INADDR_ANY (0) не зависит от порядка байтов, поэтому использование функции htonl в действительности не требуется. Но поскольку все константы INADDR_, определенные в заголовочном файле <netinet/in.h>, задаются в порядке байтов узла, с любой из этих констант следует использовать функцию htonl.
Если мы поручаем ядру выбрать для нашего сокета номер динамически назначаемого порта, то функция bind не возвращает выбранное значение. В самом деле, она не может возвратить это значение, поскольку второй аргумент функции bind имеет спецификатор const. Чтобы получить значение динамически назначаемого порта, заданного ядром, потребуется вызвать функцию getsockname , которая возвращает локальный адрес протокола.
Типичным примером процесса, связывающего с сокетом конкретный IP-адрес, служит узел, на котором работают веб-серверы нескольких организаций (см. раздел 14.2 [112]). Прежде всего, у каждой организации есть свое собственное доменное имя, например www.organization.com. Доменному имени каждой организации сопоставляется некоторый IP-адрес; различным организациям сопоставляются различные адреса, но обычно из одной и той же подсети. Например, если маска подсети 198.69.10, то IP-адресом первой организации может быть 198. 69.10.128, следующей — 198.69.10.129, и т.д. Все эти IP-адреса затем становятся псевдонимами, или альтернативными именами (alias), одного сетевого интерфейса (например, при использовании параметра alias команды ifconfig в 4.4BSD). В результате уровень IP будет принимать входящие дейтаграммы, предназначенные для любого из адресов, являющихся псевдонимами. Наконец, для каждой организации запускается по одной копии сервера HTTP, и каждая копия связывается с помощью функции bind только с IP-адресом определенной организации.
ПРИМЕЧАНИЕВ качестве альтернативы можно запустить одиночный сервер, связанный с универсальным адресом. Когда происходит соединение, сервер вызывает функцию getsockname, чтобы получить от клиента IP-адрес получателя, который (см. наше обсуждение ранее) может быть равен 198.69.10.128,198.69.10.129 и т.д. Затем сервер обрабатывает запрос клиента па основе именно того IP-адреса, к которому было направлено это соединение.
Одним из преимуществ связывания с конкретным IP-адресом является то, что демультиплексирование данного IP-адреса с процессом сервера выполняется ядром.
Следует внимательно относиться к различию интерфейса, на который приходит пакет, и IP-адреса получателя этого пакета. В разделе 8.8 мы поговорим о моделях систем с гибкой привязкой (weak end system) и с жесткой привязкой (strong end system). Большинство реализаций используют первую модель, то есть считают обычным явлением принятие пакета на интерфейсе, отличном от указанного в IP-адресе получателя. (При этом подразумевается узел с несколькими сетевыми интерфейсами.) При связывании с сокетом конкретного IP-адреса на этом сокете будут приниматься дейтаграммы с заданным IP-адресом получателя, и только они. Никаких ограничений на принимающий интерфейс не накладывается — эти ограничения возникают только в случае, если используется модель системы с жесткой привязкой.
Общей ошибкой выполнения функции bind является EADDRINUSE, указывающая на то, что адрес уже используется. Более подробно мы поговорим об этом в разделе 7.5, когда будем рассматривать параметры сокетов SO_REUSEADDR и SO_REUSEPORT.
4.5. Функция listen
Функция listen вызывается только сервером TCP и выполняет два действия.
1. Когда сокет создается с помощью функции socket, считается, что это активный сокет, то есть клиентский сокет, который запустит функцию connect. Функция listen преобразует неприсоединенный сокет в пассивный сокет, запросы на подключение к которому начинают приниматься ядром. В терминах диаграммы перехода между состояниями TCP (см. рис. 2.4) вызов функции listen переводит сокет из состояния CLOSED в состояние LISTEN.