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

Внеполосные данные

24.1. Введение

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

К сожалению, когда мы переходим от общих концепций к реальной ситуации, почти в каждом транспортном протоколе имеется своя реализация внеполосных данных. В качестве крайнего примера можно привести UDP, где внеполосных данных нет вовсе. В этой главе мы уделим основное внимание модели внеполосных данных TCP. Мы приведем различные примеры обработки внеполосных данных в API сокетов и опишем, каким образом внеполосные данные используются приложениями Telnet, Rlogin и FTP. За пределами очерченного круга удаленных интерактивных приложений найти применение внеполосным данным довольно сложно.

24.2. Внеполосные данные протокола TCP

В протоколе TCP нет настоящих внеполосных данных. Вместо этого в TCP предусмотрен так называемый срочный режим[4] (urgent mode), к рассмотрению которого мы сейчас и приступим. Предположим, процесс записал N байт данных в сокет протокола TCP, и эти данные образуют очередь в буфере отправки сокета и ожидают отправки собеседнику. Ситуацию иллюстрирует рис. 24.1. Байты данных пронумерованы от 1 до N.

Рис. 24.1. Буфер отправки сокета, содержащий данные для отправки

Теперь процесс отправляет один байт внеполосных данных, содержащий символ ASCII

а
, используя функцию
send
с флагом
MSG_OOB
:

send(fd, "a", 1, MSG_OOB);

TCP помещает данные в следующую свободную позицию буфера отправки сокета и устанавливает указатель на срочные данные (или просто срочный указатель[5] — urgent pointer) для этого соединения на первую свободную позицию. Этот буфер показан на рис. 24.2, а байт, содержащий внеполосные данные, помечен буквами

OOB
.

Рис. 24.2. Буфер отправки сокета, в который добавлен один байт внеполосных данных

ПРИМЕЧАНИЕ

Срочный указатель TCP указывает на байт данных, который следует за последним байтом внеполосных данных (то есть данных, снабженных флагом MSG_OOB). В книге [111] на с. 292-296 говорится, что это исторически сложившаяся особенность, которая теперь эмулируется во всех реализациях. Если посылающий и принимающий протоколы TCP одинаково интерпретируют срочный указатель TCP, беспокоиться не о чем.

Если состояние буфера таково, как показано на рис. 24.2, то в заголовке TCP следующего отправленного сегмента будет установлен флаг URG, а поле смещения срочных данных (или просто поле срочного смещения[6]) будет указывать на байт, следующий за байтом с внеполосными данными. Но этот сегмент может содержать байт, помеченный как OOB, а может и не содержать его. Будет ли послан этот байт, зависит от количества предшествующих ему байтов в буфере отправки сокета, от размера сегмента, который TCP пересылает собеседнику, и от текущего размера окна, объявленного собеседником.

Выше мы использовали термины «срочный указатель» (urgent pointer) и «срочное смещение» (urgent offset). На уровне TCP эти термины имеют различные значения. Величина, представленная 16 битами в заголовке TCP, называется срочным смещением и должна быть прибавлена к полю последовательного номера в заголовке TCP для получения 32-разрядного последовательного номера последнего байта срочных данных (то есть срочного указателя). TCP использует срочное смещение, только если в заголовке установлен другой бит, называемый флагом URG. Программисту можно не заботиться об этом различии и работать только со срочным указателем TCP.

Важная характеристика срочного режима TCP заключается в следующем: заголовок TCP указывает на то, что отправитель вошел в срочный режим (то есть флаг URG установлен вместе со срочным смещением), но фактической отправки байта данных, на который указывает срочный указатель, не требуется. Действительно, если поток данных TCP остановлен функциями управления потоком (когда буфер приема сокета получателя заполнен и TCP получателя объявил нулевое окно для отправляющего TCP), то срочное уведомление отправляется без каких-либо данных [128, с. 1016–1017], как показано в листингах 24.8 и 24.9. Это одна из причин, по которой в приложениях используется срочный режим TCP (то есть внеполосные данные): срочное уведомление всегда отсылается собеседнику, даже если поток данных остановлен функциями управления потоком TCP.

Что произойдет, если мы отправим несколько байтов внеполосных данных, как в следующем примере?

