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

       

Источник и приемник на базе TCP


В совете 32 объясняется, что повысить производительность TCP можно за счет выбора правильного размера буферов передачи и приема. Нужно установить размер буфера приема для сокета сервера и размер буфера передачи для сокета клиента.

Поскольку в функциях tcp_server и tcp_client используются размеры буферов по умолчанию, следует воспользоваться не библиотекой, а каркасами из совета 4. Сообщать TCP размеры буферов нужно во время инициализации соединения, то есть до вызова listen в сервере и до вызова connect в клиенте. Поэтому невозможно воспользоваться функциями tcp_server и tcp_client, так как к моменту возврата из них обращение к listen или connect уже произошло. Начнем с клиента, его код приведен в листинге 2.18.

Листинг 2.18. Функция main TCP-клиента, играющего роль источника

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

2    {

3    struct sockaddr_in peer;

4    char *buf;

5    SOCKET s;

6    int с;

7    int blks = 5000;

8    int sndbufsz = 32 * 1024;

9    int sndsz = 1440;  /* MSS для Ethernet по умолчанию. */

10   INIT();



11   opterr = 0;

12   while ( ( с = getopt( argc, argv, "s:b:c:" ) ) != EOF )

13   {

14     switch ( с )

15     {

16      case "s" :

17       sndsz = atoi( optarg ) ;

18       break;

19      case "b" :

20       sndbufsz = atoi( optarg ) ;

21       break;

22      case "c" :

23       blks = atoi( optarg );

2 4      break;

25      case "?" :

26       error( 1, 0, "некорректный параметр: %c\n", с );

27     }

28   }

28   if ( argc <= optind )

30     error( 1, 0, "не задано имя хоста\n" };

31   if ( ( buf = malloc( sndsz ) ) == NULL )

32     error( 1, 0, "ошибка вызова malloc\n" );

33   set_address( argv[ optind ], "9000", &peer, "tcp" );

34   s = socket( AF_INET, SOCK_STREAM, 0 );

35   if ( !isvalidsock( s ) )

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

37   if ( setsockopt( s, SOL_SOCKET, SO_SNDBUF,


38     ( char * )&sndbufsz, sizeof( sndbufsz ) ) )

39     error( 1, errno, "ошибка вызова setsockopt с опцией SO_SNDBUF" );

40   if ( connect( s, ( struct sockaddr * )&peer,

41     sizeof( peer ) ) )

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

43   while( blks-- > 0 )

44     send( s, buf, sndsz, 0 );

45   EXIT( 0 );

46   }

12-30 В цикле вызываем getopt для получения и обработки параметров из командной строки. Поскольку эта программа будет использоваться и далее, то делаем ее конфигурируемой в большей степени, чем необходимо для данной задачи. С помощью параметров в командной строке можно задать размер буфера передачи сокета, количество данных, передаваемых при каждой операции записи в сокет, и число операций записи.

31-42 Это стандартный код инициализации TCP-клиента, только добавлено еще обращение к setsockopt для установки размера буфера передачи, а также с помощью функции malloc выделен буфер запрошенного размера для размещения данных, посылаемых при каждой операции записи. Обратите внимание, что инициализировать память, на которую указывает buf, не надо, так как в данном случае безразлично, какие дан­ные посылать.

43-44 Вызываем функцию send нужное число раз.

Функция main сервера, показанная в листинге 2.19, взята из стандартного каркаса с добавлением обращения к функции getopt для получения из командной строки параметра, задающего размер буфера приема сокета, а также вызов функ­ции getsockopt для установки размера буфера.

Листинг 2.19. Функция main TCP-сервера, играющего роль приемника

tcpsink.с

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

