Текущий раздел:

Описания RDS

Руководство программиста

Глава 2. Создание моделей блоков

§2.15. Обмен данными по сети

§2.15.2. Пример использования функций передачи и приема данных

# Руководство программиста

## Глава 2. Создание моделей блоков

### §2.15. Обмен данными по сети

#### §2.15.2. Пример использования функций передачи и приема данных

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

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

Модель блока для включения сервера будет очень простой: при инициализации будет вызываться функция rdsNetServer с указанием какого-нибудь имени канала, а при очистке – rdsNetCloseConnection. Мы не будем ни передавать данные в этот канал, ни получать их из него – он нужен только для запуска сервера, поскольку функцию rdsNetServer нельзя вызвать с пустым именем канала. Назовем этот фиктивный канал «ProgrammersGuide.Server». Если другие блоки будут использовать его для передачи своих данных, ничего страшного не случится, поскольку наш блок не вмешивается в обмен данными по этому каналу.

Модель блока будет выглядеть следующим образом:

      // Включение сервера
      extern "C" __declspec(dllexport)
        int RDSCALL Server(int CallMode,
              RDS_PBLOCKDATA BlockData,
              LPVOID ExtParam)
      { int *pConnId;

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              // Отводим место под личную область данных размером в int
              BlockData->BlockData=pConnId=new int;
              // Запускаем сервер и соединяемся с ним
              *pConnId=rdsNetServer(-1,     // Порт по умолчанию
                                    "ProgrammersGuide.Server", // Канал
                                    FALSE); // Не принимаем данные
              break;

            // Очистка
            case RDS_BFM_CLEANUP:
              // Личная область данных – целое число
              pConnId=(int*)(BlockData->BlockData);
              // Разрываем связь с сервером (он завершится сам)
              rdsNetCloseConnection(*pConnId);
              // Удаляем личную область
              delete pConnId;
              break;
          }
        return RDS_BFR_DONE;
      }
      //=========================================

При инициализации блока мы отводим место в памяти под личную область данных размером в одно целое число – в ней мы будем хранить идентификатор соединения. Затем мы вызываем rdsNetServer для соединения с каналом «ProgrammersGuide.Server» на локальном сервере, то есть сервере в исполняемом файле RDS, который работает с данной схемой. В качестве номера порта мы передаем −1, чтобы был использован порт по умолчанию, указанный в настройках RDS. Конечно, лучше было бы дать пользователю выбор: использовать порт по умолчанию или задать свое значение, но мы не будем этого делать, чтобы не усложнять пример. Принимать данные из канала нам не нужно, поэтому в параметре Receive функции rdsNetServer передается FALSE.

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

При удалении блока из схемы его модель будет вызвана в режиме RDS_BFM_CLEANUP. При этом мы закрываем соединение, идентификатор которого хранится в личной области данных блока. Если это соединение было единственным, сервер будет автоматически отключен.

Теперь, когда у нас есть блок, превращающий машину в сервер RDS, можно заняться блоками приема и передачи данных. У этих блоков будет много общего: в настройках обоих необходимо будет вводить имя канала, оба будут подключаться к серверу, и т.д. Чтобы не писать один и тот же код два раза, мы сделаем для этих двух блоков общий класс личной области данных с полным набором функций и для приема, и для передачи данных, а из функции модели каждого блока будем вызывать только те из них, которые нужны данному блоку. Начнем с описания класса:

      // Личная область данных блоков приема и передачи по сети
      class TNetSendRcvData
      { public:
          int Mode; // Режим данного блока: прием или передача
            #define NETSRMODE_SENDER    0 // Передатчик
            #define NETSRMODE_RECEIVER  1 // Приемник
          char *ChannelName; // Имя канала

          int ConnId; // Идентификатор соединения

          // Переменные состояния блока-передатчика
          BOOL Connected;   // Соединение установлено
          BOOL DataWaiting; // Передача данных отложена

          // Функции класса
          void Connect(void);    // Установить соединение
          void Disconnect(void); // Разорвать соединение

          void SendValue(double value); // Передать число в канал
          BOOL ReceiveValue(RDS_NETRECEIVEDDATA *rcv, // Реакция на
                            double *pOut);            // пришедшие данные

          int Setup(char *title);    // Функция настройки блока
          void SaveText(void);       // Сохранить параметры
          void LoadText(char *text); // Загрузить параметры

          // Конструктор класса
          TNetSendRcvData(int mode)
            { ConnId=-1; // Нет соединения
              Connected=DataWaiting=FALSE;
              ChannelName=NULL;
              Mode=mode; // Режим передается в параметре конструктора
            };
          // Деструктор класса
          ~TNetSendRcvData()
            { Disconnect(); // Разорвать соединение
              rdsFree(ChannelName); // Освободить строку имени канала
            };
      };
      //=========================================

