KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программное обеспечение » Уильям Стивенс - UNIX: разработка сетевых приложений

Уильям Стивенс - UNIX: разработка сетевых приложений

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Уильям Стивенс, "UNIX: разработка сетевых приложений" бесплатно, без регистрации.
Перейти на страницу:

Листинг 5.12. Заголовочный файл sum.h

//tcpcliserv/sum.h

1 struct args {

2  long arg1;

3  long arg2;

4 };


5 struct result {

6  long sum;

7 };

Листинг 5.13. Функция str_cli, отправляющая два двоичных целых числа серверу

//tcpcliserv/str_cli09.c

 1 #include "unp.h"

 2 #include "sum.h"


 3 void

 4 str_cli(FILE *fp, int sockfd)

 5 {

 6  char sendline[MAXLINE];

 7  struct args args;

 8  struct result result;


 9  while (Fgets(sendline, MAXLINE, fp) != NULL) {


10   if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) {

11    printf("invalid input, %s", sendline);

12    continue;

13   }

14   Writen(sockfd, &args, sizeof(args));

15   if (Readn(sockfd, &result, sizeof(result)) == 0)

16    err_quit("str_cli: server terminated prematurely");


17   printf("%ldn", result.sum);

18  }

19 }

10-14 Функция sscanf преобразует два аргумента из текстовых строк в двоичные. Мы вызываем функцию writen для отправки структуры серверу.

15-17 Мы вызываем функцию readn для чтения ответа и выводим результат с помощью функции printf.

В листинге 5.14 показана наша функция str_echo.

Листинг 5.14. Функция str_echo, складывающая два двоичных целых числа

//tcpcliserv/str_echo09.c

 1 #include "unp.h"

 2 #include "sum.h"


 3 void

 4 str_echo(int sockfd)

 5 {

 6  ssize_t n;

 7  struct args args;

 8  struct result result;


 9  for (;;) {

10   if ((n = Readn(sockfd, &args, sizeof(args))) == 0)

11    return; /* соединение закрыто удаленным концом */


12   result.sum = args.arg1 + args.arg2;

13   Writen(sockfd, &result, sizeof(result));

14  }

15 }

9-14 Мы считываем аргументы при помощи вызова функции readn, вычисляем и запоминаем сумму и вызываем функцию writen для отправки результирующей структуры обратно.

Если мы запустим клиент и сервер на двух машинах с аналогичной архитектурой, например на двух компьютерах SPARC, все будет работать нормально:

solaris % tcpcli09 12.106.32.254

11 22 мы вводим эти числа

33    а это ответ сервера

-11 -44

-55

Но если клиент и сервер работают на машинах с разными архитектурами, например, сервер в системе FreeBSD на SPARC, в которой используется обратный порядок байтов (big-endian), а клиент — в системе Linux на Intel с прямым порядком байтов (little-endian), результат будет неверным:

linux % tcpcli09 206.168.112.96

1 2       мы вводим эти числа

3         и сервер дает правильный ответ

-22 -77   потом мы вводим эти числа

-16777314 и сервер дает неверный ответ

Проблема заключается в том, что два двоичных числа передаются клиентом через сокет в формате с прямым порядком байтов, а сервер интерпретирует их как целые числа, записанные с обратным порядком байтов. Мы видим, что это допустимо для положительных целых чисел, но для отрицательных такой подход не срабатывает (см. упражнение 5.8). Действительно, в подобной ситуации могут возникнуть три проблемы:

1. Различные реализации хранят двоичные числа в различных форматах. Наиболее характерный пример — прямой и обратный порядок байтов, описанный в разделе 3.4.

2. Различные реализации могут хранить один и тот же тип данных языка С по- разному. Например, большинство 32-разрядных систем Unix используют 32 бита для типа long, но 64-разрядные системы обычно используют 64 бита для того же типа данных (см. табл. 1.5). Нет никакой гарантии, что типы short, int или long имеют какой-либо определенный размер.

3. Различные реализации по-разному упаковывают структуры в зависимости от числа битов, используемых для различных типов данных, и ограничений по выравниванию для данного компьютера. Следовательно, неразумно передавать через сокет двоичные структуры.

Есть два общих решения проблемы, связанной с различными форматами данных:

1. Передавайте все численные данные как текстовые строки. Это то, что мы делали в листинге 5.11. При этом предполагается, что у обоих узлов один и тот же набор символов.

