17.3.2. Установка соединений
После создания потокового сокета его необходимо присоединить к чему-то часто используемому. Установка соединений сокетов является в большой степени несимметричной задачей, поскольку каждая сторона проводит соединение по-разному. Одна сторона получает сокет, который готов к соединению, и затем ожидает кого-либо для того, чтобы присоединиться к нему. Эту функцию, как правило, выполняют серверные приложения, которые однажды активизируются и постоянно продолжают работать, ожидая подключения со стороны других процессов.
Клиентские процессы, в свою очередь, создают сокет, сообщают системе адрес, к которому они хотят подключиться, и после этого пытаются установить соединение. Как только сервер (ожидающий клиента) принимает попытку соединения, устанавливается соединение между двумя сокетами. После этого сокет может использоваться для двусторонней связи.
17.3.3. Связывание адреса с сокетом
И серверный, и клиентский процессы должны сообщить системе, какой адрес использовать для сокета. Прикрепление адреса к локальной стороне сокета называется связыванием сокета и выполняется через системный вызов bind().
#include <sys/socket.h>
int bind(int sock, struct sockaddr * my_addr, socklen_t addrlen);
Первый параметр — это связываемый сокет, остальные параметры задают адрес для локальной конечной точки.
17.3.4. Ожидание соединений
После создания сокета сервер привязывает к нему адрес с помощью функции bind(). Далее процесс сообщает системе путем вызова функции listen(), что он готов разрешить другим процессам соединение с данным сокетом (по указанному адресу). Если сокет привязан к адресу, ядро получает возможность обрабатывать попытки соединения с данным адресом, поступающие от процессов. Однако соединение не устанавливается немедленно. Слушающий процесс сначала должен согласиться с попыткой соединения через системный вызов accept(). До тех пор, пока новая попытка соединения с определенным адресом не принята, она называется ожидающим соединением.
Как правило, функция accept() блокируется до тех пор, пока к ней не пытается присоединиться некоторый клиентский процесс. Если сокет был помечен как неблокируемый через fcntl(), то функция accept() возвращает значение EAGAIN в том случае, если нет ни одного доступного клиентского процесса[120]. Системные вызовы select(), poll() и epoll могут использоваться для указания, ждать ли соединению обработки (эти вызовы помечают сокет как готовый для считывания)[121].
Ниже показаны прототипы listen() и accept().
#include <sys/socket.h>
int listen(int sock, int backlog);
int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);
В обеих функциях предполагается, что первый параметр — это файловый дескриптор. Второй параметр backlog функции listen() задает максимальное количество соединений, которые могут одновременно ожидать обработки на данном сокете. Сетевые соединения не устанавливаются до тех пор, пока сервер не примет соединение через accept(); все входящие соединения считаются приостановленными. Поддерживая небольшое количество ожидающих соединений в очереди, ядро тем самым освобождает серверные процессы от необходимости быть в постоянной готовности принимать соединения. Исторически принято ограничивать в приложениях количество невыполненных заданий пятью, хотя иногда необходимо большее количество. Функция listen() возвращает ноль в случае успеха и какое-то другое число в случае неудачи.
Вызов accept() превращает отложенное соединение в установленное. Установленное соединение получает новый файловый дескриптор, который возвращает функция accept(). Новый дескриптор наследует все атрибуты того сокета, к которому обращалась функция listen(). Необычное свойство accept() состоит в том, что она возвращает сетевые ошибки, ожидающие обработки, как ошибки принятия от accept()[122]. При возврате ошибки серверы не должны прерывать работу, если параметр errno принимает одно из следующих значений: ECONNABORTED, ENETDOWN, EPROTO, ENOPROTOOPT, EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP или ENETUNREACH. Все эти ошибки необходимо игнорировать, просто вызвав функцию accept() на сервере еще раз.
Параметры addr и addrlen указывают данные, в которых ядро размещает адрес удаленного (клиентского) конца соединения. В исходном состоянии addrlen представляет собой целое число, содержащее размер буфера, на который ссылается addr. Функция accept() аналогично open() возвращает файловый дескриптор или некоторое отрицательное значение, если возникла ошибка.
17.3.5. Подключение к серверу
Как и серверы, клиенты могут сразу после создания сокета связывать с ним локальный адрес. Обычно клиент пропускает этот шаг, предоставляя ядру присвоить сокету любой подходящий локальный адрес.
После этапа связывания (который, впрочем, может быть пропущен) клиент соединяется с сервером через системный вызов connect().
#include <sys/socket.h>
int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen);
Процесс переходит к подключению, придерживаясь адреса, с которым должен соединиться сокет.
На рис. 17.1 показаны системные вызовы, которые обычно используются для установки соединений сокетов, и порядок, в котором они выполняются.
Рис 17.1. Установка соединений сокетов
17.3.6. Поиск адресов соединения
После того как соединение установлено, приложение может найти адреса как удаленного, так и локального концов сокета с помощью функций getpeername() и getsockname().
#include <sys/socket.h>
int getpeername(int s, struct sockaddr * addr, socklen_t * addrlen);
int getsockname(int s, struct sockaddr * addr, socklen_t * addrlen);
Обе функции передают адреса соединений сокета s в те структуры, на которые указывают их параметры addr. Адрес удаленной стороны возвращается функцией getpeername(), тогда как getsockname() сообщает адрес локальной части соединения. Для обеих функций в качестве первоначального целочисленного значения, на которое указывает параметр addrlen, должен быть установлен размер пространства, которое выделяется параметром addr. Это целое число заменяется количеством байт в возвращаемом адресе.
17.4. Сокеты домена Unix
Сокеты домена Unix — это простейшее семейство протоколов, доступное через API- интерфейс сокетов. Они фактически не являются сетевыми протоколами, поскольку могут соединяться с сокетами только на одном и том же компьютере. Несмотря на то что это значительно ограничивает их полезность, они все же используются многими приложениями благодаря гибкому механизму IPC, который они поддерживают. Их адреса — это путевые имена, которые создаются в файловой системе, когда сокет привязывается к путевому имени. Файлы сокетов, представляющие адреса доменов Unix, могут быть запущены функцией stat(), но не могут быть открыты с помощью open(); вместо этого нужно использовать API сокетов.
Домен Unix предусматривает как дейтаграммные, так и потоковые интерфейсы. Дейтаграммный интерфейс используется редко, и здесь обсуждаться не будет. Мы рассмотрим потоковый интерфейс, работа которого подобна именованным каналам. При этом сокеты домена Unix, однако, не идентичны именованным каналам.
Если несколько процессов одновременно открывают именованный канал, то любой из них может прочесть сообщение, передаваемое через канал другим процессом. Каждый канал похож на доску объявлений. Если процесс располагает сообщение на доске, то любой другой процесс (с достаточными полномочиями) может прочитать это сообщение с доски.
Сокеты домена Unix работают на основе соединений; в результате каждого соединения с сокетом возникает новый канал связи. Сервер, который может обрабатывать множество соединений одновременно, сохраняет для каждого из них свой файловый дескриптор. Благодаря этому свойству сокеты домена Unix лучше подходят для выполнения многих задач IPC, чем именованные каналы. Это главная причина, по которой они применяются большинством стандартных служб Linux, включал X Window System и системный регистратор.
17.4.1. Адреса домена Unix
Адреса для сокетов домена Unix являются путевыми именами в файловой системе. Если файл еще не существует, то он создается как файл сокетного типа в тот момент, когда сокет привязывается к путевому имени через функцию bind(). Если уже существует файл (или даже сокет) с указанным путевым именем, то функция bind() завершается и возвращает значение EADDRINUSE, bind() устанавливает права доступа для созданного файла сокета равными 0666, как измененные текущей маской umask.