UNIX: разработка сетевых приложений — страница 17 из 42

Доменные протоколы Unix

15.1. Введение

Доменные протоколы Unix — это не набор протоколов, а способ связи клиентов и серверов на отдельном узле, использующий тот же API, который используется для клиентов и серверов на различных узлах, — сокеты или XTI. Доменные протоколы Unix представляют альтернативу методам IPC (Interprocess Communications — взаимодействие процессов), которым посвящен второй том[2] этой серии, применяемым, когда клиент и сервер находятся на одном узле. Подробности действительной реализации доменных сокетов Unix в ядре, происходящем от Беркли, приводятся в третьей части [112].

В домене Unix предоставляются два типа сокетов: потоковые (аналогичные сокетам TCP) и дейтаграммные (аналогичные сокетам UDP). Хотя предоставляется также и символьный сокет, но его семантика никогда не документировалась, он не используется никакой из известных автору программ и не определяется в POSIX.

Доменные сокеты Unix используются по трем причинам.

1. В реализациях, происходящих от Беркли, доменные сокеты Unix часто вдвое быстрее сокетов TCP, когда оба собеседника находятся на одном и том же узле [112, с. 223–224]. Есть приложение, которое использует это преимущество: X Window System. Когда клиент X11 запускается и открывает соединение с сервером X11, клиент проверяет значение переменной окружения DISPLAY, которая задает имя узла сервера, окно и экран. Если сервер находится на том же узле, что и клиент, клиент открывает потоковое соединение с сервером через доменный сокет Unix, в противном случае клиент открывает соединение TCP.

2. Доменные сокеты Unix используются при передаче дескрипторов между процессами на одном и том же узле. Пример мы приводим в разделе 15.7.

3. Более новые реализации доменных сокетов Unix предоставляют регистрационные данные клиента (идентификатор пользователя и идентификаторы группы) серверу, что может служить дополнительной проверкой безопасности. Мы покажем это в разделе 15.8.

Адреса протоколов, используемые для идентификации клиентов и серверов в домене Unix, — это полные имена в обычной файловой системе. Вспомните, что IPv4 использует комбинацию 32-разрядных адресов и 16-разрядных номеров портов для своих адресов протоколов, а IPv6 для своих адресов протоколов использует комбинацию 128-разрядных адресов и 16-разрядных номеров портов. Эти полные имена не являются обычными именами файлов Unix: в общем случае мы не можем читать из этих файлов или записывать в них. Это может делать только программа, связывающая полное имя с доменным сокетом Unix.

15.2. Структура адреса доменного сокета Unix

В листинге 15.1[1] показана структура адреса доменного сокета Unix, задаваемая включением заголовочного файла

.

Листинг 15.1. Структура адреса доменного сокета Unix: sockaddr_un

struct sockaddr_un {

 uint8_t     sun_len;

 sa_family_t sun_family;    /* AF_LOCAL */

 char        sun_path[104]; /* полное имя, оканчивающееся нулем */

};

ПРИМЕЧАНИЕ

POSIX не задает длину массива sun_path и предупреждает, что разработчику приложения не следует делать предположений об этой длине. Воспользуйтесь оператором sizeof для определения длины массива во время выполнения программы. Убедитесь, что полное имя помещается в этот массив. Длина, скорее всего, будет где-то между 92 и 108. Причина этих ограничений — в артефакте реализации, возникшем еще в 4.2BSD, где требовалось, чтобы структура помещалась в 128-байтовый буфер памяти ядра mbuf.

Полное имя, хранимое в символьном массиве

sun_path
, должно завершаться нулем. Имеется макрос
SUN_LEN
, который получает указатель на структуру
sockaddr_un
и возвращает длину структуры, включая число непустых байтов в полном имени. Неопределенный адрес обозначается пустой строкой, то есть элемент
sun_path[0]
должен быть равен нулю. Это эквивалент константы
INADDR_ANY
протокола IPv4 и константы
IN6ADDR_ANY_INIT
протокола IPv6 для домена Unix.

ПРИМЕЧАНИЕ

В POSIX доменным протоколам Unix дали название «локального IPC», чтобы не подчеркивать зависимость от операционной системы Unix. Историческая константа AF_UNIX становится константой AF_LOCAL. Тем не менее мы будем продолжать использовать термин «домен Unix», так как он стал именем де-факто, независимо от соответствующей операционной системы. Кроме того, несмотря на попытку POSIX исключить зависимость от операционной системы, структура адреса сокета сохраняет суффикс _un!

Пример: функция bind и доменный сокет Unix

Программа, показанная в листинге 15.2, создает доменный сокет Unix, с помощью функции

bind
связывает с ним полное имя и затем вызывает функцию
getsockname
и выводит это полное имя.

Листинг 15.2. Связывание полного имени с доменным сокетом Unix

unixdomain/unixbind.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;

 6  socklen_t len;

 7  struct sockaddr_un addr1, addr2;


 8  if (argc != 2)

 9   err_quit("usage: unixbind ");


10  sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);


11  unlink(argv[1]); /* игнорируем возможную ошибку */


12  bzero(&addr1, sizeof(addr1));

13  addr1.sun_family = AF_LOCAL;

14  strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1);

15  Bind(sockfd, (SA*)&addr1, SUN_LEN(&addr1));


16  len = sizeof(addr2);

17  Getsockname(sockfd, (SA*)&addr2, &len);

18  printf("bound name = %s, returned len = %d\n", addr2.sun_path, len);


19  exit(0);

20 }

Удаление файла

11
 Полное имя, которое функция
bind
должна связать с сокетом, — это аргумент командной строки. Если это полное имя уже существует в файловой системе, при выполнении функции
bind
возникает ошибка. Следовательно, мы вызываем функцию
unlink
, чтобы удалить файл в том случае, если он уже существует. Если его не существует, функция
unlink
возвращает ошибку, которую мы игнорируем.

Вызов функций bind и getsockname

12-18
 Мы копируем аргумент командной строки, используя функцию
strncpy
, чтобы избежать переполнения структуры, если полное имя слишком длинное. Поскольку мы инициализируем структуру нулем и затем вычитаем единицу из размера массива
sun_path
, мы знаем, что полное имя оканчивается нулем. Далее вызывается функция
bind
и мы используем макрос
SUN_LEN
для вычисления длины аргумента функции. Затем мы вызываем функцию
getsockname
, чтобы получить имя, которое было только что связано с сокетом, и выводим результат.

Если мы запустим программу в Solaris, то получим следующие результаты:

solaris % umaskсначала выводим наше значение umask

022             оно отображается в восьмеричной системе

solaris % unixbind /tmp/moose