send(fd, "abc", 3, MSG_OOB);

В этом примере срочный указатель TCP указывает на байт, следующий за последним байтом, и таким образом, последний байт (

с
) считается байтом внеполосных данных.

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

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

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

SIGURG
. При этом предполагается, что для установления владельца сокета была вызвана функция
fcntl
или
ioctl
(см. табл. 7.9) и что для данного сигнала процессом был установлен обработчик сигнала. Затем, если процесс блокирован в вызове функции
select
, которая ждет возникновения исключительной ситуации для дескриптора сокета, происходит возврат из этой функции.

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

В потоке данных может быть только одна отметка OOB (один срочный указатель). Если новый срочный байт отправляется до того, как будет принят старый, последний просто сбрасывается и перестает быть срочным.

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

SO_OOBINLINE
не установлен, поэтому внеполосный байт не размещается в приемном буфере сокета. Вместо этого содержащиеся в нем данные помещаются в отдельный внеполосный буфер размером в один байт, предназначенный специально для этого соединения [128, с. 986–988]. Для процесса единственным способом прочесть данные из этого специального однобайтового буфера является вызов функции
recv
,
recvfrom
или
recvmsg
с заданием флага
MSG_OOB
. Если новый срочный байт прибывает до того, как будет считан старый, новое значение записывается в буфер поверх прежнего.

Однако если процесс устанавливает параметр сокета

SO_OOBINLINE
, то байт данных, на который указывает срочный указатель TCP, остается в обычном буфере приема сокета. В этом случае процесс не может задать флаг
MSG_OOB
для считывания данных, содержащихся во внеполосном байте. Процесс сможет распознать этот байт, только когда дойдет до него и проверит отметку внеполосных данных (out-of-band mark) для данного соединения, как показано в разделе 24.3. Возможны следующие ошибки:

1. Если процесс запрашивает внеполосные данные (то есть устанавливает флаг

MSG_OOB
), но собеседник таких данных не послал, возвращается
EINVAL
.

2. Если процесс был уведомлен о том, что собеседник послал содержащий внеполосные данные байт (например, с помощью функции

select
или сигнала
SIGURG
), и пытается считать эти данные, когда указанный байт еще не прибыл, возвращается ошибка
EWOULDBLOCK
. В такой ситуации все, что может сделать процесс, — это считать данные из приемного буфера сокета (возможно, сбрасывая данные, если отсутствует свободное место для их хранения), чтобы освободить место в буфере для приема байта внеполосных данных, посылаемых собеседником.

3. Если процесс пытается считать одни и те же внеполосные данные несколько раз, возвращается ошибка

EINVAL
.

4. Если процесс установил параметр сокета

SO_OOBINLINE
, а затем пытается считать внеполосные данные, задавая флаг
MSG_OOB
, возвращается
EINVAL
.

Простой пример использования сигнала SIGURG

Теперь мы рассмотрим тривиальный пример отправки и получения внеполосных данных. В листинге 24.1[1] показана программа отправки этих данных.

Листинг 24.1. Простая программа отправки внеполосных данных

//oob/tcpsend01.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;


 6  if (argc != 3)

 7   err_quit("usage: tcpsend01 ");

 8  sockfd = Tcp_connect(argv[1], argv[2]);

 9  Write(sockfd, "123", 3);

10  printf("wrote 3 bytes of normal data\n");

11  sleep(1);


12  Send(sockfd, "4", 1, MSG_OOB);

13  printf("wrote 1 byte of OOB data\n");

14  sleep(1);


15  Write(sockfd, "56", 2);

16  printf("wrote 2 bytes of normal data\n");

17  sleep(1);


18  Send(sockfd, "7", 1, MSG_OOB);

19  printf("wrote 1 byte of OOB data\n");

20  sleep(1);


21  Write(sockfd, "89", 2);

22  printf("wrote 2 bytes of normal data\n");

23  sleep(1);


24  exit(0);

25 }

Отправлены 9 байт, промежуток между операциями по отправке установлен с помощью функции

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

macosx % tcpsend01 freebsd 9999

wrote 3 bytes of normal data

wrote 1 byte of OOB data

