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

Дополнительные сведения о сокетах SCTP

23.1. Введение

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

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

Серверы SCTP могут быть как последовательными, так и параллельными в зависимости от того, какой тип интерфейса выберет разработчик приложения. SCTP предоставляет средства извлечения ассоциации из сокета типа «один-ко-многим» в отдельный сокет типа «один-к-одному». Благодаря этому появляется возможность создания последовательно-параллельных серверов.

23.2. Сервер типа «один-ко-многим» с автоматическим закрытием

Вспомните программу-сервер, которую мы написали в главе 10. Эта программа не отслеживала ассоциации. Сервер рассчитывал, что клиент самостоятельно закроет ассоциацию, удалив тем самым данные о ее состоянии. Однако такой подход делает сервер уязвимым: что если клиент откроет ассоциацию, но никаких данных не пришлет? Для такого клиента будут выделены ресурсы, которые он не использует. Неудачное стечение обстоятельств может привести к DoS-атаке на нашу реализацию SCTP со стороны неактивных клиентов. Для предотвращения подобных ситуаций в SCTP была добавлена функция автоматического закрытия ассоциаций (autoclose).

Автоматическое закрытие позволяет конечной точке SCTP задавать максимальную длительность бездействия ассоциации. Ассоциация считается бездействующей, если по ней не передаются никакие данные (ни в одном направлении). Если длительность бездействия превышает установленное ограничение, ассоциация автоматически закрывается реализацией SCTP.

Особое внимание следует уделить выбору ограничения на время бездействия. Значение не должно быть слишком маленьким, иначе сервер может в какой-то момент обнаружить, что ему требуется передать данные по уже закрытой ассоциации. На повторное открытие ассоциации будут затрачены ресурсы, да и вообще маловероятно, что клиент будет готов принять входящую ассоциацию. В листинге 23.1[1] приведена новая версия кода нашего сервера, в которую добавлены вызовы, защищающие этот сервер от неактивных клиентов. Как отмечалось в разделе 7.10, функция автоматического закрытия по умолчанию отключена и должна быть включена явным образом при помощи параметра сокета

SCTP_AUTOCLOSE
.

Листинг 23.1. Включение автоматического закрытия сокета на сервере

//sctp/sctpserv04.c

14 if (argc == 2)

15  stream_increment = atoi(argv[1]);

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

17 close_time = 120;

18 Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_AUTOCLOSE,

19  &close_time, sizeof(close_time));


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

21 servaddr.sin_family = AF_INET;

22 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

23 servaddr.sin_report = htons(SERV_PORT);

Установка автоматического закрытия

17-19
 Сервер устанавливает ограничение на простой ассоциаций равным 120 с и помещает это значение в переменную
close_time
. Затем сервер вызывает функцию
setsockopt
с параметром
SCTP_AUTOCLOSE
, устанавливающим выбранное ограничение. В остальном код сервера остается прежним.

Теперь SCTP будет автоматически закрывать ассоциации, простаивающие более двух минут. Автоматическое закрытие ассоциаций уменьшает расходы ресурсов сервера на неактивных клиентов.

23.3. Частичная доставка

Механизм частичной доставки (partial delivery) используется стеком SCTP каждый раз, когда требуется доставить приложению большое сообщение. Сообщение считается «большим», если SCTP решает, что у него недостаточно ресурсов на его обработку. Частичная доставка накладывает на работу SCTP определенные ограничения:

■ объем памяти, занимаемой сообщением в буфере, должен превосходить некоторое пороговое значение;

■ доставка может выполняться только последовательно от начала сообщения до первого отсутствующего блока;

■ после включения механизма частичной доставки приложение не может получить никакие другие сообщения до тех пор, пока «большое» сообщение не будет им полностью считано из буфера. Таким образом, большое сообщение блокирует все остальные, которые в противном случае могли бы быть доставлены (в том числе и по другим потокам).

В реализации SCTP, выполненной группой KAME, используется пороговое значение, равное половине объема приемного буфера сокета. На момент написания этой книги объем приемного буфера по умолчанию составляет 131 072 байта. Если параметр сокета

SO_RCVBUF
не меняется, механизм частичной доставки будет включаться только для сообщений, превышающих 65 536 байт. Мы продолжим расширение новой версии сервера из раздела 10.2, написав функцию-обертку для вызова
sctp_recvmsg
. Затем мы создадим новый сервер, который будет использовать эту функцию. В листинге 23.2 представлена функция-обертка, способная работать с механизмом частичной доставки.

Листинг 23.2. Работа с API частичной доставки

