Библиотека: К. Джамса, К. Коуп. Программирование для Internet в среде Windows
Глава 13
Время и сетевой порядок байтов
Когда вы создаете программы для использования на каком-то компьютере (например, на персональном), вы, как правило, не уделяете большого внимания тому, как компьютер хранит в себе ваши данные. Однако когда вы создаете программы, ориентированные на использование сети, вы должны выяснить не только как хранятся те или иные данные на вашем компьютере, но и как те же данные хранятся на других компьютерах, входящих в сеть. Например, представьте, что вы проиграли пари на один доллар своему приятелю, пользователю Макинтоша по имени Ларри. Чтобы дать знать Ларри о том, что вы собираетесь заплатить эту сумму, вы создаете на своем персональном компьютере специальную сетевую программу, которая посылает Ларри такое сообщение:
Ларри,я должен тебе $1
Кен
Представьте свое удивление, когда Ларри позвонит вам на следующий день и сообщит восторженным тоном, что экран его Макинтоша показывает следующий текст:
Ларри,я должен тебе $512
Кен
Отправленное и принятое сообщения отличаются потому, что Макинтош и персональный компьютер на базе Intel хранят числовые данные совершенно по-разному. Поэтому, когда вы создаете программы для работы в Интернет, вам нужно помнить, что компьютеры по-разному хранят числовые данные. Хотя все компьютеры используют один или несколько байтов для представления численных значений, сами эти последовательности, представляющие одно и то же число, у разных компьютеров отличаются (последовательность хранения численных данных в памяти компьютера называется порядком байтов этого компьютера, byte-order).
В этой главе мы будем использовать один из простейших протоколов Интернет, Time Protocol (протокол времени), чтобы ближе познакомиться с проблемами, связанными с порядком байтов. К концу этой главы вы должны хорошо владеть следующими важными понятиями:
- Чем и как порядок байтов в компьютере отличается от порядка байтов в сети.
- Как преобразовать порядок байтов на компьютере в порядок байтов в сети и обратно.
- Почему для избежания проблем нужно внимательно следить за преобразованиями типов данных, получаемых из Интернет.
- Какие проблемы могут быть вызваны смешиванием значений со знаком и без знака.
13.1 Сетевой порядок байтов
Поскольку Интернет объединяет большое количество самых разных компьютеров, в этой сети принят стандартный формат хранения числовых данных. Профессионалы Интернет называют этот формат сетевым порядком байтов. Стандартный сетевой порядок байтов позволяет минимизировать ошибки, вызванные неправильным преобразованием данных, передаваемых с хостов одного типа на хосты другого типа. Чтобы отличить сетевой порядок байтов и порядок байтов, который обычно используется в компьютере, подсоединенном к сети, порядок байтов в компьютере часто называют порядком байтов хоста. На персональном компьютере порядок байтов хоста отличается от сетевого порядка байтов. Поэтому, прежде чем ваша программа для персонального компьютера сможет передать числовое значение в модуль программы, отвечающий за связь с Интернет, она должна преобразовать это значение, изменив в нем порядок байтов хоста на сетевой порядок байтов. Подобным образом, когда ваша программа получает числовое значение из Интернет, она должна преобразовать это значение, изменив в нем сетевой порядок байтов на порядок байтов хоста.
Примечание:Вашей программе не нужно делать никаких преобразований над численными данными, хранящимися в виде текста в датаграмме или сообщении. Другими словами, преобразование порядка байтов требуется, только когда программы для работы с сетью воспринимают данные как числовые.
Практически все бизнес-приложения используют числовые значения. То же самое относится и к сетевым бизнес-приложениям, работающим в Интернет. Если даже они не предназначены для денежных операций, они все равно будут иметь дело с какими-то числовыми значениями— объемами заказов и продаж, инвентарными номерами и т.п. Если ваша программа передает численные данные по Интернет, вы должны хорошо разбираться в проблемах порядка байтов. Ни владение основными понятиями, связанными с порядком байтов, ни решение практических проблем не представляют больших трудностей. Однако на первых порах, возможно, практические аспекты порядка байтов покажутся вам несколько запутанными.
13.2 Протоколы времени Интернет
Протоколы времени разрабатываются в Интернет для достижения двух разных целей. Эти протоколы предназначены либо для синхронизации времени на компьютерах сети, либо для сообщения информации о времени пользователям. На сегодняшний день в Интернет используются четыре разных протокола времени: Time Protocol, Daytime Protocol, Network Time Protocol (NTP) и Simple Network Time Protocol (SNTP). Два из этих протоколов— Daytime Protocol и Time Protocol позволяют узнать текущее время из Интернет. Как мы увидим ниже, протоколы NTP и SNTP предназначены для совсем других целей.
RFC 1305 под названием «Спецификация протокола сетевого времени, версия3» (Network Time Protocol (Version 3) Specification, Mills, 1992) определяет протокол NTP, который позволяет точно синхронизировать время на хостах Интернет. Другими словами, этот протокол помогает установить внутренние часы на двух или более компьютерах в Интернет в одно и то же значение. В RFC 1305 описаны сложные алгоритмы, используемые в NTP для точной синхронизации времени с отклонением в пределах от 1 до 50 миллисекунд от официального стандарта времени. RFC 1361 под названием «Простой протокол сетевого времени» (Simple Network Time Protocol, Mills, 1992) определяет протокол SNTP как упрощенную версию протокола NTP. Несмотря на то, что на практике SNTP дает ту же точность, что и NTP, в отличие от NTP SNTP не гарантирует этой точности. Системы, которым не нужна гарантированная точность, обеспечиваемая протоколом NTP, чаще всего пользуются протоколом SNTP.
В отличие от NTP и SNTP протоколы Daytime и Time устроены очень просто. Эти протоколы, как описывается в RFC 867 под названием «Протокол времени суток» (Daytime Protocol, Postel, 1983) и в RFC 868 под названием «Протокол времени» (Time Protocol, Postel and Harrenstien, 1983), дают текущее время с погрешностью в 1 секунду. Протокол Time Protocol возвращает 32-битное число, которое представляет время в секундах, истекшее с 1 января 1900. Протокол Daytime Protocol возвращает текстовую строку в формате, поддающемся прочтению пользователем. Протокол Daytime Protocol не позволяет указывать формат этой текстовой строки; как правило, строка эта будет иметь следующий вид («\r\n» означает пару символов «возврат каретки» и «перевод строки»):
Thu Jan 19 15:54:25 1995\r\n
Как следует из вышеизложенного, разница между двумя группами протоколов времени весьма значительна. Так, в RFC 1305 описание алгоритмов, используемых в NTP, занимает свыше 100 страниц, в то время как RFC 867 и 868 каждый умещаются на паре страниц. Разница в длине между этими RFC говорит о том, что сложность этих двух групп протоколов несопоставима. В этой главе мы будем пользоваться протоколом Time Protocol, описанном в RFC 868, для знакомства с проблемами сетевого порядка байтов. Другие протоколы времени в этой книге не описываются.
13.3 Протокол времени
Если вы используете персональные компьютеры уже несколько лет, вы, возможно, помните, что были времена, когда в персональных компьютерах не было внутренних часов. В самых первых компьютерах этого класса пользователь должен был вручную вводить текущую дату и время каждый раз, когда он включал компьютер и загружал операционную систему. Если вы застали эти времена, то вы, вероятно, помните также и проблемы, которые могла вызвать неустановка часов компьютера. Если у вас файлы с одинаковыми именами располагались в разных каталогах, то по дате и времени этих файлов нельзя было судить о том, какой из них был последней версией, а какой— более старой.
В наши дни неправильные показания часов персонального компьютера могут вызвать еще более серьезные проблемы. Большинство программ для персональных компьютеров полагаются на значение времени, сообщаемое часами компьютера, и используют его в своих вычислениях. Если часы компьютера работают неправильно, то программы могут давать неверные результаты.
Если одна и та же программа работает на нескольких компьютерах и все копии этой программы пользуются одними и теми же данными, то на этих компьютерах может понадобиться синхронизировать встроенные часы. Для синхронизации часов компьютеры могут использовать узел сети, на котором работает сервер времени. Программы серверов времени, использующие Time Protocol, определенный в RFC 868, сообщают текущую дату и время с точностью до одной секунды программам-клиентам.
Протокол Time Protocol может использовать любой из транспортных протоколов в наборе TCP/IP. Другими словами, чтобы передать данные от сервера клиенту, используя протокол Time Protocol, вы можете применять как транспортный протокол (TCP), так и протокол пользовательских датаграмм (UDP). Официальным номером порта для протокола Time Protocol является 37. Программа сервера времени ждет установки TCP-соединения и прихода UDP-датаграмм с порта 37. Как TCP, так и UDP используют один и тот же порт— другими словами, порт номер 37 закреплен за протоколом Time Protocol, а не за TCP или UDP.
13.4 Использование сервера времени
Установив TCP-соединение на порту 37, сервер возвращает 32-битное число, которое представляет текущее значение даты и времени. Затем сервер инициирует активное закрытие TCP-соединения. Если сервер времени не может определить текущее время на своем компьютере, то, согласно протоколу, сервер должен либо отказать в соединении, либо закрыть соединение без возврата какого-либо значения. При использовании TCP происходит следующая последовательность событий:
- Сервер времени следит за состоянием порта 37.
- Клиент времени соединяется с портом 37.
- Сервер времени посылает 32-битное число, представляющее текущее значение даты и времени.
- Клиент времени получает это 32-битное число.
- Сервер времени инициирует активное закрытие.
- Клиент времени инициирует пассивное закрытие.
Если сервер времени фиксирует приход датаграммы на порт 37, то он возвращает в ответ датаграмму с тем же 32-битным значением времени. Если сервер времени не может определить текущее время на своем компьютере, то, согласно протоколу, он должен отбросить датаграмму и ничего не посылать в ответ. При использовании UDP происходит следующая последовательность событий:
- Сервер времени следит за состоянием порта 37.
- Клиент времени посылает пустую датаграмму на порт 37.
- Сервер времени получает пустую датаграмму.
- Сервер времени посылает датаграмму, содержащую 32-битное число, представляющее текущее значение даты и времени.
- Клиент времени получает датаграмму.
Обратите внимание, что последовательность событий в случае работы по протоколу UDP не включает в себя фазу активного или пассивного соединения. Запомните, что UDP— это протокол, не требующий установки или разрыва соединения. Никакой прямой связи между портом программы сервера времени и программой клиента времени не устанавливается. Поэтому при обслуживании клиентов по протоколу UDP, сервер не должен заботиться о закрытии какого-либо соединения.
13.5 Декодирование значения времени
Как вы только что узнали, протокол Time Protocol использует 32-битное число для представления текущего значения даты и времени. Это 32-битное число представляет время как число секунд, истекших с момента времени 00:00 (полночь) 1 января 1900 года. Другими словами, это значит, например, что значение 1, возвращенное протоколом Time Protocol, будет означать момент времени 12:00:01 1 января 1900 года. В табл. 13.1 приведены числа, использованные в качестве примеров в спецификации Time Protocol (в RFC 868).
Как видно из табл. 13.1, десятичные значения, используемые протоколом Time Protocol, изменяются в зависимости от того, используете ли вы для их хранения знаковые или беззнаковые переменные. Поэтому, когда вы разрабатываете и тестируете сетевые программы, использующие и передающие по сети числовую информацию, вы должны быть особенно осторожны в тех случаях, когда для представления ваших значений используются десятичные числа. (Поскольку использование десятичных чисел для визуальной оценки дат почти ничего не дает, рекомендуется всегда использовать двоичные или шестнадцатиричные значения.)
13.6 Что такое порядок байтов?
Как вы знаете, байт— это наименьшая единица данных, которой можетманипулировать компьютер. Все современные компьютеры используют байты, состоящие из восьми бит. Поэтому, когда вы посылаете или принимаете один байт, вероятность того, что вы столкнетесь с какими-то проблемами, крайне невелика. Однако не все данные могут быть представлены одиночными байтами. Например, программисты на C и C++ часто используют для хранения значений целочисленные переменные. Целочисленные переменные бывают двух размеров— короткие целые и длинные целые. Короткое целое использует 16 бит или два байта для хранения данных, а длинное целое использует 32 бита. Как правило, размер, который переменная занимает в памяти, часто называют ее длиной.
Так, вы можете сказать, что длинное целое имеет в длину 32 бита, а короткое— 16 бит. Когда вы начинаете работать с единицами данных, занимающих по нескольку бит, вы сможете встретиться с несовместимостью между разными типами компьютеров, особенно если вы работаете с таким сетевым окружением, каким является Интернет, объединяющий множество различных типов компьютеров. Все эти типы компьютеров хранят многобайтовые единицы данных по-разному. За последние десятки лет возникало немало споров по поводу того, какой тип численного хранения данных является наилучшим. Однако эти споры не привели к единому мнению, а их результатом стало лишь то, что производители компьютеров стали строить машины, хранящие численную информацию не так, как другие. Ниже мы рассмотрим основные различия между двумя наиболее распространенными форматами хранения числовых данных.
13.7 С начала или с конца?
В большинстве современных языков принят порядок записи букв на письме слева направо. В случае чисел (десятичных, шестнадцатиричных или двоичных) запись их начинается с самой значимой (крайней левой цифры) и продолжается до наименее значимой (крайней правой) цифры, двигаясь слева направо. Для программистов адреса компьютерной памяти являются ничем иным как числами. На диаграммах чаще всего фрагменты памяти компьютера изображаются полосками, расположенными по вертикали, а не по горизонтали. Если некий текст расположен по вертикали, более естественным является чтение его сверху вниз. Однако эта привычка уже не является столь непреодолимой и обычно не вызывает большого труда привыкнуть к тому, что адреса ячеек памяти в таких диаграммах, наоборот, возрастают снизу вверх.
К сожалению, если располагать диаграммы компьютерной памяти горизонтально, сразу встает новая проблема. На рис. 13.1 приведена простая диаграмма, на которой один и тот же отрезок компьютерной памяти показан дважды. Левая половина диаграммы изображает этот отрезок вертикально, так что адреса ячеек увеличиваются сверху вниз и наименьшие адреса расположены сверху. Направой половине диаграммы тот же диапазон адресов расположен горизонтально, так что наименьшие значения адреса стоят слева, а наибольшие— справа. На этой диаграмме изображены ячейки памяти с номерами от 1001 до 1008.
Каждая ячейка памяти на рис. 13.1 хранит один байт данных. Как видите, ячейки памяти, изображенные на диаграмме, хранят в себе слово INTERNET, которое размещается в восьми ячейках памяти— по одной букве в каждом байте. Данные, записанные таким образом в памяти, вы можете прочитать на рис. 13.1 без какого-либо труда: левая половина диаграммы легко читается сверху вниз, а правая— слева направо. Однако вы, вероятно, заметили, что на правой части диаграммы ячейка памяти с наименьшим номером расположена слева, а с наибольшим— справа. Это вступает в противоречие с тем, как мы записываем числа— ведь в числах запись начинается с самых значимых цифр и продолжается слева направо до наименее значимых.
Рис.13.1 Два способа представления фрагмента памяти в компьютере
Теперь давайте усложним ситуацию. Предположим, что вы хотите разместить в ячейках памяти, показанных на рис. 13.1, не текстовые данные, а числа. Чтобы не усложнять себе жизнь без надобности, вы решаете использовать шестнадцатиричные значения для представления числовых данных (как вы уже, конечно, знаете, каждый байт данных может быть представлен двузначным шестнадцатиричных числом). Предположим, что вы хотите хранить два 32-битных числа (т.е. всего восемь байт) в ячейках памяти с номерами с 1001 по 1008. Предположим также, что числа, которые вам требуется записать, равны 0x12345678 (десятичное 305419896) и 0x12ABCDEF (десятичное 313249263). Каждая пара шестнадцатиричных цифр в этой записи представляет собой один байт. Как мы только что говорили, обычно числа читаются слева направо — от наиболее значимых цифр к наименее значимым. Проблемы с порядком байтов возникают тогда, когда вам приходится решать, сохранить ли этот общепринятый порядок чтения и записи чисел или воспользоваться тем, как располагаются в памяти номера ячеек, т.е. слева направо от наименее значимых цифр к наиболее значимым. На рис. 13.2 показано размещение наших двух шестнадцатиричных чисел вместо слова INTERNET в тех же ячейках памяти.
Рис.13.2 Фрагменты памяти, содержащие два 32-битных числа,записанных с порядком байтов «с начала»
Как видно из рис. 13.2, числа, записанные так, как показано на этом рисунке, вполне поддаются прочтению прямо с диаграммы, так как порядок их записи совпадает с общепринятым. Программисты называют такой порядок байтов при хранении числовых данных порядком «с начала»— так как наиболее значимый байт расположен на том конце числа, который вы встречаете первым, продвигаясь от ячеек памяти с меньшими номерами к ячейкам памяти с большими номерами. Итак, используя порядок байтов «с начала», вы храните наиболее значимый байт данных в ячейке памяти с наименьшим (наименее значимым) номером. Другими словами, при чтении слева направо значимость цифр, составляющих данные, уменьшается, а значимость номеров ячеек памяти увеличивается. Поэтому можно сказать, что последовательность (порядок) адресов памятии порядок байтов в ваших данных направлены в противоположные стороны.
В противоположность этому при применении порядка байтов «с конца» направления, в которых увеличиваются номера ячеек памяти и значимость байтов данных, совпадают. В начале располагается наименее значимый байт данных, и по мере увеличения значимости байтов увеличиваются и номера ячеек памяти. На рис. 13.3 показана запись чисел, которые мы брали в качестве примера, с использованием порядка байтов «с конца».
Рис.13.3 Фрагменты памяти, содержащие два 32-битных числа,записанных с порядком байтов «с конца»
Сравните расположение адресов памяти на рис. 13.3 и на рис. 13.2. Как видите, на рис. 13.3 порядок номеров ячеек памяти противоположный. Это сделано для того, чтобы вы могли прочитать записанные в память числа как обычно. Если на рис. 13.2 номера ячеек памяти увеличиваются слева направо начиная с номера 1001, в противоположность этому на рис. 13.3 номера ячеек памяти увеличиваются, если идти справа налево, так что ячейка с номером 1008 расположена в самом левом конце. На рис. 13.4 показаны оба порядка байтов— «с начала» и «с конца». Как видите, ячейки памяти на рис. 13.4 пронумерованы так же, как на рис. 13.2— слева направо начиная с адреса 1001. Рис. 13.4, на котором два разных порядка байтов представлены рядом друг с другом, позволяет лучше понять различия между ними.
Рис.13.4 Сравнение порядков байтов «с начала» и «с конца»
Главный аргумент, выдвигаемый защитниками порядка байтов «с конца»,— это то, что меньший адрес ячейки памяти соответствует меньшему по значимости байту числа. Однако такой порядок байтов требует существенных умственных усилий, если вы хотите читать записанные в одном и том же сегменте памяти текст и числовые данные. Если вы хотите, чтобы текст в памяти читался естественным образом— слева направо, то числа вам придется записывать в противоположном направлении. Наоборот, при записи «с начала» как текст, так и числовые данные естественным образом читаются в одном и том же направлении. Как правило, вам не приходится заботиться о порядке байтов «с начала» или «с конца». Ваш компьютер автоматически решает, какой порядок байтов ему использовать, и споры о том, какой порядок байтов наиболее эффективен и удобен, могут вызывать лишь академический интерес. Однако как только вы начнете соединять разные компьютеры вместе, вам придется самостоятельно заняться проблемами порядка байтов. Если два компьютера используют один и тот же порядок байтов, не имеет значения, какой именно этот порядок— «сначала» или «с конца». При этом оба компьютера с гарантией будут передавать и интерпретировать данные без ошибок. Однако если компьютеры используют различные порядки байтов, они должны выработать некоторое соглашение о том, как интерпретировать числовые данные. Простейшим решением было бы принять один из двух порядков байтов как стандарт для всей сети. Как вы узнаете из следующих разделов, именно это и сделали разработчики Интернет.
13.8 Определение стандартного сетевого порядка байтов
Порядок байтов в компьютере зависит от его микропроцессора, другими словами, аппаратная конструкция компьютера определяет порядок байтов хранящихся в нем данных. В табл. 13.2 указан используемый порядок байтов для нескольких популярных компьютерных платформ. Программируя для Интернет, вы можете столкнуться с любым из этих типов компьютеров.
Вы знаете, что при передаче данных с одного компьютера на другой, физический уровень сети передает последовательный поток битов данных по сетевым линиям передачи. Очевидно, что физический уровень сети не изменяет порядок битов в передаваемых данных. Так, когда вы передаете число 0xABCD (десятичное 43981) с компьютера PC на компьютер Макинтош, ваш PC (на котором используется порядок байтов «с конца») передает сперва менее значимый байт (0xCD), а затем более знрчимый байт (0xAB). Когда Макинтош получает эту посылку, он интерпретирует, в согласии с используемым на нем порядком байтов «с начала», первый байт как старший, а второй— как младший. В результате на компьютере Макинтош будет принято число 0xCDAB (десятичное 52651). Этот пример делает очевидным, как порядок байтов может привести к искажениям данных при передаче их по сети.
Для предотвращения таких ошибок Интернет определяет стандартный порядок байтов в сети, в качестве которого принят порядок байтов «с начала». Другими словами, Интернет предписывает всем компьютерам, входящим в него, посылать числовые данные так, чтобы старший байт шел перед младшим байтом. Читая байты данных, представляющих целочисленные значения в асинхронном пакете, вы должны помнить, что наиболее значимый (старший) байт идет первым, т.е. ближе к началу пакета, а менее значимый (младший) байт идет последним, ближе к концу пакета.
Примечание:В пределах пользовательской области данных пакета, вы вполне можете хранить данные в любом формате, какой вам нравится— «с начала», «с конца» или как-либо еще. Однако вы должны понимать, что программа, получающая ваши данные, должна в свою очередь преобразовать эти данные с использованием того порядка байтов, который принят на компьютере, на котором она работает.
Вы должны преобразовать все числовые значения с использованием сетевого порядка байтов прежде, чем передавать их по сети. Кроме того, вы должны не забыть преобразовать целочисленные значения, которые ваша программа будет получать из Интернет, в тот порядок байтов, который используется на вашем компьютере. К счастью, спецификация Windows Sockets (Winsock) представляет служебные функции, которые упрощают выполнение этих преобразований. В табл. 13.3 перечислены четыре функции Winsock API, предназначенные для преобразования байтов в целых числах.
13.9 Использование протокола Time Protocol
Как мы говорили выше, протокол Time Protocol— один из простейших протоколов Интернет. Согласно RFC 867, Time Protocol возвращает текущее время дня с точностью до одной секунды, полученное от программы сервера времени на одном из узлов Интернет. Time Protocol использует 32-битное число для представления текущего значения даты и времени, в котором хранится число секунд, истекшее с полуночи 00:00 1 января 1900 года. Компьютеры PC также имеют свою точку отсчета для функции даты и времени. К сожалению, точка отсчета на PC не совпадает с точкой отсчета протокола Time Protocol. Таймеры на персональных компьютерах представляют текущее значение даты и времени в виде числа секунд, истекших с полуночи 00:00 1 января 1970 года по Гринвичу. Как следует из табл. 13.1, в полночь 1 января 1970 года истекло 2208988800 (0x83AA7E80) секунд с 1 января 1900 года. Поэтому, если вы хотите использовать функции даты и времени для интерпретации значений даты и времени, полученных из Интернет, из них необходимо вычитать 70-летнюю разницу (2208988800).
13.10 Создание программы Quick Time
Как вы знаете, обмен информацией между сервером времени и клиентом времени происходит очень просто. Общеизвестный номер порта для сервера времени равен 37. Вы можете использовать TCP для соединения с портом 37 либо послать пустую датаграмму на порт с этим номером. В любом случае сервер времени вернет вам 32-битное число, представляющее текущее значение даты и времени в виде секунд, истекших с полуночи 00:00 1 января 1900 года. В этом разделе мы с вами создадим простую программу QTime, которая использует TCP-соединение для получения значения даты и времени из Интернет. Программа QTime построена по образцу всех остальных учебных программ, на которых в этой книге мы с вами изучали программирование для Интернет. Как и ранее, для того чтобы четче осветить основные понятия программирования для Интернет, мы не стали усложнять программу QTime сверх необходимости. QTime использует явное задание в коде программы многих значений, а чтобы избежать непроизводительных затрат, связанных с интерфейсом пользователя Windows и обработкой сообщений от окон, эта программа использует окна сообщений. Файл с исходным текстом QTIME.CPP вы найдете на дискете, приложенной к этой книге. Ниже приведен полный листинг файла QTIME.CPP. Подробное описание программы QTime расположено после листинга.
Примечание:В код программы заложено обращение к серверу времени на узловом компьютере по адресу cerfnet.com. Многие компьютеры в Интернет имеют свои серверы времени, и QTime может работать с любым из них. Если вам почему-либо не удается связаться с cerfnet.com, вы можете изменить исходный текст QTime, чтобы использовать любой другой хост в Интернет.
#include <stdlib.h>
#include <time.h>
#include "..\winsock.h"
#define PROG_NAME "Simple Timeserver Query"
#define HOST_NAME "cerfnet.com"
// Здесь может стоять любое
// (реальное) имя
#define WINSOCK_VERSION 0x0101
// Необходим Winsock версии 1.1
#define DEFAULT_PROTOCOL 0
// Протокол не указан,
// используем протокол по
// умолчанию
#define NO_FLAGS 0
// Флаги не используются
#define PC_REF_TIME2208988800L
// Точка отсчета для даты и
// времени на PCSOCKET ConnectTimeServerSocket(VOID)
{
WSADATA wsaData;
// Сведения о реализации Winsock LPHOSTENT pHostEnt;
// Структура информации о хосте
// Интернет SOCKADDR_IN sockAddr;
// Структура адреса сокета LPSERVENT pServEnt;
// Структура информации о
// сетевой службе short iTimePort;
// Официальный номер порта равен 37 int nConnect;
// Результаты соединения сокетаSOCKET
//nTimeserverSocket = INVALID_SOCKET;
// Номер
// сокета по умолчанию
if (WSAStartup(WINSOCK_VERSION, &wsaData))
MessageBox(NULL, "Could not load Windows Sockets DLL.", PROG_NAME,
MB_OK|MB_ICONSTOP);
else
// Ищем хост с этим именем
{
pHostEnt = gethostbyname(HOST_NAME);
if (!pHostEnt)
MessageBox(NULL, "Could not get IP address!", HOST_NAME,
MB_OK|MB_ICONSTOP);
else
// Создаем сокет
{
nTimeserverSocket = socket
(PF_INET, SOCK_STREAM,DEFAULT_PROTOCOL);
if
(nTimeserverSocket == INVALID_SOCKET)
MessageBox(NULL, "Invalid socket!!", PROG_NAME,
MB_OK|MB_ICONSTOP);
else
// Конфигурируем сокет
{
// Получаем информацию службы времени
//pServEnt = getservbyname("time", "tcp");
if (pServEnt == NULL) iTimePort = IPPORT_TIMESERVER;
//Официальный номер порта
else
iTimePort = pServEnt->s_port;
// Определяем адрес сокета sockAddr.sin_family = AF_INET;
// Семейство адресов Интернет
sockAddr.sin_port = iTimePort;
sockAddr.sin_addr = *((LPIN_ADDR)*pHostEnt->h_addr_list);
// Соединяем сокет nConnect = connect(nTimeserverSocket,
(PSOCKADDR)&sockAddr, sizeof(sockAddr));
if( nConnect)
{
MessageBox(NULL, "Error connecting socket!!", PROG_NAME,
MB_OK|MB_ICONSTOP);
nTimeserverSocket = INVALID_SOCKET;
}
}
}
}
return(nTimeserverSocket);
}
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,
LPSTR lpszCmdParam, int nCmdShow)
{
SOCKET nSocket;
// Номер сокета для сервера времени int nCharSent;
// Число переданных символов int nCharRecv;
// Число полученных символов DWORD dwNetTime;
// Значение времени,полученное из
// Интернет,в виде длинногоцелого
// без знака LONG lNetTime;
// Значение времени,полученное из
// Интернет, в виде длинного
// целого со знаком LONG lPCTime;
// Значение времени PC в виде
// длинного целого со знаком char szHostOrder[33];
// Шестнадцатиричное значение
// времени с порядком байтов хоста char szNetOrder[33];
// Шестнадцатиричное значение
// времени с сетевым порядком
// байтов char szUnsignedValue[33];
// Значение времени, полученное
// из Интернет, в виде
// длинного целого без знака char szSignedValue[33];
// Значение времени, полученное
// из Интернет, в виде
// длинного целого со знаком char szMsg[100];
// Буфер общего пользования
// для хранения сообщений nSocket = ConnectTimeServerSocket();
if (nSocket != INVALID_SOCKET)
{
// Запрашиваем сервер о текущем времени,посылая ему
CRLF nCharSent = send(nSocket, "\n", lstrlen("\n"),NO_FLAGS);
if (nCharSent == SOCKET_ERROR)
MessageBox(NULL, "Error occurred during send()!", PROG_NAME,
MB_OK|MB_ICONSTOP);
else
// Получаем текущее времяот сервера времени
{
nCharRecv = recv(nSocket, (LPSTR)&lNetTime, sizeof(lNetTime),
NO_FLAGS);
if (nCharRecv == SOCKET_ERROR)
MessageBox(NULL, "Error occurred during recv()!", PROG_NAME,
MB_OK|MB_ICONSTOP);
else
// Преобразуем результаты из сетевого
// порядка байтов в местный
{
lPCTime = ntohl(lNetTime);
// Преобразуем числа вASCII-строки для
// использования в окне сообщения ltoa(lPCTime, szHostOrder, 16);
// Используем шестнадцатиричную запись _ltoa(lNetTime, szNetOrder,
//16);
// Используем шестнадцатиричную запись
//lPCTime = lPCTime - PC_REF_TIME;
//// Вычитаем точку отсчета времени PC
wsprintf(szMsg,"%s\n\nBYTE-ORDER\n\nNetwork:\t%s\nHost PC:\t%s",
(LPSTR)ctime(&lPCTime),(LPSTR)szNetOrder, (LPSTR)szHostOrder);
MessageBox(NULL, szMsg, PROG_NAME, MB_OK|MB_ICONINFORMATION);
// Присваиваем сетевое значение времени
// переменной без знака dwNetTime = lNetTime;
// Форматируем 32-битное значение сетевого
// времени как длинное целое без знака _ultoa(dwNetTime,
//szUnsignedValue,10);
// Форматируем 32-битное значение сетевого
// времени как длинное целое без знака _ltoa(lNetTime,
//szSignedValue,
//10);
// Выводим 32-битное значение, полученное из
// сети wsprintf(szMsg,"Unsigned:\t%s\nSigned:\t\t%s", (LPSTR)
szUnsignedValue,(LPSTR)szSignedValue);
MessageBox(NULL, szMsg, PROG_NAME, MB_OK|MB_ICONINFORMATION);
}
}
}
WSACleanup();
// Освобождаем все занятые программой
// ресурсы и заканчиваем работу
return(NULL);
}
13.11 Новое в программе
Как мы уже говорили выше, программа QTime построена по тому же образцу, что и программы QLookup и QFinger, и состоит по большей части из тех же фрагментов кода. Основные отличия программы QTime от других учебныхпрограмм, которые мы писали ранее, описываются в следующих разделах.
13.12 Как QTime посылает запрос серверу времени?
В приведенном ниже операторе программы обратите внимание на то, какие «данные» QTime посылает серверу времени на удаленном хосте. На первый взгляд может показаться, что QTime вообще не посылает никаких данных. Однако при более внимательном рассмотрении можно обнаружить, что с помощью функции send, QTime посылает в Интернет пустую строку— просто один символ перевода строки:
nCharSent = send(nSocket, "\n", lstrlen("\n"), NO_FLAGS);
Почему же QTime посылает серверу времени именно символ перевода строки? Дело в том, что сервер времени должен получить символ перевода строки или пару символов возврата каретки и перевода строки, чтобы понять, что с ним устанавливают соединение.
13.13 Определения констант
Большинство констант, используемых программой QTime, совпадают с теми, что мы видели в текстах программ QLookup и QFinger. Единственной новой константой в программе QTime является PC_REF_TIME, вводимая таким фрагментом кода:
#define PC_REF_TIME 2208988800L
// Точка отсчета даты и
// времени PC
Как мы уже знаем, в качестве точки отсчета для функций даты и времени на PC используется 1 января 1970 года, тогда как протокол Time Protocol в качестве такой точки отсчета использует 1 января 1900 года. С точки зрения Time Protocol момент полуночи 1 января 1970 года имеет значение 2208988800. Поэтому, чтобы интерпретировать значения даты и времени, которые ваш PC получает из Интернет, вы должны вычесть из 32-битного числа, возвращаемого Time Protocol, значение 2208988800. После этого можно использовать функции даты и времени вашего персонального компьютера.
13.14 Разделение программы на функции
Чтобы не увеличивать без надобности сложность программы, в предыдущих учебных программах все операторы были частью одной функции WinMain. Напротив, программа QTime использует несколько функций. В других учебных программах начальные операторы программы были практически идентичными. Первые две учебные программы, рассматривавшиеся в нашей книге, должны были сделать для вас привычными эти первые шаги алгоритма. Программа QTime, рассматриваемая в этой главе, выделяет эти первые шаги в отдельную функцию под названием ConnectTimeServerSocket. Вот как выглядит прототип функции ConnectTimeServerSocket:
SOCKET ConnectTimeServerSocket(VOID);
Как видите, функция ConnectTimeServerSocket не имеет параметров и возвращает значение типа SOCKET. Как можно догадаться по имени этой функции, ее целью является соединить сокет с сервером времени на одном из узлов Интернет и возвратить дескриптор сокета в вызывающую функцию.
13.15 Алгоритм функции ConnectTimeServerSocket
В функции ConnectTimeServerSocket операторы программы и даже имена переменных практически совпадают с теми, которые вы использовали в QLookup и QFinger. Ниже приведены шаги алгоритма, реализуемого функцией ConnectTimeServerSocket. Если вам нужна дополнительная информация о каких-либошагах этого алгоритма, обращайтесь к соответствующим разделам в главах 9 и 10.
- Функция ConnectTimeServerSocket вызывает функцию WSAStartup, чтобы инициализировать Winsock DLL.
- Функция ConnectTimeServerSocket вызывает функцию gethostbyname, чтобы найти IP-адрес узла, чей доменный адрес содержится в строчной константе HOST_NAME.
- Функция ConnectTimeServerSocket создает коммуникационный сокет и вызывает функцию socket, чтобы получить дескриптор сокета.
- Функция ConnectTimeServerSocket вызывает функцию getservbyname, чтобы получить из базы данных сетевых служб запись, соответствующую службе времени, использующей TCP.
- Чтобы определить адрес сокета, функция ConnectTimeServerSocket заполняет значениями структуру адреса сокета Winsock (которая определяется в SOCK_ADDR_IN). ConnectTimeServerSocket указывает семейство адресов, номер порта протокола и адрес хоста для этого сокета.
- Функция ConnectTimeServerSocket вызывает функцию Winsock connect, чтобы соединить сокет с удаленным хостом.
- Если функция ConnectTimeServerSocket успешно соединила коммуникационный сокет с хостом, она возвращает дескриптор сокета. В обратном случае функция ConnectTimeServerSocket возвращает константу INVALID_SOCKET.
13.16 Функция WinMain
Первые несколько строк функции WinMain в программе QTime определяют локальные переменные программы. Некоторые из этих переменных вам должны быть уже знакомы по предыдущим учебным программам. Ниже перечислено несколько новых переменных, вводимых программой QTime.
DWORD dwNetTime
// Значение времени, полученное из
// Интернет, в виде длинного целого
// без знакаLONG lNetTime;
// Значение времени, полученное из
// Интернет,в виде длинного целого
// со знакомLONG lPCTime;
// Значение времени PC в виде
// длинного целого со знаком char szHostOrder[33];
// Шестнадцатиричное значение
// времени с порядком байтов хостаchar szNetOrder[33];
// Шестнадцатиричное значение
// времени с сетевым порядком байтов char szUnsignedVal[33];
// Значение времени, полученное из
// Интернет, в виде длинного целого
// без знака char szSignedVal[33];
// Значение времени, полученное из
// Интернет, в виде длинного целого
// со знаком char szMsg[100];
// Буфер общего назначения для
// хранения сообщений
Как вы уже знаете, протокол Time Protocol возвращает 32-битное число, представляющее собой текущее значение даты и времени. Вам, вероятно, известно, что длинные целые переменные также занимают 32 бита. Поэтому QTime использует две длинные целые переменные, dwNetTime и lNetTime, для хранения числа, возвращенного протоколом Time Protocol. Префиксы имен этих переменных означают, что lNetTime хранит свое значение в виде длинного целого со знаком, а dwNetTime хранит свое значение в виде длинного целого без знака. Программе QTime требуется только одно из этих двух длинных целочисленных значений. Однако, как вы уже знаете, одна и та же последовательность битов может представлять два разных десятичных значения— одно со знаком, а другое без знака. Чтобы проиллюстрировать этот важный момент, QTime отображает полученное от Time Protocol 32-битное значение как в виде целого со знаком, так и в виде целого без знака.
Третье длинное целое, lPCTime, используется в QTime для хранения 32-битного значения, полученного от Time Protocol в формате, пригодном для использования функциями даты и времени вашего персонального компьютера. Чтобы получить это значение, QTime прежде всего вычитает точку отсчета даты и времени в Интернет (1 января 1900 года) из соответствующей точки отсчета вашего PC (1 января 1970). Затем QTime вычитает полученную разницу точек отсчета из текущего времени в Интернет. Результат этой операции QTime записывает в переменную lPCTime, с которой затем можно производить операции с помощью функций даты и времени из библиотеки времени выполнения вашего компилятора.
Оставшиеся переменные типа массив символов QTime использует для хранения различных интерпретаций 32-битного значения даты и времени. Переменные szHostOrder и szNetOrder содержат значения времени, проинтерпретированные с разным порядком байтов (с порядком байтов хоста и с сетевым порядком байтов соответственно). Переменные szUnsignedVal и szSignedVal хранят то же самое 32-битное значение даты и времени, проинтерпретированное как длинное целое со знаком и без знака. Наконец, переменная szMsg— это массив общего назначения, в котором QTime форматирует текстовые строки, выводимые в окна сообщений.
13.17 Начинаем работу
Определив локальные переменные, WinMain вызывает функцию ConnectTimeServerSocket, которая создает коммуникационный сокет и устанавливает соединение с сервером времени на удаленном хосте. Если функции ConnectTimeServerSocket не удалось создать сокет и установить соединение, она возвращает значение INVALID_SOCKET. Ниже приведены фрагменты кода, показывающие общую структуру функции WinMain:
nSocket = ConnectTimeServerSocket();
if (nSocket != INVALID_SOCKET)
{
// Выполняем операторы программы, получающие и выводящие
// на экран текущее значение даты и времени из Интернет
}
WSACleanup();
// Освобождаем все занятые программой
// ресурсы и заканчиваем работу
return(NULL);
Как видите, если функция ConnectTimeServerSocket не добилась успеха, QTime вызывает функцию WSACleanup и завершает работу. Как вы уже знаете, функция WSACleanup стирает регистрационные данные вызывающей программы из области данных Winsock DLL и освобождает все ресурсы, использовавшиеся от имени вашей программы.
13.18 Обмен данными с сервером времени
Ниже приведен фрагмент кода, почти не отличающийся от аналогичных фрагментов в других учебных программах. Сервер времени не требует никакого обмена специальными сообщениями. Как только сервер времени фиксирует TCP-соединение, он возвращает 32-битное значение даты и времени. QTime использует функцию send, чтобы передать пару символов возврата каретки и перевода строки после успешного соединения сокета. Если при этом не происходит ошибки, QTime с помощью функции recv принимает ответ сервера времени:
if
(nSocket != INVALID_SOCKET)
{
// Запрашиваем сервер о текущем времени, посылая ему
CRLF nCharSent = send(nSocket, "\n", lstrlen("\n"), NO_FLAGS);
if
(nCharSent == SOCKET_ERROR)
MessageBox(NULL, "Error occurred during send()!",PROG_NAME,
MB_OK|MB_ICONSTOP);
else
// Получаем текущее время от сервера времени
{
nCharRecv = recv(nSocket, (LPSTR)&lNetTime, sizeof(lNetTime),
NO_FLAGS);
if
(nCharRecv == SOCKET_ERROR)
MessageBox(NULL, "Error occurred during recv()!",PROG_NAME,
MB_OK|MB_ICONSTOP);
В приведенном фрагменте кода один момент заслуживает особого рассмотрения. Как вы уже знаете, прототип функции recv в Winsock выглядит так:
int PASCAL FAR recv(int s, char FAR * buf, int len,
int flags):
Как видите, второй параметр функции recv есть длинный указатель на буфер символьной строки (char), в которую recv будет записывать получаемые данные. Чтобы вы могли использовать функции библиотеки Winsock на разных сетях и с разными типами данных, функция recv в Winsock размещает по адресу, на который ссылается этот указатель, буфер сообщений. Если бы функция получения данных не пользовалась этим указателем, ваши сетевые программы должны были бы предусматривать разные варианты функции recv для каждого типа данных, которые им потребовалось бы передавать по сети. Поскольку указатель на символ ссылается на ячейку памяти с точностью до байта, он может указывать на любой тип данных. Допустим, к примеру, что из сети приходит длинное целое (длиной 32 бита). Чтобы сохранить эти данные, ваша программа должна выделить по меньшей мере четыре байта памяти. Однако сам Winsock вообще не обращает внимания на то, как вы размещаете свою память. Ваша программа может объявить длинное целое (которое займет четыре бита) или просто символьный массив длиной в четыре элемента. В любом случае функции Winsock требуется просто указатель с адресом первого байта выделенной области памяти.
Когда ваша программа, работающая с Winsock, использует функцию recv, она должна выделить область памяти для размещения буфера сообщений. Однако ваша программа может использовать буфер любого типа или размера. Чтобы использовать этот буфер с функцией recv, ваша программа просто преобразует адрес этого буфера к типу LPSTR (который определен как char FAR *). В свою очередь, ваш компилятор обработает такой вызов функции recv без каких-либо жалоб и возражений (т.е. с его точки зрения с типами параметров в этом случае все будет в порядке). Поскольку протокол Time Protocol возвращает 32-битное значение, QTime использует в качестве буфера сообщений длинное целое (длиной в 32 бита). С тем же успехом программа QTime могла бы использовать в качестве буфера сообщений массив символов. Однако в этом случае ей бы впоследствии пришлось преобразовывать содержимое символьного массива в длинное целое. Чтобы избежать этих ненужных преобразований, хороший тон программирования для Winsock требует, чтобы буферы сообщений имели правильный тип.
Например, если ваша программа объявляет длинное целое для использования в качестве буфера сообщений, другой программист, читая вашу программу, будет сразу знать, какой тип данных вы хотите получить из сети и записать в этот буфер. Явное преобразование типа в вызове функции recv свидетельствует для программиста, читающего вашу программу, о том, что функция recv требует в качестве своего второго параметра переменную типа LPSTR. Наоборот, если бы вы использовали в качестве буфера сообщений символьный массив, тот, кто читал бы вашу программу, мог прийти к выводу, что вы собираетесь получать из сети символьные данные. Дополнительной причиной для такого вывода было бы то, что функция recv требует параметр типа LPSTR (который определен как char FAR *), и в вызове этой функции не было бы никакого явного преобразования типов. Чтобы избежать этого неверного заключения, читатель вашей программы должен был бы знать подробности реализации протокола Time Protocol.
13.19 Преобразование типов значений Winsock
Как правило, программисты, пишущие на C и C++, должны по мере возможности избегать преобразований типов. Дело в том, что преобразования типов могут ввести в заблуждение современные компиляторы C и C++, в большинство из которых встроены возможности отслеживания ошибок, связанных с типами переменных. Другими словами, каждый раз, когда вы прибегаете к преобразованию типа, вы лишаете компилятор возможности предупредить вас в том случае, если вы по ошибке используете переменную не того типа, что может привести к ошибкам во время выполнения программы. Конечно, полностью избежать преобразований типов невозможно. Преобразования типов иногда выполняют законную и полезную функцию. Более того, если бы не существовало возможности преобразовывать типы, библиотеки функций, такие как Winsock API, были бы гораздо более запутанными и сложными в использовании. К несчастью, программисты на C и C++ часто бывают склонны к злоупотреблению преобразованиями типов. Поскольку библиотека функций Winsock разрабатывалась с расчетом на максимальную гибкость, преобразование типов— это неизбежная часть программирования Интернет с Winsock. Главное здесь— помнить, что вы должны использовать преобразование типов для переменных, которые вы передаете как параметры при вызовах функций Winsock. Наоборот, использование в программе преобразования типов для манипуляции полученных данных, хранящихся в буферах, является неверными подходом. Другими словами, вы должны определять буферы сообщений таким образом, чтобы они соответствовали тому типу данных, который в них будет помещен из Интернет. Следование этому принципу позволит вашему компилятору в большем числе случаев предупредить вас об ошибке несоответствия типов.
13.20 Преобразование порядка байтов
Если функция recv не возвращает кода ошибки, программа QTime должна преобразовать полученное значение сетевого времени, хранящееся в буфере сообщений, из сетевого порядка байтов в порядок байтов хоста. Как следует из приведенного ниже фрагмента кода, программа QTime вызывает для этого функцию Winsock ntohl:
if (nCharRecv == SOCKET_ERROR)
MessageBox(NULL, "Error occurred during recv()!", PROG_NAME,
MB_OK|MB_ICONSTOP);
else
// Преобразуем результаты из сетевого порядка байтов в
// местный
{
lPCTime = ntohl(lNetTime);
Примечание:Поскольку QTime использует в качестве буфера сообщений длинную целочисленную переменную (lNetTime), вызов функции ntohl не требует никакого преобразования типов.
Функция ntohl берет в качестве параметра длинное целое в сетевом порядке байтов и возвращает длинное целое в порядке байтов хоста. Вот как выглядит прототип функции ntohl:
u_long PASCAL FAR ntohl(u_long netlong);
Как уже упоминалось, Winsock API содержит четыре функции для преобразований, аналогичных тем, что делает функция ntohl. Две из этих функций меняют порядок байтов в 16-битовых целых числах (коротких целых), а две другие функции меняют порядок байтов в 32-битных целых (длинных целых). Как вы уже знаете, вы должны постоянно помнить о том, что целые значения передаются по Интернет только в сетевом порядке байтов. В обратном случае компьютеры, которые получают передаваемые вами данные, могут неправильно их интерпретировать. После того как QTime преобразует порядок байтов в полученном значении, программа сохраняет ASCII-представление полученного от Time Protocol 32-битного значения в виде строки шестнадцатиричных символов. Как следует из приведенного ниже фрагмента кода, QTime использует функцию C_ltoa для выполнения этого преобразования:
lPCTime = ntohl(lNetTime);
// Преобразуем числа в ASCII-строкидля использования в окне
// сообщения_ltoa(lPCTime, szHostOrder, 16);
// Используем
// шестнадцатиричную запись_ltoa(lNetTime, szNetOrder, 16);
// Используем
// шестнадцатиричную запись
Когда вы будете выполнять программу, QTime выведет на экран полученное от протокола Time Protocol значение как в сетевом порядке байтов, так и в порядке байтов хоста. Например, как видно на рис. 13.5, сервер времени на одном из узлов Интернет в момент времени 03:13:10 20 февраля 1995 года возвратил значение 0xC6F7F2B2.
Как видите, значение, которое QTime отображает в сетевом порядке байтов, равно 0xC6F7F2B2; то же самое значение, преобразованное к порядку байтов хоста, равно 0xB2F2F7C6. Помните, что каждая пара шестнадцатиричных цифр представляет один байт. Другими словами, эти восьмизначные шестнадцатиричные числа представляют четыре байта (или 32 бита) данных каждое. Обратите внимание на то, что младший байт значения в сетевом порядке байтов, 0xB2, становится старшим (крайним левым) байтом в порядке байтов хоста. Аналогично, старший байт в сетевом порядке байтов, 0xC6, становится младшим (крайним правым) байтом в порядке байтов хоста. Если разбить каждое число на четыре пары шестнадцатиричных цифр, то вы можете заметить, что порядок байтов в этих двух числах обратный. Другими словами, четыре байта в полученном из сети значении,
0xC6 0xF7 0xF2 0xB2
представленные в виде четырехбайтового значения на вашем компьютере, будут идти в обратном порядке:
0xB2 0xF2 0xF7 0xC6
13.21 Учет разницы точек отсчета
Точкой отсчета для даты и времени в Интернет служит 1 января 1900 года, тогда как на PC точкой отсчета является 1 января 1970 года. В соответствии со спецификацией протокола Time Protocol (RFC 868), значение 1 января 1970 года с точки зрения Интернет равно 2208988800. Программа QTime использует для хранения этого значения константу PC_REF_TIME:
#define PC_REF_TIME 2208988800L
// Точка отсчета даты и
// времени PC
Затем программа QTime преобразует порядок байтов 32-битного значения, полученного от сервера времени, и сохраняет новое значение времени в переменной lPCTime. Наконец, QTime вычитает из нового значения времени константу PC_REF_TIME. Как следует из приведенного ниже оператора программы, после этого QTime по-прежнему использует lPCTime для хранения значения времени, преобразованного теперь к порядку байтов хоста:
lPCTime = lPCTime - PC_REF_TIME;
// Вычитаем точку отсчета PC
13.22 Вывод времени, полученного из Интернет
После того как вы преобразуете порядок байтов и учтете разницу между точками отсчета в Интернет и на PC, вы можете использовать функции даты и времени из библиотеки времени выполнения вашего компилятора C/C++, чтобы манипулировать этими данными. Ниже показано, как QTime использует функцию ctime для преобразования значения времени и для последующего вывода его в окне сообщения Windows:
lPCTime = lPCTime - PC_REF_TIME;
// Вычитаем точку отсчета PC
wsprintf(szMsg, "%s\n\nBYTE ORDER\n\nNetwork:
\t%s\nHost PC:\t%s", ctime(&lPCTime), szNetOrder, szHostOrder);
MessageBox(NULL, szMsg, PROG_NAME, MB_OK|MB_ICONINFORMATION);
Вы, вероятно, знаете, что функция ctime преобразует значение времени PC в строку символов. Как видно из рис. 13.5, QTime выводит эту символьную строку в самом верху информационного окна над строками шестнадцатиричных символов, которые мы только что обсуждали.
Рис.13.5 Значения, полученные с помощью протокола Time Protocol 20 февраля 1995 года программой QTime
13.23 Данные без знака и со знаком
Последние несколько операторов программы функции WinMain просто показывают полученные из сети значения времени в виде значений со знаком и без знака. Как следует из приведенного ниже фрагмента кода, QTime присваивает значение со знаком, lNetTime, длинному целому без знака, dwNetTime, прежде, чем преобразовать десятичное значение в ASCII-строку с помощью функции _ultoa:
// Присваиваем значение времени, полученное из сети,
// переменной без знака dwNetTime = lNetTime;
// Форматируем 32-битное значение сетевого времени как
// длинное целое без знака_ultoa(dwNetTime, szUnsignedValue, 10);
Функция _ultoa преобразует длинные значения без знака в их ASCII-эквиваленты. Однако если вы просто передадите функции _ultoa целое значение со знаком, версия этой функции из библиотек многих компиляторов покажет, что создатели этих библиотек не зря получали деньги. Функция поступит так, как если бы вы на самом деле хотели бы использовать функцию _ltoa, которая преобразует длинное целое со знаком и преобразует ваше число в ASCII-представление длинного целого со знаком. Чтобы перехитрить этот механизм, QTime присваивает значение сетевого времени, хранящееся в lNetTime, переменной, объявленной как длинное целое без знака (dwNetTime), а затем использует переменную без знака в вызове функции. Как видно из рис. 13.5, в первом окне сообщения программа QTime представляет сетевое значение времени в виде двух строк шестнадцатиричных цифр. Эти значения делают очевидной разницу в порядке байтов между вашим компьютером и сетью. Однако во втором окне сообщения QTime показывает сетевое значение времени в виде двух десятичных чисел со знаком и без знака. Таким образом, вы можете наглядно видеть, как одна и та же последовательность битов представляет разные значения, если при этом используется переменная со знаком и без знака. Ниже приведен фрагмент кода, преобразующий сетевое значение времени в ASCII-представление длинного целого со знаком. В окно сообщения Windows QTime выводит это значение и ASCII-представление длинного целого без знака:
// Форматируем 32-битное значение сетевого времени как
// длинное целое со знаком_ltoa(lNetTime, szSignedValue, 10);
// Выводим 32-битное сетевое значение
wsprintf(szMsg, "Unsigned:\t%s\nSigned:\t%s", szUnsignedValue,
szSignedValue);
MessageBox(NULL, szMsg, PROG_NAME, MB_OK|MB_ICONINFORMATION);
Примечание:Второй параметр, передаваемый функциям _ltoa и _ultoa, определяет основание системы счисления, в которой числа переводятся в ASCII-строки. Например, вызов функции, показанный в предыдущем фрагменте кода, использует значение 10, чтобы заказать преобразования в десятичной системе счисления. Раньше мы использовали вызов функции _ltoa со вторым параметром, равным 16, чтобы запросить преобразования в шестнадцатиричной системе счисления.
13.24 Добавление запроса сервера времени в программу Sockman
Вероятно, на вашем компьютере есть встроенные часы. Однако скорее всего вы, как и большинство пользователей, не обращаете особого внимания на точность их хода. Так, если ваш компьютер сообщает правильную дату, то точное значение времени на его часах вас, вероятно, волнует в гораздо меньшей степени. Однако некоторые программы могут предъявлять повышенные требования к точности значений даты и времени внутренних часов. Поэтому вам может потребоваться периодически вставлять точное время на внутренних часах своего компьютера. На большинстве компьютеров само изменение показаний внутренних часов осуществляется довольно просто. Однако для получения точного времени обычно требуется позвонить по какому-то специальному номеру и выслушать сообщение автоматических часов. К счастью, когда вы соединяетесь с Интернет, вам уже не придется заниматься выяснением точного времени по телефону. Вместо этого парой щелчков мыши вы можете запустить утилиту связи с сервером времени из программы Sockman и установить точное время на часах своего компьютера. Ниже мы узнаем, как программа клиента времени встраивается в шаблон Sockman.
13.25 Общая картина
Утилита запроса к серверу времени в Sockman применяет тот же пользовательский интерфейс, который мы рассматривали в главах 11 и 12. С помощью команды меню пользователь открывает диалоговое окно, которое, в свою очередь, запускает модуль утилиты, передавая ему значение, введенное пользователем. Как и все программы Windows, функция WinMain обрабатывает все сообщения Windows, посылая их главной вызываемой функции (которая в Sockman называется WndProc). Как видно из рис. 13.6, в структуре программы Sockman можно выделить функции WndProc, DoMenuCommand и DoWinsockProgram, обособление которых позволяет облегчить разработку и чтение текста программы. Функция WndProc обрабатывает сообщения Windows общего назначения и передает все командные сообщения (WM_COMMAND) функции DoMenuCommand. В свою очередь, функция DoMenuCommand обрабатывает все сообщения, которые генерируются командами меню Sockman, за исключением тех, которые непосредственно запускают процессы Winsock. Sockman инициирует все процессы Winsock с помощью функции DoWinsockProgram.
Рис.13.6 Управляющие функции Sockman и отношения между ними
Как вы уже знаете, утилита сервера времени, как и все программные модули Sockman, может генерировать сообщения Windows, которые будут обрабатываться функциями WndProc и DoMenuCommand. При этом любой процесс запускается только из функции DoWinsockProgram.
13.26 Запуск утилиты сервера времени
Когда пользователь программы Sockman щелкает кнопкой мыши в меню Utilities по команде Time Server, Windows посылает в функцию WndProc сообщение WM_COMMAND, аргумент wParam которого равен IDM_TIME_UTIL. Функция WndProc затем передает это сообщение в функцию DoMenuCommand, которая, в свою очередь, вызывает функцию DoWinsockProgram. Как следует из приведенного ниже фрагмента кода, DoWinsockProgram прежде всего проверяет глобальную переменную hTimeServerTask, чтобы проверить, не выполняется ли в этот момент другая операция с сервером времени. Если это так, то Winsock выводит окно сообщения, прося пользователя обождать, пока закончится эта операция:
long DoWinsockProgram(HWND hwnd, UINT wParam, LONG lParam)
{
switch (wParam)
{
// ...прочие операторы case
case IDM_TIME_UTIL:
if (hTimeServerTask)
// Не более одного запроса
// на сервер времени одновременно
{
MessageBeep(0);
MessageBox(hwnd, "Timeserver utility is already in use.
Please wait...",
"SockMan - TIME SERVER", MB_ICONSTOP | MB_OK);
}
else
TimeServerDialog();
return(TRUE);
// ...прочие операторы case
}
return(FALSE);
}
Как видите, если утилита сервера времени не занята в данный момент обращением к другому серверу, DoWinsockProgram вызывает функцию TimeServerDialog, которая, в свою очередь, выводит на экран диалоговое окно. Управление запросами к серверу времени пользователь осуществляет из этого диалогового окна. Утилита сервера времени Sockman в этом аспекте отличается от опций Lookup и Finger. Диалоговые окна в модулях Lookup и Finger запускают соответствующие модули программы, но сами эти диалоговые окна после запуска закрываются. В отличие от этого, при выполнении операции с сервером времени Sockman оставляет диалоговое окно открытым на все время обмена данными с сервером в Интернет.
13.27 Диалоговое окно Time Server
Диалоговое окно Time Server остается открытым программой Sockman во время всех операций с сервером времени. Поэтому функция этого диалогового окна,TimeServerDialogProc, обрабатывает все сообщения Windows, поступающие во время обмена данными с сервером. Как видно из рис. 13.7, диалоговое окно Time Server включает три текстовых поля ввода и три командных кнопки.
Рис.13.7 Диалоговое окно программы Sockmanдля утилиты сервера времени
Из трех полей пользователь может редактировать только одно— поле Server Host. Чтобы указать адрес хоста, сервер времени которого будет запрошен, вы должны ввести в этом поле IP-адрес или имя. Как видно из рисунка, в поле Your Time отображается текущее время по часам вашего компьютера. Когда вы щелкаете мышью по кнопке Query, Sockman посылает запрос на сервер времени (точно так же, как это делалось в программе QTime), расположенный на хосте с указанным адресом. Затем Sockman отображает результаты запроса в поле Server Time.
Примечание:Большинство поставщиков услуг Интернет используют для запросов сервера времени порт с номером 37.
Кнопка Set Time так же выполняет запрос к серверу времени, как и кнопка Query. Однако кнопка Set Time, кроме того, устанавливает значения на внутренних часах вашего компьютера равными тем значениям, которые вернул сервер времени. Прежде чем вы захотите переустановить внутренние часы компьютера, вы можете с помощью кнопки Query проверить сервер времени на данном хосте и выяснить разницу между временем вашего компьютера и временем этого хоста. Sockman не требует от вас, чтобы вы посылали запрос на сервер времени, прежде чем менять показания внутренних часов. Это значит, что вы можете просто щелкнуть мышью по кнопке Set Time, и Sockman сам пошлет запрос на сервер времени и изменит показания внутренних часов в соответствии с результатами этого запроса. Кнопка Exit закрывает диалоговое окно Time Server.
13.28 Создание диалогового окна
В файле ресурсов Sockman, SOCKMAN4.RC, содержится определение ресурсовдля диалогового окна Time Server. Как очевидно из следующего фрагментакода, в диалоговом окне Time Server нет ничего слишком необычного. IDD_TIMESERVER просто определяет диалоговое окно, которое вы уже видели на рис.13.7.
IDD_TIMESERVER DIALOG
DISCARDABLE 0, 0, 206,
113STYLE DS_MODALFRAME
| WS_POPUP
| WS_VISIBLE
| WS_CAPTION
| WS_SYSMENUCAPTION "TIME SERVER"
FONT 8, "MS Sans Serif"
BEGIN
EDITTEXT IDC_TIMESERVER,54,9,140,15,ES_AUTOHSCROLL
DEFPUSHBUTTON "&Query",IDOK,55,70,65,16
DEFPUSHBUTTON "&Set Time",IDSETTIME,130,70,65,16,
NOT WS_TABSTOP
PUSHBUTTON "E&xit",IDCANCEL,130,90,65,15
RTEXT "Server Host:",IDC_STATIC,5,9,45,13
RTEXT "Your Time:",IDC_STATIC,4,32,47,11
EDITTEXT IDC_LOCAL_TIME,55,28,140,15,ES_AUTOHSCROLL
| ES_READONLY
| WS_DISABLED
| NOT WS_TABSTOP RTEXT "Server Time:",
IDC_STATIC,5,49,46,10
EDITTEXT
IDC_SERVER_TIME,54,48,140,15,ES_AUTOHSCROLL
| ES_READONLY
| WS_DISABLED
| NOT WS_TABSTOPEND
Как мы уже говорили, функция DoWinsockProgram вызывает функцию TimeServerDialog. Функция TimeServerDialog практически идентична функциям диалоговых окон, которые мы использовали для обработки команд меню Lookup и Finger. На самом деле, затратив небольшие усилия, вы сможете скомбинировать все три функции (TimeServerDialog, FingerDialog и LookupHostDialog) в одной. В программе Sockman эти три функции разделены с целью упростить примеры и собрать воедино функции программы, относящиеся к одной задаче. Вот как определена функция TimeServerDialog:
BOOL TimeServerDialog(VOID)
{
DLGPROC lpfnDialogProc;
BOOL bOkay;
// Создаем диалоговое окно для пользовательского ввода
lpfnDialogProc = MakeProcInstance
((DLGPROC)TimeServerDialogProc, hInstanceSockman);
bOkay = DialogBox(hInstanceSockman, "IDD_TIMESERVER",
hwndSockman, lpfnDialogProc);
FreeProcInstance(lpfnDialogProc);
if
(bOkay == -1)
{
wsprintf(szScratchBuffer, "Unable to create dialog box!");
MessageBeep(0);
MessageBox(hwndSockman, szScratchBuffer,
"SockMan-TIME SERVER QUERY", MB_OK|MB_ICONINFORMATION);
bOkay = FALSE;
}
return(bOkay);
}
Как видите, функция TimeServerDialog использует функцию Windows MakeProcInstance, чтобы получить дескриптор процедуры диалога IDD_TIMESERVER, которая называется TimeServerDialogProc.
Затем функция TimeServerDialog вызывает функцию DialogBox, чтобы создатьIDD_TIMESERVER. Функция TimeServerDialogProc обрабатывает сообщения Windows для этого диалогового окна и управляет всеми действиямиSockman при выполнении операций с сервером времени. Когда вы выходитеиз диалогового окна Time Server, функция TimeServerDialog освобождает дескриптор диалогового окна и возвращает управление в функцию DoWinsockProgram.
13.29 Управление диалоговым окном Time Server
В показанном ниже фрагменте кода приведена базовая структура функции TimeServerDialogProc. Как видите, функция TimeServerDialogProc обрабатывает пять сообщений, и с некоторыми из них вы уже знакомы. Например, Windows посылает сообщение WM_INITDIALOG, прежде чем вывести на экрандиалоговое окно. Когда вы щелкаете мышью по команде Close в управляющем меню окна, вы тем самым генерируете сообщение WM_CLOSE. Ниже в этой главе мы обсудим сообщения WM_GOT_SERVICE и WM_ASYNC_LOOKUP_DONE, которые являются специфическими сообщениями этого приложения (они определены в файле SOCKMAN4.H):
BOOL _export CALLBACK
TimeServerDialogProc(HWND hwndDlg, UINT iMessage, WPARAM wParam,
LPARAM lParam)
{
static BOOL bSetTime;
// Флаг установки времени PC time_t lPCTime;
// Время PC time_t lNetTime;
// Сетевое время NPSTR npTime;
// Служебный указатель для
// значений времени switch (iMessage)
{
case WM_INITDIALOG:
// Инициализируем диалоговое окно return(TRUE);
case WM_CLOSE:
PostMessage(hwndDlg, WM_COMMAND, IDCANCEL, 0L);
return(TRUE);
case WM_GOT_SERVICE:
LookupTimeServer(hwndDlg, lParam);
return(TRUE);
case WM_COMMAND:
// Выполняем команды диалогового окна break;
case WM_ASYNC_LOOKUP_DONE:
// Посылаем запрос на сервер времени и
// обрабатываем результаты
return(TRUE);
}
return(FALSE);
}
Сообщения WM_COMMAND генерируются, когда пользователь щелкает мышью по командным кнопкам диалогового окна. Как следует из приведенного ниже фрагмента, оператор case для сообщения WM_COMMAND включает еще одну вложенную конструкцию switch. Сообщение WM_COMMAND от функции TimeServerDialogProc, таким образом, обрабатывается разными участками кода в зависимости от того, какую именно из трех кнопок пользователь нажал в диалоговом окне:
case WM_COMMAND: switch (wParam)
{
case IDSETTIME: bSetTime = TRUE;
// Вызываем функции обработки нажатия
IDOK case IDOK:
GetDlgItemText(hwndDlg, IDC_TIMESERVER, (LPSTR)szTimeServer,
MAX_HOST_NAME);
if
(lstrlen(szTimeServer) > 0)
hTimeServerTask = AsyncGetServiceInfo(hwndDlg, TASK_TIMESERVER);
else
{
bSetTime = FALSE;
wsprintf(szScratchBuffer, "Please enter a host name.");
MessageBeep(0);
MessageBox(hwndDlg, szScratchBuffer, "SockMan-TIME SERVER QUERY",
MB_OK|MB_ICONINFORMATION);
}
return(TRUE);
case IDCANCEL: PostMessage(hwndSockman, WM_COMMAND, IDM_FILE_CLEAR,
0L);
EndDialog(hwndDlg, FALSE);
return(TRUE);
}
break;
Когда вы щелкаете мышью по кнопке Set Time, Windows генерирует сообщение WM_COMMAND, аргумент wParam которого равен IDSETTIME. Если пользователь щелкает по кнопке Query, аргумент wParam равен IDOK. Аналогично, щелчок по кнопке Exit приведет к тому, что wParam будет равен IDCANCEL. Когда вы щелкаете мышью по кнопке Exit, функция TimeServerDialogProc посылает сообщение WM_COMMAND/IDM_FILE_CLEAR в главное окно Sockman. Это сообщение заставляет Sockman очистить поле вывода своего окна. Это значит, что когда вы выходите из диалогового окна Time Server, Sockman стирает все сообщения от сервера времени, которые отображались в главном окне Sockman.
Командные кнопки Set Time и Query обрабатываются, по сути, одним и тем же участком кода. Как видно из показанного выше фрагмента, ветвь IDSETTIME содержит только установку булевой переменной bSetTime в значение TRUE, после чего управление перескакивает на ветвь IDOK и выполняются те же операторы, которые выполнялись бы при нажатии кнопки Query. Другими словами, так же, как и кнопка Query, кнопка Set Time прежде всего заставляет Sockman выполнить еще один запрос на сервер времени. Единственная разница между этими двумя кнопками состоит в том, что в ветви IDSETTIME устанавливается флаг, который сообщает Sockman, что нужно изменить показания внутренних часов, установив их равными тем значениям, которые получены от сервера времени.
Чтобы инициировать операцию сервера времени при получении сообщения спараметром IDOK, функция TimeServerDialogProc использует функцию GetDlgItemText, которая копирует имя или адрес хоста, введенные пользователем в поле IDC_TIMESERVER в глобальный строковый массив szTimeServer. Функция TimeServerDialogProc измеряет длину переменной szTimeServer, чтобы проверить, было ли введено какое-либо значение. Если пользователь не указал адреса хоста, функция TimeServerDialogProc выводит окно сообщения, в котором просит его указать адрес. Если же адрес был указан, TimeServerDialogProc вызывает функцию AsyncGetServiceInfo.
13.30 Получение необходимой для запроса информации из Интернет
Как вы видели на примере других учебных программ, прежде чем устанавливать соединение с программой-сервером на удаленном хосте, ваша программа должна получить определенную информацию, в частности номер протокола и IP-адрес. База данных сетевых служб содержит список протоколов Интернет и официальных номеров портов этих протоколов. Как мы говорили в предыдущих главах, функция AsyncGetServiceInfo инициализирует асинхронное получение информации из базы данных сетевых служб. Операторы функции AsyncGetServiceInfo (показанные в нижеприведенном фрагменте кода) для задачи обращения к серверу времени вполне идентичны операторам этой функции для модуля Finger, за тем исключением, что имя службы в данном случае другое (IPSERVICE_TIME):HTASK AsyncGetServiceInfo(HWND hwnd, HTASK hService) { HTASK hTask; // Дескриптор задачи// асинхронной службы LPSTR lpServiceName; // Имя службы, к которой мы// обращаемся LPSTR lpBuffer; // Указатель на буфер хранения данных int nLength; // Длина буфера хранения данных
switch (hService)
{
case TASK_ASYNC_FINGER:
// ...информация службы Finger
break;
case TASK_TIMESERVER:
lpServiceName = (LPSTR)IPSERVICE_TIME;
lpBuffer = (LPSTR)szTimeServerBuffer;
nLength = sizeof(szTimeServerBuffer);
break;
default: // ...сообщаем пользователю,что эта служба не
// поддерживается
return(0);
}
hTask = WSAAsyncGetServByName(hwnd, WM_GOT_SERVICE, lpServiceName,
NULL, lpBuffer, nLength);
if (!hTask)
// ...посылаем сообщение об ошибке
return(hTask);
}
Функция AsyncGetServiceInfo вызывает функцию Winsock WSAAsyncGetServByName, чтобы получить запись, соответствующую службе времени, из базы данных сетевых служб. Вызов функции WSAAsyncGetServByName заставляет Windows послать сообщение WM_GOT_SERVICE в указанное окно:
hTask = WSAAsync GetServByName(hwnd, WM_GOT_SERVICE, lpServiceName,
NULL, lpBuffer, nLength);
В нашем случае сообщение WM_GOT_SERVICE будет получено диалоговой процедурой TimeServerDialogProc. Как мы видели выше, операторы программы для сообщения WM_GOT_SERVICE в функции TimeServerDialogProc вызывают функцию LookupTimeServer со следующими параметрами:
LookupTimeServer(hwndDlg, lParam);
Параметр hwndDlg идентифицирует диалоговое окно, а параметр lParam содержит ошибочные условия (если таковые имели место), вызванные асинхронным поиском хоста сервера времени в DNS. Как следует из нижеприведенного фрагмента кода, функция LookupTimeServer прежде всего пытается обнаружить ошибки, возникшие при асинхронном поиске сервера времени в DNS. Если макрос WSAGETASYNCERROR фиксирует ошибку, функция LookupTimeServer использует номер порта по умолчанию (IPPORT_TIMESERVER), определенный в файле winsock.h. В обратном случае, LookupTimeServer извлекает номер протокола из записи о службе, полученной функцией WSAAsyncGetServByName и сохраненной в szTimeServerBuffer. Функция LookupTimeServer сохраняет номер порта в глобальной переменной nTimeServerPort, после чего вызывает функцию LookupHostAsync:
VOID LookupTimeServer(HWND hwnd, LPARAM lError)
{
if (WSAGETASYNCERROR(lError))
nTimeServerPort = htons(IPPORT_TIMESERVER);
else
nTimeServerPort = ((LPSERVENT)
szTimeServerBuffer)->s_port;
hTimeServerTask = LookupHostAsync(hwnd, szTimeServer,
szTimeServerBuffer, (LPDWORD)&dwTimeServerAddr);
if (!hTimeServerTask)
{
wsprintf(szTimeServerBuffer, "Unable to lookup: %s",
(LPSTR)szTimeServer);
MessageBeep(0);
MessageBox(hwnd, szTimeServerBuffer, "SockMan-TIME SERVER QUERY",
MB_OK|MB_ICONINFORMATION);
PaintWindow(szTimeServerBuffer);
}
return;
}
Хотя в приведенном фрагменте кода это не показано, функция LookupHostAsync может использовать функции Winsock WSAAsyncGetHostByName или WSAAsyncGetHostByAddr, чтобы асинхронно преобразовать имя хоста или его IP-адрес в форме «десятичное с точкой». Будучи вызвана из функции LookupHostAsync, WSAAsyncGetHostByName, как и WSAAsyncGetHostByAddr, заставляет Windows послать сообщение WM_ASYNC_LOOKUP_DONE после того, как они завершат преобразование адреса. Другими словами, когда функция LookupHostAsync завершит преобразование адреса хоста, хранящегося в переменнойszTimeServer, диалоговое окно Time Server получит сообщение WM_ASYNC_LOOKUP_DONE.
13.31 Посылка запроса на сервер времени
После того как Sockman получит номер протокола из базы данных сетевых служб с помощью функции AsyncGetServiceInfo и найдет сетевой адрес сервера времени (с помощью функции LookupHostAsync), Sockman будет иметь всю необходимую ему информацию, чтобы послать запрос на сервер времени. Когда процедура диалогового окна сервера времени, TimeServerDialogProc, получает сообщение WM_ASYNC_LOOKUP_DONE, функция TimeServerDialogProc вызывает функцию PaintWindow с сообщением, уведомляющим пользователя, что асинхронный поиск адреса для операции с сервером времени завершен.
Как следует из нижеприведенного фрагмента кода, сообщив пользователю о завершении операции поиска адреса, функция TimeServerDialogProc вызывает функцию TimeServerQuery, которая, собственно, и посылает запрос на сервер времени. После того как функция TimeServerQuery завершит свою работу, функция TimeServerDialogProc приравняет дескриптор задачи сервера времени, hTimeServerTask, к нулю, тем самым сигнализируя, что операция с сервером времени завершена:
case WM_ASYNC_LOOKUP_DONE:
PaintWindow("Asynchronous lookup for Time Server completed.");
lNetTime = TimeServerQuery(hwndDlg, lParam);
hTimeServerTask = 0;
if
(lNetTime != SOCKET_ERROR)
// Обрабатываем результаты
else
SetDlgItemText(hwndDlg, IDC_SERVER_TIME, "");
MessageBeep(0);
return(TRUE);
Функция TimeServerQuery возвращает значение времени, к которому можно сразу применять функции даты и времени вашего персонального компьютера. Другими словами, TimeServerQuery возвращает текущую дату и время в виде значения, представляющего число секунд с полуночи 1 января 1970 года. Еслизапрос на сервер времени не дал ожидаемых результатов, функция TimeServerQuery возвращает константу SOCKET_ERROR, определенную в Winsock, а функция TimeServerDialogProc устанавливает значение поля ввода IDC_SERVER_TIME в диалоговом окне равным строке нулевой длины. Иными словами, при возникновении ошибки функция TimeServerDialogProc стирает любое значение даты и времени, которые до этого момента отображала программа Sockman.
Если же никаких ошибок не произошло (т.е. функция TimeServerQuery не вернула значение SOCKET_ERROR), функция TimeServerDialogProc обрабатывает результаты запроса на сервер времени. Вне зависимости от того, произошла при запросе ошибка или нет, функция TimeServerDialogProc использует функцию Windows MessageBeep, чтобы сообщить пользователю о завершении операции с сервером времени.
13.32 Результаты запроса на сервер времени
Как мы уже говорили, когда вы щелкаете мышью по кнопке Set Time вдиалоговом окне Time Server, функция TimeServerDialogProc устанавливает булеву переменную-флаг bSetTime равной True. Когда функция TimeServerQuery возвращает осмысленное значение переменной lNetTime и при этом значение bSetTime равно True, функция TimeServerDialogProc изменяет показание встроенных часов вашего компьютера, присваивая им ту величину, которая хранится в переменной lNetTime. Изменив (если необходимо) показания системных часов, функция TimeServerDialogProc вызывает функцию ctime, чтобы преобразовать сетевое время и время PC (хранящиеся соответственно в переменных lNetTime и lPCTime) в строковые значения. Затем функция TimeServerDialogProc вызывает функцию Windows SetDlgItemText, чтобы записать эти строковые значения в поля ввода IDC_SERVER_TIME и IDC_LOCAL_TIME диалогового окна. Функция TimeServerDialogProc также вызывает функцию PaintWindow, чтобы отобразить текущее значение времени PC в главном окне Sockman.
Ниже приведен фрагмент кода, в котором TimeServerDialogProc использует стандартные функции и структуры DOS, чтобы изменить показания системных часов вашего компьютера. Функция TimeServerDialogProc использует структуры _dostime_t и _dosdate_t, а также указатель на структуру DOS tm. Чтобы преобразовать значение lNetTime, полученное от TimeServerQuery, в структуру DOS tm, функция TimeServerDialogProc вызывает функцию localtime из соответствующей библиотеки компилятора C:
if
(bSetTime)
{
struct tm *npTMStruct;
struct _dostime_t dosTimeStruct;
struct _dosdate_t dosDateStruct;
npTMStruct = localtime(&lNetTime);
dosTimeStruct.hour = (BYTE)(npTMStruct->tm_hour);
dosTimeStruct.minute = (BYTE)(npTMStruct->tm_min);
dosTimeStruct.second = (BYTE)(npTMStruct->tm_sec);
dosTimeStruct.hsecond = (BYTE)0;
_dos_settime(&dosTimeStruct);
dosDateStruct.year = npTMStruct->tm_year + 1900;
dosDateStruct.month = (BYTE)(npTMStruct->tm_mon +1);
dosDateStruct.day = (BYTE)(npTMStruct->tm_mday);
_dos_setdate(&dosDateStruct);
bSetTime = FALSE;
}
Функция localtime возвращает указатель на внутренний статический буфер, в котором хранятся значения составных частей структуры tm. Функция TimeServerDialogProc использует значения tm для хранения даты и времени, полученных от сервера времени, в структурах _dostime_t и _dosdate_t. Сохранив соответствующие значения в структурах _dostime_t и _dosdate_t, функция TimeServerDialogProc вызывает функции _dos_settime и _dos_setdate, которые и изменяют дату и время во встроенных часах вашего PC. Прежде чем закончить эту ветвь оператора if, TimeServerDialogProc сбрасывает флаг bSetTime в значение FALSE.
13.33 Запрос на сервер времени
Как показано выше, функция TimeServerDialogProc вызывает функцию TimeServerQuery, чтобы инициировать посылку запроса на сервер времени:
lNetTime = TimeServerQuery(hwndDlg, lParam);
Функция TimeServerQuery посылает запрос на сервер времени практически так же, как это делалось в программе QTime, которую мы рассматривали выше в первой части этой главы. Дискета, прилагаемая к книге, содержит полный исходный текст функции TimeServerQuery. Однако мы не будем возвращаться к программе QTime, а вместо этого рассмотрим приведенный ниже псевдокод, описывающий алгоритм функции TimeServerQuery:
LONG TimeServerQuery(HWND hwnd, LPARAM lError)
{
// Прежде всего объявляем локальные переменные,
// затем проверяем значение ошибки в lError
if (WSAGETASYNCERROR(lError))
// если ошибка зафиксирована, выводим окно сообщения и
// возвращаем значение SOCKET_ERROR,
// иначе, получаем указатель на структуру данных хоста
// и сохраняем значения в структуре адреса сокета
// Создаем сокет
if
((hSocket = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL)) ==
INVALID_SOCKET)
// Если зафиксирована ошибка, выводим окно сообщения и
// возвращаем значение SOCKET_ERROR
// иначе, соединяем сокет с удаленным хостом
if
(connect(hSocket, (PSOCKADDR)&socketAddr, sizeof(socketAddr)))
// Если зафиксирована ошибка, выводим окно сообщения и
// возвращаем значение SOCKET_ERROR
// Посылаем на сервер времени CR/LF
send(hSocket, "\n", sizeof("\n"), NO_FLAGS);
// Сохраняем значение, возвращенное сервером времени, и
// закрываем сокет
nLength = recv(hSocket, (LPSTR)&lNetTime, sizeof(lNetTime), 0);
closesocket(hSocket);
// Проверяем, не сигнализируетсяли ошибка сокета
// значением, возвращаемым функцией recv
if(nLength == SOCKET_ERROR)
// Если зафиксирована ошибка, выводим окно сообщения и
// возвращаем значение SOCKET_ERROR,
// Иначе, преобразуем порядок байтов lNetTime из
// сетевого порядка байтов в порядок байтов хоста, а
// затем выставляем время PC равным сетевому времени.
// Запомните, что в Интернет в качестве точки отсчета
// используется 1 января 1900 года,тогда как функции
// времени PC используют вкачестве точки отсчета
// 1 января 1970 года.
lPCTime = ntohl(lNetTime);
lPCTime -= PC_REF_TIME;
// Наконец, возвращаем сетевое время как время PC
return(lPCTime);
}
Подводя итоги
В этой главе вы учились использовать протокол Time Protocol Интернет для получения текущего значения даты и времени от программы-сервера времени на одном из хостов Интернет. По пути мы обнаружили, что компьютеры PC хранят числовые данные совсем не так, как это делают многие другие компьютеры, подключенные к Интернет.
В частности, мы рассмотрели, как PC и Интернет используют разные порядки байтов для хранения числовых данных. Мы также научились преобразовывать порядок байтов в числовых данных и выяснили, к чему может привести отсутствие этих преобразований в необходимых местах. Мы также узнали, как неправильное использование переменных со знаком и без знака может привести к дополнительным ошибкам и несоответствиям при отладке программ и передачи числовых данных по сети Интернет.
В следующей главе мы научимся создавать простые сокеты, которые позволят вашим программам получать доступ к низкоуровневым протоколам Интернет, таким как протокол управляющих сообщений Интернет (Control Message Protocol, ICMP). Но прежде чем мы перейдем к простым сокетам и утилите Ping, рассматриваемой в главе 14, давайте повторим основные концепции, с которыми мы познакомились в этой главе:
- Порядок байтов «с конца» означает, что наиболее значимый (старший) байт числового значения сохраняется в первой (обладающей наименьшим номером) ячейке памяти в том участке, который отведен под это значение.
- Порядок байтов «с начала» означает, что наименее значимый (младший) байт числового значения сохраняется в первой (обладающей наименьшим номером) ячейке памяти в том участке, который отведен под это значение.
- На PC для хранения числовых значений используется порядок байтов «сконца».
- Для передачи числовых значений по Интернет их требуется преобразовать к порядку байтов «с начала».
- Winsock API предоставляет четыре функции для преобразования порядка байтов; две из них преобразуют 16-битовые числа и две другие— 32-битовые числа.
- Во избежание проблем и недоразумений желательно объявлять буферы хранения данных, используя те типы данных, которые будут записываться в эти буферы данных по мере прихода из Интернет.
- Одна и та же последовательность битов может представлять два разных значения в зависимости от того, используете вы переменные со знаком или без знака.
Далее Вверх Содержание
|