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

Пример 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.

10.2. Потоковый эхо-сервер SCTP типа «один-ко-многим»: функция main

Наши клиент и сервер SCTP вызывают функции в последовательности, представленной на рис. 9.2. Код последовательного сервера представлен в листинге 10.1[1].

Листинг 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 streams\n");

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.4. Потоковый эхо-клиент SCTP: функция str_cli

В листинге 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] = '\0';

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("%.*s\n", rd_sz, recvline);

35   }

36  }

37 }

Инициализация структур данных и ожидание ввода

13-15
 Как и в предыдущем примере, клиент инициализирует структуру
sri
, предназначенную для настройки потока, с которым клиент будет работать. Кроме того, клиент обнуляет буфер данных, из которого считывается пользовательский ввод. Затем программа входит в основной цикл, блокируясь в вызове
fgets
.

Предварительная обработка сообщения

16-20
 Клиент определяет размер сообщения и удаляет символ перевода строки, если таковой находится в конце буфера.

Отправка сообщения по всем потокам

21-26
 Клиент отсылает сообщение с помощью функции
sctp_sendmsg
. Передается все содержимое буфера длиной
SCTP_MAXLINE
. Перед отправкой сообщения к нему добавляется строка
.msg
, и номер потока, чтобы мы могли впоследствии определить порядок получения сообщений и сравнить его с порядком отправки сообщений. Обратите внимание, что клиент отправляет сообщения по заданному количеству потоков, не проверяя, сколько потоков было согласовано с сервером. Может получиться так, что некоторые операции отправки сообщений завершатся с ошибкой, если количество потоков будет снижено по запросу собеседника.

ПРИМЕЧАНИЕ

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

Считывание и отображение эхо-ответа

27-35
 Клиент блокируется в цикле считывания всех ответных сообщений сервера, которые отображаются на экране по мере поступления. После считывания последнего сообщения сервера клиент возвращается к обработке ввода пользователя.

Запуск программы

Мы запустили клиент и сервер на разных компьютерах с FreeBSD, между которыми был установлен настраиваемый маршрутизатор (рис. 10.4). Маршрутизатор может создавать задержку и сбрасывать часть пакетов. Сначала мы запускаем программу без сброса пакетов на маршрутизаторе.

Рис. 10.4. Тестовая конфигурация сети

Мы запускаем сервер с аргументом 0 в командной строке, благодаря чему сервер не увеличивает номер потока при отправке эхо-ответа.

Затем мы запускаем клиент, передавая ему в командной строке адрес эхо-сервера и дополнительный аргумент, указывающий на необходимость отправки сообщений по всем потокам одновременно.

freebsd4% sctpclient01 10.1.4.1 echo

Echoing messages to all streams

Hello

From str:0 seq:0 (assoc:0xc99e15a0):Hello.msg.0

From str:1 seq:0 (assoc.0xc99e15a0):Hello.msg.1

From str:2 seq:0 (assoc:0xc99e15a0):Hello.msg.2

From str:3 seq:0 (assoc 0xc99e15a0):Hello.msg.3

From str:4 seq:0 (assoc.0xc99e15a0):Hello.msg.4

From str:5 seq:0 (assoc:0xc99e15a0):Hello.msg.5

From str:6 seq:0 (assoc.0xc99e15a0):Hello.msg.6

From str:7 seq:0 (assoc:0xc99e15a0):Hello.msg.7

From str:8 seq:0 (assoc:0xc99e15a0):Hello.msg.8

From str:9 seq:0 (assoc:0xc99e15a0).Hello.msg.9

^D

freebsd4%

В отсутствие потерь при передаче клиент получает ответы сервера в том же порядке, в котором отправляет запросы. Изменим параметры маршрутизатора таким образом, чтобы терять 10% всех пакетов, передаваемых в обоих направлениях, и перезапустим клиент.

freebsd4% sctpclient01 10.1.4.1 echo

Echoing messages to all streams

Hello

From str:0 seq:0 (assoc:0xc99e15a0):Hello.msg.0

From str:2 seq:0 (assoc:0xc99e15a0):Hello.msg.2

From str:3 seq:0 (assoc:0xc99e15a0):Hello.msg.3

