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

Потоки (STREAMS)

31.1. Введение

В этой главе мы приводим обзор потоков STREAMS и функций, используемых приложением для доступа к потоку. Наша цель — понять, как реализованы сетевые протоколы в рамках потоковых систем. Также мы создаем простой клиент TCP с использованием TPI — интерфейса, который обеспечивает доступ к транспортному уровню и обычно применяется сокетами в системах, основанных на потоках. Дополнительную информацию о потоках, в том числе о написании программ для ядер, использующих потоки, можно найти в [98].

ПРИМЕЧАНИЕ

Технология потоков была введена Денисом Ритчи (Dennis Ritchie) [104] и получила широкое распространение с появлением системы SVR3 в 1986 году. Спецификация POSIX определяет STREAMS как «дополнительную группу», то есть система может не поддерживать потоки STREAMS, но если она их поддерживает, то реализация должна соответствовать POSIX. Любая система, производная от System V, должна поддерживать потоки, а различные системы 4x.BSD потоки не поддерживают.

Потоковая система часто обозначается как STREAMS, но поскольку это название не является акронимом, то в данной книге используется слово «потоки».

Не следует смешивать «потоковую систему ввода-вывода» (streams I/O system), которую мы описываем в данной главе, и «стандартные потоки ввода-вывода» (standard I/O streams), а также программные потоки (threads). Второй термин используется применительно к стандартной библиотеке ввода-вывода (например, таким функциям, как fopen, fgets, printf и т.п.).

31.2. Обзор

Потоки обеспечивают двустороннее соединение между процессом и драйвером, как показано на рис. 31.1. Хотя нижний блок на этом рисунке мы и называем драйвером, его не следует ассоциировать с каким-либо аппаратным устройством, поскольку это может быть и драйвер псевдоустройства (например, программный драйвер).

Рис. 31.1. Поток между процессом и драйвером

Головной модуль потока (stream head) состоит из программ ядра, которые запускаются при обращении приложения к дескриптору потока (например, при вызове функций

read
,
putmsg
,
ioctl
и т.п.).

Процесс может динамически добавлять и удалять промежуточные модули обработки (processing modules) между головным модулем и драйвером. Такой модуль осуществляет некий тип фильтрации сообщений, проходящих в одну или другую сторону по потоку. Этот процесс показан на рис. 31.2.

Рис. 31.2. Поток с модулем обработки

В поток может быть помещено любое количество модулей. Под словом «поместить» (push) в данном случае понимается, что каждый новый модуль вставляется сразу после (на рисунке — ниже) головного модуля.

Определенный тип псевдодрайвера называется мультиплексором (multiplexor). Он принимает данные из различных источников. Основанная на потоках реализация набора протоколов TCP/IP, используемая, например, в SVR4, может иметь вид, показанный на рис. 31.3.

Рис. 31.3. Упрощенный вид реализации набора протоколов TCP/IP, основанной на потоках

■ При создании сокета библиотекой сокетов в поток помещается модуль

sockmod
. Именно комбинация библиотеки сокетов и потокового модуля обеспечивает API сокетов для процесса.

■ При создании точки доступа XTI библиотекой XTI в поток помещается модуль

timod
. Именно комбинация библиотеки XTI и потокового модуля обеспечивает API XTI для процесса.

ПРИМЕЧАНИЕ

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

■ Для использования функций

read
или
write
в точке доступа XTI требуется поместить в поток потоковый модуль
tirdwr
. Это осуществляется процессом, использующим TCP, который на рис. 31.3 изображен четвертым слева. Вероятно, этот процесс тем самым отказался от использования XTI, поэтому мы убрали надпись «библиотека XTI» из соответствующего блока.

■ Формат сетевых сообщений, передаваемых по потокам вверх и вниз, определяют интерфейсы различных сервисов. Мы описываем три наиболее широко распространенных. TPI (Transport Provider Interface — интерфейс поставщика транспортных служб) [126] определяет интерфейс, предоставляемый поставщиком услуг транспортного уровня (например, TCP или UDP). NPI (Network Provider Interface — интерфейс поставщика сетевого уровня) [125] определяет интерфейс, предоставляемый поставщиком услуг сетевого уровня (например, IP). DLPI (Data Link Provider Interface) — это интерфейс поставщика канального уровня [124]. Еще один источник информации по TPI и DLPI, в котором имеются также исходные коды на языке С, — это [98].

Каждый компонент потока — головной модуль, все модули обработки и драйвер — содержат по меньшей мере одну пару очередей: очередь на запись и очередь на чтение. Это показано на рис. 31.4.

Рис. 31.4. Каждый компонент потока содержит по меньшей мере одну пару очередей

Типы сообщений

