1.2. Простой клиент времени и даты

We use cookies. Read the Privacy and Cookie Policy

1.2. Простой клиент времени и даты

Рассмотрим конкретный пример, на котором мы введем многие понятия и термины, используемые в этой книге. В листинге 1.1[1] представлена реализация TCP-клиента времени и даты. Этот клиент устанавливает TCP-соединение с сервером, а сервер просто посылает клиенту время и дату в текстовом формате.

Листинг 1.1. Клиент TCP для определения времени и даты

//intro/daytimetcpcli.с

 1 #include "unp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd, n;

 6  char recvline[MAXLINE + 1];

 7  struct sockaddr_in servaddr;

 8  if (argc != 2)

 9   err_quit("usage: a.out <Ipaddress>");

10  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)

11   err_sys("socket error");

12  bzero(&servaddr, sizeof(servaddr));

13  servaddr.sin_family = AF_INET;

14  servaddr.sin_port = htons(13); /* сервер времени и даты */

15  if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)

16   err_quit("inet_pton error for %s", argv[1]);

17  if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) < 0)

18   err_sys("connect error");

19  while ((n = read(sockfd, recvline, MAXLINE)) > 0) {

20   recvline[n] = 0; /* завершающий нуль */

21   if (fputs(recvline, stdout) == EOF)

22    err_sys("fputs error");

23  }

24  if (n < 0)

25  err_sys("read error");

26  exit(0);

27 }

ПРИМЕЧАНИЕ

Это формат, который мы используем для всего исходного кода в тексте. Каждая непустая строка пронумерована. Абзац, описывающий некоторую часть кода, начинается с двух номеров — начального и конечного номеров тех строк, о которых идет речь в данном абзаце. Как правило, абзацу предшествует короткий заголовок, в котором кратко резюмируется содержание описываемого кода.

В начале фрагмента кода указано имя файла исходного кода: в данном примере это файл daytimetcpcli.c в каталоге intro. Поскольку исходный код всех примеров этой книги можно свободно скачать из Сети (см. предисловие), вы можете найти соответствующие исходные файлы по их названиям. Наилучший способ изучить концепции сетевого программирования — компилировать, запускать и особенно модифицировать эти программы в ходе изучения книги.

ПРИМЕЧАНИЕ

Мы будем использовать примечания наподобие этого для описания особенностей реализации и исторических справок.

Если мы откомпилируем эту программу в определенный по умолчанию файл a.out и выполним ее, на выходе мы получим следующее:

solaris % a.out 206.168.112.96 наш ввод

Mon May 26 20:58:40 2003 вывод программы

ПРИМЕЧАНИЕ

Отображая интерактивный ввод и вывод, мы показываем то, что мы вводим, полужирным шрифтом; вывод же компьютера показываем моноширинным шрифтом. Мы всегда указываем название системы как часть приглашения интерпретатора (в данном примере solaris), чтобы показать, на каком узле выполняется команда. Системы, используемые для выполнения большинства примеров этой книги, показаны на рис. 1.7. Имена узлов обычно соответствуют операционным системам.

В этой программе, состоящей из 27 строк, есть много важных особенностей, нуждающихся в обсуждении. Мы кратко рассмотрим их на тот случай, если это первая сетевая программа, с которой вы встретились, а более подробные сведения по соответствующим вопросам вы сможете получить в других главах.

Подключение собственного заголовочного файла

1 Мы подключаем наш собственный заголовочный файл, unp.h, текст которого приведен в разделе Г.1. Этот заголовочный файл, в свою очередь, подключает различные системные заголовочные файлы, которые необходимы большинству сетевых программ, и определяет используемые нами константы (например, MAXLINE).

Аргументы командной строки

2-3 Определение функции main вместе с аргументами командной строки. Везде в данной книге при написании кода подразумевалось, что для его компиляции должен использоваться компилятор ANSI С (American National Standards Institute — Национальный институт стандартизации США), который также называют ISO С.

Создание сокета TCP

10-11 Функция socket создает потоковый сокет (SOCK_STREAM) Интернета (AF_INET) — это красивое название для обычного TCP-сокета). Функция возвращает дескриптор (небольшое целое число), который мы используем для идентификации сокета во всех последующих вызовах (например, connect и read).