wrote 2 bytes of normal data

wrote 1 byte of OOB data

wrote 2 bytes of normal data

В листинге 24.2 показана принимающая программа.

Листинг 24.2. Простая программа для получения внеполосных данных

//oob/tcprecv01.c

 1 #include "unp.h"


 2 int listenfd, connfd;


 3 void sig_urg(int);


 4 int

 5 main(int argc, char **argv)

 6 {

 7  int n;

 8  char buff[100];


 9  if (argc == 2)

10   listenfd = Tcp_listen(NULL, argv[1], NULL);

11  else if (argc == 3)

12   listenfd = Tcp_listen(argv[1], argv[2], NULL);

13  else

14   err_quit("usage: tcprecv01 [  ] ");


15  connfd = Accept(listenfd, NULL, NULL);


16  Signal(SIGURG, sig_urg);

17  Fcntl(connfd, F_SETOWN, getpid());


18  for (;;) {

19   if ((n = Read(connfd, buff, sizeof(buff) - 1)) == 0) {

20    printf("received EOF\n");

21    exit(0);

22   }

23   buff[n] = 0; /* завершающий нуль */

24   printf("read bytes: %s\n", n, buff);

25  }

26 }


27 void

28 sig_urg(int signo)

29 {

30  int n;

31  char buff[100];


32  printf("SIGURG received\n");

33  n = Recv(connfd, buff, sizeof(buff) - 1, MSG_OOB);

34  buff[n] = 0; /* завершающий нуль */

35  printf("read OOB byte: %s\n", n, buff);

36 }

Установка обработчика сигнала и владельца сокета

16-17
 Устанавливается обработчик сигнала
SIGURG
и функция
fcntl
задает владельца сокета для данного соединения.

ПРИМЕЧАНИЕ

Обратите внимание, что мы не задаем обработчик сигнала, пока не завершается функция accept. Существует небольшая вероятность того, что внеполосные данные могут прибыть после того, как TCP завершит трехэтапное рукопожатие, но до завершения функции accept. Внеполосные данные мы в этом случае потеряем. Допустим, что мы установили обработчик сигнала перед вызовом функции accept, а также задали владельца прослушиваемого сокета (который затем стал бы владельцем присоединенного сокета). Тогда, если внеполосные данные прибудут до завершения функции accept, наш обработчик сигналов еще не получит значения для дескриптора connfd. Если данный сценарий важен для приложения, следует инициализировать connfd, «вручную» присвоив этому дескриптору значение -1, добавить в обработчик проверку равенства connfd ==-1 и при истинности этого условия просто установить флаг, который будет проверяться в главном цикле после вызова accept. За счет этого главный цикл сможет узнать о поступлении внеполосных данных и считать их. Можно заблокировать сигнал на время вызова accept, но при этом программа будет страдать от всех возможных ситуаций гонок, описанных в разделе 20.5.

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

Обработчик сигнала SIGURG

27-36
Наш обработчик сигнала вызывает функцию
printf
, считывает внеполосные данные, устанавливая флаг
MSG_OOB
, а затем выводит полученные данные. Обратите внимание, что при вызове функции recv мы запрашиваем до 100 байт, но, как мы вскоре увидим, всегда возвращается только один байт внеполосных данных.

ПРИМЕЧАНИЕ

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

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

freebsd % tcprecv01 9999

read 3 bytes: 123

SIGURG received

read 1 OOB byte: 4

read 2 bytes: 56

SIGURG received

read 1 OOB byte: 7

read 2 bytes: 89

received EOF

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

SIGURG
, после чего получатель считывает один байт, содержащий внеполосные данные.

Простой пример использования функции select

Теперь мы переделаем код нашего получателя внеполосных данных и вместо сигнала

SIGURG
будем использовать функцию
select
. В листинге 24.3 показана принимающая программа.

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

//oob/tcprecv02.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int listenfd, connfd, n;

 6  char buff[100];

 7  fd_set rset, xset;


 8  if (argc == 2)

 9   listenfd = Tcp_listen(NULL, argv[1], NULL);

10  else if (argc ==3)

11   listenfd = Tcp_listen(argv[1], argv[2], NULL);