Самое первое поля класса, Mode, указывает на то, принадлежит ли эта область данных блоку-передатчику (константа NETSRMODE_SENDER) или приемнику (константа NETSRMODE_RECEIVER). Значение этому полю присваивается в конструкторе и далее не меняется на протяжении всего существования блока: в блоке-передатчике конструктор будет вызываться с параметром NETSRMODE_SENDER, в приемнике – с параметром NETSRMODE_RECEIVER.

Мы не будем делать имя сервера и его порт параметрами блока – для простоты всегда будем использовать значения по умолчанию из сетевых настроек RDS. Поэтому соответствующие поля в классе не предусмотрены. А вот имя канала нужно обязательно сделать настраиваемым, для этого используется поле ChannelName. Оно содержит указатель на динамически отведенную строку с именем канала, к которому подключен блок, это имя будет вводиться пользователем в функции настройки блока. В конструкторе ему присваивается значение NULL: место под строку будет отведено позднее, при настройке блока или загрузке его параметров. В деструкторе класса память, занятая строкой, освобождается при помощи функции rdsFree (отводиться она тоже будет сервисными функциями RDS).

В следующем поле, ConnId, будет храниться идентификатор соединения с сервером, когда оно будет установлено. В конструкторе ему присваивается значение −1 (соединение в конструкторе установить нельзя – еще не известно имя канала), в деструкторе соединение разрывается функцией класса Disconnect.

Логические поля Connected и DataWaiting используются только в блоке-передатчике. Connected используется как флаг наличия связи с сервером – при установке связи ему будет присваиваться значение TRUE, при разрыве – FALSE. DataWaiting служит для запоминания попытки передачи данных, пока связь еще не установлена. Если значение входа блока-передатчика изменится, а связи с сервером пока нет, полю DataWaiting будет присвоено значение TRUE. Как только связь будет установлена, модель передаст данные в канал и сбросит DataWaiting.

Функции класса Connect и Disconnect предназначены для установки связи с сервером и ее разрыва соответственно. Они будут использоваться и блоками-приемниками, и блоками-передатчиками. Функция SendValue передает в канал вещественное число, она будет использоваться только в блоках-передатчиках. Функция ReceiveValue будет вызываться в реакции модели блока-приемника на поступившие данные, анализировать эти данные и, если размер переданного буфера равен размеру вещественного числа, копировать их на выход блока (указатель на переменную выхода передается в параметре функции). Наконец, функции Setup, LoadText и SaveText предназначены для настройки параметров блока (у нас только один параметр – имя канала), их загрузки и записи. В функцию Setup будет передаваться заголовок окна настройки – у передатчика и приемника он будет различаться.

Напишем функцию установки соединения с сервером. Она будет использовать имя и порт сервера по умолчанию, а имя канала будет брать из поля ChannelName, добавляя к нему префикс «ProgrammersGuide.» для того, чтобы каналы, используемые нашими блоками, не пересеклись по именам с каналами блоков других разработчиков (точно так же мы поступали, когда давали имена функциям блоков).

      // Установка соединения
      void TNetSendRcvData::Connect(void)
      { char *PrefixedName; // Полное имя канала

        // Если имя канала пустое, соединение невозможно
        if(ChannelName==NULL || (*ChannelName)==0)
          return;

        // Добавляем префикс к имени канала
        PrefixedName=rdsDynStrCat("ProgrammersGuide.",ChannelName,FALSE);
        // Устанавливаем соединение с сервером
        ConnId=rdsNetConnect(NULL, // Сервер по умолчанию
                             -1,   // Порт по умолчанию
                             PrefixedName, // Имя канала с префиксом
                             Mode==NETSRMODE_RECEIVER); // Прием данных
        // Освобождаем динамически отведенную строку
        rdsFree(PrefixedName);
      }
      //=========================================