ПРИМЕЧАНИЕ

Оператор if содержит вызов функции socket, присваивание возвращаемого значения переменной sockfd и последующую проверку, является ли это присвоенное значение меньшим нуля. Мы могли разбить этот оператор на два оператора С следующим образом:

sockfd = socket(AF_INET, SOCK_STREAM, 0);

 if (sockfd < 0)

но использованная в листинге 1.1 запись является типичным для языка С способом объединения двух строк. Поскольку в языке С оператор «меньше» (<) имеет более высокий приоритет, чем оператор присваивания, необходимо заключить в скобки операции присваивания и вызова функции (как это и сделано в листинге 1.1, в строке 10). Между двумя открывающими скобками мы всегда вставляем пробел как указание на то, что левая часть операции сравнения содержит также операцию присваивания. (Этот стиль позаимствован из исходного кода Minix [120].) Мы используем этот же прием в операторе while дальше в нашей программе.

Мы будем встречать множество различных вариантов использования термина сокет (socket). Во-первых, используемый нами API называется API сокетов. Во-вторых, в предыдущем абзаце мы упоминали функцию socket, которая входит в API сокетов. В-третьих, там же мы ссылались и на «сокет TCP», который является синонимом конечной точки TCP (TCP endpoint).

Если вызов функции socket оказывается неудачным, мы прерываем выполнение программы с помощью вызова функции err_sys. Она выдает сообщение об ошибке с ее описанием (например, «Протокол не поддерживается» — одна из возможных ошибок функции socket) и прерывает выполнение процесса. Эта функция создана нами, как и некоторые другие, начинающиеся с err_. Мы будем широко использовать их в примерах в последующих главах. Описание функций приводится в разделе Г.4.

Задание IP-адреса и порта сервера

12-16 Мы заполняем структуру адреса сокета Интернета (структура типа sockaddr_in с именем servaddr) IP-адресом и номером порта сервера. Сначала мы инициализируем всю структуру нулями, используя функцию bzero, затем устанавливаем номер порта в 13 (который является номером заранее известного порта (well-known port) сервера времени и даты на любом узле TCP/IP, поддерживающем соответствующую службу — см. табл. 2.1), после чего устанавливаем IP-адрес равным значению, определенному первым аргументом командной строки (argv[1]). В этой структуре поля IP-адреса и номера порта должны иметь определенный формат: мы вызываем библиотечную функцию htons (host to network short), чтобы преобразовать двоичный номер порта в требуемый формат, и вызываем библиотечную функцию inet_pton (presentation to numeric), чтобы преобразовать аргумент командной строки в символах ASCII (например, 206.168.112.96 при выполнении данного примера) в двоичный формат.

ПРИМЕЧАНИЕ

Функция bzero не является функцией ANSI С. Она происходит от более раннего кода сетевого программирования Беркли. Тем не менее мы используем именно ее, а не функцию ANSI С memset, потому что с функцией bzero работать проще: она вызывается с двумя аргументами, a memset — с тремя. Почти каждый производитель, поддерживающий API сокетов, также реализует и функцию bzero, а если и не реализует, мы определяем ее через макрос в нашем заголовочном файле unp.h.

Автор [112] в первом издании сделал десять ошибок, поменяв местами аргументы memset. Компилятор С не может распознать эту ошибку, поскольку оба аргумента принадлежат одному типу. В действительности второй аргумент принадлежит типу int, а третий — size_t — обычно имеет тип unsigned int (то есть целое без знака), но заданные значения, соответственно, 0 и 16, являются допустимыми для обоих типов аргумента. Вызов функции memset все равно осуществлялся, но реально функция ничего не делала, поскольку задавалось нулевое число инициализируемых байтов. Программа работала, потому что только некоторые функции сокетов действительно требуют, чтобы последние 8 байт структуры адреса сокета Интернета были установлены в 0. Тем не менее это ошибка, и ее можно избежать при использовании функции bzero, поскольку перестановка двух аргументов функции bzero всегда будет выявлена компилятором С, если используются прототипы функций.