Потоковые сообщения могут быть классифицированы как имеющие высокий приоритет (high priority), входящие в полосу приоритета (priority band) и обычные (normal). Существует 256 полос приоритета со значениями между 0 и 255, причем обычные сообщения соответствуют полосе 0. Приоритет потокового сообщения используется как при постановке сообщения в очередь, так и для управления потоком (flow control). По соглашению, на сообщения с высоким приоритетом управление потоком не влияет.

На рис. 31.5 показан порядок следования сообщений в одной конкретной очереди.

Рис. 31.5. Порядок следования потоковых сообщений в очереди в зависимости от их приоритета

Хотя потоковые системы поддерживают 256 различных полос приоритета, в сетевых протоколах обычно используется полоса 1 для срочных (внеполосных) данных и полоса 0 для обычных данных.

ПРИМЕЧАНИЕ

Внеполосные данные TCP в TPI не рассматриваются как истинные срочные данные. В самом деле, в TCP полоса 0 используется как для обычных, так и для внеполосных данных. Полоса 1 используется для отправки срочных данных в тех протоколах, в которых срочные данные (а не просто срочный указатель, как в TCP) отправляются перед обычными данными. В данном контексте следует внимательно отнестись к термину «обычный» (normal). В системах SVR, предшествующих SVR4, не было полос приоритета, а сообщения делились на обычные и приоритетные (priority messages). В SVR4 были введены полосы приоритета, что потребовало также введения функций getpmsg и putpmsg, которые мы вскоре опишем. Приоритетные сообщения были переименованы в сообщения с высоким приоритетом, и встал вопрос, как называть сообщения, относящиеся к полосам приоритета от 1 до 255. Наиболее распространенной является терминология [98], согласно которой все сообщения, которые не являются сообщениями с высоким приоритетом, называются обычными сообщениями и разделяются на подкатегории согласно своим полосам приоритета. Термин «обычное сообщение» в любом случае должен соответствовать сообщению из полосы приоритета 0.

Хотя пока мы говорили только о сообщениях с высоким приоритетом и об обычных сообщениях, существует около 12 типов обычных сообщений и около 18 типов сообщений с высоким приоритетом. С точки зрения приложений и функций

getmsg
и
putmsg
, которые мы опишем в следующем разделе, нам интересны только три различных типа сообщений:
M_DATA
,
M_PROTO
и
M_PCPROTO
(
PC
означает «priority control», то есть приоритетное управление, и подразумевает сообщения с высоким приоритетом). В табл. 31.1 показано, как эти три типа сообщений генерируются функциями
write
и
putmsg
.


Таблица 31.1. Типы потоковых сообщений, генерируемые функциями write и putmsg

ФункцияУправляющая информация?Данные?ФлагиГенерируемый тип сообщения
writeДаM_DATA
putmsgНетДа0M_DATA
putmsgДаВсе равно0M_PROTO
putmsgДаВсе равноMSG_HIPRIM_PCPROTO

31.3. Функции getmsg и putmsg

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

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

#include 


int getmsg(int fd, struct strbuf *ctlptr, struct strbuf *dataptr, int *flagsp);

int putmsg(int fd, const struct strbuf *ctlptr,

 const struct strbuf *dataptr, int flags);

Обе функции возвращают: неотрицательное значение в случае успешного выполнения (см. пояснения в тексте), -1 в случае ошибки

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

strbuf
:

struct strbuf {

 int  maxlen; /* максимальный размер буфера buf */

 int  len;    /* фактическое количество данных в buf */

 char *buf;   /* данные */

};

ПРИМЕЧАНИЕ

Обратите внимание на аналогию между структурами strbuf и netbuf. Имена элементов обеих структур одинаковы.

Однако обе длины в структуре netbuf относятся к типу данных unsigned int (целое без знака), тогда как обе длины в структуре srtbuf — к типу int (целое со знаком). Причина в том, что некоторые потоковые функции используют значение -1 элементов len и maxlen для указания на определенные специальные ситуации.

С помощью функции

putmsg
мы можем отправлять или данные, или управляющую информацию, или и то и другое вместе. Для указания на отсутствие управляющей информации мы можем или задать
ctlptr
как пустой указатель, или установить значение
ctlptr->len
равным -1. Этот же способ используется для указания на отсутствие данных.

При отсутствии управляющей информации функцией

putmsg
генерируется сообщение типа
M_DATA
(см. табл. 31.1), в противном случае генерируется сообщение типа
M_PROTO
либо
M_PCPROTO
в зависимости от значения аргумента
flags
. Этот аргумент функции
putmsg
имеет нулевое значение для обычных сообщений, а для сообщений с высоким приоритетом его значение равно
RS_HIPRI
.