bound name = /tmp/moose, returned len = 13

solaris % unixbind /tmp/mooseснова запускаем программу

bound name = /tmp/moose, returned len = 13

solaris % ls -l /tmp/moose

srwxr-xr-x 1 andy staff 0 Aug 10 13:13 /tmp/moose

solaris % ls -lF /tmp/foo.bar

srwxr-xr-x 1 andy staff 0 Aug 10 13:13 /tmp/moose=

Сначала мы выводим наше значение

umask
, поскольку в POSIX указано, что права доступа к создаваемому объекту определяются этим значением. Наше значение 022 выключает биты, разрешающие запись в файл для пользователей из группы (group-write) и прочих пользователей (other-write). Затем мы запускаем программу и видим, что длина, возвращаемая функцией
getsockname
, равна 13: один байт для элемента
sun_len
, один байт для элемента
sun_family
и 11 байт для полного имени (исключая завершающий нуль). Это пример аргумента типа «значение-результат», значение которого при завершении функции отличается от значения при вызове функции. Мы можем вывести полное имя, используя спецификатор формата
%s
функции
printf
, поскольку полное имя, хранящееся в
sun_path
, представляет собой завершающуюся нулем строку. Затем мы снова запускаем программу, чтобы проверить, что вызов функции
unlink
удаляет соответствующий файл.

Мы запускаем команду

ls -l
, чтобы увидеть биты разрешения для файла и тип файла. В Solaris (и большинстве версий Unix) тип файла — это сокет, что обозначается символом s. Мы также замечаем, что все девять битов разрешения включены, так как Solaris не изменяет принятые по умолчанию биты разрешения на наше значение
umask
. Наконец, мы снова запускаем
ls
с параметром
-F
, что заставляет Solaris добавить знак равенства (соответствующий типу «сокет») к полному имени.

ПРИМЕЧАНИЕ

Изначально значение umask не действовало на создаваемые процессами доменные сокеты Unix, но с течением времени производители исправили это упущение, чтобы устанавливаемые разрешения соответствовали ожиданиям разработчиков. Тем не менее все еще существуют системы, в которых разрешения доменного сокета могут не зависеть от значения umask. В других системах сокеты могут отображаться как каналы (символ р), а значок равенства при вызове ls -F может не отображаться вовсе. Однако поведение, демонстрируемое в нашем примере, является наиболее типичным.

15.3. Функция socketpair

Функция

socketpair
создает два сокета, которые затем соединяются друг с другом. Эта функция применяется только к доменным сокетам Unix.

#include 


int socketpair(int family, int type, int protocol, int sockfd[2]);

Возвращает: ненулевое значение в случае успешного выполнения, -1 в случае ошибки

Аргумент

family
должен быть равен
AF_LOCAL
, а аргумент
protocol
должен быть нулевым. Однако аргумент type может быть равен как
SOCK_STREAM
, так и
SOCK_DGRAM
. Два дескриптора сокета создаются и возвращаются как
sockfd[0]
и
sockfd[1]
.

ПРИМЕЧАНИЕ

Эта функция аналогична функции Unix pipe: при ее вызове возвращаются два дескриптора, причем каждый дескриптор соединен с другим. Действительно, в Беркли-реализации внутреннее устройство функции pipe полностью аналогично функции socketpair [112, с. 253-254].

Два созданных сокета не имеют имен. Это значит, что не было неявного вызова функции

bind
.

Результат выполнения функции

socketpair
с аргументом type, равным
SOCK_STREAM
, называется потоковым каналом (stream pipe). Потоковый канал является аналогом обычного канала Unix (который создается функцией
pipe
), но он двусторонний, что позволяет использовать оба дескриптора и для чтения, и для записи. Потоковый канал, созданный функцией
socketpair
, изображен на рис. 15.1.

ПРИМЕЧАНИЕ

POSIX не требует поддержки двусторонних каналов. В SVR4 функция pipe возвращает два двусторонних дескриптора, в то время как ядра, происходящие от Беркли, традиционно возвращают односторонние дескрипторы (см. рис. 17.31 [112]).

15.4. Функции сокетов

Функции сокетов применяются к доменным сокетам Unix с учетом некоторых особенностей и ограничений. Далее мы перечисляем требования POSIX, указывая, где они применимы. Отметим, что на сегодняшний день не все реализации соответствуют этим требованиям.

1. Права доступа к файлу по умолчанию для полного имени, созданного функцией

bind
, задаются значением 0777 (чтение, запись и выполнение данного файла разрешены владельцу файла, группе пользователей, в которую он входит, и всем остальным пользователям) и могут быть изменены в соответствии с текущим значением
umask
.

2. Имя, связанное с доменным сокетом Unix, должно быть абсолютным, а не относительным именем. Причина, по которой нужно избегать относительного имени, в том, что в таком случае разрешение имени зависит от текущего рабочего каталога вызывающего процесса. То есть если сервер связывается с относительным именем, клиент должен находиться в том же каталоге, что и сервер (или должен знать этот каталог), для того чтобы вызов клиентом функции

connect
или
sendto
был успешным.

ПРИМЕЧАНИЕ

В POSIX сказано, что связывание относительного имени с доменным сокетом Unix приводит к непредсказуемым результатам.

3. Полное имя, заданное в вызове функции

connect
, должно быть именем, в настоящий момент связанным с открытым доменным сокетом Unix того же типа (потоковым или дейтаграммным). Ошибка происходит в следующих случаях: если имя существует, но не является сокетом; если имя существует и является сокетом, но ни один открытый дескриптор с ним не связан; если имя существует и является открытым сокетом, но имеет неверный тип (то есть потоковый доменный сокет Unix не может соединиться с именем, связанным с дейтаграммным доменным сокетом Unix, и наоборот).

4. С функцией

connect
доменного сокета Unix связана такая же проверка прав доступа, какая имеет место при вызове функции open для доступа к файлу только на запись.

5. Потоковые доменные сокеты Unix аналогичны сокетам TCP: они предоставляют интерфейс байтового потока без границ записей.

6. Если при вызове функции connect для потокового доменного сокета Unix обнаруживается, что очередь прослушиваемого сокета переполнена (см. раздел 4.5), немедленно возвращается ошибка

ECONNREFUSED
. В этом отличие от сокета TCP: прослушиваемый сокет TCP игнорирует приходящий сегмент SYN, если очередь сокета заполнена, благодаря чему стеком клиента выполняется несколько попыток отправки сегмента SYN.

7. Дейтаграммные доменные сокеты Unix аналогичны сокетам UDP: они предоставляют ненадежный сервис дейтаграмм, сохраняющий границы записей.

