Эффективное программирование TCP-IP

       

Архитектура с двумя соединениями


Процессы xin и xout на рис. 3.4 делят между собой единственное соединение с внешней системой, но возникают трудности при организации разделения информации о состоянии этого соединения. Кроме того, с точки зрения каждого из процессов xin и xout, это соединение симплексное, то есть данные передаются по Нему только в одном направлении. Если бы это было не так, то xout «похищал» бы входные данные у xin, a xin мог бы исказить данные, посылаемые xout.

Решение состоит в том, чтобы завести два соединения с внешней системой -по одному для xin и xout. Полученная после такого изменения архитектура изображена на рис. 3.5.

Рис.3.5. Приложение, обменивающееся сообщениями по двум TCP-соединениям

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

Чтобы это проверить, запрограммируем простой процесс xout, который читает данные из стандартного ввода и записывает их в TCP-соединение. Программа, показанная в листинге 3.12, с помощью вызова select ожидает поступления данных из соединения, хотя реально может прийти только EOF или извещение об ошибке.

Листинг 3.12. Программа, готовая к чтению признака конца файла или ошибки

1    #include "etcp.h"

2    int main(   int  argc,   char  **argv  )

3    {

4    fd_set  allreads;

5    fd_set  readmask;

6    SOCKET  s;



7    int rc;

8    char  buf [ 128 ] ;

9    INIT () ;

10   s = tcp_client( argv [ 1 ],     argv[ 2 ] );

11   FD_ZERO( kallreads );

12   FD_SET( s, &allreads );

13   FD_SET( 0, &allreads );

14   for ( ; ; )

15   {

16     readmask = allreads;

17     rc = select(s + 1, &readmask, NULL, NULL, NULL );

18     if ( re <= 0)


19      error( 1, rc ? errno : 0, "select вернул %d", rc );

20     if ( FD_ISSET( 0, &readmask ) }

21     {

22      rc = read( 0, buf, sizeof( buf ) - 1 );

23      if ( rc < 0 )

24       error( 1, errno, "ошибка вызова read" };

25      if ( send( s, buf, rc, 0 ) < 0 )

26       error( 1, errno, "ошибка вызова send" );

27     }

28     if ( FD_ISSET( s, &readmask ) )

29     {

30      rc = recv( s, buf, sizeof( buf ) - 1, 0 );

31      if ( rc == 0 )

32       error( 1, 0, "сервер отсоединился\n" );

33      else if ( rc < 0 )

34       error( 1, errno, "ошибка вызова recv" );

35      else

36      {

37       buf[ rc ] = '\0';

38       error( 1, 0, "неожиданный вход [%s]\n", buf );

39      }

40     }

41   }

42   }

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

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

Обработка событий stdin

20-27 Если данные пришли из стандартного ввода, посылаем их удаленному хосту через TCP-соединение.

Обработка событий сокета

28-40 Если пришло извещение о наличии доступных для чтения данных в сокете, то проверяем, это EOF или ошибка. Никаких данных по этому соединению не должно быть получено, поэтому если пришло что-то иное, то печатаем диагностическое сообщение и завершаем работу.

Продемонстрировать работу xout1 можно, воспользовавшись программой keep (листинг 2.30) в качестве внешней системы и простым сценарием на языке интерпретатора команд shell для обработки сообщений (mр на рис. 3.5). Этот сценарий Каждую секунду выводит на stdout слово message и счетчик.

MSGNO=1

while true

do

echo message $MSGNO

sleep 1

MSGNO="expr $MSGNO + 1"

done

Обратите внимание, что в этом случае xoutl использует конвейер в качестве механизма IPC. Поэтому в таком виде программа xoutl не переносится на платформу Windows, поскольку вызов select работает под Windows только для сокетов. Можно было бы реализовать взаимодействие между процессами с помощью TCP или UDP, но тогда потребовался бы более сложный обработчик сообщений.



Для тестирования xoutl запустим сначала «внешнюю систему» в одном окне, а обработчик сообщений и xoutl - в другом.

bsd: $ keep 9000

message 1

message 2

message 3

message 4

^C"Внешняя система"

завершила работу

bsd: $

bsd: $ mp I xoutl localhost 9000

xoutl: сервер отсоединился

Broken pipe

bsd: $

Сообщение Broken pipe напечатал сценарий mp. При завершении программы xoutl конвейер между ней и сценарием закрывается. Когда сценарий пытается за­писать в него следующую строку, происходит ошибка, и сценарий завершается с сообщением Broken pipe.

Более интересна ситуация, когда между внешней системой и приложением, обрабатывающим сообщения, необходим обмен подтверждениями. В этом случае придется изменить и xin, и xout (предполагая, что подтверждения нужны в обоих направлениях; если нужно только подтверждать внешней системе прием сообщений, то изменения надо внести лишь в xin). Разработаем пример только процесса-писателя (xout). Изменения в xin аналогичны.