Последний аргумент функции

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

Предположим, что мы передаем функции

getmsg
непустые указатели
ctlptr
и
dataptr
. Тогда указанием на отсутствие управляющей информации (возвращается сообщение типа
M_DATA
) является значение
ctlptr->len
, установленное в -1. Аналогично, если отсутствуют данные, указанием на это является значение -1 элемента
dataptr->len
.

Если функция

putmsg
выполнилась успешно, то она возвращает нулевое значение, а в случае ошибки возвращается значение -1. Но функция
getmsg
возвращает нулевое значение только в том случае, если вызывающему процессу было доставлено все сообщение целиком. Если буфер, предназначенный для приема управляющей информации, слишком мал, то возвращается значение
MORECTL
(о котором заранее известно, что оно является неотрицательным). Аналогично, если буфер для приема данных оказывается слишком мал, возвращается значение
MOREDATA
. Если же оба эти буфера оказываются слишком малы, то возвращается логическая сумма этих двух флагов.

31.4. Функции getpmsg и putpmsg

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

getmsg
и
putmsg
.

#include 


int getpmsg(int fd, struct strbuf *ctlptr,

 struct strbuf *dataptr, int *bandp, int *flagsp);

int putpmsg(int fd, const struct strbuf *ctlptr,

 const struct strbuf *dataptr, int band, int flags);

Обе функции возвращают: неотрицательное значение в случае успешного выполнения, -1 в случае ошибки

Аргумент

band
функции
putpmsg
должен иметь значение в пределах от 0 до 255 включительно. Если аргумент
flags
имеет значение
MSG_BAND
, то генерируется сообщение в соответствующей полосе приоритета. Присваивание аргументу
flags
значения
MSG_BAND
и задание полосы 0 эквивалентно вызову функции
putmsg
. Если значение аргумента
flags
равно
MSG_HIPRI
, то аргумент
band
должен быть равен нулю, и тогда генерируется сообщение с высоким приоритетом. (Обратите внимание на то, что этот флаг имеет название, отличающееся от названия
RS_HIPRI
, используемого в случае функции
putmsg
.)

Два целочисленных значения, на которые указывают аргументы

bandp
и
flagsp
функции
getpmsg
, являются аргументами типа «значение-результат». Целочисленное значение, на которое указывает аргумент
flagsp
функции
getpmsg
, может соответствовать
MSG_HIPRI
(для чтения сообщений с высоким приоритетом),
MSG_BAND
(для чтения сообщений из полосы приоритета, по меньшей мере равной целочисленному значению, на которое указывает аргумент
bandp
) или
MSG_ANY
(для чтения любых сообщений). По завершении функции целочисленное значение, на которое указывает аргумент
bandp
, указывает на полосу приоритета прочитанного сообщения, а целое число, на которое указывает аргумент
flagsp
, соответствует
MSG_HIPRI
(если было прочитано сообщение с высоким приоритетом) или MSG_BAND (если было прочитано иное сообщение).

31.5. Функция ioctl

Говоря о потоках, мы снова возвращаемся к функции

ioctl
, которая уже была описана в главе 17.

#include 


int ioctl(int fd, int request, ... /* void *arg */ );

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

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

Существует примерно 30 запросов (

request
), так или иначе влияющих на головной модуль потока. Каждый из запросов начинается с
I_
, и обычно документация на них приводится на странице руководства
streamio
.

31.6. TPI: интерфейс поставщика транспортных служб

На рис. 31.3 мы показали, что TPI — это интерфейс, предоставляющий доступ к транспортному уровню для расположенных выше уровней. Этот интерфейс используется в потоковой среде как сокетами, так и XTI. Из рис. 31.3 видно, что комбинация библиотеки сокетов и

sokmod
, а также комбинация библиотеки XTI и
timod
обмениваются сообщениями TPI с TCP и UDP.

TPI является интерфейсом, основанным на сообщениях (message-based). Он определяет сообщения, которыми обменивается приложение (например, XTI или библиотека сокетов) и транспортный уровень. Точнее, TPI задает формат этих сообщений и то, какое действие производит каждое из сообщений. Во многих случаях приложение посылает запрос поставщику (например, «Связать данный локальный адрес»), а поставщик посылает обратно ответ («Выполнено» или «Ошибка»). Некоторые события, происходящие асинхронно на стороне поставщика (например, прибытие запроса на соединение с сервером), инициируют отправку сигнала или сообщения вверх по потоку.