Прежде всего, мы проверяем, введено ли имя канала, то есть не пуста ли строка ChannelName. Если имя канала не введено (поле ChannelName содержит NULL или указывает на пустую строку), соединение с сервером установить невозможно – функция завершается. В противном случае при помощи сервисной функции rdsDynStrCat к имени канала, заданному пользователем, добавляется префикс «ProgrammersGuide.», и результат записывается в переменную PrefixedName. rdsDynStrCat возвращает указатель на динамически отведенную строку, поэтому ее нужно будет освободить перед завершением функции Connect.

Теперь можно устанавливать соединение с сервером: вызывается функция rdsNetConnect, и возвращенный ей идентификатор соединения записывается в поле ConnId. Мы подключаемся к серверу по умолчанию, указанному в настройках RDS, поэтому вместо адреса сервера передается NULL, а вместо номера его порта – −1. Имя канала с префиксом сформировано в переменной PrefixedName, а последний параметр функции rdsNetConnect, определяющий, должен ли наш блок получать данные по сети, зависит от значения поля класса Mode. Блок будет принимать данные только в том случае, если в этом поле находится константа NETSRMODE_RECEIVER, то есть если эта личная область данных принадлежит блоку-приемнику. Таким образом, сервер не будет отправлять данные блокам-передатчикам, что снизит нагрузку на сеть.

Следует помнить, что при вызове rdsNetConnect соединение с сервером устанавливается не мгновенно. Эта функция только запрашивает соединение и создает внутренний объект RDS, идентификатор которого возвращается. После этого клиент отправит запрос серверу, дождется ответа от него, обменяется с ним служебной информацией и т.п. – на все это нужно время. После фактической установки соединения модель блока будет вызвана в режиме RDS_BFM_NETCONNECT, до этого момента передаваемые данные отправляться на сервер не будут. Лучше всего отложить вызов функций передачи данных до фактической установки соединения, чтобы не загружать очередь. По этой причине мы не взводим логический флаг наличия соединения Connected в функции Connect: он будет установлен только после вызова модели в режиме RDS_BFM_NETCONNECT.

Функция разрыва соединения будет короткой:

      // Разорвать соединение
      void TNetSendRcvData::Disconnect(void)
      { if(ConnId!=-1) // Соединение было создано
          rdsNetCloseConnection(ConnId); // Разорвать
        // Сбрасываем переменные состояния
        ConnId=-1;       // Соединения больше нет
        Connected=FALSE; // Связи тоже больше нет
      }
      //=========================================

Если соединение было создано (ConnId не равно −1), мы разрываем его функцией rdsNetCloseConnection, после чего присваиваем ConnId значение −1, поскольку соединения уже нет. Логический флаг Connected мы тоже сбрасываем – хотя физически соединение, может быть, еще не разорвано, данные передавать уже нельзя.

Теперь напишем функцию передачи вещественного числа – она будет вызываться при изменении входа блока-передатчика:

      // Передать данные
      void TNetSendRcvData::SendValue(double value)
      {
        if(!Connected) // Нет связи с сервером
          { // Взводим флаг наличия данных, ожидающих передачи
            DataWaiting=TRUE;
            return;
          }

        // Связь есть – передаем всем блокам канала
        rdsNetBroadcastData(ConnId, // Соединение
                            RDS_NETSEND_UPDATE|RDS_NETSEND_UDP, // Флаги
                            0,NULL, // Не передаем целое число и строку
                            &value, // Указатель на данные
                            sizeof(value)); // Размер данных
        // Сбрасываем флаг ожидания – мы только что передали данные
        DataWaiting=FALSE;
      }
      //=========================================

Сначала мы проверяем, взведен ли логический флаг Connected, то есть пришло ли подтверждение установки связи с сервером. Если он сброшен, связи еще нет, и передавать данные не следует. В этом случае взводится логический флаг DataWaiting и функция завершается – как только связь будет установлена, мы проверим этот флаг, и, если он взведен, передадим в канал значение входа блока.