12  else

13   err_quit("usage: tcprecv02 [  ] ");


14  connfd = Accept(listenfd, NULL, NULL);


15  FD_ZERO(&rset);

16  FD_ZERO(&xset);

17  for (;;) {

18   FD_SET(connfd, &rset);

19   FD_SET(connfd, &xset);


20   Select(connfd + 1, &rset, NULL, &xset, NULL);


21   if (FD_ISSET(connfd, &xset)) {

22    n = Recv(connfd, buff, sizeof(buff) - 1, MSG_OOB);

23    buff[n] =0; /* завершающий нуль */

24    printf("read OOB byte: %s\n", n, buff);

25   }

26   if (FD_ISSET(connfd, &rset)) {

27    if ((n = Read(connfd, buff, sizeof(buff) - 1)) == 0) {

28     printf("received EOF\n");

29     exit(0);

30    }

31    buff[n] = 0; /* завершающий нуль */

32    printf("read bytes: %s\n", n, buff);

33   }

34  }

35 }

15-20
 Процесс вызывает функцию
select
, которая ожидает получения либо обычных данных (набор дескрипторов для чтения,
rset
), либо внеполосных (набор дескрипторов для обработки исключений,
xset
). В обоих случаях полученные данные выводятся.

Если мы запустим эту программу, а затем — программу для отправки, которая приведена в листинге 24.1, то столкнемся со следующей ошибкой:

freebsd4 % tcprecv02 9999

read 3 bytes: 123

read 1 OOB byte: 4

recv error: Invalid argument

Проблема заключается в том, что функция

select
будет сообщать об исключительной ситуации, пока процесс не считает данные, находящиеся за отметкой внеполосных данных (то есть после них [128, с. 530-531]). Мы не можем считывать внеполосные данные больше одного раза, так как после первого же их считывания ядро очищает буфер, содержащий один байт внеполосных данных. Когда мы вызываем функцию
recv
, устанавливая флаг
MSG_OOB
во второй раз, она возвращает ошибку
EINVAL
.

Чтобы решить эту проблему, нужно вызывать функцию

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

Листинг 24.4. Модификация программы, приведенной в листинге 24.3. Функция select применяется для проверки исключительной ситуации корректным образом

//oob/tcprecv03.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int listenfd, connfd, n, justreadoob = 0;

 6  char buff[100];

 7  fd_set rset, xset;


 8  if (argc == 2)

 9   listenfd = Tcp_listen(NULL, argv[1], NULL);

10  else if (argc == 3)

11   listenfd = Tcp_1isten(argv[1], argv[2], NULL);

12  else

13   err_quit("usage: tcprecv03 [  ] ");


14  connfd = Accept(listenfd, NULL, NULL);


15  FD_ZERO(&rset);

16  FD_ZERO(&xset);

17  for (;;) {

18   FD_SET(connfd, &rset);

19   if (justreadoob == 0)

20    FD_SET(connfd, &xset);

21   Select(connfd + 1, &rset, NULL, &xset, NULL);


22   if (FD_ISSET(connfd, &xset)) {

23    n = Recv(connfd, buff, sizeof(buff) - 1, MSG_OOB);

24    buff[n] = 0; /* завершающий нуль */

25    printf("read %d OOB byte: %s\n", n, buff);

26    justreadoob = 1;

27    FD_CLR(connfd, &xset);

28   }

29   if (FD_ISSET(connfd, &rset)) {

30    if ((n = Read(connfd, buff, sizeof(buff) - 1)) == 0) {

31     printf("received EOF\n");

32     exit(0);

33    }

34    buff[n] = 0; /* завершающий нуль */

35    printf("read %d bytes: %s\n", n, buff);

36    justreadoob = 0;

37   }

38  }

39 }

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

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

Теперь программа работает так, как мы ожидали.

24.3. Функция sockatmark

С приемом внеполосных данных всегда связана так называемая отметка внеполосных данных (out-of-bandmark). Это позиция в потоке обычных данных на стороне отправителя, соответствующая тому моменту; когда посылающий процесс отправляет байт, содержащий внеполосные данные. Считывая данные из сокета, принимающий процесс путем вызова функции