8. В отличие от сокетов UDP, при отправке дейтаграммы на неприсоединенный дейтаграммный доменный сокет Unix с сокетом не связывается полное имя. (Вспомните, что отправка дейтаграммы UDP на неприсоединенный сокет UDP заставляет динамически назначаемый порт связываться с сокетом.) Это означает, что получатель дейтаграммы не будет иметь возможности отправить ответ, если отправитель не связал со своим сокетом полное имя. Аналогично, в отличие от TCP и UDP, при вызове функции

connect
для дейтаграммного доменного сокета Unix с сокетом не связывается полное имя.

15.5. Клиент и сервер потокового доменного протокола Unix

Теперь мы перепишем наш эхо-клиент и эхо-сервер TCP из главы 5 с использованием доменных сокетов Unix. В листинге 15.3 показан сервер, который является модификацией сервера из листинга 5.9 и использует потоковый доменный протокол Unix вместо протокола TCP.

Листинг 15.3. Эхо-сервер потокового доменного протокола Unix

//unixdomain/unixstrserv01.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int listenfd, connfd;

 6  pid_t childpid;

 7  socklen_t clilen;

 8  struct sockaddr_un cliaddr, servaddr;

 9  void sig_chld(int);


10  listenfd = Socket(AF_LOCAL, SOCK_STREAM, 0);


11  unlink(UNIXSTR_PATH);

12  bzero(&servaddr, sizeof(servaddr));

13  servaddr.sun_family = AF_LOCAL;

14  strcpy(servaddr.sun_path, UNIXSTR_PATH);


15  Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));


16  Listen(listenfd, LISTENQ);

17  Signal(SIGCHLD, sig_chld);


18  for (;;) {

19   clilen = sizeof(cliaddr);

20   if ((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0) {

21    if (errno == EINTR)

22     continue; /* назад в for() */

23    else

24     err_sys("accept error");

25   }

26   if ((childpid = Fork()) == 0) { /* дочерний процесс */

27    Close(listenfd); /* закрывается прослушиваемый сокет */

28    str_echo(connfd); /* обработка запроса */

29    exit(0);

30   }

31   Close(connfd); /* родитель закрывает присоединенный сокет */

32  }

33 }

8
 Теперь две структуры адреса сокета относятся к типу
sockaddr_un
.

10
 Для создания потокового доменного сокета Unix первый аргумент функции socket должен иметь значение
AF_LOCAL
.

11-15
 Константа
UNIXSTR_PATH
определяется в файле
unp.h
как
/tmp/unix/str
. Сначала мы вызываем функцию
unlink
, чтобы удалить полное имя в случае, если оно сохранилось после предыдущего запуска сервера, а затем инициализируем структуру адреса сокета перед вызовом функции
bind
. Ошибка при выполнении функции
unlink
не является аварийной ситуацией.

Обратите внимание, что этот вызов функции

bind
отличается от вызова, показанного в листинге 15.2. Здесь мы задаем размер структуры адреса сокета (третий аргумент) как общий размер структуры
sockaddr_un
, а не просто число байтов, занимаемое полным именем. Оба значения длины приемлемы, поскольку полное имя должно оканчиваться нулем.

Оставшаяся часть функции такая же, как и в листинге 5.9. Используется та же функция

str_echo
(см. листинг 5.2).

В листинге 15.4 представлен эхо-клиент потокового доменного протокола Unix. Это модификация листинга 5.3.

Листинг 15.4. Эхо-клиент потокового доменного протокола Unix

//unixdomain/umxstrcli01.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;

 6  struct sockaddr_un servaddr;


 7  sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);


 8  bzero(&servaddr, sizeof(servaddr));

 9  servaddr sun_family = AF_LOCAL;

10  strcpy(servaddr.sun_path, UNIXSTR_PATH);

11  Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));

12  str_cli(stdin, sockfd); /* выполняет всю работу */


13  exit(0);

14 }

6
 Теперь структурой адреса сокета, которая должна содержать адрес сервера, будет структура
sockaddr_un
.

7
 Первый аргумент функции
socket
AF_LOCAL
.

8-10
 Код для заполнения структуры адреса сокета идентичен коду, показанному для сервера: инициализация структуры нулем, установка семейства протоколов
AF_LOCAL
и копирование полного имени в элемент
sun_path
.

12
 Функция
str_cli
— та же, что и раньше (в листинге 6.2 представлена последняя разработанная нами версия).

15.6. Клиент и сервер дейтаграммного доменного протокола Unix

Теперь мы перепишем наши клиент и сервер UDP из разделов 8.3 и 8.5 с использованием сокетов. В листинге 15.5 показан сервер, который является модификацией листинга 8.1.

Листинг 15.5. Эхо-сервер дейтаграммного доменного протокола Unix

//unixdomain/unixdgserv01.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;

 6  struct sockaddr_un servaddr, cliaddr;


 7  sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);


 8  unlink(UNIXDG_PATH);

 9  bzero(&servaddr, sizeof(servaddr));

10  servaddr.sun_family = AF_LOCAL;

11  strcpy(servaddr.sun_path, UNIXDG_PATH);


12  Bind(sockfd, (SA*)&servaddr, sizeof(servaddr));


13  dg_echo(sockfd, (SA*)&cliaddr, sizeof(cliaddr));

14 }

6
 Две структуры адреса сокета относятся теперь к типу
sockaddr_un
.

7
 Для создания дейтаграммного доменного сокета Unix первый аргумент функции
socket
должен иметь значение
AF_LOCAL
.

8-12
 Константа
UNIXDG_PATH
определяется в заголовочном файле
unp.h
как
/tmp/unix.dg
. Сначала мы вызываем функцию
unlink
, чтобы удалить полное имя в случае, если оно сохранилось после предыдущего запуска сервера, а затем инициализируем структуру адреса сокета перед вызовом функции
bind
. Ошибка при выполнении функции
unlink
— это нормальное явление.

13
 Используется та же функция
dg_echo
(см. листинг 8.2).

В листинге 15.6 представлен эхо-клиент дейтаграммного доменного протокола Unix. Это модификация листинга 8.3.

Листинг 15.6. Эхо-клиент дейтаграммного доменного протокола Unix

//unixdomain/unixdgcli01.с

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;

 6  struct sockaddr_un cliaddr, servaddr;


 7  sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);


 8  bzero(&cliaddr, sizeof(cliaddr)); /* связывание сокета с адресом */

 9  cliaddr.sun_family = AF_LOCAL;