Мы можем обойти как XTI, так и сокеты, и использовать непосредственно TPI. В этом разделе мы заново перепишем код нашего простого клиента времени и даты с использованием TPI вместо сокетов (сокетная версия представлена в листинге 1.1). Если провести аналогию с языками программирования, то использование XTI или сокетов можно сравнить с программированием на языках высокого уровня, таких как С или Pascal, а непосредственно TPI — с программированием на ассемблере. Мы не являемся сторонниками непосредственного использования TPI в реальной жизни. Но понимание того, как работает TPI, и написание примера с использованием этого протокола позволит нам глубже понять, как работает библиотека сокетов в потоковой среде.

В листинге 31.1[1] показан наш заголовочный файл

tpi_daytime.h
.

Листинг 31.1. Наш заголовочный файл tpi_daytime.h

//streams/tpi_daytime.h

 1 #include "unpxti.h"

 2 #include 

 3 #include 


 4 void tpi_bind(int, const void*, size_t);

 5 void tpi_connect(int, const void*, size_t);

 6 ssize_t tpi_read(int, void*, size_t);

 7 void tpi_close(int);

Нам нужно включить еще один дополнительный заголовочный файл помимо

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

Листинг 31.2. Функция main для нашего клиента времени и даты с использованием TPI

//streams/tpi_daytime.c

 1 #include "tpi_daytime.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int fd, n;

 6  char recvline[MAXLINE + 1];

 7  struct sockaddr_in myaddr, servaddr;


 8  if (argc != 2)

 9   err_quit("usage: tpi_daytime ");


10  fd = Open(XTI_TCP, O_RDWR, 0);


11  /* связываем произвольный локальный адрес */

12  bzero(&myaddr, sizeof(myaddr));

13  myaddr.sin_family = AF_INET;

14  myaddr.sin_addr.s_addr = htonl(INADDR_ANY);

15  myaddr.sin_port = htons(0);


16  tpi_bind(fd, &myaddr, sizeof(struct sockaddr_in));


17  /* заполняем адрес сервера */

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

19  servaddr.sin_family = AF_INET;

20  servaddr.sin_port = htons(13); /* сервер времени и даты */

21  Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);


22  tpi_connect(fd, &servaddr, sizeof(struct sockaddr_in));


23  for (;;) {

24   if ((n = tpi_read(fd, recvline, MAXLINE)) <= 0) {

25    if (n == 0)

26     break;

27    else

28    err_sys("tpi_read error");

29   }

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

31   fputs(recvline, stdout);

32  }

33  tpi_close(fd);

34  exit(0);

35 }

Открытие транспортного устройства, связывание локального адреса

10-16
 Мы открываем устройство, соответствующее поставщику транспортных служб (обычно
/dev/tcp
). Мы заполняем структуру адреса сокета Интернета значениями
INADDR_ANY
и 0 (для порта), указывая тем самым TCP связать произвольный локальный адрес с нашей точкой доступа. Мы вызываем свою собственную функцию
tpi_bind
(которая будет приведена чуть ниже) для выполнения этого связывания.

Заполнение структуры адреса сервера, установление соединения

17-22
 Мы заполняем другую структуру адреса сокета Интернета, внося в нее IP-адрес сервера (из командной строки) и порт (13). Мы вызываем нашу функцию
tpi_connect
для установления соединения.

Считывание данных с сервера, копирование в стандартный поток вывода

23-33
 Как и в случае других клиентов времени и даты, мы просто копируем данные, пришедшие по соединению, в стандартный поток вывода, останавливаясь при получении признака конца файла, присланного сервером (например, сегмент FIN). Мы сделали этот цикл похожим на тот, который использовался в коде сокетного клиента (см. листинг 1.1), поскольку наша функция
tpi_read
при нормальном завершении соединения на стороне сервера будет возвращать нулевое значение. Затем мы вызываем нашу функцию
tpi_close
для того, чтобы закрыть эту точку доступа.

Наша функция

tpi_bind
показана в листинге 31.3.

Листинг 31.3. Функция tpi_bind: связывание локального адреса с точкой доступа