//sctp/sctp_pdapirev.c

 1 #include "unp.h"


 2 static uint8_t *sctp_pdapi_readbuf=NULL;

 3 static int sctp_pdapi_rdbuf_sz=0;


 4 uint8_t*

 5 pdapi_recvmsg(int sock_fd,

 6  int *rdlen,

 7  SA *from,

 8  int *from_len, struct sctp_sndrcvinfo *sri, int *msg_flags)

 9 {

10  int rdsz, left, at_in_buf;

11  int frmlen=0;


12  if (sctp_pdapi_readbuf == NULL) {

13   sctp_pdapi_readbuf = (uint8_t*)Malloc(SCTP_PDAPI_INCR_SZ);

14   sctp_pdapi_rdbuf_sz = SCTP_PDAPI_INCR_SZ;

15  }

16  at_in_buf = Sctp_recvmsg(sock_fd, sctp_pdapi_readbuf, sctp_pdapi_rdbuf_sz,

17   from, from_len,

18   sri.msg_flags);

19  if (at_in_buf < 1) {

20   *rdlen = at_in_buf;

21   return(NULL);

22  }

23  while ((*msg_flags & MSG_EOR) == 0) {

24   left = sctp_pdapi_rdbuf_sz = at_in_buf;

25   if (left < SCTP_PDAPI_NEED_MORE_THRESHOLD) {

26    sctp_pdapi_readbuf =

27     realloc(sctp_pdapi_readbuf,

28      setp_pdapi_rdbuf_sz + SCTP_PDAPI_INCR_SZ);

29    if (sctp_pdapi_readbuf == NULL) {

30     err_quit("sctp_pdapi ran out of memory");

31    }

32    sctp_pdapi_rdbuf_sz += SCTP_PDAPI_INCR_SZ;

33     left = sctp_pdapi_rdbuf_sz - at_in_buf;

34   

35   rdsz = Sctp_recvmsg(sock_fd, &sctp_pdapi_readbuf[at_in_buf],

36    left, NULL, &frmlen, NULL, msg_flags);

37   at_in_buf += rdsz;

38  }

39  *rdlen = at_in_buf;

40  return(sctp_pdapi_readbuf);

41 }

Подготовка статического буфера

12-15
 Если статический буфер не выделен, функция выделяет его и инициализирует переменную, хранящую информацию о состоянии этого буфера.

Чтение сообщения

16-18
 Первое сообщение считывается из сокета вызовом
sctp_recvmsg
.

Обработка ошибки чтения

19-22
 Если
sctp_recvmsg
возвращает ошибку или признак конца файла EOF, соответствующий код возвращается вызвавшему нашу функцию процессу без всяких изменений.

Если сообщение считано не полностью

23-24
 Если флаги сообщения показывают, что оно было считано не полностью, мы вызываем функцию
sctp_recvmsg
снова. Предварительно мы вычисляем объем свободного места в буфере.

Проверка необходимости увеличения статического буфера

25-34
 Если остаток приемного буфера оказался меньше некоторого минимального значения, этот буфер необходимо увеличить. С этой целью мы вызываем функцию
realloc
, выделяющую буфер большего размера, после чего копируем в новый буфер данные из старого буфера. Если по какой-то причине размер буфера не может быть увеличен, функция завершает свою работу с выводом сообщения об ошибке.

Получение данных

35-36
 Новые данные считываются из буфера вызовом
sctp_recvmsg
.

Шаг вперед

37-38
 Функция увеличивает индекс буфера, после чего возвращается на проверку полного считывания сообщения.

После завершения цикла

39-40
 После завершения цикла функция копирует количество считанных байтов в буфер, указатель на который передается ей вызвавшим процессом, и возвращает этому процессу указатель на собственный буфер.

Теперь мы можем изменить сервер SCTP таким образом, чтобы он использовал нашу новую функцию. Новый код представлен в листинге 23.3.

Листинг 23.3. Сервер SCTP, использующий API частичной доставки

//sctp/sctpserv05.c

26 for (;;) {

27  len = sizeof(struct sockaddr_in);

28  bzero(&sri,.sizeof(sri));

29  readbuf = pdapi_recvmsg(sock_fd, &rd_sz,

30   (SA*)&cliaddr, &len, &sri, &msg_flags);

31  if (readbuf == NULL)

32   continue;

Чтение сообщения

29-30
 Сервер вызывает новую функцию-обертку интерфейса частичной доставки. Предварительно обнуляется переменная
sri
.

Проверка наличия считанных данных

31-32
 Обратите внимание, что теперь серверу приходится проверять объем буфера, чтобы убедиться, что чтение было успешным. Если буфер оказывается нулевым, программа переходит на начало цикла.

23.4. Уведомления

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

Листинг 23.4. Функция вывода уведомлений

 1 #include "unp.h"


 2 void

 3 print_notification(char *notify_buf)

 4 {

 5  union sctp_notification *snp;

 6  struct sctp_assoc_change *sac;

 7  struct sctp_paddr_change *spc;

 8  struct sctp_remote_error *sre;

 9  struct sctp_send_failed *ssf;

10  struct sctp_shutdown_event *sse;

11  struct sctp_adaption_event *ae;

12  struct sctp_pdapi_event *pdapi,

13  const char *str;


14  snp = (union sctp_notification*)notify_buf;

15  switch (snp->sn_header.sn_type) {

16  case SCTP_ASSOC_CHANGE:

17   sac = &snp->sn_assoc_change;

18   switch (sac->sac_state) {

19    case SCTP_COMM_UP:

20    str = "COMMUNICATION UP";

21    break;

22   case SCTP_COMM_LOST:

23    str = "COMMUNICATION LOST";

24    break;

25   case SCTP_RESTART:

26    str = "RESTART";

27    break;

28   case SCTP_SHUTDOWN_COMP:

29    str = "SHUTDOWN COMPLETE";

30    break;

31   case SCTP_CANT_STR_ASSOC:

32    str = "CAN'T START ASSOC";

33    break;

34   default:

35    str = "UNKNOWN";

36    break;

37   } /* конец ветвления switch (sac->sac_state) */

38   printf("SCTP_ASSOC_CHANGE %s, assoc=0x%x\n", str,

39    (uint32_t)sac->sac_assoc_id);

40   break;

41  case SCTP_PEER_ADDR_CHANGE:

42   spc = &snp->sn_paddr_change;

43   switch (spc->spc_state) {

44   case SCTP_ADDR_AVAILABLE:

45    str = "ADDRESS AVAILABLE";

46    break;

47   case SCTP_ADDR_UNREACHABLE:

48    str = "ADDRESS UNREACHABLE";

49    break;

50   case SCTP_ADDR_REMOVED:

51    str = "ADDRESS REMOVED";

52    break;

53   case SCTP_ADDR_ADDED:

54    str = "ADDRESS ADDED";

55    break;

56   case SCTP_ADDR_MADE_PRIM:

57    str = "ADDRESS MADE PRIMARY";

58    break;

59   default:

60    str = "UNKNOWN";

61    break;

62   } /* конец ветвления switch (spc->spc_state) */

63   printf("SCTP_PEER_ADDR_CHANGE %s, addr=%s, assoc=0x%x\n", str,

64    Sock_ntop((SA*)&spc->spc_aaddr, sizeof(spc->spc_aaddr)),

65    (uint32_t)spc->spc_assoc_id);

66   break;

67  case SCTP_REMOTE_ERROR:

68   sre = &snp->sn_remote_error;

69   printf("SCTP_REMOTE_ERROR: assoc=0x%x error=%d\n",

70    (uint32_t)sre->sre_assoc_id, sre->sre_error);

71   break;

72  case SCTP_SEND_FAILED:

73   ssf = &snp->sn_send_failed;

74   printf("SCTP_SEND_FAILED: assoc=0x%x error=%d\n",

75    (uint32_t)ssf->ssf_assoc_id, ssf->ssf_error);

76   break;

77  case SCTP_ADAPTION_INDICATION:

78   ae = &snp->sn_adaption_event;

79   printf("SCTP_ADAPTION_INDICATION: 0x%x\n",

80    (u_int)ae->sai_adaption_ind);

81   break;

82  case SCTP_PARTIAL_DELIVERY_EVENT:

83   pdapi = &snp->sn_pdapi_event;

84   if (pdapi->pdapi_indication == SCTP_PARTIAL_DELIVERY_ABORTED)

85    printf("SCTP_PARTIAL_DELIEVERY_ABORTED\n");

86   else

87    printf("Unknown SCTP_PARTIAL_DELIVERY_EVENT 0x%x\n",

88     pdapi->pdapi_indication);

89   break;

90  case SCTP_SHUTDOWN_EVENT:

91   sse = &snp->sn_shutdown_event;

92   printf("SCTP_SHUTDOWN_EVENT: assoc=0x%x\n",

93    (uint32_t)sse->sse_assoc_id);

94   break;

95  default:

96   printf("Unknown notification event type=0x%x\n",

97    snp->sn_header.sn_type);

98  }

99 }

Преобразование буфера и начало ветвления

14-15
 Функция преобразует буфер вызова к типу union, после чего разыменовывает структуру
sn_header
и тип
sn_type
и выполняет ветвление по значению соответствующего поля.

Обработка изменения состояния ассоциации

16-40
 Если функция обнаруживает в буфере уведомление об изменении ассоциации, она выводит тип происшедшего изменения.

Изменение адреса собеседника

16-40
 Если получено уведомление об изменении адреса собеседника, функция распечатывает событие и новый адрес.

Ошибка на удаленном узле

67-71
 Если получено уведомление об ошибке на удаленном узле, функция отображает сообщение об этом вместе с идентификатором ассоциации, для которой получено уведомление. Мы не пытаемся декодировать и отобразить сообщение об ошибке, присланное собеседником. При необходимости эти сведения можно получить из поля
sre_data
структуры
sctp_remote_error
.

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

72-76
 Если получено уведомление об ошибке отправки сообщения, мы можем сделать вывод, что сообщение не было отправлено собеседнику. Это означает, что либо ассоциация завершает работу и вскоре будет получено уведомление об изменении ее состояния (если оно еще не было получено) или же сервер использует расширение частичной надежности и отправка сообщения оказалась неудачной из-за наложенных ограничений. Данные, которые все-таки были переданы, помещаются в поле
ssf_data
, которая наша функция не использует.

Индикация уровня адаптера

77-81
 Если получено уведомление об уровне адаптера, функция отображает соответствующее 32-разрядное значение, полученное в сообщении INIT или INIT-ACK.

Уведомление механизма частичной доставки

82-89
 Если получено уведомление механизма частичной доставки, функция выводит на экран соответствующее сообщение. Единственное определенное на момент написания этой книги событие, связанное с частичной доставкой, состоит в ее аварийном завершении.

Уведомление о завершении ассоциации

90-94
 Если получено уведомление о завершении ассоциации, мы можем сделать вывод, что собеседник выполняет корректное закрытие. За этим уведомлением обычно следует уведомление об изменении состояния ассоциации, которое приходит б момент окончания последовательности пакетов, завершающих ассоциацию. Код сервера, использующего нашу новую функцию, приведен в листинге 23.5.

Листинг 23.5. Сервер, обрабатывающий уведомления о событиях

//sctp/sctpserv06.c

21 bzero(&evnts, sizeof(evnts));

22 evnts.sctp_data_io_event = 1;

23 evnts.sctp_association_event = 1;

24 evnts.sctp_address_event = 1;

25 evnts.sctp_send_failure_event = 1;

26 evnts.sctp_peer_error_event = 1;

27 evnts.sctp_shutdown_event = 1;

28 evnts.sctp_partial_delivery_event = 1;

29 evnts.sctp_adaption_layer_event = 1;

30 Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));


