Воробьёвы

(-:

№44. "Подбираем" socket'ы и оптимизируем программы

Пора, мой друг, пора!
покоя сердце просит —
Летят за днями дни,
и каждый час уносит
Частичку бытия,
а мы с тобой вдвоем
Предполагаем жить,
и глядь — как раз умрем.
А.С. Пушкин

# СловоБредие

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

Суббота это такой день, когда можно развалится перед экраном, почитать какую-нибудь «лабулабуду», вроде этой рассылки :), на что оно в принципе и рассчитано. Можно почитать что-то из классики, как, например эта рассылка (что тоже продумано), или немного утреннего юмора, (можно и здесь), или мистики, что тоже предусмотрено.

Те, кто не умеет читать, могут просто долго смотреть на заставку, и медитировать с дымящим кофе.

Во-первых, хочу поблагодарить всех тех, кто откликнулся на предыдущий номер. Во-вторых, если есть желающие написать интересный материал о Дебаггерах – милости прошу.

Кстати нас наконец-то затопили. Это приятно. Неприятно, то, что затопили сверху. Вообще сплошная чёрная полоса. Вчера уборщица Соня, пытаясь довести корпус компьютера до блеска стёрла наклейку Win98 ОEM, и довела нашего начальника до инфаркта. Теперь она убирает не выше плинтуса.

Ещё немного новостей из Underworld.

Намедни нашему товарищу букинисту попалось замечательное издание из 5 корпуса общежития некого факультета. Книга называется: «Учёт посещений общежития #5». Я бы и не рассказывал об этом случае, и не рассказывал о как обычно присутствующих записях: «Шварцнегер к Терминатору», «Чебурашка к Гене», если бы не интересный факт. Из 10634 записей, более 4000 – были сделаны на 626 комнату. Что ещё более странно, что в комнате живут обычные студенты, и пока никаких аномалий не было выявлено.

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

Сегодня мне вспомнилась замечательная история, не менее загадочная, чем с 626 комнатой.

* * *

В одном местечке, где теперь провисает потрёпанный плакат «Выставка», раньше было село. Как звалось оно, мало кто помнит. Вообще солнечный край – тут сады, там поля, да и город рядом. Вот - 17-ый год пришел. Террор, реки крови по великой России. Добежало все это и до нашего местечка. Но, Семнадцатый – это потом, а пока… Хозяин – Барин.

Был камердинер один - старичок, лет 60-ти. Как соберутся гости у барина в бильярд играть, он навытяжку стоит, в руке поднос. На подносе: выпивка, закуски, сигары дорогие, фрукты разные. Компания раззадорится, до утра гуляет, а он, бедный, навытяжку стоит, с подносом. Это в 60-то лет то! И так он этот биллирад возненавидел, что даже во сне кричал.

Думал и умрёт так, стоя за бильярдом. Но судьба дура, не знаешь, где порадует, а где огорчит. Пришёл семнадцатый год, власть поменялась, мужики шабаш подняли.

Радуются – барина на фонарный столб повесили, и петуха красного запускают.

Камердинер тоже ошалел: сукно на столе рвет, кии ломает, табуретом тяжелым зеркала все раскрошил. Всё разгромил, только одни шары бильярдные остались, а они были слоновой кости отличной работы. И камердинер то ли устал уже, толи работу мастерскую пожалел, толи разбить их не смог. Собрал он их в обрывки сукна и закинул в болото. Туда подальше, в самую трясину, чтоб наверняка уже.

А потом в селе чекисты объявились. Бильярд оказался работы одного очень известного итальянского мастера, барин за него 1000 рублей золотом отвалил, да скакуна лучшего из конюшен, крови арабской. Чекисты хотели Самому его подарить. Но пропал Бильярд. Мужички не видали, а камердинер, хоть и перепужался до заикания, а по дурости не пропал. Не видал и все тут. Пожар был, сгорело все. Да, стол обгоревший так и стоял там, а еще что в доказательство? Вот так покрутились, поискали ну и пошли ни с чем. Старичок поволновался-поволновался, да все так и устаканилось.

Потом Гражданская, потом репрессии и голодомор, неспокойное время. А там и Великая Отечественная грянула. Камердинера сын, Александр, Сашка, всю войну прошагал до самого Берлина, без ранений, ни царапины, домой героем пришел, в медалях весь, даже орден был! Потом - город восстанавливать - в войну здесь просто мясорубка была, два артбатальона так вспахали плавни, что железо военное до сих пор откапывается. А потом - в слесаря на завод. Первый инструментальщик был на заводе. Там и сын родился, Санька, Сан Саныч значит...

(продолжение следует...)

DirectOr

Socket vs Socket

или использование сокетов MS Windows в ассемблерных программах