Если флаг взведен, вызывается функция rdsNetBroadcastData, передающая через соединение ConnId данные всем блокам, подключившимся к этому же каналу сервера. При передаче используются флаги RDS_NETSEND_UPDATE (если в очереди на передачу уже стоят данные для этого канала, они будут выброшены) и RDS_NETSEND_UDP (использовать при передаче протокол UDP, если возможно). Вещественное число мы отправляем в канал как восемь байтов двоичных данных – в качестве указателя на буфер с данными мы передаем в функцию указатель на параметр value, а в качестве длины буфера – размер этого параметра sizeof(value) (value имеет тип double, поэтому sizeof(value) будет равно восьми). Функция rdsNetBroadcastData одновременно с блоком двоичных данных может передать целое число и строку, но это нам не нужно, поэтому вместо строки передается NULL, а вместо числа – 0. На самом деле, число 0 все равно будет передано, но блок-приемник никак не будет на него реагировать.

Сразу после вызова rdsNetBroadcastData мы сбрасываем флаг DataWaiting – теперь данные уже не ожидают передачи, они только что были переданы.

Функция ReceiveValue будет вызываться в реакции модели на поступление данных из сети (RDS_BFM_NETDATARECEIVED). В нее будет передаваться указатель на структуру RDS_NETRECEIVEDDATA, описывающую принятые данные (в функцию модели этот указатель передается в параметре ExtParam) и указатель на вещественный выход блока-приемника, в который нужно записать принятое значение. Функция будет возвращать TRUE, если размер принятых данных соответствует типу double, то есть равен восьми.

      // Прием данных
      BOOL TNetSendRcvData::ReceiveValue(
               RDS_NETRECEIVEDDATA *rcv, // Указатель на структуру с данными
        double *pOut)             // Указатель на выход блока
      {
        if(rcv==NULL || pOut==NULL) // Нет одного из указателей
          return FALSE;

        // Проверяем, есть ли среди принятых данных двоичные,
        // и равен ли размер этих данных размеру double
        if(rcv->Buffer==NULL || rcv->BufferSize!=sizeof(double))
          return FALSE; // Нет данных или не совпал размер

        // Копируем принятое числов pOut
        memcpy(pOut,rcv->Buffer,sizeof(double));
        return TRUE; // Приняты правильные данные
      }
      //=========================================

В начале функции мы, на всякий случай, проверяем, не переданы ли в нее случайно нулевые указатели на структуру с принятыми данными и выход блока – без этих указателей функция не сможет работать. Затем мы проверяем, какие именно данные приняты. В канал можно одновременно передать целое число, строку и буфер с двоичными данными, но нас интересуют только двоичные данные, причем их размер должен совпадать с размером вещественного числа двойной точности (double). Если в принятых данных нет двоичных (указатель rcv->Buffer равен NULL) или размер данных неправильный (rcv->BufferSize не равно sizeof(double)), функция возвращает FALSE – блок не может обработать такие данные. В противном случае весь принятый двоичный буфер копируется в выход блока, указатель на который передан в параметре pOut, и функция возвращает TRUE.

Осталось написать три вспомогательные функции. Начнем с функций сохранения и загрузки параметров блока. В данном случае, параметр у нас один – строка имени канала. Сохранять параметры мы будем в текстовом формате, аналогичном формату INI-файлов Windows (см. §2.8.5):

      // Сохранение параметров блока
      void TNetSendRcvData::SaveText(void)
      { RDS_HOBJECT ini; // Вспомогательный объект
        // Создаем вспомогательный объект
        ini=rdsINICreateTextHolder(TRUE);
        // Создаем в объекте секцию "[General]"
        rdsSetObjectStr(ini,RDS_HINI_CREATESECTION,0,"General");
        // Записываем в эту секцию имя канала
        rdsINIWriteString(ini,"Channel",ChannelName);
        // Сохраняем текст, сформированный объектом, как параметры блока
        rdsCommandObject(ini,RDS_HINI_SAVEBLOCKTEXT);
        // Удаляем вспомогательный объект
        rdsDeleteObject(ini);
      }
      //=========================================