31 Listen(sock_fd, LISTENQ);

32 for (;;) {

33  len = sizeof(struct sockaddr_in);

34  rd_sz = Sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf),

35   (SA*)&cliaddr, &len, &sri, &msg_flags);

36  if (msg_f1ags & MSG_NOTIFICATION) {

37   print_notification(readbuf);

38   continue;

39  }

Подписка на уведомления

21-30
 Сервер изменяет параметры подписки на события таким образом, чтобы получать все возможные уведомления.

Получение данных

31-35
 Эта часть кода сервера осталась неизменной.

Обработка уведомлений

36-39
 Сервер проверяет поле
msg_flags
. Если сообщение представляет собой уведомление, сервер вызывает рассмотренную ранее функцию
sctp_print_notification
и переходит к обработке следующего сообщения.

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

Мы запускаем клиент и отправляем одно сообщение.

FreeBSD-lap: ./sctpclient01 10.1.1.5

[0]Hello

From str:1 seq:0 (assoc:c99e15a0):[0]Hello

Control-D

FreeBSD-lap:

Сервер отображает сообщения обо всех происходящих событиях (приеме входящего соединения, получении сообщения, завершении соединения).

FreeBSD-lap:./sctpserv06

SCTP_ADAPTION_INDICATION:0x504c5253

SCTP_ASSOC_CHANGE: COMMUNICATION UP, assoc=c99e2680h

SCTP_SHUTDOWN_EVENT; assoc=c99e2680h

SCTP_ASSOC_CHANGE: SHUTDOWN COMPLETE, assoc=c99e2680h

Control-C

Как видите, сервер действительно выводит сообщения обо всех происходящих событиях транспортного уровня.

23.5. Неупорядоченные данные

В обычном режиме SCTP обеспечивает надежную упорядоченную доставку данных. Кроме того, SCTP предоставляет и сервис надежной неупорядоченной доставки. Сообщение с флагом

MSG_UNORDERED
отправляется вне очереди и делается доступным для чтения сразу же после приема на удаленном узле. Такое сообщение может быть отправлено по любому потоку. Ему не присваивается порядковый номер внутри какого-либо потока. В листинге 23.6 представлены изменения кода клиента, позволяющие ему отправлять внеочередные запросы серверу.

Листинг 23.6. Функция sctp_strcli, отправляющая внеочередные данные

//sctp/sctp_strcli_un.c

18 out_sz = strlen(sendline);

19 Sctp_sendmsg(sock_fd, sendline, out_sz,

20  to, tolen, 0, MSG_UNORDERED, sri.sinfo_stream, 0, 0);

Отправка внеочередных данных

18-20
 Функция