Теория применения сокетов совсем недавно была описана в статье “Сокеты M$ Windows”, которую вы можете легко найти в разделе “Статьи/Сеть”. Не будем повторяться, но вкратце скажем, что сокеты определяют логику для программирования сети аналогичную работе с файлами. В разделе “Исходники/Сеть и коммуникации” уже давно (со времен wasm.zite.ru) имеется рабочая небольшая программа “ledilog.zip\connect.asm”, в которой на основе использования сокетов реализован обмен текстовыми сообщениями между двумя компьютерами в сети. Таким образом, накопилась критическая масса, состоящая из некоторого минимума теории и кое-каких практических материалов. Все это и подтолкнуло автора к использованию в своем проекте (детали в данном случае не важны) связи по локальной сети между компьютерами на основе сокетов MS Windows. Однако все оказалось не так просто, как хотелось. Впрочем, как и всегда...

Куски кода, связанные с созданием и использованием сокетов из программы-оригинала легко переносились и компилировались, но все работало не так, как надо, либо вообще не работало. Обнаружились значительные провалы между кажущимся пониманием теории и практикой, к тому же комментарии у источника были на итальянском языке, что не добавляло ясности. Пришлось отказаться от первоначальной идеи и писать отдельную программку с минимумом интерфейса специально для проверки, что ж это за зверь такой: сокеты Windows. В данной статье приведена попытка описать полученный опыт на примере реальной, хотя и учебной программы. Тут не будет много теории, только необходимый для понимания кода минимум. Также предполагается, что читатель уже знаком с тем, как создавать приложение под Windows, имеющее основное окно и процедуру этого окна, как вызываются диалоговые окна... И последнее, так уж получилось, что моя программа внешне вышла очень похожей на программу-оригинал. Не судите меня строго...

Для впервые заинтересовавшихся сетевым программированием есть смысл пояснить, что же из себя представляют сокеты. Сокет можно рассматривать, как конечный пункт передачи данных по сети. Сетевое соединение - это процесс передачи данных по сети между двумя компьютерами или процессами. Тогда сокет - конечный пункт такой передачи данных. Другими словами, когда программы используют сокет, для них он является абстракцией, представляющий одно из окончаний сетевого соединения. Для установления соединения в абстрактной модели сокетов необходимо, чтобы каждая из сетевых программ имела свой собственный сокет. Недаром слово socket переводится с английского как гнездо или разъем!

Связь между двумя сокетами может быть ориентирована на соединение, а может быть и нет. С чем это едят? Все дело в том, в сетевом протоколе TCP/IP (а на сегодня это “родной” протокол интернета, да и большинства локальных сетей) предусмотрено два режима: ориентированный и не ориентированный на соединение. В ориентированных на соединение протоколах данные перемещаются как единый, последовательный поток байт без какого либо деления на блоки. Конечно, имеется в виду логика процесса, а не то, что физически происходит в среде передачи. В не ориентированных на соединение протоколах сетевые данные перемещаются в виде отдельных пакетов, называемых датаграммами. Как мы уже говорили, сокеты могут работать как с одними, так и с другими. В дальнейшем при создании сокета мы с этим столкнемся. А сейчас достаточно запомнить, что датаграммы могут приходить к получателю не подряд, а в непpедсказуемой последовательности. Так что датаграммы мы использовать в данном примере не будем, а только режим, ориентированный на соединение!

С аналогией между сокетами и файлами тоже не все просто. Конечно, для начала тоже нужно создать сокет, получить его дескриптор, а потом, используя этот дескриптор, писать или читать… Но отличие в том, что дескриптор файла указывает на уже существующий или только что созданный файл или на устройство, дескриптор сокета не содержит каких-либо определенных адресов или пунктов назначения сетевого соединения. Этот факт существенно отличает его от любого другого дескриптора стандартной системы ввода-вывода. Программа, работающая с сокетами, сначала образует сокет и только потом соединяет его с точкой назначения на другом конце сетевого соединения. Если бы файловый ввод-вывод состоял из таких же шагов, приложение сначала получало бы дескриптор файла, а затем привязывало бы его к имени определенного файла на диске.

Несмотря на то, что первоначально сокеты появились в системе UNIX (т.н. сокеты Беркли), на данный момент разработчики Windows давно модифицировали и расширили интерфейс для работы сокетов. Таким образом, сейчас мы имеем многочисленные функции API, так или иначе связанные с сокетами:

WSAStartup()* инициализирует Windows Socket dll
WSACleanup()* прекращает использование этой dll
socket()* функция создает сокет с заданными параметрами
WSAAsyncSelect()* функция указывает посылать сообщение от
сокета заданному окну при любом из
заданных сетевых событий
bind()* ассоциирует локальный адрес с сокетом
listen()* устанавливает сокет в состояние, в котором он
слушает порт на предмет входящих соединений
accept()* функция извлекает из очереди ожидающих
подключений первое, создает новый сокет и
возвращает его дескриптор
connect()* функция подключает созданный сокет к
указанному адресу
select() функция определяет статус одного или более
сокетов
shutdown() функция запрещает посылать и/или принимать
данные от сокета
ioctlsocket() функция управляет режимом сокета
getsockopt() функция возвращает установки сокета
recv()* функция получает данные от сокета
send()* функция посылает данные в ранее
подключенный сокет
sendto() функция посылает данные по указанному адресу
recvfrom() функция получает датаграммы от сокета

