Уильям Стивенс - UNIX: разработка сетевых приложений
//bcast/dgclibcast6.c
1 #include "unp.h"
2 static void recvfrom_alarm(int);
3 static int pipefd[2];
4 void
5 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
6 {
7 int n, maxfdp1;
8 const int on = 1;
9 char sendline[MAXLINE], recvline[MAXLINE + 1];
10 fd_set rset;
11 socklen_t len;
12 struct sockaddr *preply_addr;
13 preply_addr = Malloc(servlen);
14 Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
15 Pipe(pipefd);
16 maxfdp1 = max(sockfd, pipefd[0]) + 1;
17 FD_ZERO(&rset);
18 Signal(SIGALRM, recvfrom_alarm);
19 while (Fgets(sendline, MAXLINE, fp) != NULL) {
20 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
21 alarm(5);
22 for (;;) {
23 FD_SET(sockfd, &rset);
24 FD_SET(pipefd[0], &rset);
25 if ((n = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {
26 if (errno == EINTR)
27 continue;
28 else
29 err_sys("select error");
30 }
31 if (FD_ISSET(sockfd, &rset)) {
32 len = servlen;
33 n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr,
34 &len);
35 recvline[n] = 0; /* null terminate */
36 printf("from %s: %s",
37 Sock_ntop_host(preply_addr, len), recvline);
38 }
39 if (FD_ISSET(pipefd[0], &rset)) {
40 Read(pipefd[0], &n, 1); /* истекшее время */
41 break;
42 }
43 }
44 }
45 free(preply_addr);
46 }
47 static void
48 recvfrom_alarm(int signo)
49 {
50 Write(pipefd[1], "", 1); /* в канал пишется один нулевой байт */
51 return;
52 }
Создание канала15 Мы создаем обычный канал Unix. Возвращаются два дескриптора: pipefd[0] доступен для чтения, а pipefd[0] — для записи.
ПРИМЕЧАНИЕМы могли бы использовать функцию socketpair и получить двусторонний канал. В некоторых системах, особенно SVR4, обычный канал Unix всегда является двусторонним, и мы можем и читать, и записывать на любом конце этого канала.
Функция select на сокете и считывающем конце канала23-30 Мы вызываем функцию select и на сокете, и на считывающем конце канала.
47-52 Когда доставляется сигнал SIGALRM, наш обработчик сигналов записывает в канал 1 байт, в результате чего считывающий конец канала становится готовым для чтения. Наш обработчик сигнала также возвращает управление, возможно, прерывая функцию select. Следовательно, если функция select возвращает ошибку EINTR, мы игнорируем эту ошибку, зная, что считывающий конец канала также готов для чтения, что завершит цикл for.
Чтение из канала38-41 Когда считывающий конец канала готов для чтения, мы с помощью функции read считываем нулевой байт, записанный обработчиком сигнала, и игнорируем его. Но прибытие этого нулевого байта указывает нам на то, что истекло время таймера, и мы с помощью функции break выходим из бесконечного цикла for.
20.6. Резюме
При широковещательной передаче посылается дейтаграмма, которую получают все узлы. Недостатком широковещательной передачи является то, что каждый узел в подсети должен обрабатывать дейтаграмму, вплоть до уровня UDP в случае дейтаграммы UDP, даже если на узле не выполняется приложение-адресат. Для приложений с большими потоками данных, таких как аудио- и видео-приложения, это может привести к повышенной нагрузке на все узлы. В следующей главе мы увидим, что многоадресная передача решает эту проблему, поскольку позволяет не получать дейтаграмму узлам, не заинтересованным в этом.
Использование версии нашего эхо-клиента UDP, который отправляет серверу времени и даты широковещательные дейтаграммы и затем выводит все его ответы, полученные в течение 5 с, позволяет нам рассмотреть ситуацию гонок, возникающую при применении сигнала SIGALRM. Общим способом помещения тайм-аута в операцию чтения является использование функции alarm и сигнала SIGALRM, но он несет в себе неявную ошибку, типичную для сетевых приложений. Мы показали один некорректный и три корректных способа решения этой проблемы:
■ использование функции pselect,
■ использование функций sigsetjmp и siglongjmp,
■ использование средств IPC (обычно канала) между обработчиком сигнала и главным циклом.
Упражнения
1. Запустите клиент UDP, используя функцию dg_cli, выполняющую широковещательную передачу (см. листинг 20.1). Сколько ответов вы получаете? Всегда ли ответы приходят в одном и том же порядке? Синхронизированы ли часы у узлов в вашей подсети?
2. Поместите несколько функций printf в листинг 20.6 после завершения функции select, чтобы увидеть, возвращает ли она ошибку или указание на готовность к чтению одного из двух дескрипторов. Возвращает ли ваша система ошибку EINTR или сообщение о готовности канала к чтению, когда истекает время таймера alarm?
3. Запустите такую программу, как tcpdump, если это возможно, и просмотрите широковещательные пакеты в вашей локальной сети (команда tcpdump ether broadcast). К каким наборам протоколов относятся эти широковещательные пакеты?
Глава 21
Многоадресная передача
21.1. Введение
Как показано в табл. 20.1, адрес направленной передачи идентифицирует одиночный интерфейс, широковещательный адрес идентифицирует все интерфейсы в подсети, а адрес многоадресной передачи — набор (множество) интерфейсов. Направленная и широковещательная передача — это конечные точки спектра адресации (один интерфейс или все), а цель многоадресной передачи — обеспечить возможность адресации на участок спектра между этими конечными точками. Дейтаграмму многоадресной передачи должны получать только заинтересованные в ней интерфейсы, то есть интерфейсы на тех узлах, на которых запущены приложения, желающие принять участие в сеансе многоадресной передачи. Кроме того, широковещательная передача обычно ограничена локальными сетями, в то время как многоадресная передача может использоваться как в локальной, так и в глобальной сети. Существуют приложения, которые ежедневно участвуют в многоадресной передаче через всю сеть Интернет.
Дополнения к API сокетов, необходимые для поддержки многоадресной передачи, — это девять параметров сокетов. Три из них влияют на отправку дейтаграмм UDP на адрес, а шесть — на получение узлом дейтаграмм многоадресной передачи.
21.2. Адрес многоадресной передачи
При описании адресов многоадресной передачи необходимо провести различия между IPv4 и IPv6.
Адреса IPv4 класса D
Адреса класса D, лежащие в диапазоне от 224.0.0.0 до 239.255.255.255, в IPv4 являются адресами многоадресной передачи (см. табл. А.1). Младшие 28 бит адреса класса D образуют идентификатор группы многоадресной передачи (multicast group ID), а 32-разрядный адрес называется адресом группы (group address).
На рис. 21.1 показано, как адреса многоадресной передачи сопоставляются адресам Ethernet. Сопоставление адресов групп IPv4 для сетей Ethernet описывается в RFC 1112 [26], для сетей FDDI — в RFC 1390 [59], а для сетей типа Token Ring — в RFC 1469 [97]. Чтобы обеспечить возможность сравнения полученных в результате адресов Ethernet, мы также показываем сопоставление для адресов групп Ipv6.
Рис. 21.1. Сопоставление адресам Ethernet адресов многоадресной передачи IPv4 и IPv6
Если рассматривать лишь сопоставление адресов IPv4, то в 24 старших битах адреса Ethernet всегда будет 01:00:5е. Следующий бит всегда нулевой, а 23 младших бита копируются из 23 младших битов группового адреса. Старшие 5 бит группового адреса при сопоставлении игнорируются. Это значит, что 32 групповых адреса сопоставляются одиночному адресу Ethernet, то есть соответствие не является взаимнооднозначным.
Младшие 2 бита первого байта адреса Ethernet идентифицируют адрес как универсально управляемый групповой адрес. «Универсально управляемый» означает то, что 24 старших бита были присвоены IEEE (Institute of Electrical and Electronics Engineers — Институт инженеров по электротехнике и электронике), а групповые адреса многоадресной передачи распознаются и обрабатываются получающими интерфейсами специальным образом.
Существует несколько специальных адресов многоадресной передачи IPv4:
■ 224.0.0.1 — это группа всех узлов (all-hosts group). Все узлы в подсети, имеющие возможность многоадресной передачи, должны присоединиться к этой группе интерфейсами, поддерживающими многоадресную передачу. (Мы поговорим о том, что значит присоединиться к группе, несколько позже.)