sctp_str_cli
практически не отличается от той, которую мы разработали в разделе 10.4. Единственное изменение произошло в строке 21: клиент передает флаг
MSG_UNORDERED
, включающий механизм частичной доставки. Обычно все сообщения внутри потока упорядочиваются по номерам. Флаг
MSG_UNORDERED
позволяет отправить сообщение без порядкового номера. Такое сообщение доставляется адресату сразу после получения его стеком SCTP, даже если другие внеочередные сообщения, отправленные ранее по тому же потоку, еще не были приняты.

23.6. Связывание с подмножеством адресов

Некоторым приложениям требуется связывать один сокет с некоторым конкретным подмножеством всех адресов узла. Протоколы TCP и UDP не позволяют выделить подмножество адресов. Системный вызов

bind
позволяет приложению связать сокет с единственным адресом или сразу со всеми адресами узла (то есть с универсальным адресом). Поэтому в SCTP был добавлен новый системный вызов
sctp_bindx
, который позволяет приложению связываться с произвольным количеством адресов. Все адреса должны иметь один и тот же номер порта, а если ранее вызывалась функция
bind
, то номер порта должен быть таким, как в вызове
bind
. Если указать не тот порт, вызов
sctp_bindx
завершится с ошибкой. В листинге 23.7 представлена функция, которую мы добавим к нашему серверу, чтобы получить возможность связывать сокет с адресами, передаваемыми в качестве аргументов командной строки.

Листинг 23.7. Функция, связывающая сокет с набором адресов

 1 #include "unp.h"


 2 int

 3 sctp_bind_arg_list(int sock_fd, char **argv, int argc)

 4 {

 5  struct addrinfo *addr;

 6  char *bindbuf, *p, portbuf[10];

 7  int addrcnt=0;

 8  int i;


 9  bindbuf = (char*)Calloc(argc, sizeof(struct sockaddr_storage));

10  p = bindbuf;

11  sprintf(portbuf, "%d", SERV_PORT);

12  for (i=0; i

13   addr = Host_serv(argv[i], portbuf, AF_UNSPEC, SOCK_SEQPACKET);

14   memcpy(p, addr->ai_addr, addr->ai_addrlen);

15   freeaddrinfo(addr);

16   addrcnt++;

17   p += addr->ai_addrlen;

18  }

19  Sctp_bindx(sock_fd, (SA*)bindbuf, addrent, SCTP_BINDX_ADD_ADDR);

20  free(bindbuf);

21  return(0);

22 }

Выделение памяти под аргументы bind

9-10
 Наша новая функция начинает работу с выделения памяти под аргументы функции
sctp_bindx
. Обратите внимание, что функция
sctp_bindx
может принимать в качестве аргументов адреса IPv4 и IPv6 в произвольных комбинациях. Для каждого адреса мы выделяем место под структуру
sockaddr_storage
несмотря на то, что соответствующий аргумент
sctp_bindx
представляет собой упакованный список адресов (см. рис. 9.3). В результате мы расходуем зря некоторый объем памяти, но зато функция работает быстрее, потому что ей не приходится вычислять точный объем памяти и лишний раз обрабатывать список аргументов.

Обработка аргументов

11-18
 Мы подготавливаем
portbuf
к хранению номера порта в ASCII-представлении, имея в виду вызов нашей обертки для
getaddrinfo
, которая называется
host_serv
. Каждый адрес с номером порта мы передаем
host_serv
, указывая константы
AF_UNSPEC
(протоколы IPv4 и IPv6) и
SOCK_SEQPACKET
(протокол SCTP). Мы копируем первую возвращаемую структуру
sockaddr
, игнорируя все остальные. Поскольку аргументами этой функции должны быть адреса в строковом представлении, а не имена, с каждым из которых может быть связано несколько адресов, это не вызывает проблем. Мы освобождаем буфер, увеличиваем количество адресов на единицу и перемещаем указатель на следующий элемент в упакованном массиве структур
sockaddr
.

Вызов связывающей функции

19
 Указатель устанавливается на начало буфера адресов, после чего вызывается функция
sctp_bindx
, в качестве аргументов которой используется раскодированный ранее набор адресов.

Успешное завершение

20-21
 Если мы добрались до этого места, можно считать, что выполнение прошло успешно, поэтому мы освобождаем память и возвращаем управление вызвавшему процессу.

В листинге 23.8 представлен модифицированный эхо-сервер, связывающий сокет с набором адресов, передаваемых в командной строке. Мы слегка изменили код сервера, чтобы он отправлял эхо-сообщения по тем потокам, по которым были приняты исходные сообщения.

Листинг 23.8. Сервер, работающий с произвольным набором адресов

if (argc < 2)

 err_quit("Error, use %s [list of addresses to bind]\n", argv[0]);

sock_fd = Socket(AF_INET6, SOCK_SEQPACKET, IPPROTO_SCTP);


if (sctp_bind_arg_list(sock_fd, argv + 1, argc — 1))

 err_sys("Can't bind the address set");


bzero(&evnts, sizeof(evnts));

evnts sctp_data_io_event = 1;

Работа с IPv6

14
 Это тот же сервер, с которым мы работали во всех предыдущих разделах этой главы, но с незначительным изменением. Сервер создает сокет
AF_INET6
, что позволяет ему работать с протоколом IP обеих версий.

Вызов sctp_bind_arg_list

15-16
 Сервер вызывает новую функцию
sctp_bind_arg_list
и передает ей список аргументов для обработки.

23.7. Получение адресов

Поскольку протокол SCTP ориентирован на многоинтерфейсные узлы, для определения адресов локального и удаленного узла не могут использоваться те же механизмы, что и в TCP. В этом разделе мы изменим код клиента, добавив в него подписку на уведомление о событии COMMUNICATION UP. В этом уведомлении клиент будет получать сведения об адресах, между которыми установлена ассоциация. В листингах 23.9 и 23.10 представлены изменения в коде клиента. Листинги 23.11 и 23.12 содержат добавления к коду клиента.

Листинг 23.9. Клиент включает уведомления

16 bzero(&evnts, sizeof(evnts));

17 evnts.sctp_data_io_event = 1;

18 evnts.sctp_association_event = 1;

19 Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));


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

Включение событий и вызов функции отправки сообщения

16-20
 Функция
main
клиента претерпевает не слишком значительные изменения. Клиент явным образом подписывается на уведомления об изменении состояния ассоциации.

