Архитектура с двумя соединениями
Процессы 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: $ |
Более интересна ситуация, когда между внешней системой и приложением, обрабатывающим сообщения, необходим обмен подтверждениями. В этом случае придется изменить и 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 завершает сеанс, не получив подтверждения от удаленного хоста