Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 29 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]
SCTP предлагает разработчику приложений два вида интерфейсов: «один-к-одному», облегчающий миграцию существующих TCP-приложений на SCTP, и «один-ко-многим», реализующий все новые возможности SCTP. Функция sctp_peeloff
позволяет выделять ассоциации из множественных сокетов в одиночные. Кроме того, SCTP предоставляет множество уведомлений о событиях транспортного уровня, на которые приложение при необходимости может подписываться. События помогают приложению управлять ассоциациями, с которыми оно работает.
Поскольку протокол SCTP ориентирован на многоинтерфейсные узлы, не все стандартные функции сокетов, рассмотренные в главе 4, оказываются эффективны при работе с ним. Функции sctp_bindx
, sctp_connectx
, sctp_getladdrs
и sctp_getpaddrs
позволяют управлять адресами и ассоциациями. Функции sctp_sendmsg
и sctp_recvmsg
упрощают использование расширенных возможностей SCTP. В главах 10 и 23 мы приведем примеры, наглядно демонстрирующие рассмотренные в этой главе новые концепции.
1. В какой ситуации разработчик приложения скорее всего воспользуется функцией sctp_peeloff
?
2. Говоря о сокетах типа «один-ко-многим», мы утверждаем, что на стороне сервера также происходит автоматическое закрытие. Почему это верно?
3. Почему передача пользовательских данных в третьем пакете рукопожатия возможна только для сокетов типа «один-ко-многим»? (Подсказка: нужно иметь возможность отправлять данные во время установки ассоциации.)
4. В какой ситуации пользовательские данные могут быть переданы в третьем и четвертом пакетах четырехэтапного рукопожатия?
5. В разделе 9.7 говорится о том, что набор локальных адресов может быть подмножеством связанных адресов. В какой ситуации это возможно?
Глава 10
Пример SCTP-соединения клиент-сервер
10.1. ВведениеВоспользуемся некоторыми элементарными функциями из глав 4 и 9 для написания полнофункционального приложения SCTP с архитектурой клиент-сервер типа «один-ко-многим». Сервер из нашего примера будет аналогичен эхо-серверу из главы 5. Приложение будет функционировать следующим образом:
1. Клиент считывает строку текста из стандартного потока ввода и отсылает ее серверу. Строка имеет формат [#]text
, где номер в скобках обозначает номер потока SCTP, по которому должно быть отправлено это текстовое сообщение.
2. Сервер принимает текстовое сообщение из сети, увеличивает номер потока, по которому было получено сообщение, на единицу и отправляет сообщение обратно клиенту через поток с новым номером.
3. Клиент считывает полученную строку и выводит ее в стандартный поток вывода, добавляя к ней номер потока и порядковый номер для данного потока.
Наше приложение вместе с функциями, используемыми для операций ввода и вывода, изображено на рис. 10.1.
Рис. 10.1. Простое потоковое приложение SCTP с архитектурой клиент-сервер
Две стрелки между клиентом и сервером обозначают два однонаправленных потока (ассоциация в целом является полностью двусторонней). Функции fgets
и fputs
входят в стандартную библиотеку ввода-вывода. Мы не пользуемся функциями writen
и readline
из раздела 3.9, потому что в них нет необходимости. Вместо них мы вызываем sctp_sendmsg
и sctp_recvmsg
из разделов 9.9 и 9.10 соответственно.
Сервер в нашем примере будет относиться к типу «один-ко-многим». Этот вариант был выбран нами по одной важной причине. Примеры из главы 5 могут быть переделаны под SCTP внесением крайне незначительных изменений: достаточно изменить вызов socket, указав в качестве третьего аргумента IPPROTO_SCTP
вместо IPPROTO_TCP
. Однако приложение, полученное таким образом, не использовало бы дополнительные возможности, предоставляемые SCTP, за исключением поддержки многоинтерфейсных узлов. Написав сервер типа «один-ко-многим», мы смогли показать все достоинства SCTP.
Наши клиент и сервер SCTP вызывают функции в последовательности, представленной на рис. 9.2. Код последовательного сервера представлен в листинге 10.1 [1]1
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http://www.piter.com.
[Закрыть].
Листинг 10.1. Потоковый эхо-сервер SCTP
//sctp/sctpserv01.c
1 #include "unp.h"
2 int
3 main(int argc, char **argv)
4 {
5 int sock_fd, msg_flags;
6 char readbuf[BUFFSIZE];
7 struct sockaddr_in servaddr, cliaddr;
8 struct sctp_sndrcvinfo sri;
9 struct sctp_event_subscribe evnts;
10 int stream_increment=1;
11 socklen_t len;
12 size_t rd_sz;
13 if (argc == 2)
14 stream_increment = atoi(argv[1]);
15 sock_fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
16 bzero(&servaddr, sizeof(servaddr));
17 servaddr.sin_family = AF_INET;
18 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
19 servaddr.sin_port = htons(SERV_PORT);
20 Bind(sock_fd, (SA*)&servaddr, sizeof(servaddr));
21 bzero(&evnts, sizeof(evnts));
22 evnts.sctp_data_io_event = 1;
23 Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));
24 Listen(sock_fd, LISTENQ);
25 for (;;) {
26 len = sizeof(struct sockaddr_in);
27 rd_sz = Sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf),
28 (SA*)&cliaddr, &len, &sri, &msg_flags);
29 if (stream_increment) {
30 sri.sinfo_stream++;
31 if (sri.sinfo_stream >=
32 sctp_get_no_strms(sock_fd, (SA*)&cliaddr, len))
33 sri.sinfo_stream = 0;
34 }
35 Sctp_sendmsg(sock_fd, readbuf, rd_sz,
36 (SA*)&cliaddr, len,
37 sri.sinfo_ppid,
38 sri.sinfo_flags, sri.sinfo_stream, 0, 0);
39 }
40 }
Настройка приращения номера потока
13-14
По умолчанию наш сервер отвечает клиенту через поток, номер которого на единицу больше номера потока, по которому было получено сообщение. Если приложению в строке вызова передается целочисленный аргумент, он интерпретируется как значение флага stream_increment
, с помощью которого приращение номера потока можно отключить. Мы воспользуемся этим параметром командной строки, когда будем говорить о блокировании в разделе 10.5.
Создание сокета SCTP
15
Создается сокет SCTP типа «один-ко-многим».
Связывание с адресом
16-20
Структура адреса сокета Интернета заполняется универсальным адресом ( INADDR_ANY
) и номером заранее известного порта сервера SERV_PORT
. Связывание с универсальным адресом означает, что конечная точка SCTP будет использовать все доступные локальные адреса для всех создаваемых ассоциаций. Для многоинтерфейсных узлов это означает, что удаленная конечная точка сможет устанавливать ассоциации и передавать пакеты на любой локальный интерфейс. Выбор номера порта SCTP основывался на рис. 2.10. Обратите внимание, что ход рассуждений для сервера тот же, что и в одном из предшествовавших примеров в разделе 5.2.
Подписка на уведомления
21-23
Сервер изменяет параметры подписки на уведомления для сокета SCTP. Сервер подписывается только на событие sctp_data_io_event
, что позволяет ему получать структуру sctp_sndrcvinfo
. По ее содержимому сервер сможет определять номер потока полученного сообщения.
Разрешение установки входящих ассоциаций
24
Сервер разрешает устанавливать входящие ассоциации, вызывая функцию listen
. Затем управление передается главному циклу.
Ожидание сообщения
26-28
Сервер инициализирует размер структуры адреса сокета клиента, после чего блокируется в ожидании сообщения от какого-либо удаленного собеседника.
Увеличение номера потока
29-34
Сервер проверяет состояние флага stream_increment
и определяет, нужно ли увеличивать номер потока. Если флаг установлен (никакие аргументы в командной строке не передавались), сервер увеличивает номер потока, по которому было получено сообщение, на единицу. Если полученное число достигает предельного количества потоков (получаемого вызовом sctp_get_no_strms
), сервер сбрасывает номер потока в 0. Функция sctp_get_no_strms
в листинге не приведена. Она использует параметр SCTP_STATUS
(см. раздел 7.10) для определения согласованного количества потоков.
Отправка ответа
35-38
Сервер отсылает сообщения, используя идентификатор протокола, флаги и номер потока (который, возможно, был увеличен), хранящиеся в структуре sri
.
Заметьте, что нашему серверу не нужны уведомления об установке ассоциаций, поэтому он отключает все события, которые привели бы к передаче сообщений в буфер сокета. Сервер полагается на сведения из структуры sctp_sndrcvinfo
, а обратный адрес берет из переменной cliaddr
. Этого оказывается достаточно для отправки эхо-ответа собеседнику через установленную им ассоциацию.
Программа работает до тех пор, пока пользователь не завершит ее передачей сигнала.
10.3. Потоковый эхо-клиент SCTP типа «один-ко-многим»: функция mainВ листинге 10.2 приведена функция main
нашего клиента SCTP.
Листинг 10.2. Потоковый эхо-клиент SCTP
//sctp/sctpclient01.c
1 #include "unp.h"
2 int
3 main(int argc, char **argv)
4 {
5 int sock_fd;
6 struct sockaddr_in servaddr;
7 struct sctp_event_subscribe evnts;
8 int echo_to_all=0;
9 if (argc < 2)
10 err_quit("Missing host argument – use '%s host [echo]'n", argv[0]);
11 if (argc > 2) {
12 printf("Echoing messages to all streamsn");
13 echo_to_all = 1;
14 }
15 sock_fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
16 bzero(&servaddr, sizeof(servaddr));
17 servaddr.sin_family = AF_INET;
18 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
19 servaddr.sin_port = htons(SERV_PORT);
20 Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
21 bzero(&evnts, sizeof(evnts));
22 evnts.sctp_data_io_event = 1;
23 Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));
24 if (echo_to_all == 0)
25 sctpstr_cli(stdin, sock_fd, (SA*)&servaddr, sizeof(servaddr));
26 else
27 sctpstr_cli_echoall(stdin, sock_fd, (SA*)&servaddr,
28 sizeof(servaddr));
29 Close(sock_fd);
30 return(0);
31 }
Проверка аргументов и создание сокета
9-15
Клиент проверяет переданные ему при запуске аргументы командной строки. Сначала проверяется, указан ли в строке IP-адрес узла, на который нужно отправлять сообщения. Затем проверяется, указан ли параметр отправки эхо-сообщений всем (мы воспользуемся им в разделе 10.5). Наконец, клиент создает сокет SCTP типа «один-ко-многим».
Подготовка адреса сервера
16-20
Клиент преобразует IP-адрес сервера, переданный ему в командной строке, с помощью функции inet_pton
. К адресу он добавляет заранее известный номер порта сервера. Полученная структура используется для всех обращений к данному серверу.
Подписка на уведомления
21-23
Клиент явно указывает, какие именно уведомления он хочет получать от созданного сокета SCTP. События MSG_NOTIFICATION
ему не нужны, поэтому он отключает их, оставляя лишь структуру sctp_sndrcvinfo
.
Вызов функции обработки сообщений
24-28
Если флаг echo_to_all
не установлен, клиент вызывает функцию sctpstr_cli
, которая будет обсуждаться в разделе 10.4. В противном случае вызывается sctpstr_cli_echoall
(раздел 10.5, где рассматривается применение потоков SCTP).
Завершение работы
29-31
Закончив работу с сообщениями, клиент закрывает сокет SCTP, что приводит к закрытию всех ассоциаций, использующих этот сокет. Затем функция main
завершается и возвращает код 0 – никаких ошибок не произошло.
В листинге 10.3 приведена основная функция эхо-клиента SCTP.
Листинг 10.3. Функция sctp_strcli
//sctp/sctp_strcli.c
1 #include "unp.h"
2 void
3 sctpstr_cli(FILE *fp, int sock_fd, struct sockaddr *to, socklen_t tolen)
4 {
5 struct sockaddr_in peeraddr;
6 struct sctp_sndrcvinfo sri;
7 char sendline[MAXLINE], recvline[MAXLINE];
8 socklen_t len;
9 int out_sz, rd_sz;
10 int msg_flags;
11 bzero(&sri, sizeof(sri));
12 while (fgets(sendline, MAXLINE, fp) != NULL) {
13 if (sendline[0] != '[') {
14 printf("Error, line must be of the form '[streamnum]text'n");
15 continue;
16 }
17 sri.sinfo_stream = strtol(&sendline[1], NULL, 0);
18 out_sz = strlen(sendline);
19 Sctp_sendmsg(sock_fd, sendline, out_sz,
20 to, tolen, 0, 0, sri.sinfo_stream, 0, 0);
21 len = sizeof(peeraddr);
22 rd_sz = Sctp_recvmsg(sock_fd, recvline, sizeof(recvline),
23 (SA*)&peeraddr, &len, &sri, &msg_flags);
24 printf("From str:%d seq:%d (assoc:0x%x):",
25 sri.sinfo_stream.sri.sinfo_ssn, (u_int)sri.sinfo_assoc_id);
26 printf("%*s", rd_sz.recvline);
27 }
28 }
Инициализация структуры sri и вход в цикл
11-12
Основная функция клиента начинает работу с очистки структуры sctp_sndrcvinfo
(переменная sri
). Затем функция входит в цикл, считывающий из дескриптора fp
, переданного вызывающей функцией, при помощи блокирующего вызова fgets
. Главная программа ( main
) передает этой функции stdin
в качестве аргумента fp
, поэтому функция считывает и обрабатывает пользовательский ввод до тех пор, пока пользователь не введет завершающий EOF (Ctrl+D). При этом функция завершается и управление передается вызвавшей функции.
Проверка ввода
13-16
Клиент проверяет введенный пользователем текст на соответствие шаблону [#]текст
. Если формат строки нарушен, клиент выводит сообщение об ошибке и снова вызывает fgets
.
Преобразование номера потока
17
Клиент записывает запрошенный пользователем номер потока из текстовой строки в поле sinfo_stream
структуры sri
.
Отправка сообщения
18-20
После инициализации длины структуры адреса и размера пользовательских данных клиент отсылает сообщение серверу при помощи функции sctp_sendmsg
.
Блокирование в ожидании ответа
21-23
Клиент блокируется и ожидает получения эхо-ответа сервера.
Отображение полученного эхо-ответа
24-26
Клиент выводит на экран полученное от сервера сообщение, вместе с номером потока и последовательным номером сообщения в этом потоке. После этого клиент возвращается на начало цикла, ожидая, что пользователь введет следующую строку.
Мы запустили эхо-сервер SCTP без аргументов командной строки на компьютере, работающем под управлением FreeBSD. Клиенту при запуске необходимо указать IP-адрес сервера.
freebsd4% sctpclient01 10.1.1.5
[0]Hello Отправка сообщения по потоку 0
From str:1 seq:0 (assoc:0xc99e15a0):[0]Hello Эхо-ответ сервера в потоке 1
[4]Message two Отправка сообщения по потоку 4
From str:5 seq:0 (assoc.0xc99e15a0):[4]Message two Эхо-ответ сервера
в потоке 5
[4]Message three Отправка сообщения по потоку 4
From str:5 seq:1 (assoc 0xc99e15a0):[4]Message three Эхо-ответ сервера
в потоке 5
^D Ввод символа EOF
freebsd4%
Обратите внимание, что клиент отправляет сообщения по потокам 0 и 4, а сервер отвечает ему по потокам 1 и 5. Именно такое поведение и ожидается в том случае, когда наш сервер запускается без аргументов командной строки. Заметьте также, что порядковый номер сообщения по пятому потоку увеличился на единицу при приеме третьего сообщения, как и должно было произойти.
10.5. Блокирование очередиНаш сервер позволяет отправлять текстовые сообщения по любому из нескольких потоков. Поток SCTP – это вовсе не поток байтов, как в TCP. Это последовательность сообщений, упорядоченных в пределах ассоциации. Потоки с собственным порядком используются для того, чтобы обойти блокирование очереди( head-of-line blocking), которое может возникать в TCP.
Блокирование возникает при потере сегмента TCP при передаче и приходе следующего за ним сегмента, который удерживается до тех пор, пока утраченный сегмент не будет передан повторно и получен адресатом. Задержка доставки последующих сегментов гарантирует, что приложение получит данные в том порядке, в котором они были отправлены. Это совершенно необходимая функция, которая, к сожалению, обладает определенными недостатками. Представьте, что семантически независимые сообщения передаются по одному соединению TCP. Например, веб-сервер может передать браузеру три картинки для отображения на экране. Чтобы картинки выводились на экран одновременно, сервер передает сначала часть первого изображения, затем часть второго и часть третьего. Процесс повторяется до тех пор, пока все три картинки не будут переданы клиенту целиком. Что произойдет, если потеряется сегмент TCP, относящийся к первому изображению? Клиент не получит никаких данных до тех пор, пока недостающий сегмент не будет передан повторно и доставлен ему. Задержаны будут все три изображения, хотя сегмент относился только к одному из них (первому). Эту ситуацию иллюстрирует рис. 10.2.
Рис. 10.2. Отправка трех изображений по одному TCP-соединению
ПРИМЕЧАНИЕ
Хотя HTTP работает иначе, были предложены расширения этого протокола, такие как SCP [108] и SMUX [33], которые обеспечивают описанную функциональность поверх TCP. Эти протоколы мультиплексирования позволяют избежать проблем, связанных с параллельными TCP-соединениями, не имеющими общей информации о состоянии [123]. Несмотря на то что создание одного TCP-соединения для каждого изображения (как обычно и делают клиенты HTTP) позволяет избежать блокирования, каждому соединению приходится тратить время на определение времени обращения и доступной пропускной способности. Потеря сегмента, относящегося к одному соединению (признак затора на линии) не обязательно приводит к замедлению передачи по остальным соединениям. В результате совокупное использование загруженных сетей падает.
Для приложения было бы лучше, если бы транспортный протокол вел себя иначе. В идеале задерживаться должны только сегменты первой картинки, тогда как сегменты второй и третьей должны доставляться так, как если бы сегмент первой картинки не был утерян вовсе.
Многопоточный режим SCTP позволяет свести к минимуму блокирование очереди. На рис. 10.3 мы показываем процесс отправки тех же трех изображений. На этот раз сервер использует потоки, так что блокируется только одно изображение, а второе и третье доставляются без помех. Первое изображение не доставляется до тех пор, пока не будет восстановлен порядок сегментов.
Рис. 10.3. Отправка трех изображений по потокам SCTP
Теперь мы можем привести полный код нашего клиента (с функцией sctpstr_cli_echoall
, листинг 10.4), чтобы на его примере продемонстрировать устранение проблем с блокированием очереди при помощи SCTP. Новая функция аналогична sctpstr_cli
за тем исключением, что клиент больше не требует указания номера потока в квадратных скобках в каждом сообщении. Функция передает сообщение пользователя по всем потокам, количество которых определяется константой SERV_MAX_SCTP_STRM
. После отправки сообщения клиент ждет прихода всех ответных сообщений сервера. Запуская сервер, мы передаем ему аргумент командной строки, указывающий на то, что сервер должен отвечать на сообщения по тем же потокам, по которым они приходят. Это позволяет пользователю отслеживать ответы и порядок их прибытия.
Листинг 10.4. Функция sctp_strcliecho
1 #include "unp.h"
2 #define SCTP_MAXLINE 800
3 void
4 sctpstr_cli_echoall(FILE *fp, int sock_fd, struct sockaddr to,
5 socklen_t tolen)
6 {
7 struct sockaddr_in peeraddr;
8 struct sctp_sndrcvinfo sri;
9 char sendline[SCTP_MAXLlNE], recvline[SCTP_MAXLINE];
10 socklen_t len;
11 int rd_sz, i, strsz;
12 int msg_flags;
13 bzero(sendline, sizeof(sendline));
14 bzero(&sri, sizeof(sri));
15 while (fgets(sendline, SCTP_MAXLINE – 9, fp) != NULL) {
16 strsz = strlen(sendline);
17 if (sendline[strsz-1] == 'n') {
18 sendline[strsz-1] = ' ';
19 strsz–;
20 }
21 for (i=0; i
22 snprintf(sendline + strsz, sizeof(sendline) – strsz,
23 ".msg %d", i);
24 Sctp_sendmsg(sock_fd, sendline, sizeof(sendline),
25 to, tolen, 0, 0, i, 0, 0);
26 }
27 for (i =0; i < SERV_MAX_SCTP_STRM; i++) {
28 len = sizeof(peeraddr);
29 rd_sz = Sctp_recvmsg(sock_fd, recvline, sizeof(recvline),
30 (SA*)&peeraddr, &len, &sri, &msg_flags);
31 printf("From str:%d seq:%d (assoc:0x%x)",
32 sri.sinfo_stream, sri.sinfo_ssn,
33 (u_int)sri, sinfo_assoc_id);
34 printf("%.*sn", rd_sz, recvline);
35 }
36 }
37 }
Инициализация структур данных и ожидание ввода
13-15
Как и в предыдущем примере, клиент инициализирует структуру sri
, предназначенную для настройки потока, с которым клиент будет работать. Кроме того, клиент обнуляет буфер данных, из которого считывается пользовательский ввод. Затем программа входит в основной цикл, блокируясь в вызове fgets
.
Предварительная обработка сообщения
16-20
Клиент определяет размер сообщения и удаляет символ перевода строки, если таковой находится в конце буфера.
Отправка сообщения по всем потокам
21-26
Клиент отсылает сообщение с помощью функции sctp_sendmsg
. Передается все содержимое буфера длиной SCTP_MAXLINE
. Перед отправкой сообщения к нему добавляется строка .msg
, и номер потока, чтобы мы могли впоследствии определить порядок получения сообщений и сравнить его с порядком отправки сообщений. Обратите внимание, что клиент отправляет сообщения по заданному количеству потоков, не проверяя, сколько потоков было согласовано с сервером. Может получиться так, что некоторые операции отправки сообщений завершатся с ошибкой, если количество потоков будет снижено по запросу собеседника.
ПРИМЕЧАНИЕ
Этот код может работать неправильно, если окна приема и отправки будут слишком малы. Если окно приема собеседника слишком мало, клиент может заблокироваться. Поскольку клиент не переходит к считыванию данных из приемного буфера, пока он не отправит все сообщения, сервер также может заблокироваться в ожидании освобождения буфера клиента. В результате обе конечные точки SCTP зависнут. Приведенная программа не рассчитана на масштабирование. Она предназначена лишь для иллюстрации потоков и блокирования очередей в простейшем варианте.
Считывание и отображение эхо-ответа
27-35
Клиент блокируется в цикле считывания всех ответных сообщений сервера, которые отображаются на экране по мере поступления. После считывания последнего сообщения сервера клиент возвращается к обработке ввода пользователя.