Звездочками отмечены те функции, которые будут встречаться в тестовой программе. Учтите, что кроме перечисленных существуют и многие другие… А теперь приступим к более подробному рассмотрению того, как этим хозяйством пользоваться. Программа начинается как обычно с подключения необходимых библиотек, имеет секцию инициализированных и неинициализированных данных (.data и .data?). Обратите внимание на задание констант. Все константы (определение использованных ресурсов и др.) вынесены в отдельный файл SocSoc.inc и они нам в этот раз не интересны. Другое дело строка:

    WM_SOCKET equ WM_USER + 100

Это задание численного значения сообщения WM_SOCKET, того самого сообщения, которое нам в дальнейшем будет посылать Windows при работе с сокетами. Дело в том, что Windows не использует для своих стандартных сообщений значения выше WM_USER, поэтому мы легко можем использовать этот диапазон для нужд своего приложения. Отметим этот важный момент, в дальнейшем мы еще раз вернемся к обсуждению сообщений типа WM_USER+...

Секция кода начинается с определений макросов, используемых при анализе сообщений нашим окном. Конечно, надо бы вынести макросы в отдельный файл .inc – файл и подключить его с помощью include. Но в данном случае их всего два и хотелось максимально упростить для понимания начинающими этот проект... Только поэтому макросы помещены в начало секции .code. Ну не данные же это в самом деле! Так что не делайте, как я.

Cначала опишем вкратце общий алгоритм работы нашей программы, а затем приступим к подробному анализу кода:

1) при запуске получаем и сохраняем хэндл программы, регистрируем у Windows текстовую строку в качестве сообщения и сохраняем возвращенный нам код сообщения для дальнейшего использования (об этом позже);

2) инициализируем dll, ответственную за использование сокетов Windows;

3) если получена ошибка инициализации, то делать дальше нечего – выходим с ошибкой из программы! Если же вызов dll прошел успешно, то продолжим работу: получаем адрес командной строки, (если таковая была при запуске – в данном примере не используется). Вызываем функцию WinMain – которая, собственно и определяет логику работы приложения и которая будет подробно рассмотрена ниже;

4) после выхода из WinMain возвращаемся в Windows.

А теперь разберем, что происходит в третьем пункте. В объявлении функции WinMain все достаточно традиционно и подробный анализ пропускаем. Для нас интерес составляет процедура WndProc главного окна программы, которая получает и реагирует на сообщения Windows. Сразу же после создания нашего окна процедура WndProc получит соответствующее сообщение: WM_CREATE. Это удобный момент для приведения в исходное состояние всего нашего хозяйства. Раз мы собираемся обмениваться с другими компьютерами, хорошо бы узнать кое-что и о себе, вернее о том компьютере, где запущен экземпляр нашей программы. Можно получить имя компьютера в виде текстовой строки в buffer соответствующей функцией, а затем оттуда скопировать в выходной буфер:

    invoke gethostname, addr buffer, sizeof buffer 
    invoke wsprintf, addr buf_out, addr szName, addr buffer

Аналогично получаем IP адрес компьютера по имени:

    invoke gethostbyname, addr buffer

Для этого есть в API специальная функции gethostbyname, которая возвращает информацию о компьютере по его имени, заполняя специальную структуру hostent. Приведем ее для лучшего понимания дальнейших действий:

    hostent STRUCT
     h_name  DWORD ? ; char FAR *
     h_alias  DWORD ?
     h_addr  WORD ?
     h_len  WORD ?
     h_list  DWORD ? ; char FAR * FAR *
    hostent ENDS

Элемент структуры h_name – не что иное, как указатель на строку с именем нашего компьютера. А вот элемент h_list более интересен! Это указатель на список IP-адресов компьютера. Их может быть несколько, по числу сетевых интерфейсных карт, установленных в компьютере. Причем адреса представлены в сетевом порядке байт. Нас интересует первый из них. Вот как добраться до этого адреса:

    mov eax, [eax+12] ; получаем указатель на элемент h_list в HOSTENT
    mov eax, [eax]     ; получаем указатель на указатель на IP
    mov eax, [eax]  ; получаем указатель на строку IP в сетевом порядке байт

После преобразования функцией inet_ntoa, сохраняем полученный строковый формат IP адреса (вида 127.0.0.1) в еще один выходной буфер. Во всем этом есть один тонкий момент, непонимание которого может привести к дальнейшему использованию неправильных данных! Те функции Windows Sockets API, которые возвращают указатели на различные данные (а это часто так и есть) гарантирует их (данных) сохранность только до следующего вызова функций Sockets API. Поэтому необходимо сразу же копировать все необходимые нам значения в отведенные нашей программой для этого переменные! Это в полной мере относится и к функции gethostbyname.

Собственно, и имя компьютера, и его IP-адрес в данном случае используются программой только для информации, и их можно было и не получать. На работе программы это не скажется. Другое дело, что пример-то учебный, а в реальном проекте при обмене данными скорее всего нам понадобится указывать от кого, собственно, посылочка...

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

Итак. Сначала нам нужно открыть сокет:

    invoke socket, AF_INET, SOCK_STREAM, 0;