sockatmark
определяет, находится ли он в данный момент на этой отметке.

#include 


int sockatmark(int sockfd);

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

ПРИМЕЧАНИЕ

Эта функция появилась в POSIX. Разработчики стандарта POSIX стремятся заменить отдельными функциями все вызовы ioctl с различными параметрами.

В листинге 24.5 показана реализация этой функции с помощью поддерживаемого в большинстве систем параметра

SIOCATMARK
функции
ioctl
.

Листинг 24.5. Функция sockatmark реализована с использованием функции ioctl

//lib/sockatmark.c

1 #include "unp.h"


2 int

3 sockatmark(int fd)

4 {

5  int flag;


6  if (ioctl(fd, SIOCATMARK, &flag) < 0)

7   return (-1);

8  return (flag != 0 ? 1 : 0);

9 }

Отметка внеполосных данных применима независимо от того, как принимающий процесс получает внеполосные данные: вместе с обычными данными (параметр сокета

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

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

Далее мы приводим простой пример, иллюстрирующий следующие две особенности отметки внеполосных данных:

1. Отметка внеполосных данных всегда указывает на один байт дальше конечного байта обычных данных. Это означает, что, когда внеполосные данные получены вместе с обычными, функция

sockatmark
возвращает 1, если следующий считываемый байт был послан с флагом
MSG_OOB
. Если параметр
SO_OOBINLINE
не включен (состояние по умолчанию), то функция
sockatmark
возвращает 1, когда следующий байт данных является первым байтом, посланным следом за внеполосными данными.

2. Операция считывания всегда останавливается на отметке внеполосных данных [128, с. 519–520]. Это означает, что если в приемном буфере сокета 100 байт, но только 5 из них расположены перед отметкой внеполосных данных, то когда процесс выполнит функцию

read
, запрашивая 100 байт, возвратятся только 5 байт, расположенные до этой отметки. Эта вынужденная остановка на отметке позволяет процессу вызвать функцию
sockatmark
, которая определит, находится ли указатель буфера на отметке внеполосных данных.

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

В листинге 24.7 показана принимающая программа. В ней не используется ни функция

select
, ни сигнал
SIGURG
. Вместо этого в ней вызывается функция
sokatmark
, определяющая положение байта внеполосных данных.

Листинг 24.6. Программа отправки

//oob/tcpsen04.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;


 6  if (argc != 3)

 7   err_quit("usage: tcpsend04 ");


 8  sockfd = Tcp_connect(argv[1], argv[2]);


 9  Write(sockfd, "123", 3);

10  printf("wrote 3 bytes of normal data\n");


11  Send(sockfd, "4", 1, MSG_OOB);

12  printf("wrote 1 byte of OOB data\n");


13  Write(sockfd, "5", 1);

14  printf("wrote 1 byte of normal data\n");


15  exit(0);

16 }

Листинг 24.7. Принимающая программа, в которой вызывается функция sokatmark

//oob/tcprecv04.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int listenfd, connfd, n, on = 1;

 6  char buff[100];


 7  if (argc == 2)

 8   listenfd = Tcp_listen(NULL, argv[1], NULL);

 9  else if (argc == 3)

10   listenfd = Tcp_listen(argv[1], argv[2], NULL);

11  else

12   err_quit("usage- tcprecv04 [  ] ");


13  Setsockopt(listenfd, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on));


14  connfd = Accept(listenfd, NULL, NULL);

15  sleep(5);


16  for (;;) {

17   if (Sockatmark(connfd))

18    printf("at OOB mark\n");


19   if ((n = Read(connfd, buff, sizeof(buff) - 1)) == 0) {

20    printf("received EOF\n");

21    exit(0);

22   }

23   buff[n] = 0; /* завершающий нуль */

24   printf("read %d bytes: %s\n", n; buff);

25  }

26 }

Включение параметра сокета SO_OOBINLINE

13
 Мы хотим принимать внеполосные данные вместе с обычными данными, поэтому нам нужно включить параметр
