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

       

Скрытая ошибка


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

int pkt[ 3 ] ;

for ( ; ; )

{

 rc = recv( s, ( char * ) pkt, sizeof( pkt ), 0 );

 if (rc != sizeof( int ) * 2 && rc != sizeof( int ) * 3 )

  /* Протоколировать ошибку и выйти. */

 else

  /* Обработать rc / sizeof( int ) значений. */

}

Из совета 6 вы знаете, что этот код некорректен, но попробуем провести простое моделирование. Напишем сервер (листинг 2.33), в котором реализован только что показанный цикл.

Листинг 2.33. Моделирование сервера телеметрии

telemetrys.c

1    #include "etcp.h"

2    #define TWOINTS ( sizeoff int ) * 2 )

3    #define THREEINTS ( sizeof( int ) * 3 )



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

5    {

6    SOCKET s;

7    SOCKET s1;

8    int rc;

9    int i = 1;

10   int pkt [ 3 ] ;

11   INIT();

12   s = tcp_server( NULL, argv[ 1 ] );

13   s1 = accept( s, NULL, NULL );

14   if ( !isvalidsock( s1 ) )

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

16   for ( ; ; )

17   {

18     rc =  recv( s1, ( char * }pkt, sizeoff pkt ), 0 );

19     if ( rc != TWOINTS && rc != THREEINTS )

20      error( 1, 0, "recv  вернула  %d\n", rc );

21     printf( "Пакет %d содержит %d значений в %d байтах\n" ,

22     i ++, ntohl pkt[ 0 ] ) , rc );

23   }

24   }

11-15 В этих строках реализована стандартная инициализация и прием соединения.

16-23 В данном цикле принимаются данные от клиента. Если получено при чтении не в точности sizeof ( int ) * 2 или sizeof ( int ) * 3 байт, то протоколируем ошибку и выходим. В противном случае байты первого числа преобразуются в машинный порядок (совет 28), а затем результат и число прочитанных байтов печатаются на stdout. В листинге 2.34 вы увидите, что клиент помещает число значений в первое число, посылаемое в пакете. Это поможет разобраться в том, что происходит. Здесь не используется это число как «заголовок сообщениям, содержащий его размер (совет 6).


Для тестирования этого сервера также необходим клиент, который каждую секунду посылает пакет целых чисел, имитируя работу удаленного датчика. Текст клиента приведен в листинге 2.34.

Листинг 2.34. Имитация клиента для сервера телеметрии

1    #include "etcp.h"

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

3    {

4    SOCKET s;

5    int rc;

6    int i;

7    int pkt[ 3 ];

8    INIT();

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

Ю    for ( i = 2;; i = 5 - i )

И    {

12     pkt[ 0 ] = htonl( i ) ;

13     rc = send( s, ( char * )pkt, i * sizeof( int ), 0 );

14     if ( rc < 0 )

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

16     sleep( 1 );

17   }

18   }

8-9 Производим инициализацию и соединяемся с сервером.

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

Для тестирования модели запустим сервер на машине bsd, а клиента – на машине spare. Сервер печатает следующее:

bsd: $ telemetrys 9000

Пакет 1 содержит 2 значения в 8 байтах

Пакет 2 содержит 3 значения в 12 байтах

Много строк опущено.

Пакет 22104 содержит 3 значения в 12 байтах

Пакет 22105 содержит 2 значения в 8 байтах

Клиент завершил сеанс через 6 ч 8 мин 15 с.

telemetrys: recv вернула 0

bsd: $

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

Примечание: Протокол сервера проверен с помощью сценария, написанного на awk - необходимо убедиться, что каждая операция чтения вернула правильное число байтов.

Однако при запуске того же сервера через Internet результаты получились совсем другие. Опять запустим клиента на машине spare, а сервер - на машине bsd, но на этот раз заставим клиента передавать данные через глобальную сеть, указав ему адрес сетевого интерфейса, подключенного к Internet. Как видно из последних строк, напечатанных сервером, фатальная ошибка произошла уже через 15 мин.



Пакет 893 содержит 2 значения в 8 байтах

Пакет 894 содержит 3 значения в 12 байтах

Пакет 895 содержит 2 значения в 12 байтах

Пакет 896 содержит -268436204 значения в 8 байтах

Пакет 897 содержит 2 значения в 12 байтах

Пакет 898 содержит -268436204 значения в 8 байтах

Пакет 899 содержит 2 значения в 12  байтах

Пакет 900 содержит -268436204 значения  в 12 байтах

telemetrys: recv вернула 4

bsd: $

Ошибка произошла при обработке пакета 895, когда нужно было прочесть8 байт, а прочли 12. На рис. 2.21 представлено, что произошло.

Числа слева показывают, сколько байтов было в приемном буфере TCP на стороне сервера. Числа справа - сколько байтов сервер реально прочитал. Вы видите, что пакеты 893 и 894 доставлены и обработаны, как и ожидалось. Но, когда telemetrys вызвал recv для чтения пакета 895, в буфере было 20 байт.

Примечание: Трассировка сетевого трафика, полученная с помощью программы tcpdump (совет 34), показывает, что в этот момент были потеряны TCP-сегменты, которыми обменивались два хоста. Вероятно, причиной послужила временная перегрузка сети, из-за которой промежуточный маршрутизатор отбросил пакет. Перед доставкой пакета 895 клиент telemetryc yжe подготовил пакет 896, и оба были доставлены вместе.

В пакете 895 было 8 байт, но, поскольку уже пришел пакет 896, сервер прочитал пакет 895 и первое число из пакета 896. Поэтому в распечатке видно, что было прочитано 12 байт, хотя пакет 895 содержит только два целых. При следующем чтении возвращено два целых из пакета 896, и telemetrys напечатал мусор вместо числа значений, так как telemetryc не инициализировал второе значение.



Рис. 2.21. Фатальная ошибка

Как видно из рис. 2.21, то же самое произошло с пакетами 897 и 898, так что при следующем чтении было доступно уже 28 байт. Теперь telemetrys читает пакет 899 и первое значение из пакета 900, остаток пакета 900 и первое значение из пакета 901 и наконец последнее значение из пакета 901. Последняя операция чтения возвращает только 4 байта, поэтому проверка в строке 19 завершается неудачно, а моделирование - с ошибкой.



К сожалению, на более раннем этапе моделирования произошло еще худшее:

Пакет 31 содержит 2 значения в 8 байтах

Пакет 32 содержит 3 значения в 12 байтах

Пакет 33 содержит 2 значения в 12 байтах

Пакет 34 содержит -268436204 значения в 8 байтах

Пакет 35 содержит 2 значения в 8 байтах

Пакет 36 содержит 3 значения в 12 байтах

Всего через 33 с после начала моделирования произошла ошибка, оставшаяся необнаруженной. Как показано на рис. 2.22, когда telemetrys читал пакет 33 в буфере было 20 байт, поэтому операция чтения вернула 12 байт вместо 8. Это означает, что пакет с двумя значениями ошибочно был принят за пакет с тремя значениями, а затем наоборот. Начиная с пакета 35, telemetrys восстановил синхронизацию, и ошибка прошла незамеченной.



Рис. 2.22. Незамеченная ошибка


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