Посмотрим, что нам придется изменить в функции

sctpstr_cli
, чтобы она смогла вызывать нашу новую функцию обработки уведомлений.

Листинг 23.10. Функция sctp_strcli, способная работать с уведомлениями

21 do {

22  len = sizeof(peeraddr);

23  rd_sz = Sctp_recvmsg(sock_fd, recvline, sizeof(recvline),

24   (SA*)&peeraddr, &len, &sri, &msg_flags);

25  if (msg_flags & MSG_NOTIFICATION)

26   check_notification(sock_fd, recvline, rd_sz);

27 } while (msg_flags & MSG_NOTIFICATION);

28 printf("From str:%d seq:%d (assoc.0x%x) ",

29 sri.sinfo_stream, sri.sinfo_ssn, (u_int)sri.sinfo_assoc_id);

30 printf("%.*s", rd_sz.recvline);

Цикл ожидания сообщения

21-24
 Клиент устанавливает переменную, в которой хранится длина адреса, и вызывает функцию
sctp_recvmsg
для получения эхо-ответа сервера на свое сообщение.

Проверка уведомлений

25-26
 Клиент проверяет, не является ли полученное сообщение уведомлением. В последнем случае он вызывает функцию обработки уведомлений, представленную в листинге 23.11.

Переход на начало цикла

27
 Если сообщение действительно было уведомлением, происходит переход на начало цикла ожидания сообщений.

Отображение сообщения

28-30
Клиент отображает сообщение и переходит к ожиданию пользовательского ввода.

Теперь мы можем изучить новую функцию

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

Листинг 23.11. Обработка уведомлений

//sctp/sctp_check_notify.c

 1 #include "unp.h"


 2 void

 3 check_notification(int sock_fd, char *recvline, int rd_len)

 4 {

 5  union sctp_notification *snp;

 6  struct sctp_assoc_change *sac;

 7  struct sockaddr_storage *sal, *sar;

 8  int num_rem, num_loc;


 9  snp = (union sctp_notification*)recvline;

10  if (snp->sn_header.sn_type == SCTP_ASSOC_CHANGE) {

11   sac = &snp->sn_assoc_change;

12   if ((sac->sac_state == SCTP_COMM_UP) ||

13    (sac->sac_state == SCTP_RESTART)) {

14    num_rem = sctp_getpaddrs(sock_fd, sac->sac_assoc_id, &sar);

15    printf("There are %d remote addresses and they are:\n", num_rem);

16    sctp_print_addresses(sar, num_rem);

17    sctp_freepaddrs(sar);


18    num_loc = sctp_getladdrs(sock_fd.sac->sac_assoc_id, &sal);

19    printf("There are %d local addresses and they are:\n", num_loc);

20    sctp_print_addresses(sal, num_loc);

21    sctp_freeladdrs(sal);

22   }

23  }

24 }

Проверка типа уведомления

9-13
 Функция преобразует буфер приема к типу универсального указателя на уведомления, чтобы определить тип полученного уведомления. Из всех уведомлений нас интересуют только уведомления об изменении ассоциации, а из них — уведомления о создании или перезапуске ассоциации (
SCTP_COMM_UP
и
SCTP_RESTART
). Все прочие уведомления нас не интересуют.

Получение и вывод адресов собеседника

14-17
 Функция
sctp_getpaddrs
возвращает нам список удаленных адресов, которые мы выводим при помощи функции
sctp_print_addresses
, представленной в листинге 23.12. После работы с ней мы освобождаем ресурсы, выделенные
sctp_getpaddrs
, вызывая функцию
sctp_freepaddrs
.

Получение и вывод локальных адресов

18-21
 Функция
sctp_getladdrs
возвращает нам список локальных адресов, которые мы выводим на экран вместе с их общим количеством. После завершения работы с адресами мы освобождаем память вызовом
sctp_freeladdrs
.

Последняя из новых функций называется

sctp_print_addresses
. Она выводит на экран адреса из списка, возвращаемого функциями
sctp_getpaddrs
и
sctp_getladdrs
. Текст функции представлен в листинге 23.12.

Листинг 23.12. Вывод списка адресов

//sctp/sctp_print_addrs.c

 1 #include "unp.h"


 2 void

 3 sctp_print_addresses(struct sockaddr_storage *addrs, int num)

 4 {

 5  struct sockaddr_storage *ss;

 6  int i, salen;


 7  ss = addrs;

 8  for (i=0; i

 9   printf("%s\n", Sock_ntop((SA*)ss, salen));

10 #ifdef HAVE_SOCKADDR_SA_LEN

11   salen = ss->ss_len;

12 #else

13   swilch (ss->ss_family) {

14   case AF_INET:

15    salen = sizeof(struct sockaddr_in);

16    break;

17 #ifdef IPV6

18   case AF_INET6:

19    salen = sizeof(struct sockaddr_in6);

20    break;

21 #endif

22   default:

23    err_auit("sctp_print_addresses: unknown AF");

24    break;

25   }

26 #endif

27   ss = (struct sockaddr_storage*)((char*)ss + salen);

28  }

29 }

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

7-8
 Функция перебирает адреса в цикле. Общее количество адресов указывается вызывающим процессом.

Вывод адреса

9
 Адрес преобразуется к удобочитаемому виду функцией
sock_ntop
, которая, как вы помните, должна работать со структурами адреса сокета всех форматов, поддерживаемых системой.

Определение размера адреса

10-26
 Список адресов передается в упакованном формате. Это не просто массив структур s
ockaddr_storage
. Дело в том, что структура
sockaddr_storage
достаточно велика, и ее нецелесообразно использовать при передаче адресов между ядром и пользовательскими процессами. В системах, где эта структура содержит внутреннее поле длины, обработка списка является делом тривиальным: достаточно извлекать длину из текущей структуры
sockaddr_storage
. В прочих системах длина определяется на основании семейства адреса. Если семейство не определено, функция завершает работу с сообщением об ошибке.

Перемещение указателя

27
 К указателю на элемент списка прибавляется размер адреса. Таким образом осуществляется перемещение по списку адресов.

Выполнение программы

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

FreeBSD-lap: ./sctpclient01 10.1.1.5

[0]Hi

There are 2 remote addresses and they are:

10.1.1.5:9877

127.0.0.1:9877

There are 2 local addresses and they are:

10.1.1.5:1025