//streams/tpi_bind.c

 1 #include "tpi_daytime.h"


 2 void

 3 tpi_bind(int fd, const void *addr, size_t addrlen)

 4 {

 5  struct {

 6   struct T_bind_req msg_hdr;

 7   char addr[128];

 8  } bind_req;

 9  struct {

10   struct T_bind_ack msg_hdr;

11   char addr[128];

12  } bind_ack;

13  struct strbuf ctlbuf;

14  struct T_error_ack *error_ack;

15  int flags;

16  bind_req.msg_hdr.PRIM_type = T_BIND_REQ;

17  bind_req.msg_hdr.ADDR_length = addrlen;

18  bind_req.msg_hdr.ADDR_offset = sizeof(struct T_bind_req);

19  bind_req.msg_hdr.CONIND_number = 0;

20  memcpy(bind_req.addr, addr, addrlen); /* sockaddr_in{} */


21  ctlbuf.len = sizeof(struct T_bind_req) + addrlen;

22  ctlbuf.buf = (char*)&bind_req;

23  Putmsg(fd, &ctlbuf, NULL, 0);


24  ctlbuf.maxlen = sizeof(bind_ack);

25  ctlbuf.len = 0;

26  ctlbuf.buf = (char*)&bind_ack;

27  flags = RS_HIPRI;

28  Getmsg(fd, &ctlbuf, NULL, &flags);

29  if (ctlbuf.len < (int)sizeof(long))

30   err_quit("bad length from getmsg");


31  switch (bind_ack.msg_hdr.PRIM_type) {

32  case T_BIND_ACK:

33   return;


34  case T_ERROR_ACK:

35   if (ctlbuf.len < (int)sizeof(struct T_error_ack))

36    err_quit("bad length for T_ERROR_ACK");

37   error_ack = (struct T_error_ack*)&bind_ack.msg_hdr;

38   err_quit("T_ERROR_ACK from bind (%d, %d)",

39    error_ack->TLI_error, error_ack->UNIX_error);


40  default:

41   err_quit("unexpected message type: %d", bind_ack.msg_hdr.PRlM_type);

42  }

43 }

Заполнение структуры T_bind_req

16-20
 Заголовочный файл
определяет структуру
T_bind_req
:

struct T_bind_req {

 long          PRIM_type;     /* T_BIND_REQ */

 long          ADDR_length;   /* длина адреса */

 long          ADDR_offset;   /* смещение адреса */

 unsigned long CONIND_number; /* сообщения о соединении */

 /* далее следует адрес протокола для связывания */

};

Все запросы TPI определяются как структуры, начинающиеся с поля типа

long
. Мы определяем свою собственную структуру
bind_req
, начинающуюся со структуры
T_bind_req
, после которой располагается буфер, содержащий локальный адрес для связывания. TPI ничего не говорит о содержимом буфера — оно определяется поставщиком. Поставщик TCP предполагает, что этот буфер содержит структуру
sockaddr_in
.

Мы заполняем структуру

T_bind_req
, устанавливая элемент
ADDR
_length равным размеру адреса (16 байт для структуры адреса сокета Интернета), а элемент
ADDR_offset
— равным байтовому сдвигу адреса (он следует непосредственно за структурой
T_bind_req
). У нас нет гарантии, что это местоположение соответствующим образом выровнено для записи структуры
sockaddr_in
, поэтому мы вызываем функцию
memcpy
, чтобы скопировать структуру вызывающего процесса в нашу структуру
bind_req
. Мы присваиваем элементу
CONIND_number
нулевое значение, потому что мы находимся на стороне клиента, а не на стороне сервера.

Вызов функции putmsg

21-23
TPI требует, чтобы только что созданная нами структура была передана поставщику как одно сообщение
M_PROTO
. Следовательно, мы вызываем функцию
putmsg
, задавая структуру
bind_req
в качестве управляющей информации, без каких-либо данных и с флагом 0.

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

24-30
Ответом на наш запрос
T_BIND_REQ
будет либо сообщение
T_BIND_ACK
, либо сообщение
T_ERROR_ACK
. Сообщения, содержащие подтверждение, отправляются как сообщения с высоким приоритетом (
M_PCPROTO
), так что мы считываем их при помощи функции
getmsg
с флагом
RS_HIPRI
. Поскольку ответ является сообщением с высоким приоритетом, он получает преимущество перед всеми обычными сообщениями в потоке.

Эти два сообщения выглядят следующим образом:

struct T_bind_ack {

 long          PRIM_type;     /* T_BIND_ACK */

 long          ADDR_length;   /* длина адреса */

 long          ADDR_offset;   /* смещение адреса */

 unsigned long CONIND_number; /* индекс подключения для помещения

                                 в очередь */

};


 /* затем следует связанный адрес */

struct T_error_ack {

 long PRIM_type;  /* T_ERROR_ACK */

 long ERROR_prim; /* примитивная ошибка ввода */

 long TLI_error;  /* код ошибки TLI */

 long UNIX_error; /* код ошибки UNIX */

};

В начале каждого сообщения указан его тип, так что мы можем начать считывать ответ, предполагая, что это сообщение

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

ПРИМЕЧАНИЕ

Когда мы проверяем, соответствует ли количество возвращенной управляющей информации по меньшей мере размеру длинного целого, нужно проявить осторожность, преобразуя значение sizeof в целое число. Оператор sizeof возвращает целое число без знака, но существует вероятность того, что значение возвращенного поля len будет -1. Поскольку при выполнении операции сравнения слева располагается значение со знаком, а справа — без знака, компилятор преобразует значение со знаком в значение без знака. Если рассматривать -1 как целое без знака в архитектуре с дополнением до 2, это число получается очень большим, то есть -1 оказывается больше 4 (если предположить, что длинное целое число занимает 4 байта).

