На аргументах addr и addrlen вызова bind() следует остановиться отдельно. В табл. 52.1 вы можете видеть, что во всех доменах применяются адреса в разных форматах. Например, сокеты домена UNIX задействуют пути к файлам, тогда как в интернет-доменах адрес состоит из IPC-адреса и номера порта. Для каждого домена предусмотрена отдельная структура данных, хранящая адрес сокета. Но ввиду того, что системные вызовы наподобие bind() являются универсальными и охватывают все домены, они должны иметь возможность принимать адреса любых типов. Для этого в программном интерфейсе сокетов объявлена универсальная структура данных, struct sockaddr. Ее единственное назначение — привести различные адреса, использующиеся в разных доменах, к единому типу, который можно передавать в системные вызовы для работы с сокетами. Структура sockaddr обычно имеет следующий вид:
struct sockaddr {
sa_family_t sa_family; /* Семейство адресов (константы вида AF_*) */
char sa_data[14]; /* Адрес сокета (размер зависит от домена) */
};
Эта структура служит шаблоном для всех других хранящих адреса определенных доменов; все они начинаются с поля family, которое соотносится с полем sa_family структуры sockaddr (согласно стандарту SUSv3 тип данных sa_family_t представляет собой целое число). Значения поля family должно быть достаточно для определения размера и формата адреса, хранящегося в остальной части структуры.
В некоторых реализациях UNIX структура sockaddr содержит дополнительное поле sa_len, обозначающее ее общий размер. Стандарт SUSv3 не требует наличия этого поля; к тому же оно не поддерживается программным интерфейсом сокетов в Linux.
Если определить макрос проверки возможностей _GNU_SOURCE, то библиотека glibc будет использовать расширение компилятора gcc для прототипирования системных вызовов в заголовочном файле
Принцип работы потоковых сокетов можно объяснить на примере телефонной сети.
1. Системный вызов socket(), создающий сокет, аналогичен подключению телефонного аппарата. Чтобы приложения могли взаимодействовать друг с другом, каждое из них должно создать свой сокет.
2. Взаимодействие с помощью потоковых сокетов аналогично телефонному звонку. Прежде чем начать общение, приложения должны соединить свои сокеты. Это делается следующим образом.
• Одно приложение делает вызов bind(), чтобы привязать свой сокет к общеизвестному адресу, и затем вызывает listen() для уведомления ядра о своей готовности принимать входящие соединения. Возвращаясь к нашей аналогии: чтобы другие люди могли нам звонить, у нас должен быть телефонный номер, зарегистрированный на АТС.
• Другое приложение устанавливает соединение с помощью вызова connect(), указывая адрес сокета, к которому оно хочет подключиться. Данное действие аналогично набору телефонного номера.
• Затем приложение, вызвавшее listen(), принимает соединение, используя вызов accept(). Это похоже на то, как мы снимаем телефонную трубку, когда слышим звонок. Вызов accept() блокируется, если сделать его до того, как другое приложение выполнит connect() («в ожидании звонка»).
3. Подключившись, можно передавать данные в обоих направлениях (аналогично ведению диалога по телефону), пока одно из приложений не закроет соединение с помощью вызова close(). Взаимодействие выполняется с использованием традиционных системных вызовов read() и write() или же ряда специальных операций для работы с сокетами (таких как send() и recv()), которые предоставляют дополнительные возможности.
Применение системных вызовов для работы с потоковыми сокетами проиллюстрировано на рис. 52.1.
Потоковые сокеты часто делят на активные и пассивные.
• Сокет, созданный с помощью вызова socket(), по умолчанию является
•
В большинстве приложений, использующих потоковые сокеты, сервер выполняет пассивное открытие, а клиент — активное. Мы руководствуемся данным правилом в последующих разделах, поэтому приложение, выполняющее активное открытие сокета, будем называть просто клиентом. Аналогично вместо словосочетания приложение, которое выполняет пассивное открытие сокета» будет задействован термин «сервер».