Параметры следующие:

AF_INET – семейство, в версии 1.1 только AF_INET;

SOCK_STREAM - тип сокета, который вы желаете использовать.

Помните, мы выбираем связь, ориентированную на соединение, и говорим НЕТ датаграммам (SOCK_DGRAM);

0 – протокол, не устанавливать никакого протокола.

И если нет ошибки, сохранить его дескриптор для дальнейшего использования:

    mov hSocket2, eax

Далее нужно указать Windows, какому окну надо посылать сообщения об определенных событиях, связанных с открытым сокетом. Это очень важный момент:

    invoke WSAAsyncSelect, hSocket2, hWnd, WM_SOCKET, \
                                    FD_ACCEPT+FD_READ

где hSocket2 - дескриптор сокета (вот сразу и пригодился)

hWnd - дескриптор главного окна приложения

WM_SOCKET - сообщение, нами же определенное

FD_ACCEPT+FD_READ – маска, задающая интересующие нас сообщения от этого сокета (в данном случае мы хотим получать уведомление о попытке подключения и уведомление о готовности данных для чтения).

Затем приступаем к уточнению деталей. Для этого сначала надо преобразовать номер порта в так называемый сетевой порядок байт. Хорошо, что для этого есть специальная API-функция. Воспользуемся ей и заполняем структуру <sin> другими необходимыми параметрами:

    invoke htons, Port
      mov sin.sin_port, ax
      mov sin.sin_family, AF_INET
      mov sin.sin_addr, INADDR_ANY

Далее необходимо сопоставить локальный адрес, представленный в структуре <sin> с ранее открытым сокетом:

    invoke bind, hSocket2, addr sin, sizeof sin

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

    invoke listen, hSocket2, 5 

Где указываем, естественно, дескриптор сокета и некое число, определяющее максимальную длину очереди ожидаемых подключений. Так что число пять просто перекочевало из программы – оригинала. Поставьте десять, если это нужно. На этом, собственно, и заканчивается настройка сокета на прием, т.е. слушающего указанный порт.

Затем, если пользователь отважится и выберет пункт меню “Подключить”, то создаем диалоговое окно выбора IP адреса подключения:

    invoke DialogBoxParam, hInstance, addr DlgNameZ, hWnd, addr DlgProcZ, 0

После выхода из которого (если адрес был указан) приходит пора создавать сокет на передачу:

    invoke socket, AF_INET, SOCK_STREAM, 0
     .if eax != INVALID_SOCKET 
      mov hSocket, eax 
     .else
      invoke ERROR, addr ErrorCrSocket, 1
     .endif
    invoke WSAAsyncSelect, hSocket, hWnd, WM_SOCKET,\
                                         FD_CONNECT+FD_CLOSE

Здесь пока все тоже, что и ранее с первым сокетом, только теперь нас интересуют сообщение о состоявшемся подключении и уведомление о закрытии сокета (FD_CONNECTи FD_CLOSE).

Далее по аналогии формируем структуру , содержащую адрес подключения:

    invoke htons, Port
    mov sin.sin_port, ax
    mov sin.sin_family, AF_INET
     
    invoke inet_addr, addr AdresIP
    mov sin.sin_addr, eax

Тут немного пояснений. После выхода из диалогового окна выбора адреса в буфере AdresIP содержится IP-адрес в привычном для нас виде a.b.c.d. Для удобства отладки приложения указан и адрес “самого себя”. Это - 127.0.0.1. Дело в том, что согласно соглашениям TCP гарантируется, что все пакеты на все адреса, начинающиеся со 127.ххх в физическую линию передаваться не будут. Таким образом, это сильно облегчает жизнь тем людям, у которых нет возможности все время сидеть на двух компьютерах в сети сразу. Пример полностью работает и на ОДНОМ компьютере. Только адрес надо предварительно преобразовать функцией inet_addr из строкового формата с точками, а потом уже заносить в <sin>.

И апофеоз, надо подключить созданный сокет к указанному в <sin> IP- адресу:

    invoke connect, hSocket, addr sin, sizeof sin

Вот после этих строк процедура главного окна получит сообщение от Windows: WM_SOCKET с lParam = FD_CONNECT. Тут самое время сообщить о состоявшемся (или нет) соединении с помощью простого MessageBox-а. Ничего дополнительно делать не надо.

Что же произойдет, когда слушающий сокет обнаруживает попытку соединения? Не забывайте, что мы можем коннектиться к самим себе, но это существа дела не меняет. Все просто. При наступлении какого-либо события, связанного с сокетом, Windows шлет сообщение процедуре главного окна, в данном случае пошлет тот же WM_SOCKET, теперь уже с параметром FD_ACCEPT. Делать нечего, нужно вызвать специальную функцию API, чтобы разрешить входящее соединение:

     invoke  accept, hSocket2, 0, 0
     mov  hClient, eax ; сохранить дескриптор сокета 