10  strcpy(cliaddr.sun_path, tmpnam(NULL);


11  Bind(sockfd, (SA*)&cliaddr, sizeof(cliaddr));


12  bzero(&servaddr, sizeof(servaddr)); /* заполняем структуру адреса

                                           сокета сервера */

13  servaddr.sun_family = AF_LOCAL;

14  strcpy(servaddr.sun_path, UNIXDG_PATH);


15  dg_cli(stdin, sockfd, (SA*)&servaddr, sizeof(servaddr));


16  exit(0);

17 }

6
 Структурой адреса сокета, содержащей адрес сервера, теперь будет структура
sockaddr_un
. Мы также размещаем в памяти одну из этих структур, чтобы она содержала адрес клиента, о чем мы расскажем далее.

7
 Первый аргумент функции
socket
— это
AF_LOCAL
.

8-11
 В отличие от клиента UDP при использовании дейтаграммного доменного протокола Unix требуется явно связать с помощью функции
bind
полное имя с нашим сокетом, чтобы сервер имел полное имя, на которое он мог бы отправить свой ответ. Мы вызываем функцию
tmpnam
, чтобы получить уникальное полное имя, с которым затем при помощи функции
bind
свяжем наш сокет. Вспомните из раздела 15.4, что при отправке дейтаграммы на неприсоединенный дейтаграммный доменный сокет Unix не происходит неявного связывания полного имени с сокетом. Следовательно, если мы опустим этот шаг, вызов сервером функции
recvfrom
в функции
dg_echo
возвращает пустое полное имя, что затем приведет к ошибке, когда сервер вызовет функцию
sendto
.

12-14
 Код для заполнения структуры адреса сокета заранее известным полным именем идентичен коду, представленному ранее для сервера.

15
 Функция
dg_cli
остается той же, что и раньше (см. листинг 8.4).

15.7. Передача дескрипторов

Когда нам требуется передать дескриптор от одного процесса другому, обычно мы выбираем одно из двух решений:

1. Дочерний процесс использует все открытые дескрипторы совместно с родительским процессом после вызова функции

fork
.

2. Все дескрипторы обычно остаются открытыми при вызове функции

exec
.

В первом случае процесс открывает дескриптор, вызывает функцию

fork
, а затем родительский процесс закрывает дескриптор, позволяя дочернему процессу с ним работать. При этом открытый дескриптор передается от родительского процесса дочернему. Но нам также хотелось бы, чтобы у дочернего процесса была возможность открывать дескриптор и передавать его обратно родительскому процессу.

Современные системы Unix предоставляют способ передавать любой открытый дескриптор от одного процесса любому другому процессу. При этом вовсе не обязательно, чтобы процессы были родственными, как родительский и дочерний. Эта технология требует, чтобы мы сначала создали между двумя процессами доменный сокет Unix и затем использовали функцию

sendmsg
для отправки специального сообщения через этот доменный сокет. Ядро обрабатывает это сообщение специальным образом, передавая открытый дескриптор от отправителя получателю.

ПРИМЕЧАНИЕ

Передача ядром 4.4BSD открытого дескриптора через доменный сокет Unix описывается в главе 18 [112].

SVR4 использует другую технологию внутри ядра для передачи открытого дескриптора: команды I_SENDFD и I_RECVFD функции ioctl, описанные в разделе 15.5.1 [110]. Но процесс все же имеет возможность доступа к указанному свойству ядра за счет доменного сокета Unix. В этой книге мы описываем применение доменных сокетов Unix для передачи открытых дескрипторов, поскольку это наиболее переносимая технология программирования: она работает как с Беркли-ядрами, так и с SVR4, в то время как команды I_SENDFD и I_RECVFD функции ioctl работают только в SVR4.

Технология 4.4BSD позволяет передавать множество дескрипторов с помощью одиночной функции sendmsg, в то время как технология SVR4 передает за один раз только один дескриптор. Во всех наших примерах за один раз передается один дескриптор.

Шаги при передаче дескриптора между процессами будут такими:

1. Создание доменного сокета Unix, или потокового сокета, или дейтаграммного сокета.

Если целью является породить с помощью функции

fork
дочерний процесс, с тем чтобы дочерний процесс открыл дескриптор и передал его обратно родительскому процессу, родительский процесс может вызвать функцию
socketpair
для создания потокового канала, который может использоваться для передачи дескриптора.

Если процессы не являются родственными, сервер должен создать потоковый доменный сокет Unix, связать его при помощи функции

bind
с полным именем, тем самым позволяя клиенту соединиться с этим сокетом при помощи функции connect. Затем клиент может отправить запрос серверу для открытия некоторого дескриптора, а сервер может передать дескриптор обратно через доменный сокет Unix. Как альтернатива между клиентом и сервером может также использоваться дейтаграммный доменный сокет Unix, однако преимущества этого способа невелики, к тому же существует возможность игнорирования дейтаграммы. Далее в примерах этой главы мы будем использовать потоковый сокет между клиентом и сервером.

2. Один процесс открывает дескриптор при помощи вызова любой из функций Unix, возвращающей дескриптор, например

open
,
piре
,
mkfifo
,
socket
или
accept
. От одного процесса к другому можно передать дескриптор любого типа, поэтому мы называем эту технологию «передачей дескриптора», а не «передачей дескриптора файла».

3. Отправляющий процесс строит структуру

msghdr
(см. раздел 14.5), содержащую дескриптор, который нужно передать. В POSIX определено, что дескриптор должен отправляться как вспомогательные данные (элемент
msg_control
структуры
msghdr
, см. раздел 14.6), но более старые реализации используют элемент
msg_accrights
. Отправляющий процесс вызывает функцию
sendmsg
для отправки дескриптора через доменный сокет Unix, созданный на шаге 1. На этом этапе мы говорим, что дескриптор находится «в полете». Даже если отправляющий процесс закроет дескриптор после вызова функции
sendmsg
, но до вызова принимающим процессом функции
recvmsg
, дескриптор останется открытым для принимающего процесса. Отправка дескриптора увеличивает счетчик ссылок дескриптора на единицу.

4. Принимающий процесс вызывает функцию

recvmsg
для получения дескриптора через доменный сокет Unix, созданный на шаге 1. Номер дескриптора в принимающем процессе может отличаться от номера дескриптора в отправляющем процессе. Передача дескриптора — это не передача номера дескриптора. Этот процесс включает создание нового дескриптора в принимающем процессе, который ссылается на ту же запись таблицы файлов в ядре, что и дескриптор, отправленный отправляющим процессом.

Клиент и сервер должны располагать некоторым протоколом уровня приложения, с тем чтобы получатель дескриптора имел информацию о времени его появления. Если получатель вызывает функцию