127.0.0.1:1025

From str:0 seq:0 (assoc:c99e2680):[0]Hi

Control-D

FreeBSD-lap:

23.8. Определение идентификатора ассоциации по IP-адресу

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

sac_assoc_id
. Но что если приложение не отслеживает идентификаторы ассоциаций, а ему вдруг понадобилось определить какой- либо идентификатор по адресу собеседника? В листинге 23.13 представлена простая функция, преобразующая адрес собеседника в идентификатор ассоциации. Эта функция будет использоваться сервером из раздела 23.10.

Листинг 23.13. Преобразование адреса в идентификатор ассоциации

//sctp/sctp_addr_to_associd.с

 1 #include "unp.h"


 2 sctp_assoc_t

 3 sctp_address_to_associd(int sock_fd, struct sockaddr *sa, socklen_t salen)

 4 {

 5  struct sctp_paddrparams sp;

 6  int siz;


 7  siz = sizeof(struct sctp_paddrparams);

 8  bzero(&sp, siz);

 9  memcpy(&sp, spp_address, sa.salen);

10  sctp_opt_info(sock_fd, 0, SCTP_PEER_ADDR_PARAMS, &sp, &siz);

11  return(sp.spp_assoc_id);

12 }

Инициализация

7-8
 Функция начинает работу с инициализации структуры
sctp_paddrparams
.

Копирование адреса

9
 Мы копируем адрес в структуру
sctp_paddrparams
, используя переданную нам вызвавшим процессом информацию о длине этого адреса.

Вызов параметра сокета

10
 При помощи параметра сокета
SCTP_PEER_ADDR_PARAMS
наша функция запрашивает параметры адреса собеседника. Обратите внимание, что мы используем
sctp_opt_info
вместо
getsockopt
, потому что параметр
SCTP_PEER_ADDR_PARAMS
требует копирования аргументов как в ядро, так и из ядра. Вызов, который мы делаем, возвратит нам текущий интервал проверки работоспособности соединения, максимальное количество попыток повторной передачи перед принятием решения о признании адреса собеседника отказавшим, и, что самое важное, идентификатор ассоциации. Мы не проверяем возвращаемое значение, потому что если вызов оказывается неудачным, мы хотим вернуть 0.

11
 Функция возвращает идентификатор ассоциации. Если вызов
sctp_opt_info
оказался неудачным, обнуление структуры гарантирует, что вызвавший нашу функцию процесс получит 0. Идентификатор ассоциации нулевым быть не может. Это значение используется реализацией SCTP для указания на отсутствие ассоциации.

23.9. Проверка соединения и ошибки доступа

Механизм периодической проверки соединения, предоставляемый протоколом SCTP, основан на той же концепции, что и параметр поддержания соединения TCP keep-alive. Однако в SCTP этот механизм по умолчанию включен, тогда как в TCP он выключен. Приложение может устанавливать пороговое значение количества неудачных проверок при помощи того же параметра сокета, который использовался в разделе 23.8. Порог ошибок — это количество пропущенных проверочных пакетов и тайм-аутов повторной передачи, после которого адрес получателя считается недоступным. Когда доступность адреса восстанавливается (о чем сообщают все те же проверочные пакеты), он снова становится активным.

Приложение может отключить проверку соединения, но без нее SCTP не сможет узнать о доступности адреса собеседника, который ранее был признан недоступным. Без вмешательства пользователя такой адрес не сможет стать активным.

Параметр проверки соединения задается полем

spp_hbinterval
структуры
sctp_paddrparams
. Если приложение устанавливает это поле равным
SCTP_NO_HB
(эта константа имеет значение 0), проверка соединения отключается. Ненулевое значение устанавливает задержку проверки соединения в миллисекундах. К фиксированной задержке прибавляется текущее значение таймера повторной передачи и некоторое случайное число, в результате чего получается реальный промежуток времени между проверками соединения. В листинге 23.14 приводится небольшая функция, которая позволяет устанавливать задержку проверки соединения, или вовсе отключать этот механизм протокола SCTP для конкретного адресата. Обратите внимание, что если поле
spp_pathmaxrxr
структуры
sctp_paddrparams
оставить равным нулю, текущее значение задержки останется неизменным.

Листинг 23.14. Управление периодической проверкой соединения

//sctp/sctp_modify_hb.c

 1 #include "unp.h"


 2 int

 3 heartbeat_action(int sock_fd, struct sockaddr *sa, socklen_t salen,

 4 u_int value)

 5 {

 6  struct sctp_paddrparams sp;

 7  int siz;


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

 9  sp.spp_hbinterval = value;

10  memcpy((caddr_t)&sp, spp_address, sa.salen);

11  Setsockopt(sock_fd, IPPROTO_SCTP,

12   SCTP_PEER_ADDR_PARAMS, &sp, sizeof(sp));

13  return(0);

14 }

Обнуление структуры sctp_paddrparams и копирование аргумента

8-9
 Мы обнуляем структуру
sctp_paddrparams
, чтобы случайно не изменить какой-нибудь параметр, который нас не интересует. Затем мы копируем в нее переданное пользователем значение задержки:
SCTP_ISSUE_HB
,
SCTP_NO_HB
или конкретное число.

Установка адреса

10
 Функция подготавливает адрес и копирует его в структуру
sctp_paddrparams
, чтобы реализация SCTP знала, к какому адресу относятся устанавливаемые нами параметры периодической проверки соединения.

Выполнение действия

11-12
 Наконец, функция делает вызов параметра сокета, чтобы выполнить запрошенную пользователем операцию.

23.10. Выделение ассоциации

Пока что мы занимались исключительно интерфейсом типа «один-ко-многим». Этот интерфейс имеет несколько преимуществ перед традиционным интерфейсом «один-к-одному»:

■ программа работает с единственным дескриптором;

■ программисту достаточно написать простой последовательный сервер;

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

sendmsg
и
sctp_sendmsg
;

■ отсутствует необходимость в отслеживании состояния на транспортном уровне. Другими словами, приложение просто запрашивает данные из дескриптора сокета, не вызывая традиционных функций

connect
и
accept
для получения сообщений.

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

sctp_peeloff
. Она принимает в качестве аргумента дескриптор сокета типа «один-ко-многим» и идентификатор ассоциации, а возвращает новый дескриптор сокета типа «один-к-одному» с единственной ассоциацией (сохраняя все уведомления и данные, помещенные в очередь этой ассоциации). Исходный сокет остается открытым, причем все остальные ассоциации проведенной операцией извлечения никак не затрагиваются.

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