Возможно, вы впервые встречаете функцию inet_pton. Она появилась вместе с протоколом IPv6 (о котором более подробно мы поговорим в приложении А). В старых программах для преобразования точечно-десятичной записи (dotted-decimal string) ASCII в необходимый формат использовалась функция inet_addr, но у нее есть ряд ограничений, которых не имеет функция inet_pton. Не беспокойтесь, если ваша система (еще) не поддерживает эту функцию; реализация ее приведена в разделе 3.7.

Установка соединения с сервером

17-18 Функция connect, применяемая к сокету TCP, устанавливает соединение по протоколу TCP с сервером, адрес сокета которого содержится в структуре, на которую указывает второй аргумент. Мы также должны задать длину структуры адреса сокета в качестве третьего аргумента функции connect, а для структур адреса интернет-сокета мы всегда предоставляем вычисление длины компилятору, используя оператор С sizeof.

ПРИМЕЧАНИЕ

В заголовочном файле unp.h мы используем директиву #define SA, чтобы определить SA как struct sockaddr, что соответствует общей структуре адреса сокета. Каждый раз, когда одна из функций сокетов требует указателя на структуру адреса сокета, этот указатель должен быть преобразован к указателю на общую структуру адреса сокета. Это происходит потому, что функции сокетов появились раньше, чем стандарт ANSI С. Соответственно, тип указателя void* не был доступен в начале 80-х, когда эти функции были разработаны. Проблема состоит в том, что "struct sockaddr" занимает 15 символов и часто заставляет выходить строку исходного кода за правую границу экрана (или за страницу книги), поэтому мы сократили ее до SA. Более подробно мы исследуем общие структуры адресов сокетов на примере листинга 3.2.

Чтение и отображение ответа сервера

19-25 Мы читаем ответ сервера и отображаем результат, используя стандартную функцию ввода-вывода fputs. Нужно быть внимательным при использовании TCP, поскольку это потоковый (byte-stream) протокол без границ записей. Обычно ответом сервера является 26-байтовая строка следующей формы:

Fri Jan 12 14:27:52 1996

где — это возврат каретки, а — перевод строки (в символах ASCII). В случае потокового протокола эти 26 байт можно получить в нескольких вариантах: в виде отдельного сегмента TCP, содержащего все 26 байт данных, либо в виде 26 сегментов, каждый из которых содержит по одному байту данных, или в виде любой другой комбинации, в сумме дающей 26 байт. Обычно возвращается один сегмент, содержащий все 26 байт, но при больших объемах данных нельзя рассчитывать, что ответ сервера будет получен с помощью одного вызова read. Следовательно, при чтении из сокета TCP нужно всегда вызывать функцию read циклически и прерывать цикл либо когда функция возвращает 0 (например, соединение было разорвано другой стороной), либо когда возвращенное значение оказывается меньше нуля (ошибка).

В приведенном примере конец записи обозначается сервером, закрывающим соединение. Эта технология используется также версией 1.0 протокола передачи гипертекста (Hypertext Transfer Protocol, HTTP). Существуют и другие способы обозначения конца записи. Например, протокол передачи файлов (File Transfer Protocol, FTP) и простой протокол передачи почты (Simple Mail Transfer Protocol, SMTP) обозначают конец записи 2-байтовой последовательностью, состоящей из символов ASCII возврата каретки и перевода строки. Служба вызова удаленных процедур (Remote Procedure Call, RPC) и система именования доменов (Domain Name System, DNS) помещают перед каждой записью, отсылаемой по протоколу TCP, двоичное число, соответствующее длине этой записи. Здесь важно осознать, что протокол TCP сам по себе не предоставляет никаких меток записей: если приложение хочет отделять записи одну от другой, оно должно делать это самостоятельно, и для этого имеются стандартные методы.

Завершение программы

26 Функция exit завершает программу. Unix всегда закрывает все открытые дескрипторы при завершении процесса, поэтому теперь наш сокет TCP закрыт.

Как уже говорилось, пока мы лишь выделили наиболее важные моменты, детальным исследованием которых займемся в дальнейшем.

Данный текст является ознакомительным фрагментом.