Будьте бдительны! Это еще один важный момент, который в свое время попортил мне немало нервов. Конечно, входящее соединение надо сначала разрешить, а если мы не прореагируем на попытку соединения, то следующего такого сообщения от сокета больше не получим. Но более того, возвращаемое функцией accept значение представляет из себя дескриптор НОВОГО сокета, который мы должны будем далее использовать для приема данных. Его, конечно, надо сохранить. И именно его использовать в функции приема данных! Первоначальный слушающий сокет остается открытым.

Теперь о том, как передать данные, если уж связь налажена. Специально в программе есть два места, откуда можно передавать. Это и главное окно, пункт меню “Передать…”, и диалоговое окно приема/передачи. Рассмотрим второе. Это участок процедуры DlgProcZ1 диалогового окна:

    .if eax == IDC_BUTT6; нажата кнопка "отослать"
      .if Connected == 1
      invoke GetDlgItemText, hDlg, IDC_EDIT02, addr BytSend, 64
      invoke lstrlen, addr BytSend
        .if eax < 64
        invoke send, hSocket, addr BytSend, 64, 0
         .if eax == SOCKET_ERROR
        invoke ERROR, addr ErrorReseau, 0
         .endif
        .endif
      .endif

При нажатии кнопки “Отослать” проверяем, установлено ли уже соединение и если да, то получаем с элемента редактирования текста IDC_EDIT02 собственно текст (до 64 байт) в буфер BytSend. Проверяем длину строки. И передаем весь буфер в сокет. Функция send имеет следующие параметры:

hSocket – дескриптор (раннее подключенного функцией connect) сокета

addr BytSend – указатель на передаваемый буфер

64 – длина буфера

0 - флаг, определяющий поведение функции (для нас только такой).

Сразу же не забудем проверить признак ошибки при передаче. Мало ли чего!

А что же прием? Если мы (или какой-нибудь другой компьютер) прислали данные на слушающий сокет, для которого была выполнена функция accept (помните, мы сохраняли дескриптор в hClient), опять процедуре главного окна летит сообщение WM_SOCKET уже c параметром FD_READ. Вот кусок соответствующего кода:

    .elseif ax == FD_READ
      HIWORD lParam
      .if ax == NULL  ; отсутствует ошибка
       invoke recv, hClient, addr BytRecu, 64, 0
      mov eax, 1      ;  установить признак, что в буфер чтения получено...
     invoke SendMessage, HWND_BROADCAST, MessageIPC, eax, 0

Ну, с функцией собственно приема recv, надеюсь ясно. Параметры аналогичны функции send:

hClient – дескриптор сокета (ранее полученный функцией accept!)
addr BytRecu – указатель на приемный буфер
64 – длина этого буфера
0 - флаг (для нас такой).

Далее наступает ключевой момент. Теперь, когда принятые данные уже в буфере BytRecu, пришла пора еще одного трюка.

Картинка у меня долго не складывалась, пока не появилась статья от CyberManiac (см. “Статьи/Секреты Win32/IPC”). Посвящена она механизму обмена данными между приложениями (Interprocess communication, сокращенно – IPC). Почитайте. Но у нас ситуация несколько иная. Приложение у нас одно и в данном случае не надо пересылать данные. Пусть они себе лежат в буфере. Надо всего лишь подать сигнал процедуре диалогового окна приема/передачи о факте приема от сокета. А как? Сокет–то шлет сообщения главному окну, а индицируем принятые данные в другом, диалоговом, которое ничего не знает... Да и самого диалогового окна приема/передачи в момент приема данных может не быть на экране. Но нас это не сильно беспокоит, не хотите – не надо. Наше дело сообщить. Посылаем широковещательное сообщение всем окнам функцией SendMessage. Параметры следующие:

    HWND_BROADCAST – идентификатор окна, процедура которого получит
                   сообщение, или  HWND_BROADCAST,
                   тогда сообщение посылается всем окнам верхнего уровня в системе,
                   в том числе и невидимым,…но сообщение не посылается дочерним окнам;
    MessageIPC  – номер зарегистрированного ранее сообщения
    eax         – wParam, дополнительный параметр сообщения (DWORD)
    0           – lParam, аналогично.

А вот и часть кода в процедуре окна DlgProcZ1, ответственная за прием такого сообщения:

    ; если получено зарегистрированное нами сообщение
     .elseif eax == MessageIPC        
    ; вывести полученные данные из буфера приема в контрол IDC_EDIT03
      invoke SetDlgItemText, hDlg, IDC_EDIT03, addr BytRecu

Можно заметить, что не анализируется wParam и lParam. Нам достаточно только факта самого сообщения. А теперь рассмотрим, как все-таки регистрировалось сообщение MessageIPC при входе в программу. Для этого надо было придумать уникальную текстовую строку:

    MsgString  db "MessageSocDirectOr", 0

Это своего рода пароль для разных приложений, по которому они могут узнавать “свое” сообщение. Такой себе “у вас продается славянский шкаф?”. Далее надо провести собственно регистрацию:

    invoke RegisterWindowMessage, addr MsgString

