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

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 <sys/stream.h>

 3 #include <sys/tihdr.h>

 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);

Нам нужно включить еще один дополнительный заголовочный файл помимо <sys/tihdr.h>, содержащего определения структур для всех сообщений 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 <Ipaddress>");

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 Заголовочный файл <sys/tihdr.h> определяет структуру 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].

Данный текст является ознакомительным фрагментом.