From str:5 seq:0 (assoc:0xc99e15a0):Hello.msg.5

From str:1 seq:0 (assoc:0xc99e15a0):Hello.msg.1

From str:8 seq:0 (assoc:0xc99e15a0):Hello.msg.8

From str:4 seq:0 (assoc:0xc99e15a0):Hello.msg.4

From str:7 seq:0 (assoc:0xc99e15a0):Hello.msg.7

From str:9 seq:0 (assoc:0xc99e15a0):Hello.msg.9

From str:6 seq:0 (assoc:0xc99e15a0):Hello msg.6

^D

freebsd4%

Можно проверить, действительно ли сообщения в каждом из потоков доставляются в правильном порядке, если изменить клиента так, чтобы он отправлял по два сообщения в поток. Кроме того, мы добавим к сообщению суффикс с его номером, чтобы отличать эхо-ответы друг от друга. Измененная функция клиента представлена в листинге 10.5.

Листинг 10.5. Изменения в функции sctp_strcliecho

//sctp/sctp_strcliecho2.c

21 for (i =0; i < SERV_MAX_SCTP_STRM; i++) {

22  snprintf(sendline + strsz, sizeof(sendline) - strsz,

23   ".msg.%d 1", i);

24  Sctp_sendmsg(sock_fd, sendline, sizeof(sendline),

25   to, tolen, 0, 0, i, 0, 0);

26  snprintf(sendline + strsz, sizeof(sendline) - strsz,

27   ".msg.%d 2", i);

28  Sctp_sendmsg(sock_fd, sendline, sizeof(sendline),

29   to, tolen, 0, 0, i, 0, 0);

30 }

31 for (i = 0; i < SERV_MAX_SCTP_STRM*2, i++) {

32  len = sizeof(peeraddr);

Первое сообщение: добавление номера и отправка

22-25
 Клиент добавляет к первому сообщению его номер, с помощью которого мы сможем отслеживать отправленные сообщения. Затем сообщение отсылается вызовом
sctp_sendmsg
.

Второе сообщение: добавление номера и отправка

26-29
 Номер сообщения изменяется с единицы на двойку, после чего сообщение отсылается по тому же потоку.

Считывание и отображение эхо-ответа

31
 Здесь требуется лишь одно незначительное изменение: количество ожидаемых ответов эхо-сервера должно быть удвоено.

Запуск измененной программы

Запустив сервер и измененный клиент, мы получаем следующий результат:

freebsd4% sctpclient01 10.1.4.1 echo

Echoing messages to all streams

Hello

From str:0 seq:0 (assoc:0xc99e15a0):Hello.msg.0 1

From str:0 seq:1 (assoc:0xc99e15a0):Hello.msg.0 2

From str:1 seq:0 (assoc:0xc99e15a0):Hello.msg.1 1

From str:4 seq:0 (assoc:0xc99e15a0):Hello.msg.4 1

From str:5 seq:0 (assoc:0xc99e15a0):Hello.msg.5 1

From str:7 seq:0 (assoc:0xc99e15a0):Hello.msg.7 1

From str:8 seq:0 (assoc:0xc99e15a0):Hello.msg.8 1

From str:9 seq:0 (assoc:0xc99e15a0):Hello.msg.9 1

From str:3 seq:0 (assoc:0xc99e15a0):Hello.msg.3 1

From str:3 seq:0 (assoc:0xc99e15a0):Hello.msg.3 2

From str:1 seq:0 (assoc:0xc99e15a0):Hello.msg.1 2

From str:5 seq:0 (assoc:0xc99e15a0):Hello.msg.5 2

From str:2 seq:0 (assoc:0xc99e15a0):Hello.msg.2 1

From str:6 seq:0 (assoc:0xc99e15a0):Hello.msg.6 1

From str:6 seq:0 (assoc:0xc99e15a0):Hello.msg.6 2

From str:2 seq:0 (assoc:0xc99e15a0):Hello.msg.2 2

From str:7 seq:0 (assoc:0xc99e15a0):Hello.msg.7 2

From str:8 seq:0 (assoc:0xc99e15a0):Hello.msg.8 2

From str:9 seq:0 (assoc:0xc99e15a0):Hello.msg.9 2

From str:4 seq:0 (assoc:0xc99e15a0):Hello.msg.4 2

^D

freebsd4%

Как видно из вывода, сообщения действительно теряются, но при этом задерживаются только те, которые относятся к конкретному потоку. Данные в остальных потоках не задерживаются. Потоки SCTP могут послужить мощным средством борьбы с блокированием очереди, позволяющим в то же время сохранять порядок данных в рамках конкретного набора сообщений.

10.6. Управление количеством потоков

Мы рассмотрели пример использования потоков SCTP, но пока что мы не знаем, каким образом можно контролировать количество потоков, запрашиваемых конечной точкой в процессе инициализации ассоциации. В предыдущих примерах мы работали с тем количеством исходящих потоков, которое было установлено в системе по умолчанию. В реализации SCTP для FreeBSD, созданной в рамках проекта KAME, это значение равно 10. А что, если серверу и клиенту нужно больше десяти потоков? В листинге 10.6 мы приводим модификацию кода сервера, позволяющую увеличивать количество потоков, запрашиваемое при создании ассоциации. Обратите внимание, что данный параметр сокета должен быть изменен до создания ассоциации.

Листинг 10.6. Вариант сервера, допускающий увеличение числа потоков

//sctp/sctpserv02.c

14 if (argc 2)

15  stream_increment = atoi(argv[1]);

16 sock_fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);