И на выходе, если eax не равно нулю, значит ошибки нет и там содержится код сообщения. Какой он? Но мы же не хотим, знать больше, чем положено. Надо просто запомнить его и использовать по мере надобности:

    mov MessageIPC, eax ; сохранить присвоенный сообщению номер

Идея в том, что при первом вызове этой функции Windows резервирует и отдает в наше полное распоряжение первое попавшееся ей на глаза свободное сообщение. А затем при повторном вызове этой функции с такой же точно текстовой строкой в качестве параметра, приложение (и наше, и любые другие) в качестве результата получат именно этот номер, привязанный теперь к этой текстовой строке. Другое дело, что механизма освобождения сообщения нет и оно наше до конца сеанса.

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

    ; закрываем сокеты и сообщаем о том, что dll нам больше уже не нужна
    invoke closesocket, hSocket
    invoke closesocket, hSocket2
    invoke WSACleanup       ;  dll больше не нужна
    invoke PostQuitMessage, NULL 

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

    .elseif ax == FD_CLOSE
       HIWORD lParam
       .if ax == NULL ; отсутствует ошибка
           invoke closesocket, hSocket
           mov hSocket, 0

И еще. Возвращаемся к обсуждению сообщений типа WM_USER+… Цитата из CyberManiac-а: “…в некоторых программах для обмена информацией используются сообщения WM_USER+N, в частности, именно так реализован механизм IPC в WinAmp. Однако Microsoft имеет по этому поводу свое особое мнение - согласно MSDN, сообщения от WM_USER+0 до WM_USER+3FFFh включительно используются только для передачи данных внутри или между окнами одного класса.” Так что одно дело получать сообщения в процедуре окна от сокетов Windows, и совсем другое, слать IPC широковещательные сообщения с WM_USER+… в качестве параметра.

Все происходящее при открытии/закрытии сокетов очень удобно наблюдать при помощи утилиты Tcpview.exe (Mark Russinovich - www.sysinternals.com). Рекомендую! Хорошо видны порты, а также протоколы, их использующие… В частности, хорошо отслеживается эффект “расщепления” сокета на два после выполнения функции accept. Кстати, появляется еще один (кроме первоначального 3030) используемый порт, что-нибудь типа 30хх, в зависимости от наличия свободных.

Подведем итог. Для того, чтобы организовать обмен информацией между двумя компьютерами в сети, в программе создается два сокета, используемые раздельно для приема и для передачи. Имеется главное окно приложения и в разных местах программы могут создаваться два диалоговых окна. Сокеты создаются в процедуре главного окна, но это не принципиально. Существенно важно то, что оба они при наступлении соответствующих событий посылают предопределенное нами самими сообщение WM_SOCKET именно главному окну. Где в цикле разбора сообщений от Windows уже анализируются и обрабатываются… Таким образом, ВСЯ работа с сокетами сосредоточена в процедуре главного окна. Существует затруднение, состоящее в том, что данные, полученные с сокета на прием, могут быть нужны нам в диалоговом окне приема/передачи, которое у нас не получает соответствующего сообщения от сокетов Windows. Вопрос решается с помощью посылки главным окном широковещательного сообщения о факте приема данных всем другим окнам. Диалоговое окно (или другое приложение, если захотите) может легко получить такое сообщение и обработать… Конечно, это не единственный способ взаимодействия между главным окном и диалоговым. Но соль в данном случае в том, что при поступлении данных от сокета диалогового окна-то может и не быть. Поэтому был выбран путь, когда программа просто сигнализирует о факте приема данных от сокета, а далее уже не важно, нужны они кому-нибудь, или нет.

В описываемом примере было рассмотрено использование только некоторых основных функции API, ответственных за работу с сокетами Windows, в основном разобраны их параметры, кроме тех, значение которых не критично. Но! Возникает (у меня, по крайней мере) несколько вопросов. Ну, во-первых, номер порта. Какой он может быть? А должен? Какие правила на этот счет. Кроме тех, которые широко известны: 80 порт для HTTP и др. Конечно, Windows при попытке создать сокет с уже используемым портом вернет код ошибки. Попробуйте запустить два экземпляра тестовой программы! Но разве от этого легче? Можно, конечно, в программе перебирать номера, но как тот, другой компьютер узнает, чем сердце успокоилось?

Во-вторых, понятно, почему используется два раздельных сокета. Чтобы можно было независимо передавать данные и читать их тогда, когда они поступают извне. А как можно обойтись одним сокетом? Если уж действительно сокеты задают логику работы с сетью, аналогичную работе с файлами, то хорошо было бы и писать, и читать, работая с одним “файлом”. И более того. Что делать, когда надо посылать сообщения нескольким компьютерам, например, десяти… Создавать десять сокетов или перебирать один с разными IP–адресами по очереди? Что-нибудь типа широковещательного сообщения? Вообще, тот же пример, расширенный на n-компьютеров, где каждый может обмениваться с каждым, выглядит пока туманно. Более или менее ясно, как на один слушающий сокет принимать входящие с разных адресов. Не зря же мы писали: invoke listen, hSocket2, 5. 5! Хотя программа все равно могла сохранить только один дескриптор сокета после invoke accept…