Новый процесс-писатель обязан решать те же проблемы, с которыми вы столкнулись при обсуждении пульсаций в совете 10. После отправки сообщения удаленный хост должен прислать нам подтверждение до того, как сработает таймер. Если истекает тайм-аут, необходима какая-то процедура восстановления после ошибки. В примере работа просто завершается.

При разработке нового «писателя» xout2 вы не будете принимать сообщений из стандартного ввода, пока не получите подтверждения от внешней системы о том, что ей доставлено последнее ваше сообщение. Возможен и более изощренный под­ход с использованием механизма тайм-аутов, описанного в совете 20. Далее он будет рассмотрен, но для многих систем вполне достаточно той простой схемы, которую будет применена. Текст xout2 приведен в листинге 3.13.

Листинг 3.13. Программа, обрабатывающая подтверждения

1    #include "etcp.h"

2    #define АСК 0х6 /*Символ подтверждения АСК. */

3    int  main( int argc, char **argv)

4    {

5    fd_set allreads;



6    fd_set readmask;

7    fd_set sockonly;

8    struct timeval   tv;

9    struct timeval   *tvp  =  NULL;

10   SOCKET  s;

11   int rc;

12   char  buf[ 128 ];

13   const  static struct  timeval TO  =   {   2,   0   } ;

14   INIT();

15   s = tcp_client( argv[ 1 ], argv[ 2 ] );

16   FD_ZERO( &allreads );

17   FD_SET( s, &allreads ) ;

18   sockonly = allreads;

19   FD_SET( 0, &allreads );

20   readmask = allreads;

21   for ( ;; )

22   {

23   rc = select( s + 1, &readmask, NULL, NULL, tvp );

24   if ( rc < 0 )

25     error( 1, errno, "ошибка вызова select" );

26   if ( rc == 0 )

27     error( 1, 0, "тайм-аут при приеме сообщения\n" );

28   if ( FD_ISSET( s, &readmask ) )

29   {

30     rc = recv( s, buf, sizeof( buf }, 0 );

31     if ( rc == 0 )

32      error( 1, 0, "сервер отсоединился\n" );

33     else if ( rc < 0 )

34      error(   1,   errno,   "ошибка  вызова recv");

35     else if (rc != 1 buf[ 0 ] !=  ACK)

36      error( 1, 0, "неожиданный вход [%c]\n",   buf[   0   ]   ) ;

37     tvp   =  NULL;     /*   Отключить  таймер   */

38     readmask = allreads;   /* и продолжить чтение из  stdin.   */

39   }

40   if ( FD_ISSET( 0, &readmask ) }

41   {

42     rc = read( 0, buf, sizeof( buf ) ) ;

43     if ( rc < 0 )

44      error( 1, errno, "ошибка вызова read" );

45     if ( send( s, buf, rc, 0 ) < 0 )

46      error( 1, errno, "ошибка вызова send" );

47     tv = T0;  /* Переустановить таймер. */

48     tvp = &tv;  /* Взвести таймер */

49     readmask = sockonly; /* и прекратить чтение из stdin. */

50   }

51   }

52   }

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

14-15 Стандартная инициализация TCP-клиента.

16-20 Готовим две маски для select: одну для приема событий из stdin и ТСР-сокета, другую для приема только событий из сокета. Вторая маска sockonly применяется после отправки данных, чтобы не читать новые данные из stdin, пока не придет подтверждение.



Обработка событий таймера

26- 27 Если при вызове select произошел тайм-аут (не получено вовремя подтверждение), то печатаем диагностическое сообщение и завершаем сеанс,

Обработка событий сокета

28-39 Если пришло извещение о наличии доступных для чтения данных в сокете, проверяем, это EOF или ошибка. Если да, то завершаем работу так же, как в листинге 3.12. Если получены данные, убеждаемся, что это всего один символ АСК. Тогда последнее сообщение подтверждено, поэтому сбрасываем таймер, устанавливая переменную tvp в NULL, и разрешаем чтение из стандартного ввода, устанавливая маску readmask так, чтобы проверялись и сокет, и stdin.

Обработка событий в stdin

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

47-50 Поскольку данные только что переданы внешней системе, ожидается подтверждение. Взводим таймер, устанавливая поля структуры tv и направляя на нее указатель tvp. В конце запрещаем события stdin,записывая в переменную readmask маску sockonly.

Для тестирования программы xout2 следует добавить две строки

if ( send( si, "\006", 1, 0 ) < 0 ) /* \006 = АСК */

 error( 1, errno, "ошибка вызова send");

перед записью на строке 24 в исходном тексте keep. с (листинг 2.30). Если выполнить те же действия, как и для программы xoutl, то получим тот же результат с тем отличием, что xout2 завершает сеанс, не получив подтверждения от удаленного хоста


Содержание раздела