Обработка ответа

31-33
 Если ответ — это сообщение
T_BIND_ACK
, то связывание прошло успешно, и мы возвращаемся. Фактический адрес, связанный с точкой доступа, возвращается в элементе
addr
нашей структуры
bind_ack
, которую мы игнорируем.

34-39
 Если ответ — это сообщение
T_ERROR_ACK
, мы проверяем, было ли сообщение получено целиком, и выводим три значения, содержащиеся в возвращенной структуре. В этой простой программе при возникновении ошибки мы просто прекращаем выполнение и ничего не возвращаем вызывающему процессу.

Чтобы увидеть ошибки, которые могут возникнуть в результате запроса на связывание, мы слегка изменим нашу функцию

main
и попробуем связать какой- либо порт, отличный от 0. Например, если мы попробуем связать порт 1 (что требует прав привилегированного пользователя, так как это порт с номером меньше 1024), мы получим следующий результат:

solaris % tpi_daytime 127.0.0.1

T_ERROR_ACK from bind (3, 0)

В этой системе значение константы

EACCESS
равно 3. Если мы поменяем номер порта, задав значение большее 1023, но используемое в настоящий момент другой точкой доступа TCP, мы получим:

solaris % tpi_daytime 127.0.0.1

T_ERROR_ACK from bind (23, 0)

В данной системе значение константы

EADDRBUSY
равно 23.

Следующая функция показана в листинге 31.4. Это функция

tpi_connect
, устанавливающая соединение с сервером.

Листинг 31.4. Функция tpi_connect: установление соединения с сервером

//streams/tpi_connect.c

 1 #include "tpi_daytime.h"


 2 void

 3 tpi_connect(int fd, const void *addr, size_t addrlen)

 4 {

 5  struct {

 6   struct T_conn_req msg_hdr;

 7   char addr[128];

 8  } conn_req;

 9  struct {

10   struct l_conn_con msg_hdr;

11   char addr[128];

12  } conn_con;

13  struct strbuf ctlbuf;

14  union T_primitives rcvbuf;

15  struct T_error_ack *error_ack;

16  struct T_discon_ind *discon_ind;

17  int flags;


18  conn_req.msg_hdr.PRIM_type = T_CONN_REQ;

19  conn_req.msg_hdr.DEST_length = addrlen;

20  conn_req.msg_hdr.DEST_offset = sizeof(struct T_conn_req);

21  conn_req.msg_hdr.OPT_length = 0;

22  conn_req.msg_hdr.OPT_offset = 0;

23  memcpy(conn_req.addr, addr, addrlen); /* sockaddr_in{} */

24  ctlbuf.len = sizeof(struct T_conn_req) + addrlen;

25  ctlbuf.buf = (char*)&conn_req;

26  Putmsg(fd, &ctlbuf, NULL, 0);


27  ctlbuf.maxlen = sizeof(union T_primitives);

28  ctlbuf.len = 0;

29  ctlbuf.buf = (char*)&rcvbuf;

30  flags = RS_HIPRI;

31  Getmsg(fd, &ctlbuf, NULL, &flags);

32  if (ctlbuf.len < (int)sizeof(long))

33   err_quit("tpi_connect: bad length from getmsg");


34  switch (rcvbuf.type) {

35  case T_OK_ACK:

36   break;


37  case T_ERROR_ACK:

38   if (ctlbuf.len < (int)sizeof(struct T_error_ack))

39    err_quit("tpi_connect: bad length for T_ERROR_ACK");

40   error_ack = (struct T_error_ack*)&rcvbuf;

41   err_quit("tpi_connect: T_ERROR_ACK from conn %d, %d)",

42    error_ack->TLI_error, error_ack->UNIX_error);


43  default:

44   err_quit("tpi connect, unexpected message type: &d", rcvbuf.type);

45  }


46  ctlbuf.maxlen = sizeof(conn_con);

47  ctlbuf.len = 0;

48  ctlbuf.buf = (char*)&conn_con;

49  flags = 0;

50  Getmsg(fd, &ctlbuf, NULL, &flags);

51  if (ctlbuf.len < (int)sizeof(long))

52   err_quit("tpi_connect2: bad length from getmsg");


53  switch (conn_con.msg_hdr.PRIM_type) {

54  case T_CONN_CON:

55   break;


56  case T_DISCON_IND:

57   if (ctlbuf.len < (int)sizeof(struct T_discon_ind))

58    err_quit("tpi_connect2: bad length for T_DISCON_IND");

59   discon_ind = (struct T_discon_ind*)&conn_con.msg_hdr;

60   err_quit("tpi_connect2: T_DISCON_IND from conn (%d)",

61   discon_ind->DISCON_reason);


62  default:

63   err_quit("tpi_connect2: unexpected message type. %d",

64   conn_con.msg_hdr PRIM_type);

65  }

66 }