sctp_peeloff
, порождает дочерний процесс и вызывает функцию
str_echo
для TCP, которая была написана в разделе 5.3. Адрес из полученного сообщения мы передаем нашей функции из раздела 23.8, которая по этому адресу определяет идентификатор ассоциации. Идентификатор хранится также в поле
sri
,
sinfo_assoc_id
. Наша функция служит лишь иллюстрацией использования альтернативного метода. Породив процесс, сервер переходит к обработке следующего сообщения.

Листинг 23.15. Параллельный сервер SCTP

//sctp/sctpserv_fork.c

23 for (;;) {

24  len = sizeof(struct sockaddr_in);

25  rd_sz = Sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf),

26   (SA*)&cliaddr, &len, &sri, &msg_flags);

27  Sctp_sendmsg(sock_fd, readbuf, rd_sz,

28   (SA*)&cliaddr, len,

29   sri.sinfo_ppid,

30   sri.sinfo_flags, sn.sinfo_stream, 0, 0);

31  assoc = sctp_address_to_associd(sock_fd, (SA*)&cliaddr, len);

32  if ((int)assoc == 0) {

33   err_ret("Can't get association id");

34   continue;

35  }

36  connfd = sctp_peeloff(sock_fd, assoc);

37  if (connfd == -1) {

38   err_ret("sctp_peeloff fails");

39   continue;

40  }

41  if ((childpid = fork()) == 0) {

42   Close(sock_fd);

43   str_echo(connfd);

44   exit(0);

45  } else {

46   Close(connfd);

47  }

48 }

Получение и обработка первого сообщения

26-30
 Сервер получает и обрабатывает первое сообщение клиента.

Преобразование адреса в идентификатор ассоциации

31-35
 Сервер вызывает функцию из листинга 23.13 для получения идентификатора ассоциации по ее адресу. Если что-то мешает серверу получить идентификатор, он не делает попыток породить дочерний процесс, а просто переходит к обработке следующего сообщения.

Выделение ассоциации

36-40
 Сервер выделяет ассоциацию в отдельный дескриптор сокета при помощи
sctp_peeloff
. Полученный сокет типа «один-к-одному» может быть без проблем передан написанной ранее для TCP функции
str_echo
.

Передача работы дочернему процессу

41-47
 Сервер порождает дочерний процесс, который и выполняет всю обработку по конкретному дескриптору.

23.11. Управление таймерами

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

Время обнаружения отказа в SCTP определяется семью переменными (табл. 23.1).


Таблица 23.1. Поля таймеров SCTP

ПолеОписаниеПо умолчаниюЕдиницы
srto_minМинимальный тайм-аут повторной передачи1000Мс
srto_maxМаксимальный тайм-аут повторной передачи60000Мс
srto_initialНачальный тайм-аут повторной передачи3000Мс
sinit_max_init_timeoМаксимальный тайм-аут повторной передачи сегмента INIT3000Мс
sinit_max_attemptsМаксимальное количество повторных передач сегмента INIT8попыток
spp_pathmaxrxtМаксимальное количество повторных передач по адресу5попыток
sasoc_asocmaxrxtМаксимальное количество повторных передач на ассоциацию10попыток

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

1. Конечная точка SCTP пытается открыть ассоциацию с собеседником, отключившимся от сети.

2. Две многоинтерфейсные конечные точки SCTP обмениваются данными. Одна из них отключается от сети питания в момент передачи данных. Сообщения ICMP фильтруются защитными экранами и потому не достигают второй конечной точки.

В сценарии 1 система, пытающаяся открыть соединение, устанавливает таймер RTO равным

srto_initial
(3000 мс). После первой повторной передачи пакета INIT таймер устанавливается на значение 6000 мс. Это продолжается до тех пор, пока не будет сделано
sinit_max_attempts
попыток (9 штук), между которыми пройдут семь тайм-аутов. Удвоение таймера закончится на величине
sinit_max_init_timeo
, равной 60 000 мс. Таким образом, через 3 + 6 + 12 + 24 + 48 + 60 + 60 + 60 = 273 с стек SCTP объявит потенциального собеседника недоступным.

Вращением нескольких «ручек» мы можем удлинять и укорачивать это время. Начнем с двух параметров, позволяющих уменьшить общую задержку. Сократим количество повторных передач, изменив переменную

sinit_max_attempts
. Альтернативное изменение может состоять в уменьшении максимального тайм- аута для пакета INIT (переменная
srto_max_init_timeo
). Если количество попыток снизить до 4, время детектирования резко упадет до 45 с (одна шестая первоначального значения). Однако у этого метода есть недостаток: из-за проблем в сети или перегруженности собеседника мы можем объявить его недоступным, даже если это состояние является лишь временным.

Другой подход состоит в уменьшении

srto_max_init_timeo
до 20 с. При этом задержка до обнаружения недоступности сократится до 121 с — менее половины исходной величины. Однако и это решение является компромиссным. Если мы выберем слишком низкое значение тайм-аута, при большой сетевой задержке мы будем отправлять гораздо больше пакетов INIT, чем это требуется на самом деле.

Перейдем теперь к сценарию 2, описывающему взаимодействие двух многоинтерфейсных узлов. Одна конечная точка имеет адреса IP-A и IP-B, другая IP-X и IP-Y. Если одна из них становится недоступна, а вторая отправляет какие-то данные, последней приходится делать повторные передачи по каждому из адресов с задержкой, начинающейся с

srto_min
(по умолчанию 1 с) и последовательно удваивающейся до значения
srto_max
(по умолчанию 60 с). Повторные передачи будут продолжаться до тех пор, пока не будет достигнуто ограничение на их количество
sasoc_asocmaxrxt
(по умолчанию 10 повторных передач).

В нашем сценарии последовательность тайм-аутов будет иметь вид 1(IP-A) + 1(IP-B) + 2(IP-A) + 2(IP-B) + 4(IP-A) + 4(IP-B) + 8(IP-A) + 8(IP-B) + 16(IP-A) + 16(IP-B), что в общей сложности составит 62 с. Параметр srto_max не влияет на работу многоинтерфейсного узла, если его значение совпадает с установленным по умолчанию, потому что ограничение на количество передач для ассоциации