17 bzero(&initm, sizeof(initm));

18 initm.sinit_num_ostreams = SERV_MORE_STRMS_SCTP;

19 Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_INITMSG, &initm, sizeof(initm));

Предварительная настройка

14-16
 Как и в предыдущей версии программы, сервер устанавливает флаг
stream_increment
в соответствии с дополнительным параметром командной строки, после чего открывает сокет.

Изменение запрашиваемого количества потоков

17-19
 Все сделанные модификации относятся именно к этим строкам. Сначала сервер обнуляет структуру
sctp_initmsg
. Это изменение гарантирует, что вызов
setsockopt
не приведет к непреднамеренному изменению каких-либо иных значений кроме того, которое нас интересует. Затем сервер устанавливает поле
sinit_max_ostreams
равным количеству запрашиваемых потоков. После этого вызывается функция
setsockopt
с параметром сокета
SCTP_INITMSG
для установки параметров сообщения INIT.

Альтернативой установке параметра сокета может быть вызов функции

sendmsg
со вспомогательными данными, запрашивающими требуемое количество потоков. Передача вспомогательных данных приведет к желаемому результату только для сокетов типа «один-ко-многим».

10.7. Управление завершением соединения

В наших примерах на клиента была возложена ответственность по завершению ассоциации, для чего ему приходилось закрывать сокет. Но закрытие сокета не всегда является желаемой операцией с точки зрения приложения. Кроме того, серверу не нужно оставлять ассоциацию открытой после отправки эхо-ответа. В описанных ситуациях применяются альтернативные механизмы завершения ассоциации. Для сокетов типа «один-ко-многим» доступно два метода: корректное и аварийное закрытие.

Если сервер хочет закрыть ассоциацию после отправки сообщения, он должен добавить флаг MSG_EOF в это сообщение, поместив его в поле

sinfo_flags
структуры
sctp_sndrcvinfo
. Этот флаг закрывает ассоциацию после подтверждения приема отсылаемого сообщения. Альтернативный метод состоит в установке флага
MSG_ABORT
в том же поле
sinfo_flags
. При этом происходит немедленное закрытие ассоциации с отправкой порции ABORT (аналог TCP-сегмента RST). Данные, находящиеся в буфере отправки, сбрасываются. Однако закрытие сеанса SCTP порцией ABORT не приводит к негативным последствиям типа пропущенного состояния TIME_WAIT, как это происходит в TCP. В листинге 10.7 показана новая версия эхо-сервера, инициирующая корректное завершение соединения одновременно с отправкой эхо-ответа клиенту. В листинге 10.8 показана версия клиента, отправляющая порцию ABORT перед закрытием сокета.

Листинг 10.7. Сервер, закрывающий ассоциацию после отправки ответа

