(c) 2007 А.В. Черномырдин aka chav1961
.

Пишем DDE-сервер для SCADA-систем

Эта статья написана для тех читателей, которым не нужно объяснять значение слов "SCADA-система", которые работали с такими шедеврами, как Intouch и WinCC, не шарахаются от языка С/С++, и очень сожалеют о том, что эти системы не работают с нестандартным оборудованием (например, с собственноручно изготовленной махарайкой). Проблема эта достаточно легко решается с помощью протокола обмена данными DDE. Все, что для этого требуется - суметь написать простенький DDE-сервер, а после "достучаться" до него из соответствующей SCADA-системы.

Что такое протокол DDE и как он реализуется в ОС Windows

Не ждите от этого раздела детального описание протокола - кроме самых необходимых сведений, без которых просто невозможно написать DDE-сервер, здесь не будет больше ничего. Протокол DDE - давняя фишка Windows, он существовал еще в Windows 3.1x, если кто из читателей застал такую древность. Основное его назначение - обмен данными между различными приложениями. Вначале такой обмен осуществлялся в пределах одного компьютера, поэтому для него использовался обмен сообщениями между окнами приложений. От того времени в Windows до сих пор осталась целая куча сообщений с наименованиями WM_DDE_xxxxx. Эти сообщения работают в Windows и сейчас (правда, с некоторыми приседаниями и подпрыгиваниями), и это - единственный ныне способ обмена данными между 32-битными и 16-битными приложениями, если у кого из читателей осталось что-то 16-битное . С переходом на 32-битный Windows (т.е. Windows 95 и далее) скорость работы прежнего протокола DDE резко упала - дело в том, что обмен данными в Windows 3.1x производился фактически в пределах единого адресного пространства, и все адреса передаваемых данных (поле lParam сообщения) были валидными в любом приложении. В Windows 95 и далее все 32-битные приложения имеют свои собственные адресные пространства, и "достучаться" до адресов, переданных из других приложения не могут (если не считать разной экзотики наподобие функций ReadProcessMemory/WriteProcessMemory). Выходов из этой ситуации было два - полностью переделать формат сообщений в Windows либо "ручками" переписывать данные из процесса в процесс внутри ядра ОС. Понятно, что Microsoft пошла по второму пути, иначе ее бы просто порвали разработчики, успевшие к тому времени наделать немало 16-битных приложений (которые давным-давно никому не нужны - а дурь с сообщениями осталась!). Итог - скорость протокола DDE упала ниже плинтуса. А когда вдобавок ко всему появилась необходимость в обмене данными между разными компьютерами, старая версия протокола DDE просто скончалась. Новый протокол DDE - фактически особая надстройка над сетевой "кухней" Windows (наподобие протокола NetBIOS). Описываю историю протокола столь подробно, потому что без знания этой истории будут малопонятны некоторые "фишки" самого протокола.

Обмен данными по протоколу DDE состоит из трех этапов:

  • установка соединения
  • собственно обмен данными
  • разрыв соединения

Каких-то особенностей, характерных именно для протокола DDE, здесь, как видите, нет: можно легко провести параллели между протоколом DDE и файлами (открытие, работа, закрытие), между протоколом DDE и протоколом TCP (установка соединения, обмен данными, разрыв соединения) и т.д. В процессе установки соединения один из участников является его инициатором (в терминологии DDE - клиентом), а второй находится в постоянной готовности к установке соединения (в терминологии DDE - сервер). И сервер, и клиент - это некие приложения (упрощенно - *.exe-файлы), запущенные на одном или на разных компьютерах, хотя одно и то же приложение в принципе может являться одновременно и сервером, и клиентом протокола DDE (пример такого приложения - электронные таблицы Microsoft Excel). Со стороны клиентского приложения каких-либо предварительных действий в протоколе DDE делать не требуется, а вот серверное приложения должно сначала зарегистрироваться в Windows в качестве сервера протокола DDE. Если этого не сделать, клиент DDE не сможет подсоединиться к серверу. Сервер DDE при регистрации сообщает Windows то имя, под которым он будет "известен" на данном компьютере. В терминологии DDE это имя называется Application. Разработчики DDE-серверов обычно стараются сделать это имя совпадающим с именем самого приложения, например Application-имя упомянутых выше таблиц Microsoft Excel - "EXCEL", DDE-сервер системы Intouch имеет имя "VIEW" и т.д. В принципе, имя для своего DDE-сервера можно задавать любое, лишь бы оно вписывалось в традиционные понятия об имени (латинские буквы, цифры, знаки подчеркивания) и имело длину не более 8 символов. Именно по этому имени и будет подсоединяться к серверу клиент DDE-протокола.