Функция загрузки параметров будет аналогична функции сохранения:

      // Загрузка параметров блока
      void TNetSendRcvData::LoadText(char *text)
      { RDS_HOBJECT ini; // Вспомогательный объект
        char *str;
        // Создаем вспомогательный объект
        ini=rdsINICreateTextHolder(TRUE);
        // Записываем в объект полученный текст с параметрами блока
        rdsSetObjectStr(ini,RDS_HINI_SETTEXT,0,text);
        // Начинаем чтение секции "[General]", если она есть
        if(rdsINIOpenSection(ini,"General")) // Секция есть
          { // Освобождаем старое имя канала
            rdsFree(ChannelName);
            ChannelName=NULL;
            // Получаем у объекта указатель на строку с именем
            str=rdsINIReadString(ini,"Channel","",NULL);
            // Если такая строка есть в тексте, копируем ее в ChannelName
            if(str)
              ChannelName=rdsDynStrCopy(str);
          }
        // Удаляем вспомогательный объект
        rdsDeleteObject(ini);

        // Поскольку имя канала могло измениться, соединяемся с
        // сервером заново
        Disconnect(); // Разрываем старое соединение
        Connect();    // Создаем новое
      }
      //=========================================

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

Теперь напишем функцию настройки, которая позволит пользователю задавать имя канала для блока-приемника и блока-передатчика. Будем использовать для ввода строки сервисную функцию rdsInputString:

      // Настройка параметров
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      int TNetSendRcvData::Setup(char *title)
      { char *NewName;

        // Запрос строки у пользователя
        NewName=rdsInputString(
                  title,         // Заголовок окна
                  "Имя канала:", // Текст перед полем
                  ChannelName,   // Исходное значение
                  200);          // Ширина поля

        if(NewName==NULL) // Нажата кнопка "Отмена"
          return 0;

        if(ChannelName!=NULL && strcmp(NewName,ChannelName)==0)
          { // Имя канала не изменилось
            rdsFree(NewName); // Освобождаем вовращенную строку
            return 0;
          }

        // Освобождаем старое имя канала
        rdsFree(ChannelName);
        // Запоминаем новое
        ChannelName=NewName;
        // Имя канала изменилось – устанавливаем связь заново
        Disconnect();
        Connect();
        return 1; // Параметры блока изменены
      }
      //=========================================

Заголовок окна, которое откроет функция rdsInputString, передается в параметре функции Setup, он будет зависеть от того, какому типу блока принадлежит объект этого класса. Функция вернет нам управление только после того, как пользователь закроет окно кнопкой «ОК» или «Отмена». При нажатии кнопки «Отмена» функция возвращает NULL, при этом мы завершаем функцию настройки, возвращая ноль – параметры блока не изменились. Если же пользователь нажмет «ОК», функция вернет указатель на динамически отведенную строку с текстом, введенным в окне. В этом случае имеет смысл проверить, изменил ли пользователь имя канала, или оно осталось прежним. Если строки ChnName и NewName совпадают, то есть пользователь не изменил имя канала, возвращенная функцией rdsInputString строка NewName освобождается, и мы завершаем функцию настройки. В противном случае мы освобождаем строку со старым именем канала и копируем в ChnName новое из переменной NewName. Затем, как и после загрузки параметров, мы разрываем соединение с сервером и устанавливаем его заново, но уже с новым именем канала.

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

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

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | x | double | 8 | Вход | ✓ | 0

Для входа блока «x» установлен флаг «пуск», чтобы при срабатывании связи, подключенной к этому входу, модель блока автоматически запускалась в следующем такте расчета – передавать данные на сервер блок будет именно в такте расчета. Для правильной работы модели необходимо будет также установить в параметрах блока флаг «запуск по сигналу», иначе модель будет передавать данные не при изменении входа, а в каждом такте расчета, что приведет к неоправданно большой нагрузке на сеть.