recvmsg
, не выделив места в памяти для получения дескриптора, и дескриптор передается как готовый для чтения, то передаваемый дескриптор закрывается [128, с. 518]. Кроме того, нужно избегать установки флага
MSG_PEEK
в функции
recvmsg
, если предполагается получение дескриптора, поскольку в этом случае результат непредсказуем.

Пример передачи дескриптора

Теперь мы представим пример передачи дескриптора. Мы напишем программу под названием

mycat
, которой в качестве аргумента командной строки передается полное имя файла. Эта программа открывает файл и копирует его в стандартный поток вывода. Но вместо вызова обычной функции Unix
open
мы вызываем нашу собственную функцию
my_open
. Эта функция создает потоковый канал и вызывает функции
fork
и
exec
для запуска другой программы, открывающей нужный файл. Эта программа должна затем передать дескриптор обратно родительскому процессу по потоковому каналу.

На рис. 15.1 показан первый шаг: наша программа

mycat
после создания потокового канала при помощи вызова функции
socketpair
. Мы обозначили два дескриптора, возвращаемых функцией
socketpair
, как
[0]
и
[1]
.

Рис. 15.1. Программа mycat после создания потокового канала при использовании функции socketpair

Затем процесс взывает функцию

fork
, и дочерний процесс вызывает функцию
exec
для выполнения программы
openfile
. Родительский процесс закрывает дескриптор
[1]
, а дочерний процесс закрывает дескриптор
[0]
. (Нет разницы, на каком конце потокового канала происходит закрытие. Дочерний процесс мог бы закрыть
[1]
, а родительский —
[0]
.) При этом получается схема, показанная на рис. 15.2.

Рис. 15.2. Программа mycat после запуска программы openfile

Родительский процесс должен передать программе

openfile
три фрагмента информации: полное имя открываемого файла, режим открытия (только чтение чтение и запись или только запись) и номер дескриптора, соответствующий его концу потокового канала (который мы обозначили
[1]
). Мы выбрали такой способ передачи этих трех элементов, как ввод аргументов командной строки при вызове функции
exec
. Альтернативным способом будет отправка этих элементов в качестве данных по потоковому каналу. Программа отправляет обратно открытый дескриптор по потоковому каналу и завершается. Статус выхода программы сообщает родительскому процессу, смог ли файл открыться, и если нет, то какого типа ошибка произошла.

Преимущество выполнения дополнительной программы для открытия файла заключается в том, что за счет приравнивания привилегий пользователя к привилегиям владельца файла мы получаем возможность открывать те файлы, которые не имеем права открывать в обычной ситуации. Эта программа позволяет расширить концепцию обычных прав доступа Unix (пользователь, группа и все остальные) и включить любые формы проверки прав доступа. Мы начнем с программы

mycat
, показанной в листинге 15.7.

Листинг 15.7. Программа mycat: копирование файла в стандартный поток вывода

//unixdomain/mycat.c

 1 #include "unp.h"


 2 int my_open(const char*, int);


 3 int

 4 main(int argc, char **argv)

 5 {

 6  int fd, n;

 7  char buff[BUFFSIZE];


 8  if (argc != 2)

 9   err_quit("usage: mycat ");


10  if ((fd = my_open(argv[1], O_RDONLY)) < 0)

11   err_sys("cannot open %s", argv[1]);


12  while ((n = Read(fd, buff, BUFFSIZE)) > 0)

13   Write(STDOUT_FILENO, buff, n);


14  exit(0);

15 }

Если мы заменим вызов функции

my_open
вызовом функции
open
, эта простая программа всего лишь скопирует файл в стандартный поток вывода.

Функция

my_open
, показанная в листинге 15.8, должна выглядеть для вызывающего процесса как обычная функция Unix
open
. Она получает два аргумента — полное имя и режим открытия (например,
O_RDONLY
обозначает, что файл доступен только для чтения), открывает файл и возвращает дескриптор.

Листинг 15.8. Функция my_open: открытие файла и возвращение дескриптора

//unixdomain/myopen.c

 1 #include "unp.h"


 2 int

 3 my_open(const char *pathname, int mode)

 4 {

 5  int fd, sockfd[2], status;

 6  pid_t childpid;

 7  char c, argsockfd[10], argmode[10];


 8  Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);


 9  if ((childpid = Fork()) == 0) { /* дочерний процесс */

10   Close(sockfd[0]);

11   snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]);

12   snprintf(argmode, sizeof(argmode), "%d", mode);

13   execl("./openfile", "openfile", argsockfd, pathname, argmode,

14    (char*)NULL);

15   err_sys("execl error");

16  }

17  /* родительский процесс - ожидание завершения дочернего процесса */

18  Close(sockfd[1]); /* закрываем конец, который мы не используем */


19  Waitpid(childpid, &status, 0);

20  if (WIFEXITED(status) == 0)

21   err_quit("child did not terminate");

22  if ((status = WEXITSTATUS(status)) == 0)

23   Read_fd(sockfd[0], &c, 1, &fd);

24  else {

25   errno = status; /* установка значения errno в статус дочернего

                        процесса */

26   fd = -1;

27  }


28  Close(sockfd[0]);

29  return (fd);

30 }

Создание потокового канала

8
 Функция
socketpair
создает потоковый канал. Возвращаются два дескриптора:
sockfd[0]
и
sockfd[1]
. Это состояние, которое мы показали на рис. 15.1.

Функции fork и exec

9-16
 Вызывается функция
fork
, после чего дочерний процесс закрывает один конец потокового канала. Номер дескриптора другого конца потокового канала помещается в массив
argsockfd
, а режим открытия помещается в массив
argmode
. Мы вызываем функцию
snprintf
, поскольку аргументы функции exec должны быть символьными строками. Выполняется программа
openfile
. Функция
execl
возвращает управление только в том случае, если она встретит ошибку. При удачном выполнении начинает выполняться функция
main
программы
openfile
.

Родительский процесс в ожидании завершения дочернего процесса

17-22
 Родительский процесс закрывает другой конец потокового канала и вызывает функцию
waitpid
для ожидания завершения дочернего процесса. Статус завершения дочернего процесса возвращается в переменной
status
, и сначала мы проверяем, что программа завершилась нормально (то есть не была завершена из-за возникновения какого-либо сигнала). Затем макрос
WEXITSTATUS
преобразует статус завершения в статус выхода, значение которого должно быть между 0 и 255. Мы вскоре увидим, что если при открытии необходимого файла программой
openfile
происходит ошибка, то эта программа завершается, причем статус ее завершения равен соответствующему значению переменной
errno
.

Получение дескриптора

23
 Наша функция
read_fd
, которую мы показываем в следующем листинге, получает дескриптор потокового канала. Кроме получения дескриптора мы считываем 1 байт данных, но ничего с этими данными не делаем.