2    {

3    struct sockaddr_in local;

4    struct sockaddr_in peer;

5    int peerlen;

6    SOCKET s1;

7    SOCKET s;

8    int c;

9    int rcvbufsz = 32 * 1024;

10   const int on = 1;

11   INIT();

12   opterr = 0;

13   while ( ( с = getopt( argc, argv, "b:" ) ) != EOF )

14   {

15     switch ( с )

16     {

17      case "b" :

18       rcvbufsz = atoi( optarg };



19       break;

20      case ".?" :

21       error( 1, 0, "недопустимая опция: %c\n", с );

22     }

23   }

24   set_address( NULL, "9000", &local, "tcp" );

25   s = socket( AF_INET, SOCK_STREAM, 0 );

26   if ( !isvalidsock( s ) )

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

28   if ( setsockopt( s, SOL_SOCKET, SO_REUSEADDR,

29     ( char * )&on, sizeof( on ) ) )

30     error( 1, errno, "ошибка вызова setsockopt SO_REUSEADDR")

31   if ( setsockopt( s, SOL_SOCKET, SO_RCVBUF,

32     ( char * )&rcvbufsz, sizeof( rcvbufsz ) ) )

33     error( 1, errno, "ошибка вызова setsockopt SO_RCVBUF")

34   if ( bind( s, ( struct sockaddr * ) &local,

35     sizeof( local ) ) )

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

37   listen( s, 5 );

38   do

39   {

40     peerlen = sizeof( peer );

41     s1 = accept( s, ( struct sockaddr *)&peer, &peerlen );

42     if ( !isvalidsock( s1 ) )

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

44     server( s1, rcvbufsz );

45     CLOSE( s1 );

46   } while ( 0 );

47   EXIT( 0 );

48   }

Функция server читает и подсчитывает поступающие байты, пока не обнаружит конец файла (совет 16) или не возникнет ошибка. Она выделяет память под буфер того же размера, что и буфер приема сокета, чтобы прочитать максимальное количество данных за одно обращение к recv. Текст функции server приведен в листинге 2.20.

Листинг 2.20. Функция server



1    static void server(   SOCKET  s, int rcvbufsz )

2    {

3    char  *buf;

4    int rc;

5    int bytes =0;

6    if ( ( buf   =  malloc( rcvbufsz ) ) == NULL )

7      error( 1, 0, "ошибка  вызова malloc\n"};

8    for ( ; ;  )

9    {

10     rc = recv( s, buf, rcvbufsz, 0 );

11     if ( rc <= 0 )

12      break;

13     bytes += rc;

14   }

15   error( 0, 0, "получено байт: %d\n", bytes );

16   }

Для измерения сравнительной производительности протоколов TCP и UDP при передаче больших объемов данных запустим клиента на машине bsd, а сервер- на localhost. Физически хосты bsd localhost - это, конечно, одно и то же, но, как вы увидите, результаты работы программы в значительной степени зависят от того, какое из этих имен использовано. Сначала запустим клиента и сервер на одной машине, чтобы оценить производительность TCP и UDP, устранив влияние сети. В обоих случаях сегменты TCP или датаграммы UDP инкапсулируются в IP-датаграммах и посылаются возвратному интерфейсу 1оО, который немедленно переправляет их процедуре обработки IP-входа, как показано на рис. 2.17.





Рис. 2.17. Возвратный интерфейс

Каждый тест был выполнен 50 раз с заданным размером датаграмм (в случае UDP) или числом передаваемых за один раз байтов (в случае TCP), равным 1440. Эта величина выбрана потому, что она близка к максимальному размеру сегмента, который TCP может передать по локальной сети на базе Ethernet.

Примечание: Это число получается так. В одном фрейме Ethernet может быть передано не более 1500 байт. Каждый заголовок IP и TCP занимает 20 байт, так что остается 1460. Еще 20 байт резервировано для опций TCP. В системе BSD TCP посылает 12 байт с опциями, поэтому в этом случае максимальный размер сегмента составляет 1448 байт.

В табл. 2.2 приведены результаты, усредненные по 50 прогонам. Для каждого протокола указано три времени: по часам - время с момента запуска до завершения работы клиента; пользовательское - проведенное программой в режиме пользователя; системное - проведенное программой в режиме ядра. В колонке «Мб/с» указан результат деления общего числа посланных байтов на время по часам. В колонке «Потеряно» для UDP приведено среднее число потерянных датаграмм.

Первое, что бросается в глаза, - TCP работает намного быстрее, когда в качестве имени сервера выбрано localhost, а не bsd. Для UDP это не так – заметной разницы в производительности нет. Чтобы понять, почему производительность TCР так возрастает, когда клиент отправляет данные хосту localhost, запустим программу netstat (совет 38) с опцией -i. Здесь надо обратить внимание на две строки (ненужная информация опущена):

Name    Mtu         Network    Address

Ed0    1500       172.30       bsd

lo0      16384    127              localhost

Таблица 2.2. Сравнение производительности TCP и UDP при количестве посылаемых байтов, равном 1440

TCP

Сервер

Время по часам

Пользовательское время

Системное время

Мб/с

bsd

2,88

0,0292

1,4198

2,5

localhost

0,9558

0,0096

0,6316

7,53

sparс

7,1882

0,016

1,6226

1,002

UDP

Сервер

Время по часам

Пользовательское время