После установки соединения можно выполнять обмен данными. В принципе, инициатором обмена данными в протоколе DDE может быть как клиент, так и сервер, но для написания DDE-сервера под SCADA можно считать, что обмен данными инициирует только клиент. Связано это с тем, что практически все SCADA-системы работают с драйверами устройств (чем, по сути, и является наш DDE-сервер) в режиме периодического опроса (polling), который, естественно, не подразумевает какой-либо самостоятельной активности сервера. Обмен данными может происходить в одном из двух направлений - чтение данных клиентом (если продолжить аналогии - аналог операции read для файла) и передача данных серверу (аналог операции write для файла). Сервер в этом обмене играет исключительно пассивную роль - при получении запроса на "чтение" он должен сформировать блок данных и возвратить его клиенту, при получении запроса на "запись" - разобрать и обработать полученный от клиента блок данных. Для обработки таких запросов в DDE-сервере должна быть реализована программа выхода (callback), адрес которой указывается Windows при инициализации приложения. Программа выхода вызывается Windows автоматически и асинхронно по отношению к основному процессу, в отличие от запросов, выдаваемых клиентом, которые обрабатываются синхронно. При вызове программы выхода ей в качестве параметров передаются две символьные строки, одна из которых в протоколе DDE носит название Topic, а вторая - Item. В прежней версии протокола такого деления на две строки не было, строка была всего одна и называлась Topic. В новой версии протокола в эту строку ввели иерархию: считается, что строка, соответствующая Topic, содержит имя какой-либо группы данных, а строка, соответствующая Item, содержит имя элемента данных внутри группы. По этим именам сервер должен определить, какие именно данные нужны в данном запросе клиенту. Впрочем, ничто не мешает серверу просто наплевать на эти строки, и всегда отвечать на все запросы одними и теми же данными . Вопрос "на кой нужен такой сервер" остается при этом открытым .

Разрыв соединения - операция, которой должны завершаться обмен данными по протоколу DDE. Дополнительная операция, которую должен выполнить сервер протокола DDE - разрегистрироваться в качестве такового. Обычно это делается непосредственно перед завершением работы приложения.

Простейший пример сервера DDE

Итак, скачайте исходный текст простейшего DDE-сервера вот отсюда, создайте проект, откомпилируйте его и запустите на выполнение. Затем запустите в качестве клиента протокола DDE супер-программу Microsoft EXCEL, и наберите в ячейке A1 формулу "=DDECLI|topic1!Item1", а в ячейке A2 формулу "=DDECLI|topic1!Item2". Вот что у Вас должно в результате получиться:

А теперь вывешиваем перед глазами исходный текст, и начинаем разбираться. Первое, на что нужно обратить внимание - как в Microsoft EXCEL (а также в Intouch) задаются источники данных для протокола DDE. В общем виде такой источник задается следующим образом:

[\\Server\]Application|Topic!Item

В нашем примере и сервер, и клиент DDE располагаются на одной машине, поэтому сервер в нашей формуле не указан. Кстати, небольшое замечение - указание имени сервера в виде "\\.\" или "\\localhost\" не всегда правильно воспринимается клиентами DDE, так что для таких случаев имя сервера лучше не задавать вовсе. Поле Application - имя DDE-сервера. В нашем исходнике это имя употребляется при вызове функции DdeNameService с третьим параметром, равным DNS_REGISTER. Имя DDE-сервера может быть любым, лишь бы оно было не длиннее 8 символов, и было известно клиенту. В нашем примере имя сервера - DDECLI. Обычно это имя стараются делать совпадающим (или близким) к имени *.exe-файла DDE-сервера (как, например, у EXCEL), но это не правило, а, скорее, традиция. После регистрации DDE-сервера в системе он будет получать от клиентов протокола DDE различные сообщения, на которые ему следует (или не следует) отвечать. Все сообщения от клиентов будут поступать в программу выхода (в нашем примере она называется exitFunction). Адрес этой программ передается системе DDE при ее инициализации (вторым параметром функции DdeInitialize). Что касается двух других параметров - Topic и Item, то их DDE-сервер получает через параметры hsz1 и hsz2 программы выхода соответственно. Что они означают для DDE-сервера, решает программист. Для "известных" DDE-серверов, таких как EXCEL или Intouch, смысл этих параметров оговорен в документации. В нашем примере поле Topic просто анализируется на равенство заранее заданной строке, а поле Item - на равенство одной из двух строк. Этот анализ производится при обработке сообщения класса XCLASS_DATA, само же сообщение называется XTYP_REQUEST. Это сообщение - запрос данных от DDE-сервера. На него мы отвечаем одной из двух строк (ItemData1 или ItemData2 в функции exitFunction). Это хорошо видно по трассе работы DDE-сервера (строки request на картинке). Второе место, где производится анализ полей Topic и Item - обработка сообщения класса XCLASS_FLAGS с типом XTYP_POKE. Это сообщение - изменение значений строк на DDE-сервере. Наш DDE-сервер просто сохраняет пришедшую к нему новую информацию в одну из двух строк. Вы можете попробовать изменить содержимое ячейки EXCEL и пронаблюдать за появлением трассы "poke", но перед этим необходимо в EXCEL изменить формат ячеек A1 и A2 на числовой (это - фишка EXCEL, никакого отношения к протоколу DDE не имеющая). Остальные моменты, полагаю, заинтересованные товарищи смогут узнать из хелпов MSDN.