ПРИМЕЧАНИЕ

При отправке и получении дескриптора по потоковому каналу мы всегда отправляем как минимум 1 байт данных, даже если получатель никак эти данные не обрабатывает. Иначе получатель не сможет распознать, что значит нулевое возвращаемое значение из функции read_fd: отсутствие данных (но, возможно, есть дескриптор) или конец файла.

В листинге 15.9 показана функция

readfd
, вызывающая функцию
recvmsg
для получения данных и дескриптора через доменный сокет Unix. Первые три аргумента этой функции те же, что и для функции
read
, а четвертый (
recvfd
) является указателем на целое число. После выполнения этой функции
recvfd
будет указывать на полученный дескриптор.

Листинг 15.9. Функция read_fd: получение данных и дескриптора

//lib/read_fd.c

 1 #include "unp.h"


 2 ssize_t

 3 read_fd(int fd, void *ptr, size_t nbytes, int *recvfd)

 4 {

 5  struct msghdr msg;

 6  struct iovec iov[1];

 7  ssize_t n;

 8  int newfd;


 9 #ifdef HAVE_MSGHDR_MSG_CONTROL

10  union {

11   struct cmsghdr cm;

12   char control[CMSG_SPACE(sizeof(int))];

13  } control_un;

14  struct cmsghdr *cmptr;


15  msg.msg_control = control_un.control;

16  msg.msg_controllen = sizeof(control_un.control);

17 #else

18  msg.msg_accrights = (caddr_t)&newfd;

19  msg.msg_accrightslen = sizeof(int);

20 #endif


21  msg.msg_name = NULL;

22  msg.msg_namelen = 0;


23  iov[0].iov_base = ptr;

24  iov[0].iov_len = nbytes;

25  msg.msg_iov = iov;

26  msg.msg_iovlen = 1;


27  if ((n = recvmsg(fd, &msg, 0)) <= 0)

28   return (n);


29 #ifdef HAVE_MSGHDR_MSG_CONTROL

30  if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&

31   mptr->cmsg_len == CMSG_LEN(sizeof(int))) {

32   if (cmptr->cmsg_level != SOL_SOCKET)

33    err_quit("control level != SOL_SOCKET");

34   if (cmptr->cmsg_type != SCM_RIGHTS)

35    err_quit("control type != SCM_RIGHTS");

36   *recvfd = *((int*)CMSG_DATA(cmptr));

37  } else

38   *recvfd = -1; /* дескриптор не был передан */

39 #else

40  if (msg.msg_accrightslen == sizeof(int))

41   *recvfd = newfd;

42  else

43   *recvfd = -1; /* дескриптор не был передан */


44 #endif


45  return (n);

46 }

8-26
 Эта функция должна работать с обеими версиями функции
recvmsg
: с элементом
msg_control
и с элементом
msg_accrights
. Наш заголовочный файл
config.h
(см. листинг Г.2) определяет константу
HAVE_MSGHDR_MSG_CONTROL
, если поддерживается версия функции
recvmsg
с
msg_control
.

Проверка выравнивания буфера msg_control

10-13
 Буфер
msg_control
должен быть выровнен в соответствии со структурой
msghdr
. Просто выделить в памяти массив типа
char
недостаточно. Здесь мы объявляем объединение, состоящее из структуры
cmsghdr
и символьного массива, что гарантирует необходимое выравнивание массива. Возможно и другое решение — вызвать функцию
malloc
, но это потребует освобождения памяти перед завершением функции.

27-45
 Вызывается функция
recvmsg
. Если возвращаются вспомогательные данные, их формат будет таким, как показано на рис. 14.4. Мы проверяем, верны ли длина, уровень и тип, затем получаем вновь созданный дескриптор и возвращаем его через указатель вызывающего процесса
recvfd
. Макрос
CMSG_DATA
возвращает указатель на элемент
cmsg_data
объекта вспомогательных данных как указатель на элемент типа
unsigned char
. Мы преобразуем его к указателю на элемент типа
int
и получаем целочисленный дескриптор, на который указывает этот указатель.

Если поддерживается более старый элемент

msg_accrights
, то длина должна быть равна размеру целого числа, а вновь созданный дескриптор возвращается через указатель
recvfd
вызывающего процесса.

В листинге 15.10 показана программа

openfile
. Она получает три аргумента командной строки, которые должны быть переданы, и вызывает обычную функцию
open
.

Листинг 15.10. Программа openfile: открытие файла и передача дескриптора обратно

//unixdomain/openfile.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int fd;

 6  ssize_t n;


 7  if (argc != 4)

 8   err_quit("openfile ");


 9  if ((fd = open(argv[2], atoi(argv[3]))) < 0)

10   exit((errno > 0) ? errno : 255);


11  if ((n = write_fd(atoi(argv[1]), "", 1, fd)) < 0)

12   exit((errno > 0) ? errno : 255);


13  exit(0);

14 }

Аргументы командной строки

6-7
 Поскольку два из трех аргументов командной строки были превращены в символьные строки функцией
my_open
, они преобразуются обратно в целые числа при помощи функции
atoi
.

Открытие файла

9-10
 Файл открывается с помощью функции
open
. Если встречается ошибка, статус завершения этого процесса содержит значение переменной
errno
, соответствующее ошибке функции
open
.

Передача дескриптора обратно

11-12
 Дескриптор передается обратно функцией
write_fd
, которую мы покажем в следующем листинге. Затем этот процесс завершается, но ранее в этой главе мы сказали, что отправляющий процесс может закрыть переданный дескриптор (это происходит, когда мы вызываем функцию
exit
), поскольку ядро знает, что дескриптор находится в состоянии передачи («в полете»), и оставляет его открытым для принимающего процесса.

ПРИМЕЧАНИЕ

Статус выхода должен лежать в пределах от 0 до 255. Максимальное значение переменной errno — около 150. Альтернативный способ, при котором не требуется, чтобы значение переменной errno было меньше 256, заключается в том, чтобы передать обратно указание на ошибку в виде обычных данных при вызове функции sendmsg.

В листинге 15.11 показана последняя функция,

write_fd
, вызывающая функцию
sendmsg
для отправки дескриптора (и, возможно, еще каких-либо данных, которые мы не используем) через доменный сокет Unix.

Листинг 15.11. Функция write_fd: передача дескриптора при помощи вызова функции sendmsg

