В настоящем разделе представлены основные системные вызовы для создания и инициализации сокетов для любого протокола. Для того чтобы не зависеть от протоколов, информация в некоторой степени абстрагирована, по этой же причине мы не приводим примеры. Следующие два раздела посвящены применению сокетов в двух различных протоколах (домен Unix и TCP/IP). Здесь вы найдете подробные примеры использования большинства системных вызовов, описанных ниже.
17.3.1. Создание сокета
Новые сокеты создаются системным вызовом
socket
, который возвращает файловый дескриптор для неинициализированного сокета. При создании сокет привязывается к определенному протоколу, однако соединение для сокета не устанавливается. На данном этапе еще невозможно считывать информацию из сокета и записывать
в него.
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Подобно
open
, функция
socket
возвращает значение меньше 0, если имела место ошибка, и файловый дескриптор, больший или равный нулю, если все прошло благополучно. Три параметра устанавливают протокол, который нужно использовать.
Первый параметр указывает семейство протоколов и, как правило, принимает одно из значений, перечисленных в табл. 17.1.
указывает потоковый протокол из данного семейства, a
SOCK_DGRAM
специфицирует дейтаграммный протокол из того же семейства. Параметр
SOCK_RAW
предоставляет возможность передавать пакеты прямо в драйвер сетевого устройства, что позволяет пользовательским приложениям поддерживать сетевые протоколы, которые не воспринимаются ядром.
119
Допустимы еще несколько значений данного параметра, однако они редко применяются в коде приложений.
Последний параметр устанавливает протокол для использования с учетом всех ограничений, введенных первыми двумя параметрами. Как правило, значение этого параметра равно 0, что позволяет ядру использовать стандартный протокол установленного типа из указанного семейства. В табл. 17.2 перечислены некоторые допустимые протоколы для семейства
PF_INET
. Стандартными протоколами здесь считаются
IPPROTO_TCP
(потоковый) и
IPPROTO_UDP
(дейтаграммный).
Таблица 17.2. Протоколы IP
Протокол
Описание
IPPROTO_ICMP
Internet Control Message Protocol (протокол управляющих сообщений в сети Internet) для IPv4.
IPPROTO_ICMPV6
Internet Control Message Protocol (протокол управляющих сообщений в сети Internet) для IPv6.
IPPROTO_IPIP
Тоннели IPIP
IPPROTO_IPV6
Заголовки IPv6.
IPPROTO_RAW
Пакеты Raw IP.
IPPROTO_TCP
Transmission Control Protocol (TCP) (протокол управления передачей).
IPPROTO_UDP
User Datagram Protocol (UDP) (протокол передачи дейтаграмм пользователя).
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] .
120
Системный вызов
connect
может также быть неблокируемым, что позволит пользователям гораздо быстрее открывать несколько TCP соединений (он позволяет продолжать работу программы, в то время как выполняется процесс синхронизации TCP). Подробности по этой теме можно найти в [33].
121
Различные формы
select
помечают сокет как открытый для чтения, тогда как
accept
не может блокировать его, даже если сокет не помечен как неблокируемый. Для обеспечения максимальной переносимости функцию
select
необходимо применять только для принятия соглашений с неблокируемыми сокетами, хотя в системе Linux это фактически не нужно. Причины этого подробно рассматриваются в [33].
Ниже показаны прототипы
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
возвращает ноль в случае успеха и какое-то другое число в случае неудачи.