Поговорим о нюансах

Некоторые моменты при написании DDE-сервера:

  • Система DDE не использует напрямую символьные строки. Вместо этого для каждой символьной строки, будь то названия приложений, топиков или элементов Item, необходимо создавать описатель (handle) символьной строки. Эта особенность в протоколе DDE присутствует еще со времен Windows 3.1. С ней просто надо свыкнуться. Описатель строки создается функцией DdeCreateStringHandle, а удаляется функцией DdeFreeStringHandle. Саму символьную строку, связанную с описателем, можно получить с помощью функции DdeQueryString. В нашем примере мы именно так и поступали, но это делалось больше для того, чтобы вывести их содержимое на консоль. Если список топиков и меток известен заранее, можно заранее насоздавать описатели соответствующих строк, а вместо вызова функций DdeQueryString и strcmpi просто сравнивать параметры hsz1 и hsz2 с этими описателями. Система гарантирует, что строки с одинаковым содержимым получат одинаковые дескрипторы. Программа будет и быстрее, и проще.
  • Вызов функции MеssageBox в программе имеет тайный смысл. Если бы его не было, сервер бы закончил работу, так ее и не начав . Регистрация DDE-сервера вовсе не означает, что теперь DDE-сервер будет сидеть и ждать Ваших сообщений. Он будет заниматься своей работой (в нашем случае - побыстрее закончить работу и свалить из программы), а когда получит от Вас какое-нибудь сообщение, асинхронно обработает его. Функция MеssageBox - не самый лучший, но надежный способ остановить сервер, чтобы он не закончил работу раньше времени. Вторым по надежности способом можно назвать любое модальное диалоговое окно. И, наконец, самый идеальный вариант - классичесский цикл обработки сообщений (GetMessage/TranslateMessage/DispatchMessage), беда только в том, что во всяких приблудах типа MFC или борландовских "кубиках" до этого цикла нужно еще добраться. Никакие другие способы остановки работы сервера DDE недопустимы - если не будет работать механизм диспетчеризации сообщений, ничего вы в программу выхода не получите. Вслед - ненавязчивый совет: - не делайте реальный DDE-сервер в виде консольного приложения.
  • Во многих вызовах функций протокола DDE требуется указывать формат обрабатываемых данных в виде CF_xxx (форматы хранения данных в буфере обмена). Ненавязчивый совет - ничего, кроме CF_TEXT, в качестве этого параметра не задавать. Разумеется, если не хотите по-настоящему потрахаться .

Вот, собственно и все. В системе WinCC для доступа к нашему серверу необходимо в списке тегов установить драйвер "Windows DDE", создать в нем новое соединение, а в свойствах соединения указать для Application строку "DDECLI", а для Topic - строку "TOPIC1". Затем в пределах этого соединения необходимо создать два символьных тега, и установить для них через кнопку "Select" имя Item": для одного - "Item1", для другого - "Item2". Тип данных следует выбрать TEXT8. В системе Intouch необходимо создать в словаре тегов два тега типа "I/O Message", создать для них Access Name, в котором задать поле Application и Topic равными "DDECLI" и "TOPIC1". В поле Item самих тегов необходимо указать "Item1" и "Item2". Сервер DDE всегда лучше запускать раньше, чем клиента - так удастся избежать массы проблем. На первый раз сервер и клиент лучше запустить на одной машине, а затем попробовать то же самое на разных (при этом имя машины, на которой будет запущен наш сервер DDE, необходимо указать в поле Server для описателя соединения в WinCC, или в поле Server для Access Name в Intouch соответственно).

И последнее. По большому счету, для того, чтобы DDE-сервер заработал со SCADA-системами, никаких сообщений, кроме XTYP_REQUEST и XTYP_POKE, обрабатывать не требуется. Ну, разве для полного счастья, можно добавить обработку сообщения XTYP_EXECUTE. Если интересно - сделайте эту доработку самостоятельно. По этому сообщению Вы получите символьную строку, в которой будет написана всякая бредятина наподобие "[open('c:\file.txt')][print]". Что Вы с ней будете делать - решайте сами.

Успехов !

Hosted by uCoz