2. Явно определяйте двоичные форматы поддерживаемых типов данных (число битов и порядок байтов) и передавайте все данные между клиентом и сервером в этом формате. Пакеты удаленного вызова процедур (Remote Procedure Call, RPC) обычно используют именно эту технологию. В RFC 1832 [109] описывается стандарт представления внешних данных (External Data Representation, XDR), используемый с пакетом Sun RPC.

5.19. Резюме

Первая версия наших эхо-клиента и эхо-сервера содержала около 150 строк (включая функции readline и writen), но многие ее детали пришлось модифицировать. Первой проблемой, с которой мы столкнулись, было превращение дочерних процессов в зомби, и для обработки этой ситуации мы перехватывали сигнал SIGCHLD. Затем наш обработчик сигнала вызывал функцию waitpid, и мы показали, что должны вызывать именно эту функцию вместо более старой функции wait, поскольку сигналы Unix не помещаются в очередь. В результате мы рассмотрели некоторые подробности обработки сигналов POSIX, аза дополнительной информацией по этой теме вы можете обратиться к [110, глава 10].

Следующая проблема, с которой мы столкнулись, состояла в том, что клиент не получал уведомления о завершении процесса сервера. Мы видели, что TCP нашего клиента получал уведомление, но оно не доходило до клиентского процесса, поскольку тот был блокирован в ожидании ввода пользователя. В главе 6 для обработки этого сценария мы будем использовать функции select или poll, позволяющие ожидать готовности любого из множества дескрипторов вместо блокирования при обращении к одному дескриптору.

Мы также обнаружили, что если узел сервера выходит из строя, мы не можем определить это до тех пор, пока клиент не пошлет серверу какие-либо данные. Некоторые приложения должны узнавать об этом факте раньше, о чем мы поговорим далее, когда в разделе 7.5 будем рассматривать параметр сокета SO_KEEPALIVE.

В нашем простом примере происходил обмен текстовыми строками, и поскольку от сервера не требовалось просматривать отражаемые им строки, все работало нормально. Передача численных данных между клиентом и сервером может привести к ряду новых проблем, что и было продемонстрировано.

Упражнения

1. Создайте сервер TCP на основе листингов 5.1 и 5.2 и клиент TCP на основе листингов 5.3 и 5.4. Запустите сервер, затем запустите клиент. Введите несколько строк, чтобы проверить, что клиент и сервер работают. Завершите работу клиента, введя символ конца файла, и заметьте время. Используйте программу netstat на узле клиента для проверки того, что клиентский конец соединения проходит состояние TIME_WAIT. Запускайте netstat примерно каждые 5 с, чтобы посмотреть, когда закончится состояние TIME_WAIT. Каково время MSL для вашей реализации?

2. Что происходит с нашим соединением клиент-сервер, если мы запускаем клиент и подключаем к стандартному потоку ввода двоичный файл?

3. В чем разница между нашим соединением клиент-сервер и использованием клиента Telnet для взаимодействия с нашим эхо-сервером?

4. В нашем примере в разделе 5.12 мы проверили, что первые два сегмента завершения соединения (сегмент FIN от сервера, на который затем клиент отвечает сегментом ACK) отправляются, при просмотре состояний сокета с помощью программы netstat. Происходит ли обмен двумя последними сегментами (FIN от клиента, на который затем сервер отвечает сегментом ACK)? Если да, то когда? Если нет, то почему?

5. Что произойдет с примером, рассмотренным в разделе 5.14, если между шагами 2 и 3 мы перезапустим сервер на узле сервера?

6. Чтобы проверить, что происходит с сигналом SIGPIPE в разделе 5.13, измените листинг 5.3 следующим образом. Напишите обработчик сигнала для SIGPIPE, который будет просто выводить сообщение и возвращать управление. Установите этот обработчик сигнала перед вызовом функции connect. Измените номер порта сервера на 13 (порт сервера времени и даты). Когда соединение установится, с помощью функции sleep войдите в состояние ожидания на 2 с, с помощью функции write запишите несколько байтов в сокет, проведите в состоянии ожидания (sleep) еще 2 с и с помощью функции write запишите еще несколько байтов. Запустите программу. Что происходит?

7. Что произойдет на рис. 5.5, если IP-адрес узла сервера, заданный клиентом при вызове функции connect, является IP-адресом, связанным с крайним правым канальным уровнем на стороне сервера, а не IP-адресом, связанным с крайним левым канальным уровнем?

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*