//lib/write_fd.c

 1 #include "unp.h"


 2 ssize_t

 3 write_fd(int fd, void *ptr, size_t nbytes, int sendfd)

 4 {

 5  struct msghdr msg;

 6  struct iovec iov[1];


 7 #ifdef HAVE_MSGHDR_MSG_CONTROL

 8  union {

 9   struct cmsghdr cm;

10   char control[CMSG_SPACE(sizeof(int))];

11  } control_un;

12  struct cmsghdr *cmptr;


13  msg.msg_control = control_un.control;

14  msg.msg_controllen = sizeof(control_un.control);


15  cmptr = CMSG_FIRSTHDR(&msg);

16  cmptr->cmsg_len = CMSG_LEN(sizeof(int));

17  cmptr->cmsg_level = SOL_SOCKET;

18  cmptr->cmsg_type = SCM_RIGHTS;

19  *((int*)CMSG_DATA(cmptr)) = sendfd;

20 #else

21  msg.msg_accrights = (caddr_t)&sendfd;

22  msg.msg_accrightslen = sizeof(int);

23 #endif


24  msg.msg_name = NULL;

25  msg.msg_namelen = 0;


26  iov[0].iov_base = ptr;

27  iov[0].iov_len = nbytes;

28  msg.msg_iov = iov;

29  msg.msg_iovlen = 1;


30  return (sendmsg(fd, &msg, 0));

31 }

Как и в случае функции

read_fg
, эта функция обрабатывает либо вспомогательные данные, либо права доступа, которые предшествовали вспомогательным данным в более ранних реализациях. В любом случае инициализируется структура
msghdr
и затем вызывается функция
sendmsg
.

В разделе 28.7 мы приводим пример передачи дескриптора, в котором участвуют неродственные (unrelated) процессы, а в разделе 30.9 — пример, где задействованы родственные процессы. В них мы будем использовать функции

read_fd
и
write_fd
, которые только что описали.

15.8. Получение информации об отправителе

На рис. 14.4 мы показали другой тип информации, передаваемой через доменный сокет Unix в виде вспомогательных данных: информацию об отправителе, которая передается с помощью структуры

cmsgcred
, определяемой путем включения заголовочного файла
. Упаковка и формат данных зависят от операционной системы. Такая возможность появилась только в BSD/OS 2.1. Мы описываем FreeBSD, а прочие варианты Unix во многом подобны ей (проблема обычно состоит в выборе структуры, которую следует использовать для передачи данных). Рассказ об этой возможности мы считаем необходимым, поскольку это важное, хотя и простое дополнение доменных протоколов Unix. Когда клиент и сервер связываются с помощью этих протоколов, серверу часто бывает необходим способ точно узнать, кто является клиентом, чтобы убедиться, что клиент имеет право запрашивать определенный сервис.

struct fcred {

 uid_t fc_ruid;            /* действующий идентификатор пользователя */

 gid_t fc_rgid;            /* действующий групповой идентификатор */

 char  fc_login[MAXLOGNAME]; /* имя setlogin() */

 uid_t fc_uid;             /* идентификатор пользователя */

 short fc_ngroups;         /* количество групп */

 gid_t fc_groups[NGROUPS]; /* дополнительные групповые идентификаторы */

};

#define fc_gid fc_groups[0] /* групповой идентификатор */

Обычно

MAXLONGNAME
и
NGROUPS
имеют значение 16. Значение
fc_ngroups
равно как минимум 1, а первым элементом массива является идентификатор группы.

Эта информация всегда доступна через доменный сокет Unix, хотя отправителю часто приходится принимать дополнительные меры для обеспечения ее отправки вместе с данными, и получателю также приходится выполнять некоторые действия (например, устанавливать параметры сокета). В системе FreeBSD получатель может обойтись вызовом

recvmsg
с достаточно большим буфером для вспомогательных данных, чтобы туда поместились идентифицирующие данные (листинг 15.12). Однако отправитель обязан включить структуру
cmsgcred
при отправке данных посредством
sendmsg
. Хотя включение структуры осуществляется отправителем, заполняется она ядром. Благодаря этому передача идентифицирующих данных через доменный сокет Unix является надежным способом проверки клиента.

Пример

В качестве примера передачи идентифицирующих данных мы изменим наш потоковый доменный сервер Unix, так чтобы он запрашивал идентифицирующие данные клиента. В листинге 15.12 показана новая функция,

read_cred
, аналогичная функции
read
, но возвращающая также структуру
fcred
, содержащую идентифицирующие данные отправителя.

Листинг 15.12. Функция read_cred: чтение и возвращение идентифицирующих данных отправителя

//unixdomain/readcred.c

 1 #include "unp.h"


 2 #define CONTROL_LEN (sizeof(struct cmsghdr) + sizeof(struct cmsgcred))


 3 ssize_t

 4 read_cred(int fd, void *ptr, size_t nbytes, struct cmsgcred *cmsgcredptr)

 5 {

 6  struct msghdr msg;

 7  struct iovec iov[1];

 8  char control[CONTROL_LEN];

 9  int n;


10  msg.msg_name = NULL;

11  msg.msg_namelen = 0;

12  iov[0].iov_base = ptr;

13  iov[0].iov_len = nbytes;

14  msg.msg_iov = iov;

15  msg.msg_iovlen = 1;

16  msg.msg_control = control;

17  msg.msg_controllen = sizeof(control);

18  msg.msg_flags = 0;


19  if ((n = recvmsg(fd, &msg, 0)) < 0)

20   return(n);


21  cmsgcredptr->cmcred_ngroups = 0; /* идентифицирующие данные не получены */

22  if (cmsgcredptr && msg.msg_controllen > 0) {

23   struct cmsghdr *cmptr = (struct cmsghdr*)control;


24   if (cmptr->cmsg_len < CONTROL_LEN)

25    err_quit("control length = %d", cmptr->cmsg_len);

26   if (cmptr->cmsg_level != SOL_SOCKET)

27    err_quit("control level != SOL_SOCKET");

28   if (cmptr->cmsg_type != SCM_CREDS)

29    err_quit("control type != SCM_CREDS");

30   memcpy(cmsgcredptr, CMSG_DATA(cmptr), sizeof(struct cmsgcred));

31  }


32  return(n);

33 }

3-4
 Первые три аргумента идентичны аргументам функции
read
, а четвертый аргумент — это указатель на структуру
cmsgcred
, которая будет заполнена.

22-31
 Если данные были переданы, проверяются длина, уровень и тип вспомогательных данных, и результирующая структура копируется обратно вызывающему процессу. Если никаких идентифицирующих данных не было передано, мы обнуляем структуру. Поскольку число групп (
cmcred_ngroups
) всегда равно 1 или больше, нулевое значение указывает вызывающему процессу, что ядро не возвратило никаких идентифицирующих данных.

Функция