SO_OOBINLINE
. Но если мы будем ждать, когда выполнится функция accept и установит этот параметр для присоединенного сокета, трехэтапное рукопожатие завершится и внеполосные данные могут уже прибыть. Поэтому нам нужно установить этот параметр еще для прослушиваемого сокета, помня о том, что все параметры прослушиваемого сокета наследуются присоединенным сокетом (см. раздел 7.4).

Вызов функции sleep после вызова функции accept

14-15
 После того как выполнена функция
accept
, получатель переходит в спящее состояние, что позволяет получить все данные, посланные отправителем. Это позволяет нам продемонстрировать, что функция read останавливается на отметке внеполосных данных, даже если в приемном буфере сокета имеются дополнительные данные.

Считывание всех отправленных данных

16-25
 В программе имеется цикл, в котором вызывается функция
read
и выводятся полученные данные. Но перед вызовом функции
read
функция
sockatmark
проверяет, находится ли указатель буфера на отметке внеполосных данных.

После выполнения этой программы мы получаем следующий результат:

freebsd4 % tcprecv04 6666

read 3 bytes: 123

at OOB mark

read 2 bytes: 45

received EOF

Хотя принимающий TCP получил все посланные данные, первый вызов функции

read
возвращает только три байта, так как была обнаружена отметка внеполосных данных. Следующий считанный байт — это байт, содержащий внеполосные данные (его значение равно 4), так как мы дали ядру указание поместить внеполосные данные вместе с обычными.

Пример: дополнительные свойства внеполосных данных

Теперь мы покажем другой столь же простой пример, иллюстрирующий две дополнительные особенности внеполосных данных, о которых мы уже упоминали ранее.

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

2. Принимающий процесс может получить уведомление о том, что отправитель отослал внеполосные данные (с помощью сигнала

SIGURG
или функции
select
) до того, как эти данные фактически прибудут. Если после получения этого уведомления процесс вызывает функцию
recv
, задавая флаг
MSG_OOB
, а внеполосные данные еще не прибыли, то будет возвращена ошибка
EWOULDBLOCK
.

В листинге 24.8 приведена программа отправки.

Листинг 24.8. Программа отправки

//oob/tcpsend05.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd, size;

 6  char buff[16384];


 7  if (argc != 3)

 8   err_quit("usage: tcpsend04 ");


 9  sockfd = Tcp_connect(argv[1], argv[2]);


10  size = 32768;

11  Setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));


12  Write(sockfd, buff, 16384);

13  printf("wrote 16384 bytes of normal data\n");

14  sleep(5);


15  Send(sockfd, "a", 1, MSG_OOB);

16  printf("wrote 1 byte of OOB data\n");


17  Write(sockfd, buff, 1024);

18  printf("wrote 1024 bytes of normal data\n");


19  exit(0);

20 }

9-19
 Этот процесс устанавливает размер буфера отправки сокета равным 32 768 байт, записывает 16 384 байт обычных данных, а затем на 5 с переходит в спящее состояние. Чуть ниже мы увидим, что приемник устанавливает размер приемного буфера сокета равным 4096 байт, поэтому данные, отправленные отсылающим TCP, с гарантией заполнят приемный буфер сокета получателя. Затем отправитель посылает один байт внеполосных данных, за которым следуют 1024 байт обычных данных, и, наконец, закрывает соединение.

В листинге 24.9 представлена принимающая программа.

Листинг 24.9. Принимающая программа

//oob/tcprecv05.c

 1 #include "unp.h"


 2 int listenfd, connfd;


 3 void sig_urg(int);


 4 int

 5 main(int argc, char **argv)

 6 {

 7  int size;


 8  if (argc == 2)

 9   listenfd = Tcp_listen(NULL, argv[1], NULL);

10  else if (argc == 3)

11   listenfd = Tcp_listen(argv[1], argv[2], NULL);

12  else

13   err_quit("usage: tcprecv05 [  ] ");


14  size = 4096;

15  Setsockopt(listenfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));


16  connfd = Accept(listenfd, NULL, NULL);


17  Signal(SIGURG, sig_urg);

18  Fcntl(connfd, F_SETOWN, getpid());


19  for (;;)

20   pause();

21 }


22 void

23 sig_urg(int signo)

