В листинге 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 % <b>tcpcli09 12.106.32.254</b>
<b>11 22</b> <i>мы вводим эти числа</i>
33 <i>а это ответ сервера</i>
<b>-11 -44</b>
-55
Но если клиент и сервер работают на машинах с разными архитектурами, например, сервер в системе FreeBSD на SPARC, в которой используется обратный порядок байтов (big-endian), а клиент — в системе Linux на Intel с прямым порядком байтов (little-endian), результат будет неверным:
linux % <b>tcpcli09 206.168.112.96</b>
<b>1 2</b> <i>мы вводим эти числа</i>
3 <i>и сервер дает правильный ответ</i>
<b>-22 -77</b> <i>потом мы вводим эти числа</i>
-16777314 <i>и сервер дает неверный ответ</i>
Проблема заключается в том, что два двоичных числа передаются клиентом через сокет в формате с прямым порядком байтов, а сервер интерпретирует их как целые числа, записанные с обратным порядком байтов. Мы видим, что это допустимо для положительных целых чисел, но для отрицательных такой подход не срабатывает (см. упражнение 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 для вашей реализации?