//sctp/sctpserv03.c

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 | MSG_EOF), sri.sinfo_stream, 0, 0);

39 }

Отправка ответа с закрытием ассоциации

38
 Изменение кода сервера состоит в том, что мы добавляем флаг
MSG_EOF
к прочим флагам в вызове
sctp_sendmsg
операцией логического ИЛИ. Благодаря этому сервер закрывает ассоциацию после подтверждения доставки сообщения.

Листинг 10.8. Клиент, выполняющий аварийное закрытие ассоциации

//sctp/sctpclient02.c

25 if (echo_to_all == 0)

26  sctpstr_cli(stdin, sock_fd, (SA*)&servaddr, sizeof(servaddr));

27 else

28  sctpstr_cli_echoall(stdin, sock_fd, (SA*)&servaddr,

29   sizeof(servaddr));

30 strcpy(byemsg, "goodbye");

31 Sctp_sendmsg(sock_fd, byemsg, strlen(byemsg),

32  (SA*)&servaddr, sizeof(servaddr), 0, MSG_ABORT, 0, 0, 0);

33 Close(sock_fd);

Аварийное закрытие ассоциации

30-32
 Клиент подготавливает сообщение об аварийном закрытии ассоциации, вызванном пользовательской ошибкой. Затем функция
sctp_sendmsg
вызывается с флагом
MSG_ABORT
. При этом отправляется порция данных ABORT, что приводит к немедленному закрытию ассоциации. В порцию данных включается код пользовательской ошибки и сообщение («goodbye») в поле причины ошибки вышележащего уровня.

Закрытие дескриптора сокета

33
 Хотя ассоциация и была завершена, дескриптор сокета все равно закрыть нужно, чтобы освободить связанные с ним системные ресурсы.

10.8. Резюме

Мы изучили простой пример клиента и сервера SCTP общим объемом около 150 строк кода. Обе программы работали с сокетами SCTP типа «один-ко-многим». Сервер был написан в последовательном стиле, который часто используется при работе с такими сокетами. Он считывал сообщения и отвечал на них по тому же потоку, из которого они приходили, или по потоку с увеличенным на единицу номером. Затем мы исследовали проблему блокирования очереди, изменив программу клиента таким образом, чтобы подчеркнуть особенности ситуации и продемонстрировать использование потоков SCTP для решения проблемы. После этого мы показали, каким образом можно изменить количество потоков при помощи одного из множества параметров сокета, используемых для управления поведением SCTP. Наконец, мы снова изменили код сервера и клиента, чтобы показать корректное и аварийное закрытие ассоциации.

Углубленное исследование SCTP будет проведено в главе 23.

Упражнения

1. Что произойдет с программой в листинге 10.1, если SCTP вернет сообщение об ошибке? Каким образом вы можете устранить указанный недостаток программы?

2. Что произойдет, если сервер завершит работу, не ответив на сообщения? Может ли клиент каким-либо образом получить уведомление об этом событии?

3. В листинге 10.7 в строке 22 аргумент

out_sz
устанавливается равным 800 байт. Как вы думаете, почему мы выбрали именно это значение? Существует ли лучший способ найти оптимальное значение этого аргумента?

4. Как повлияет алгоритм Нагла (см. раздел 7.10) на нашего клиента из листинга 10.7? Не лучше ли будет отключить алгоритм Нагла для этой программы? Воплотите это изменение в код клиента и сервера.

5. В разделе 10.6 мы утверждали, что приложению следует изменять количество потоков до установки ассоциации. Что произойдет в противном случае?

6. Когда мы говорили о количестве потоков, мы подчеркнули, что только для сокетов типа «один-ко-многим» можно увеличить количество потоков при помощи вспомогательных данных. Почему это так? (Подсказка: вспомогательные данные необходимо передавать с сообщениями.)

7. Почему сервер может не отслеживать открытые ассоциации? Опасно ли это?

8. В разделе 10.7 мы изменили сервер так, что он стал закрывать ассоциацию после отправки каждого сообщения. Вызовет ли это какие-либо проблемы? Хорошее ли это решение с точки зрения архитектуры приложения?

Глава 11