24 {

25  int n;

26  char buff[2048];


27  printf("SIGURG received\n");

28  n = Recv(connfd, buff, sizeof(buff) - 1, MSG_OOB);

29  buff[n] = 0; /* завершающий пустой байт */

30  printf("read %d OOB byte\n", n);

31 }

14-20
 Принимающий процесс устанавливает размер приемного буфера сокета приемника равным 4096 байт. Этот размер наследуется присоединенным сокетом после установления соединения. Затем процесс вызывает функцию
accept
, задает обработчик для сигнала
SIGURG
и задает владельца сокета. В главном цикле (бесконечном) вызывается функция
pause
.

22-31
 Обработчик сигнала вызывает функцию
recv
для считывания внеполосных данных.

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

macosx % tcpsend05 freebsd 5555

wrote 16384 bytes of normal data

wrote 1 byte of OOB data

wrote 1024 bytes of normal data

Как и ожидалось, все данные помещаются в буфер отправки сокета отправителя, и программа завершается. Ниже приведен результат работы принимающей программы:

freebsd4 % tcprecv05 5555

SIGURG received

recv error: Resource temporarily unavailable

Сообщение об ошибке, которое выдает наша функция

err_sys
, соответствует ошибке
EAGAIN
, которая в FreeBSD аналогична ошибке
EWOULDBLOCK
. TCP посылает уведомление об отправке внеполосных данных принимающему TCP, который в результате генерирует сигнал
SIGURG
для принимающего процесса. Но когда вызывается функция
recv
и задается флаг
MSG_OOB
, байт с внеполосными данными не может быть прочитан.

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

ПРИМЕЧАНИЕ

В реализациях, происходящих от Беркли [128, с. 1016-1017], можно отметить две близких проблемы. Во-первых, даже если приемный буфер сокета заполнен, ядро всегда принимает от процесса внеполосные данные для отправки собеседнику. Во-вторых, когда отправитель посылает байт с внеполосными данными, немедленно посылается сегмент TCP, содержащий срочное уведомление. Все обычные проверки вывода TCP (алгоритм Нагла, предотвращение синдрома «глупого окна») при этом блокируются.

Пример: единственность отметки внеполосных данных в TCP

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

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

send
для отправки внеполосных данных и еще одну функцию
write
для записи обычных данных.

Листинг 24.10. Отправка двух байтов внеполосных данных друг за другом

//oob/tcpsend06.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;


 6  if (argc != 3)

 7   err_quit("usage: tcpsend04 ");


 8  sockfd = Tcp_connect(argv[1], argv[2]);


 9  Write(sockfd, "123", 3);

10  printf("wrote 3 bytes of normal data\n");


11  Send(sockfd, "4", 1, MSG_OOB);

12  printf("wrote 1 byte of OOB data\n");


13  Write(sockfd, "5", 1);

14  printf("wrote 1 byte of normal data\n");


15  Send(sockfd,. "6", 1, MSG_OOB);

16  printf("wrote 1 byte of OOB data\n");


17  Write(sockfd, "7", 1);

18  printf("wrote 1 byte of normal data\n");


19  exit(0);

20 }

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

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

sleep
, которая после установления соединения переводит получателя в спящее состояние на 5 с, чтобы позволить данным прибыть на принимающий TCP. Ниже приводится результат выполнения этой программы:

freebsd4 % tcprecv06 5555

read 5 bytes: 12345

at OOB mark

read 2 bytes: 67

received EOF

Прибытие второго байта внеполосных данных (

6
) изменяет отметку, которая ассоциировалась с первым прибывшим байтом внеполосных данных (
4
). Как мы сказали, для конкретного соединения TCP допускается только одна отметка внеполосных данных.

24.4. Резюме по теме внеполосных данных TCP

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

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

SIGURG
, либо с помощью функции
select
. Это уведомление передается сразу же после того, как отправитель посылает байт внеполосных данных, поскольку, как показано в листинге 24.9, TCP посылает уведомление, даже если поток каких-либо данных от сервера к клиенту остановлен функциями управления потоком. В результате получения такого уведомления получатель может входить в определенный специальный режим обработки последующих данных.

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

3. Фактическое значение внеполосного байта. Поскольку TCP является потоковым протоколом, который не интерпретирует данные, посланные приложением, это может быть любое 8-разрядное значение.