Подводя итог итогам, можно сказать, что вопросы остаются. Надеюсь, что новичкам в работе с сокетами пример пригодится, а если появятся работающие ответы на вопросы (эти и другие), то будет и продолжение…

(c) DirectOr октябрь 2003г.

Крис Касперски

ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

(избранное)

Часть 1

Профилировка программ

Профилировкой здесь и на протяжении всей книги мы будем называть измерение производительности как всей программы в целом, так и отдельных ее фрагментов, с целью нахождения "горячих" точек (Hot Spots), — тех участков программы, на выполнение которых расходуется наибольшее количество времени.

Согласно правилу "10/90", десять процентов кода "съедают" девяносто процентов производительности системы (равно как и десять процентов людей выпивают девяносто процентов всего пива). Если время, потраченное на выполнение каждой машинной инструкции, изобразить графически в порядке возрастания их линейных адресов, на полученной диаграмме мы обнаружим несколько высоченных пиков, горделиво возвышающихся над практически пустой равниной, усеянной множеством низеньких холмиков (пример показан далее на рисунке разд. " Практический сеанс профилировки с VTune в десяти шагах") Вот эти самые пики — "горячие" точки и есть.

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

Громоздкие и тормозные, но редко вызываемые функции оптимизировать нет ни какой нужды, — это практически не увеличит быстродействия приложения (ну разве что только в том случае, если они совсем уж будут "криво" написаны).

Когда же алгоритм программы прост, а ее исходный текст свободно умещается в сотню–другую строк, то "горячие" точки нетрудно обнаружить и визуальным просмотром листинга. Но с увеличением объема кода это становится все сложнее и сложнее. В программе, состоящей из тысяч сложно взаимодействующих друг с другом функций (часть из которых это функции внешних библиотек и API — Application Programming Interface, интерфейс прикладного программирования — операционной системы) далеко не так очевидно: какая же именно из них в наибольшей степени ответственна за низкую производительность приложения. Естественный выход — прибегнуть к помощи специализированных программных средств.

Профилировщик (так же называемый "профайлером") — основной инструмент оптимизатора программ. Оптимизация "в слепую" редко дает хороший результат. Помните пословицу "самый медленный верблюд определяет скорость каравана"? Программный код ведет себя полностью аналогичным образом, и производительность приложения определяется самым узким его участком. Бывает, что виновницей оказывается одна–единственная машинная инструкция (например, инструкция деления, многократно выполняющаяся в глубоко вложенном цикле). Программист, затратив воистину титанические усилия на улучшение остального кода, окажется премного удивлен, что производительность приложения едва ли возросла процентов на десять–пятнадцать.

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

Цели и задачи профилировки

Основная цель профилировки — исследовать характер поведения приложения во всех его точках. Под "точкой" в зависимости от степени детализации может подразумеваться как отдельная машинная команда, так целая конструкция языка высокого уровня (например: функция, цикл или одна–единственная строка исходного текста).

Большинство современных профилировщиков поддерживают следующий набор базовых операций:

  • определение общего времени исполнения каждой точки программы (total [spots] timing);
  • определение удельного времени исполнения каждой точки программы ([spots] timing);
  • определение причины и/или источника конфликтов и пенальти (penalty information);
  • определение количества вызовов той или иной точки программы ([spots] count);
  • определение степени покрытия программы ([spots] covering).

Общее время исполнения

Сведения о времени, которое приложение тратит на выполнение каждой точки программы, позволяют выявить его наиболее "горячие" участки. Правда, здесь необходимо сделать одно уточнение. Непосредственный замер покажет, что, по крайней мере, 99,99% всего времени выполнения профилируемая программа проводит внутри функции main, но ведь очевидно, что "горячей" является отнюдь не сама main, а вызываемые ею функции! Чтобы не вызывать у программистов недоумения, профилировщики обычно вычитают время, потраченное на выполнение дочерних функций, из общего времени выполнения каждой функции программы.

Рассмотрим, например, результат профилировки некоторого приложения профилировщиком profile.exe, входящего в комплект поставки компилятора Microsoft Visual C++.

Листинг 1.1. Пример профилировки приложения профилировщиком profile.exe
	        Func          Func+Child           Hit
Time % Time % Count Function
---------------------------------------------------------
350,192 95,9 360,982 98,9 10000 _do_pswd (pswd_x.obj)
5,700 1,6 5,700 1,6 10000 _CalculateCRC (pswd_x.obj)
5,090 1,4 10,790 3,0 10000 _CheckCRC (pswd_x.obj)
2,841 0,8 363,824 99,6 1 _gen_pswd (pswd_x.obj)
1,226 0,3 365,148 100,0 1 _main (pswd_x.obj)
0,098 0,0 0,098 0,0 1 _print_dot (pswd_x.obj)

В средней колонке (Func+Child Time) приводится полное время исполнения каждой функции, львиная доля которого принадлежит функции main (ну этого и следовало ожидать), за ней с минимальным отрывом следует gen_pswd со своими 99,6%, далее идет do_pswd — 98,9% и, сильно отставая от нее, где-то там на отшибе плетется CheckCRC, оттягивая на себя всего лишь 3,0%. А функцией CalculateCRC, с ее робким показателем 1,6%, на первый взгляд можно и вовсе пренебречь! Итак, судя по всему, мы имеем три "горячих" точки: main, gen_pswd и do_pswd.