Заполнение структуры запроса и отправка поставщику

18-26
 В TPI определена структура
T_conn_req
, содержащая адрес протокола и параметры для соединения:

struct T_conn_req {

 long PRIM_type;   /* T_CONN_REQ */

 long DEST_length; /* длина адреса получателя */

 long DEST_offset; /* смещение адреса получателя */

 long OPT_length;  /* длина параметров */

 long OPT_offset;  /* смещение параметров */

 /* затем следуют адреса протокола и параметры соединения */

};

Как и в случае функции

tpi_bind
, мы определяем свою собственную структуру с именем
conn_req
, которая включает в себя структуру
T_conn_req
, а также содержит место для адреса протокола. Мы заполняем структуру
conn_req
, обнуляя поля
OPT_length
и
OPT_offset
. Мы вызываем функцию
putmsg
только с управляющей информацией и флагом 0 для отправки сообщения типа
M_PROTO
вниз по потоку.

Чтение ответа

27-45
 Мы вызываем функцию
getmsg
, ожидая получить в ответ либо сообщение
T_OK_ACK
, если было начато установление соединения, либо сообщение
T_ERROR_ACK
(которые мы уже показывали выше). В случае ошибки мы завершаем выполнение программы. Поскольку мы не знаем, сообщение какого типа мы получим, то определяем объединение с именем
T_primitives
для приема всех возможных запросов и ответов и размещаем это объединение в памяти как входной буфер для управляющей информации при вызове функции
getmsg
.

struct T_ok_ack {

 long PRIM_type;    /* T_OK_ACK */

 long CORRECT_prim; /* корректный примитив */

};

Ожидание завершения установления соединения

46-65
 Сообщение
T_OK_ACK
, полученное нами на предыдущем этапе, указывает лишь на то, что соединение успешно начало устанавливаться. Теперь нам нужно дождаться сообщения
T_CONN_CON
, указывающего на то, что другой конец соединения подтверждает получение запроса на соединение.

struct T_conn_con {

 long PRIM_type;  /* T_CONN_CON */

 long RES_length; /* длина адреса собеседника */

 long RES_offset; /* смещение адреса собеседника */

 long OPT_length; /* длина параметра */

 long OPT_offset; /* смещение параметра */

 /* далее следуют адрес протокола и параметры собеседника */

};

Мы снова вызываем функцию

getmsg
, но ожидаемое нами сообщение посылается как сообщение типа
M_PROTO
, а не как сообщение
M_PCPROTO
, поэтому мы обнуляем флаги. Если мы получаем сообщение
T_CONN_CON
, значит, соединение установлено, и мы возвращаемся, но если соединение не было установлено (по причине того, что процесс собеседника не запущен, истекло время ожидания или еще по какой-либо причине), то вместо этого вверх по потоку отправляется сообщение
T_DISCON_IND
:

struct T_discon_ind {

 long PRIM_type;     /* T_DISCON_IND */

 long DISCON_reason; /* причина разрыва соединения */

 long SEQ_number;    /* порядковый номер */

};

Мы можем посмотреть, какие ошибки могут быть возвращены поставщиком. Сначала мы задаем IP-адрес узла, на котором не запущен сервер времени и даты:

solaris26 % tpi_daytime 192.168.1.10

tpi_connect2: T_DISCON_IND from conn (146)

Код 146 соответствует ошибке

ECONNREFUSED
. Затем мы задаем IP-адрес, который не связан с Интернетом:

solaris26 % tpi_daytime 192.3.4.5

tpi_connect2: T_DISCON_IND from conn (145)

На этот раз возвращается ошибка

ETIMEDOUT
. Но если мы снова запустим нашу программу, задавая тот же самый IP-адрес, то получим другую ошибку:

solaris26 % tpi_daytime 192.3.4.5

tpi_connect2: T_DISCON_IND from conn (148)

На этот раз мы получаем ошибку

EHOSTUNREACH
. Различие в том, что в первый раз не было возвращено сообщение ICMP о недоступности узла, а во второй раз мы получили это сообщение.

Следующая функция, которую мы рассмотрим, — это

tpi_read
, показанная в листинге 31.5. Она считывает данные из потока.

Листинг 31.5. Функция tpi_read: считывание данных из потока