sasoc_asocmaxrxt
действует раньше, чем
srto_max
. Опять-таки, у нас есть два параметра влияющих на длительность тайм-аутов и эффективность обнаружения отказов. Мы можем уменьшить количество попыток, изменив значение
sasoc_asocmaxrxt
(по умолчанию 10), или снизить максимальное значение тайм-аута, изменив значение
srto_max
(по умолчанию 60 с). Если мы сделаем
srto_max
равным 10 с, время обнаружения отказа собеседника снизится на 12 с и станет равным 50 с. Альтернативой может быть уменьшение количества повторных передач до 8; при этом время обнаружения снизится до 30 с. Изложенные ранее соображения относятся и к этому сценарию: кратковременные неполадки в сети и перегрузка удаленной системы могут привести к обрыву работоспособного соединения.

Одну из множества альтернатив мы не рассматриваем в качестве рекомендуемой. Это снижение минимального тайм-аута (

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

Для каждого приложения выбор конкретных значений параметров повторной передачи должен определяться несколькими факторами:

■ Насколько быстро нужно приложению обнаруживать отказы?

■ Будет ли приложение выполняться в частных сетях, где условия передачи заранее известны и меняются не так резко, как в Интернете?

■ Каковы последствия неправильного обнаружения отказа?

Только внимательно подумав над ответами на эти вопросы, программист может правильно настроить параметры тайм-аутов SCTP.

23.12. Когда SCTP оказывается предпочтительнее TCP

Изначально протокол SCTP разрабатывался для управления сигналами и реализации интернет-телефонии. Однако в процессе разработки область применения этого протокола значительно расширилась. Фактически он превратился в общецелевой транспортный протокол. SCTP поддерживает почти все функции TCP и значительно расширяет их новыми сервисами транспортного уровня. Маловероятно, чтобы сетевое приложение ничего не выиграло от перехода на SCTP. Так в каких же случаях следует использовать этот протокол? Начнем с перечисления его достоинств.

1. Протокол SCTP обеспечивает явную поддержку многоинтерфейсных узлов. Конечная точка может передавать данные по нескольким сетям для повышения надежности. Никаких особых действий, кроме перехода на SCTP, для использования новых сервисов SCTP предпринимать не требуется. Подробнее об SCTP для многоинтерфейсных узлов читайте в [117, раздел 7.4].

2. Протокол SCTP устраняет блокирование очереди. Приложение может передавать данные параллельно по нескольким потокам одной ассоциации. Потеря пакета в одном потоке не приведет к задержке передачи по другим потокам той же ассоциации (см. раздел 10.5 настоящей книги).

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

4. SCTP предоставляет сервис неупорядоченной доставки. Некоторые приложения не нуждаются в сохранении порядка сообщений при передаче их по сети. Раньше такому приложению, использующему TCP для обеспечения надежности, приходилось мириться с задержками, вызванными блокированием очереди и необходимостью упорядоченной доставки (хотя само приложение в ней не нуждалось). SCTP предоставляет таким приложениям именно тот тип сервиса, который им нужен.

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

sinfo_timetolive
структуры
sctp_sndrcvinfo
. (Это время жизни отличается от TTL IPv4 и ограничения на количество прыжков IPv6 тем, что оно на самом деле измеряется в единицах времени.) Если частичная надежность поддерживается обоими узлами, не доставленные вовремя данные могут сбрасываться транспортным уровнем, а не приложением, даже если они были переданы и утеряны. Таким образом оптимизируется передача данных в условиях загруженных линий.

6. Легкость перехода с TCP на SCTP обеспечивается сокетами типа «один-к-одному». Сокеты этого типа предоставляют типичный для TCP интерфейс, так что приложение может быть перенесено на новый протокол с самыми незначительными изменениями.

7. Многие функции TCP поддерживаются и SCTP: уведомление о приеме, повторная передача утерянных данных, сохранение последовательности данных, оконное управление передачей, медленное начало и алгоритмы предотвращения перегрузки линий, а также выборочные уведомления. Есть и два исключения: состояние неполного закрытия и срочные данные.

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

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

SCTP не поддерживает и такую функцию TCP, как обработка внеочередных данных (urgent data). Для доставки срочных данных в SCTP можно использовать отдельный поток, однако это не позволяет в точности воспроизвести поведение TCP.

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

telnet
,
rlogin
,
rsh
и
ssh
. TCP сегментирует поток байтов на пакеты IP более эффективно, чем SCPT, который пытается сохранять границы сообщений, из-за чего могут получаться блоки, не помещающиеся целиком в IP-дейтаграммы и вызывающие избыточные накладные расходы на передачу.

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

23.13. Резюме

В этой главе мы изучили функцию автоматического закрытия ассоциации SCTP и исследовали, каким образом она может быть использована для ограничения неактивных соединений через сокет типа «один-ко-многим». Мы написали простую функцию, при помощи которой приложение может получать большие сообщения, используя механизм частичной доставки. Мы узнали, каким образом приложение может декодировать уведомления о событиях, происходящих на транспортном уровне. Мы достаточно коротко рассказали о том, как процесс может отправлять неупорядоченные данные, связывать сокет с подмножеством адресов, получать адреса собеседника и свои собственные, а также преобразовывать IP-адрес в идентификатор ассоциации.

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

sctp_peeloff
и написали параллельно-последовательный сервер, использующий эту возможность. Мы обсудили проблему настройки тайм-аутов повторной передачи SCTP, а также раскрыли преимущества и недостатки перехода на SCTP.

Упражнения

1. Напишите клиент для тестирования интерфейса частичной доставки из раздела 23.3.

2. Каким образом можно задействовать механизм частичной доставки, если не отправлять очень больших сообщений?

3. Перепишите сервер, использующий механизм частичной доставки, таким образом, чтобы он умел обрабатывать соответствующие уведомления.

4. Каким приложениям пригодится механизм передачи неупорядоченных данных? А каким он не нужен? Поясните.

5. Каким образом можно протестировать сервер, связывающийся с подмножеством IP-адресов узла?

6. Предположим, ваше приложение работает в частной сети, причем конечные точки находятся в одной локальной сети. Все серверы и клиенты являются многоинтерфейсными узлами. Каким образом следует настроить параметры повторной передачи, чтобы обнаруживать отказ узла не более, чем за 2 с?

Глава 24