Говоря о срочном режиме TCP, мы можем рассматривать флаг URG как уведомление, а срочный указатель как внеполосную отметку.

Проблемы, связанные с концепцией внеполосных данных, сформулированы в следующих пунктах:

1. Для каждого соединения имеется только один срочный указатель.

2. Для каждого соединения допускается только одна отметка внеполосных данных.

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

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

Типичный пример использования внеполосных данных — протокол Rlogin, задействующий эту концепцию в ситуации, когда клиент прерывает программу, выполняемую на стороне сервера [111, с. 393–394]. Сервер должен сообщить клиенту, что нужно сбросить все данные, принятые от сервера, буферизованные и предназначенные для вывода на терминал. Сервер посылает клиенту специальный байт внеполосных данных, указывая тем самым, что необходимо сбросить все полученные данные. Когда клиент получает сигнал

SIGURG
, он просто считывает данные из сокета, пока не встречает отметку внеполосных данных, после чего он сбрасывает все данные вплоть до этой отметки. (В [111, с. 398–401] показан пример подобного использования внеполосных данных вместе с выводом программы
tcpdump
.) Если в этом сценарии сервер посылает несколько внеполосных байтов, следующих с небольшими промежутками друг за другом, то такая последовательность не оказывает влияния на клиента, поскольку тот просто сбрасывает все данные, расположенные до последней отметки внеполосных данных.

В итоге можно сказать, что польза применения внеполосных данных зависит от того, для каких целей они служат в приложении. Если их назначение в том, чтобы сообщить собеседнику о необходимости сбросить все обычные данные, расположенные до отметки, то утрата промежуточных внеполосных данных и их отметок не повлечет никаких последствий. Но если потеря внеполосных данных недопустима, то эти данные следует получать вместе с обычными данными. Более того, байты, посланные как внеполосные данные, требуется каким-то образом отличать от обычных данных, так как промежуточные отметки могут быть перезаписаны при получении новых внеполосных данных. Telnet, например, посылает свои собственные команды в потоке обычных данных между клиентом и сервером, но ставит перед этими командами байт, содержащий 255 (поэтому для отправки этого значения требуется послать последовательно два байта, содержащих 255). Эти байты позволяют отличить команды сервера от обычных пользовательских данных, но при этом для обнаружения команд сервера требуется, чтобы клиент и сервер обрабатывали каждый байт данных.

24.5. Резюме

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

В API сокетов срочный режим TCP сопоставляется внеполосным данным. Отправитель входит в срочный режим, задавая флаг

MSG_OOB
при вызове функции send. Последний байт данных, переданных с помощью этой функции, считается внеполосным байтом. Приемник получает уведомление о том, что его TCP получил новый срочный указатель. Это происходит либо с помощью сигнала
SIGURG
, либо с помощью функции
select
, которая указывает, что на сокете возникла исключительная ситуация. По умолчанию TCP извлекает байт с внеполосными данными и помещает его в специальный однобайтовый буфер для внеполосных данных, откуда принимающий процесс считывает его с помощью вызова функции
recv
с флагом
MSG_OOB
. Имеется другой вариант — получатель может включить параметр сокета
SO_OOBINLINE
, и тогда внеполосный байт остается в потоке обычных данных. Независимо от того, какой метод используется принимающей стороной, уровень сокета поддерживает отметку внеполосных данных в потоке данных, и операция считывания остановится, когда дойдет до этой отметки. Чтобы определить, достигнута ли эта отметка, принимающий процесс использует функцию
sockatmark
.

Внеполосные данные применяются не очень широко. Они используются в протоколах Telnet и Rlogin, а также FTP. Во всех случаях внеполосные данные уведомляют собеседника об исключительной ситуации (например, прерывании на стороне клиента), после чего собеседник сбрасывает все принятые данные до отметки внеполосных данных.

Упражнения

1. Есть ли разница между одним вызовом функции

send(fd, "ab", 2, MSG_OOB);

и двумя последовательными вызовами

send(fd, "a", 1, MSG_OOB);

send(fd, "b", 1, MSG_OOB);

?

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

poll
вместо функции
select
.

Глава 25