{рисунок поскипан}

Впрочем, функцию main можно откинуть сразу. Она, понятное дело, ни в чем не "виновата". Остаются функции gen_pswd и do_pswd. Если бы это были абсолютно независимые функции, то "горячих" точек было бы и впрямь две, но в нашем случае это не так. И, если из полного времени выполнения функции gen_pswd, вычесть время выполнения ее дочерней функции do_pswd у "матери" останется всего лишь… 0,7%. Да! Меньше процента времени выполнения!

Обратимся к крайней левой колонке (листинг 1.1) таблицы профилировщика (Funct Time), чтобы подтвердить наши предположения. Действительно, в программе присутствует всего лишь одна "горячая" точка — do_pswd, и только ее оптимизация способна существенно увеличить быстродействие приложения.

{рисунок поскипан}

Хорошо, будем считать, что наиболее "горячая" функция определена и теперь мы горим желанием ее оптимизировать. А для этого недурно бы узнать картину распределения "температуры" внутри самой функции. К сожалению, профилировщик profile.exe (и другие подобные ему) не сможет ничем нам помочь, поскольку его разрешающая способность ограничивается именно функциями.

Но, на наше счастье существуют и более "продвинутые" профилировщики, уверенно различающие отдельные строки и даже машинные команды! К таким профилировщикам в частности относится VTune от Intel. Давайте запустим его и заглянем внутрь функции do_pswd (подробнее о технике работы с VTune см. "Практический сеанс профилировки с VTune").

Листинг 1.2. Карта распределения "температуры" внутри функции do_pswd, полученная с помощью профилировщика VTune

Line Clock ticks Source    temperature
105 729 while((++pswd[p])>'z'){ ***********************>>>
106 14 pswd[p] = '!'; **************
107 1 y = y | y << 8; *
108 2 x -= k; **
109 k = k << 8; *
110 3 k += 0x59; ***
111 2 p++; **
112 1 } *

Вот теперь совсем другое дело — сразу видно, что целесообразно оптимизировать, а что и без того уже "вылизано" по самые помидоры. "Горячие" точки главным образом сосредоточены вокруг конструкции pswd[p], — она очень медленно выполняется. Почему? Исходный текст не дает непосредственного ответа на поставленный вопрос и потому совсем не ясно: что конкретно следует сделать для понижения "температуры" "горячих" точек.

Приходится спускаться на уровень "голых" машинных команд (благо профилировщик VTune это позволяет). Вот, например, во что компилятор превратил безобидный на вид оператор присвоения pswd[p] = '!'

Листинг 1.3. Исследование температуры машинных команд внутри конструкции pswd[p] = '!'


 Line    Instructions                    Cycles Count temperature  


 107    mov      edx, DWORD PTR [ebp+0ch] 143   11   *******************
 107    ^ загрузить в регистр EDX указатель pswd

 107    add      edx, DWORD PTR [ebp-4]   22    11   *****
 107    ^ сложить EDX с переменной p

 107    mov      BYTE PTR [edx], 021h     33    11   *******
 107    ^ по полученному смещению записать значение 0х21 ('!')

 

Смотрите! В одной строке исходного текста происходит целых три обращения к памяти! Сначала указатель pswd загружается в регистр EDX, затем он суммируется с переменной p, которая так же расположена в памяти, и лишь затем по рассчитанному смещению в память благополучно записывается константа '!' (021h).

Тем не менее, все равно остается не ясно, почему загрузка указателя pswd занимает столько времени? Может быть, кто-то постоянно вытесняет указатель pswd из кэша, заставляя процессор обращаться к медленной оперативной памяти? Так ведь нет! Программа работает с небольшим количеством переменных, заведомо умещающихся в кэше второго уровня.

(продолжение следует)

# Эпилог

(Ужасы нашего городка)

00F: Тут письмо пришло...
00E: Что, опять??
00F: Не, другое :) Пишут что рассылка у нас нудная
00E: не понял, типа наезд?
00F: скорее самооборона - это Bob Johnson. Он думает, что это я - Наталья
Березовец :)) Так вот, ничего общего! Даже рядом не лежали! Хм...
00E: ну ну, продолжай, даже интересно ;)
00F: вроде ничего не сказал о нем такого - только "гарный хлопец" :( Даю
опровержение: никакой он не гарный, никакой не хлопец!
00E: и кто же он?
00F: кто, кто... Мордоворот в 2 метра ростом, бритый, шрам над бровью и нос
свернут. И руки - все в татуировках!
00E: а ты откуда знаешь?
00F: откуда, откуда... я эти руки хорошо рассмотрел. Вчера позвонили в
дверь, открываю - стоит такой детина и говорит "Ты что ли приколист типа?"
(немая сцена - застывшие физии 00F и 00Е на затекающем кровью экране)