Модель блока будет следующей:

      // Блок-передатчик
      extern "C" __declspec(dllexport)
        int RDSCALL NetSend(int CallMode,
              RDS_PBLOCKDATA BlockData,
              LPVOID ExtParam)
      { // Указатель на личную область данных, приведенный к нужному типу
        TNetSendRcvData *data=(TNetSendRcvData*)(BlockData->BlockData);
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define x (*((double *)(pStart+2*RDS_VSZ_S)))

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              // Создаем объект класса TNetSendRcvData с
              // Mode==NETSRMODE_SENDER (передатчик)
              BlockData->BlockData=new TNetSendRcvData(NETSRMODE_SENDER);
              break;

            // Очистка
            case RDS_BFM_CLEANUP:
              delete data;
              break;

            // Проверка типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSD}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // Связь с сервером установлена
            case RDS_BFM_NETCONNECT:
              // Взводим флаг наличия связи
              data->Connected=TRUE;
              // Если были данные, ожидающие отправки,
              // посылаем значение входа блока
              if(data->DataWaiting)
                data->SendValue(x);
              break;

            // Связь с сервером разорвана
            case RDS_BFM_NETDISCONNECT:
              // Сбрасываем флаг наличия связи
              data->Connected=FALSE;
              break;

            // Запуск расчета
            case RDS_BFM_STARTCALC:
              // Если это – первый запуск после сброса,
              // передаем значение входа
              if(((RDS_PSTARTSTOPDATA)ExtParam)->FirstStart)
                data->SendValue(x);
              break;

            // Такт расчета
            case RDS_BFM_MODEL:
              data->SendValue(x); // Передаем значение входа
              break;

            // Вызов настройки
            // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
            //   в противном случае необходимо использовать версии функций
            //   с суффиксом "W" и символьные константы с префиксом "L"
            case RDS_BFM_SETUP:
              return data->Setup("Передача double");

           // Сохранение параметров в текстовом виде
           case RDS_BFM_SAVETXT:
              data->SaveText();
              break;

           // Загрузка параметров в текстовом виде
           case RDS_BFM_LOADTXT:
              data->LoadText((char*)ExtParam);
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef x
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

При инициализации модели мы создаем личную область данных блока – объект класса TNetSendRcvData. В параметре конструктора класса мы передаем константу NETSRMODE_SENDER – она будет записана в поле Mode, чтобы все функции класса знали, что объект принадлежит блоку-передатчику.

Как только связь с сервером будет установлена (а устанавливается она после загрузки параметров или ввода имени канала пользователем в функциях, которые мы уже написали), модель будет вызвана в режиме RDS_BFM_NETCONNECT. При этом будет взведен флаг наличия связи data->Connected, и, если есть данные, ожидающие передачи (взведен data->DataWaiting), значение входа блока x будет передано на сервер функцией SendValue. Флаг DataWaiting взводится внутри функции SendValue при попытке передать данные в отсутствие связи с сервером, и сбрасывается там же после успешной передачи. Если на момент установления связи DataWaiting будет истинным, значит, до этого были неудачные попытки передачи, и вход блока нужно передать на сервер.

При разрыве связи модель будет вызвана в режиме RDS_BFM_NETDISCONNECT, при этом будет сброшен флаг data->Connected. Начиная с этого момента все вызовы функции SendValue будут приводить не к передаче данных, а к взведению флага DataWaiting. Как только RDS сможет восстановить связь, данные немедленно будут переданы.

При самом первом запуске расчета (сразу после загрузки схемы или после сброса) блок отправляет значение своего входа на сервер – таким образом мы передаем всем блокам-приемникам начальное значение входа блока передатчика. Текущее значение входа x при его изменениях передается в такте расчета (RDS_BFM_MODEL). В трех оставшихся реакциях модели вызываются функции класса для настройки, сохранения и загрузки параметров, которые мы уже написали.

Теперь напишем модель блока-приемника. У него будет очень похожая структура переменных, только место входа «x» займет выход «y»:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | y | double | 8 | Выход |  | 0

Для этого блока тоже желательно установить в параметрах блока флаг «запуск по сигналу», чтобы он не тратил зря процессорное время: в его модели не будет реакции на выполнение такта расчета.

Модель блока-приемника будет выглядеть так:

      // Блок-приемник
      extern "C" __declspec(dllexport)
        int RDSCALL NetReceive(int CallMode,
                RDS_PBLOCKDATA BlockData,
                LPVOID ExtParam)
      { // Указатель на личную область данных, приведенный к нужному типу
        TNetSendRcvData *data=(TNetSendRcvData*)(BlockData->BlockData);
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define y (*((double *)(pStart+2*RDS_VSZ_S)))

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              // Создаем объект класса TNetSendRcvData с
              // Mode==NETSRMODE_RECEIVER (приемник)
              BlockData->BlockData=
                new TNetSendRcvData(NETSRMODE_RECEIVER);
              break;

            // Очистка
            case RDS_BFM_CLEANUP:
              delete data;
              break;

            // Проверка типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSD}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // По сети получены данные
            case RDS_BFM_NETDATARECEIVED:
              if(data->ReceiveValue((RDS_NETRECEIVEDDATA*)ExtParam,&y))
                Ready=1; // Если данные верны, взводим флаг готовности
                         // для передачи выхода по связям
              break;

            // Вызов настройки
            // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
            //   в противном случае необходимо использовать версии функций
            //   с суффиксом "W" и символьные константы с префиксом "L"
            case RDS_BFM_SETUP:
              return data->Setup("Прием double");

           // Сохранение параметров в текстовом виде
           case RDS_BFM_SAVETXT:
              data->SaveText();
              break;

           // Загрузка параметров в текстовом виде
           case RDS_BFM_LOADTXT:
              data->LoadText((char*)ExtParam);
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef y
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

В этой модели, как и в модели передатчика, при инициализации тоже создается объект класса TNetSendRcvData, но в его конструктор передается константа NETSRMODE_RECEIVER, указывающая на то, что этот объект принадлежит блоку-приемнику. Реакции на установку и разрыв связи в модели отсутствуют: этому блоку не нужно знать, есть ли в данный момент связь с сервером. Он реагирует на пришедшие от сервера данные – если связи нет, данные просто не будут приходить.

Полученные по сети данные обрабатываются в реакции модели на событие RDS_BFM_NETDATARECEIVED. При этом вызывается функция ReceiveValue класса TNetSendRcvData, которую мы написали ранее. В функцию передается указатель на структуру, описывающую принятые данные (для этого ExtParam приводится к типу RDS_NETRECEIVEDDATA*) и указатель на выход блока y, в который она запишет принятое вещественное число. Если ReceiveValue вернет TRUE, то есть если принятые данные соответствуют размеру числа double, будет взведен сигнал готовности блока Ready, чтобы новое значение выхода было передано по связям.

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

Для проверки работы созданных блоков потребуется две машины, соединенные сетью. В сетевых настройках RDS (см. рис. 108) на каждой из них в поле «сервер по умолчанию» необходимо указать IP-адрес той машины, которая будет играть роль сервера, и задать один и тот же номер порта. На сервере следует собрать схему, изображенную на рис. 109 а: в ней должен присутствовать блок, запускающий сервер (модель Server), блок-передатчик (модель NetSend) и блок-приемник (модель NetReceive). В параметрах всех трех блоков необходимо установить флаг «запуск по сигналу», к входу блока-передатчика подключить поле ввода, а к выходу блока-приемника – индикатор. В параметрах передатчика и приемника нужно ввести разные имена каналов (например, «Channel1» и «Channel2»).

(а)

(б)

Рис. 109. Схемы для тестирования блоков сетевого обмена: схема на сервере (а) и на клиенте (б)

На машине-клиенте следует собрать аналогичную схему (рис. 109 б), но без блока включения сервера. Имена каналов у приемника и передатчика нужно задать такими, чтобы приемник клиента работал с каналом передатчика сервера, а передатчик клиента – с каналом приемника сервера. Например, если в блоке-передатчике на сервере задан канал «Channel1», это же имя канала нужно ввести в настройках блока-приемника клиента.

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

Если открыть на сервере окно сетевых соединений (пункт главного меню RDS «Окна | Сетевые соединения», рис. 110), можно будет увидеть два IP-адреса подключенных клиентов. Один из них, «127.0.0.1», это адрес этой же машины – она выступает сервером и для блоков в своей собственной схеме. Второй – адрес машины, на которой работает схема-клиент. Можно также видеть имена трех созданных блоками каналов, один из которых используется только для включения сервера, а два других связывают передатчики и приемники в двух схемах.

Рис. 110. Окно сетевых соединений