//streams/tpi_read.c

 1 #include "tpi_daytime.h"


 2 ssize_t

 3 tpi_read(int fd, void *buf, size_t len)

 4 {

 5  struct strbuf ctlbuf;

 6  struct strbuf datbuf;

 7  union T_primitives rcvbuf;

 8  int flags;


 9  ctlbuf maxlen = sizeof(union T_primitives);

10  ctlbuf.buf = (char*)&rcvbuf;


11  datbuf.maxlen = len;

12  datbuf.buf = buf;

13  datbuf.len = 0;


14  flags = 0;

15  Getmsg(fd, &ctlbuf, &datbuf, &flags);


16  if (ctlbuf.len >= (int)sizeof(long)) {

17   if (rcvbuf.type == T_DATA_IND)

18    return (datbuf.len);

19   else if (rcvbuf.type == T_ORDREL_IND)

20    return (0);

21   else

22    err_quit("tpi_read: unexpected type %d", rcvbuf.type);

23  } else if (ctlbuf.len == -1)

24   return (datbuf.len);

25  else

26   err_quit("tpi_read: bad length from getmsg");

27 }

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

9-26
 На этот раз мы вызываем функцию
getmsg
для считывания как данных, так и управляющей информации. Структура
strbuf
, предназначенная для данных, указывает на буфер вызывающего процесса. В потоке события могут развиваться по четырем различным сценариям.

■ Данные могут прибыть в виде сообщения

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

■ Данные могут прибыть как сообщение

T_DATA_IND
, в этом случае управляющая информация будет содержаться в структуре
T_data_ind
:

struct T_data_ind {

 long PRIM_type; /* T_DATA_IND */

 long MORE_flag; /* еще данные */

};

Если возвращено такое сообщение, мы игнорируем поле

MORE_flag
(оно вообще не задается для таких протоколов, как TCP) и просто возвращаем длину данных, скопированных в буфер вызывающего процесса функцией
getmsg
.

■ Сообщение

T_ORDREL_IND
возвращается, если все данные получены и следующим элементом является сегмент
FIN
:

struct T_ordrel_ind {

 long PRIM_type; /* T_ORDREL_IND */

};

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

■ Сообщение

T_DISCON_IND
возвращается, если произошел разрыв соединения. Наша последняя функция — это
tpi_close
, показанная в листинге 31.6.

Листинг 31.6. Функция tpi_close: отправка запроса о завершении собеседнику

//streams/tpi_close.c

 1 #include "tpi_daytime.h"


 2 void

 3 tpi_close(int fd)

 4 {

 5  struct T_ordrel_req ordrel_req;

 6  struct strbuf ctlbuf;


 7  ordrel_req PRIM_type = T_ORDREL_REQ;

 8  ctlbuf.len = sizeof(struct T_ordrel_req);

 9  ctlbuf.buf = (char*)&ordrel_req;

10  Putmsg(fd, &ctlbuf, NULL, 0);


11  Close(fd);

12 }

Отправка запроса о завершении собеседнику

7-10
 Мы формируем структуру
T_ordrel_req
:

struct T_ordrel_req {

 long PRIM_type; /* T_ORDREL_REQ */

};

и посылаем ее как сообщение

M_PROTO
с помощью функции
putmsg
. Это соответствует функции XTI
t_sndrel
.

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

ПРИМЕЧАНИЕ

Можно сравнить количество системных вызовов, необходимых для осуществления определенных сетевых операций, показанных в этой главе, в случае применения TPI и когда используется ядро, реализующее сокеты. Связывание с локальным адресом в случае TPI требует двух системных вызовов, но в случае сокетного ядра требуется только один вызов [128, с. 454]. Для установления соединения на блокируемом дескрипторе с использованием TPI требуется три системных вызова, а в случае сокетного ядра — только один [128, с. 466].

31.7. Резюме

Иногда сокеты реализуются с использованием потоков STREAMS. Для обеспечения доступа к потоковой подсистеме вводятся четыре новые функции:

getmsg
,
putmsg
,
getpmsg
и
putpmsg
. Также в потоковой подсистеме широко используется уже описанная ранее функция
ioctl
.

TPI представляет собой потоковый интерфейс системы SVR4, предоставляющий доступ из верхних уровней на транспортный уровень. Он используется как сокетами, так и XTI, как показано на рис. 31.3. В этой главе в качестве примера использования основанного на сообщениях интерфейса мы разработали версию клиента времени и даты, в котором непосредственно применяется интерфейс TPI.

Упражнения

1. В листинге 31.6 мы вызываем функцию

putmsg
, чтобы отправить вниз по потоку запрос на нормальное завершение соединения, а затем немедленно вызываем функцию
close
для закрытия потока. Что произойдет, если наш запрос будет потерян потоковой подсистемой, а мы закроем поток?

Приложения