Системное время

Мб/с

Потеряно

bsd

1,9618

0,0316

1,1934

3,67

336

localhost

1,9748

0,031

1,1906

3,646

272

sparс

5,8284

0,0564

0,844

1,235

440

<


Как видите, максимальный размер передаваемого блока (MTU - maximum transmission unit) для bsd равен 1500, а для localhost - 16384.

Примечание: Такое поведение свойственно реализациям TCP в системах, производных от BSD. Например, в системе Solaris это уже не так. При первом построении маршрута к хосту bsd в коде маршрутизации предполагается, что хост находится в локальной сети, поскольку сетевая часть IP-адреса совпадает с адресом интерфейса Ethernet. И лишь при первом использовании маршрута TCP обнаруживает, что он ведет на тот же хост и переключается на возвратный интерфейс. Однако к этому моменту все метрики маршрута, в том числе и MTU, уже установлены в соответствии с интерфейсом к локальной сети.

Это означает, что при посылке данных на localhost TCP может отправлять сегменты длиной до 16384 байт (или 16384 - 20 - 20 - 12 - 16332 байт). Однако при посылке данных на хост bsd число байт в сегменте не превышает 1448 (как было сказано выше). Но чем больше размер сегментов, тем меньшее их количество приходится посылать, а это значит, что требуется меньший объем обработки, и соответственно снижаются накладные расходы на добавление к каждому сегменту заголовков IP и TCP. А результат налицо - обмен данными с хостом localhost происходит в три раза быстрее, чем с хостом bsd.

Можно заметить, что на хосте localhost TCP работает примерно в два раза быстрее, чем UDP. Это также связано с тем, что TCP способен объединять несколько блоков по 1440 байт в один сегмент, тогда как UDP посылает отдельно каждую датаграмму длиной 1440 байт.

Следует отметить, что в локальной сети UDP примерно на 20% быстрее TCP, потеря датаграмм значительнее. Потери имеют место даже тогда, когда и сервер и клиент работают на одной машине; связаны они с исчерпанием буферов. Хотя передача 5000 датаграмм на максимально возможной скорости - это скорее отклонение, чем нормальный режим работы, но все же следует иметь в виду возможность такого результата. Это означает, что UDP не дает никакой гарантии относительно доставки данной датаграммы, даже если оба приложения работают на одной машине.



По результатам сравнения сеансов с хостами localhost и bsd можно предположить, что на производительность влияет также длина посылаемых датаграмм. Например, если прогнать те же тесты с блоком длиной 300 байт, то, как следует из табл. 2.3, TCP работает быстрее UDP и на одной машине, и в локальной сети.

Из этих примеров следует важный вывод: нельзя строить априорные предположения о сравнительной производительности TCP и UDP. При изменении условий, даже очень незначительном, показатели производительности могут очень резко измениться. Для обоснованного выбора протокола лучше сравнить их производительность на контрольной задаче (совет 8). Когда это неосуществимо на практике, все же можно написать небольшие тестовые программы для получения хотя бы приблизительного представления о том, чего можно ожидать,

Таблица. 2.3. Сравнение производительности TCP и UDP при количестве посылаемых байтов, равном 300

TCP

Сервер

Время по часам

Пользовательское время

Системное время

Мб/с

bsd

1,059

0,0124

0,445

1,416

sparс

1,5552

0,0084

1,2442

0,965

UDP

Сервер

Время по часам

Пользовательское время

Системное время

Мб/с

Потеряно

bsd

1,6324

0,0324

0,9998

0,919

212

sparс

1,9118

0,0278

1,4352

0,785

306

Если говорить о практической стороне вопроса, то современные реализации статочно эффективны. Реально продемонстрировано, что TCP может работать со скоростью аппаратуры на стомегабитных сетях FDDI. В недавних экспериментах были достигнуты почти гигабитные скорости при работе на персональном компьютере [Gallatin et al. 1999].

Примечение: 29 июля 1999 года исследователи из Университета Дъюка на рабочей станции ХР1000 производства DEC/Compaq на базе процессора Alpha в сети Myrinet получили скорости передачи порядка гигабита в секунду. В экспериментах использовался стандартный стек TCP/IP из системы FreeBSD 4.0, модифицированный по технологии сокетов без копирования (zero-copy sockets). В том же эксперименте была получена скорость более 800 Мбит/с на персональном компьютере PII 450 МГц и более ранней версии сети Myrinet. Подробности можно прочитать на Web-странице http://www.cs.duke.edu/ari/trapeze.


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