main
для нашего эхо-сервера (см. листинг 15.3) остается неизменной. В листинге 15.13 показана новая версия функции
str_echo
, полученная путем модификации листинга 5.2. Эта функция вызывается дочерним процессом после того, как родительский процесс принял новое клиентское соединение и вызвал функцию
fork
.

Листинг 15.13. Функция str_echo, запрашивающая идентифицирующие данные клиента

//unixdomain/strecho.c

 1 #include "unp.h"


 2 ssize_t read_cred(int, void*, size_t, struct cmsgcred*);


 3 void

 4 str_echo(int sockfd)

 5 {

 6  ssize_t n;

 7  int i;

 8  char buf[MAXLINE];

 9  struct cmsgcred cred;

10 again:

11  while ((n = read_cred(sockfd, buf, MAXLINE, &cred)) > 0) {

12   if (cred.cmcred_ngroups == 0) {

13    printf("(no credentials returned)\n");

14   } else {

15    printf("PID of sender = %d\n", cred.cmcred_pid);

16    printf("real user ID = %d\n", cred.cmcred_uid);

17    printf("real group ID = %d\n", cred.cmcred_gid);

18    printf("effective user ID = %d\n", cred.cmcred_euid);

19    printf("%d groups:", cred.cmcred_ngroups - 1);

20    for (i = 1; i < cred.cmcred_ngroups; i++)

21     printf(" %d", cred.cmcred_groups[i]);

22    printf("\n");

23   }

24   Writen(sockfd, buf, n);

25  }


26  if (n < 0 && errno == EINTR)

27   goto again;

28  else if (n < 0)

29   err_sys("str_echo: read error");

30 }

11-23
 Если идентифицирующие данные возвращаются, они выводятся.

24-25
 Оставшаяся часть цикла не меняется. Этот код считывает строки от клиента и затем отправляет их обратно клиенту.

Наш клиент, представленный в листинге 15.4, остается практически неизменным. Мы добавляем передачу пустой структуры

cmsgcred
при вызове
sendmsg
, которая заполняется ядром.

Перед запуском клиента определим свои личные данные командой

id
:

freebsd % id

uid=1007(andy) gid=1007(andy) groups=1007(andy), 0(wheel)

Если мы запустим сервер в одном окне, а клиент в другом, то для сервера после однократного выполнения клиента получим представленный ниже вывод.

freebsd % unixstrserv02

PID of sender = 26881

real user ID = 1007

real group ID = 1007

effective user ID = 1007

2 groups: 1007 0

Информация выводится только после отправки клиентом данных серверу. Мы видим, что сведения соответствуют тем, которые были получены командой

id
.

15.9. Резюме

Доменные сокеты Unix являются альтернативой IPC, когда клиент и сервер находятся на одном узле. Преимущество использования доменных сокетов Unix перед некоторой формой IPC состоит в том, что используемый API практически идентичен клиент-серверному сетевому соединению. Преимущество использования доменных сокетов Unix перед TCP, когда клиент и сервер находятся на одном узле, заключается в повышенной производительности доменных сокетов Unix относительно TCP во многих реализациях.

Мы изменили наш эхо-сервер и эхо-клиент TCP и UDP для использования доменных протоколов Unix, и единственным главным отличием оказалась необходимость при помощи функции

bind
связывать полное имя с клиентским сокетом UDP так, чтобы серверу UDP было куда отправлять ответы.

Передача дескрипторов между клиентами и серверами, находящимися на одном узле, — это мощная технология, которая используется при работе с доменными сокетами Unix. Мы показали пример передачи дескриптора от дочернего процесса обратно родительскому процессу в разделе 15.7. В разделе 28.7 мы покажем пример, в котором клиент и сервер не будут родственными, а в разделе 30.9 — другой пример, когда дескриптор передается от родительского процесса дочернему.

Упражнения

1. Что произойдет, если доменный сервер Unix вызовет функцию

unlink
после вызова функции
bind
?

2. Что произойдет, если доменный сервер Unix при завершении не отсоединит с помощью функции

unlink
свое известное полное имя, а клиент будет пытаться с помощью функции
connect
соединиться с сервером через некоторое время после того, как тот завершит работу?

3. Измените листинг 11.5 так, чтобы после того как будет выведен адрес протокола собеседника, вызывалась бы функция

sleep(5)
, а также чтобы вывести число байтов, возвращаемых функцией
read
всякий раз, когда она возвращает положительное значение. Измените листинг 11.8 так, чтобы для каждого байта результата, отправляемого клиенту, вызывалась функция
write
. (Мы обсуждаем подобные изменения в решении упражнения 1.5.) Запустите клиент и сервер на одном узле, используя TCP. Сколько байтов считывает клиент с помощью функции
read
?

Запустите клиент и сервер на одном узле, используя доменный сокет Unix. Изменилось ли что-нибудь?

Теперь для сервера вместо функции write вызовите функцию

send
и задайте флаг
MSG_EOR
(чтобы выполнить это упражнение, вам нужно использовать Беркли-реализацию). Запустите клиент и сервер на одном узле, используя доменный сокет Unix. Изменилось ли что-нибудь?

4. Напишите программу, определяющую значения, показанные в табл. 4.6. Один из подходов — создать потоковый канал и затем с помощью функции

fork
разветвить родительский и дочерний процессы. Родительский процесс входит в цикл
for
, увеличивая на каждом шаге значение
backlog
от 0 до 14. Каждый раз при прохождении цикла родительский процесс сначала записывает значение
backlog
в потоковый канал. Дочерний процесс читает это значение, создает прослушиваемый сокет, связанный с адресом закольцовки, и присваивает
backlog
считанное значение. Затем дочерний процесс делает запись в потоковый канал просто для того, чтобы сообщить родительскому процессу о своей готовности. Затем родительский процесс пытается установить как можно больше соединений, задав предварительно аргумент функции
alarm
равным 2 с, поскольку при достижении предельного значения
backlog
вызов функции connect заблокируется, и отправляет еще раз сегмент
SYN
. Дочерний процесс никогда не вызывает функцию
accept
, что позволяет ядру установить в очередь все соединения с родительским процессом. Когда истекает время ожидания родительского процесса (аргумент функции
alarm
, в данном случае 2 с), по счетчику цикла он может определить, какая по счету функция
connect
соответствует предельному значению
backlog
. Затем родительский процесс закрывает свои сокеты и пишет следующее новое значение в потоковый канал для дочернего процесса. Когда дочерний процесс считывает новое значение, он закрывает прежний прослушиваемый сокет и создает новый, заново начиная процедуру.

5. Проверьте, вызывает ли пропуск вызова функции

bind
в листинге 15.6 ошибку сервера.

Глава 16