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

Описания RDS

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

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

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

§2.15.3. Способы снижения нагрузки на сеть

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

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

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

#### §2.15.3. Способы снижения нагрузки на сеть

Рассматриваются два способа снижения нагрузки на сеть: ограничение минимального интервала между моментами передачи данных и объединение передаваемых данных в один блок. Приводятся примеры реализации этих способов.

В схемах, собранных в §2.15.2, входы блоков-передатчиков были соединены с полями ввода. Когда пользователь меняет значения в одном из этих полей, это значение поступает по связям на вход передатчика, его модель запускается, и значение передается на сервер. Как бы быстро ни менял пользователь значения в полях ввода, его скорость не может сравниться со скоростью передачи данных по сети, поэтому нагрузка на сеть от этих схем незначительна. Ситуация изменится, если к входу блока-передатчика подключить выход блока, выдающего новое значение в каждом такте расчета. Такты расчета следуют друг за другом очень быстро, и, если несколько блоков-передатчиков будут отправлять данные на сервер в каждом такте, это может привести к перегрузке сети.

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

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

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

          // Переменные состояния блока-передатчика
          BOOL Connected;      // Соединение установлено
          BOOL DataWaiting;    // Передача данных отложена
          RDS_TIMERID Timer;   // Таймер для отсчета интервала
          BOOL WaitingForTimer;// Таймер запущен - ждем
          DWORD LastSendTime;  // Время последней отправки
          // Функции класса
          void Connect(void);    // Установить соединение
          void Disconnect(void); // Разорвать соединение

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

          void CreateTimer(void);    // Создать таймер
          void DeleteTimer(void);    // Удалить таймер
          BOOL CheckSendTimer(void); // Проверить, можно ли передавать,
                                     // и запустить таймер, если нельзя
          int Setup(char *title);    // Функция настройки блока
          void SaveText(void);       // Сохранить параметры
          void LoadText(char *text); // Загрузить параметры

          // Конструктор класса
          TNetSendRcvData(int mode)
            { ConnId=-1; // Нет соединения
              Connected=DataWaiting=FALSE;

              LimitSpeed=WaitingForTimer=FALSE;Timer=NULL;Delay=100;
              ChannelName=NULL;
              Mode=mode; // Режим передается в параметре конструктора
            };
          // Деструктор класса
          ~TNetSendRcvData()
            { Disconnect(); // Разорвать соединение

              DeleteTimer(); // Удалить таймер
              rdsFree(ChannelName); // Освободить строку имени канала
            };
      };
      //=========================================

Поля LimitSpeed и Delay – настроечные параметры блока. Логическое поле LimitSpeed будет содержать TRUE, если интервал между передачами данных на сервер должен быть ограничен, и FALSE, если блок, как и прежде, должен работать без ограничений. Поле Delay содержит минимально разрешенный интервал в миллисекундах между передачами данных.

Остальные добавленные поля нужны для создания задержки в передаче данных. В поле LastSendTime при каждой передаче будет записываться время в миллисекундах, прошедшее с момента старта операционной системы (его можно получить при помощи функции Windows API GetTickCount). В поле Timer будет находиться идентификатор таймера, который будет отвечать за задержку передачи. Если вход блока изменился, но с момента последней передачи (LastSendTime) прошло менее Delay миллисекунд, этот таймер будет запускаться, и одновременно с его запуском будет взводиться логический флаг WaitingForTimer. Когда таймер сработает, значение входа блока будет передано на сервер и флаг WaitingForTimer будет сброшен. Если до момента срабатывания таймера, то есть при взведенном WaitingForTimer, вход блока изменится еще раз, данные передаваться не будут – они отправятся на сервер только тогда, когда таймер сработает.

Три новых функции, добавленные в класс, отвечают за работу с таймером: функция CreateTimer будет создавать таймер, если в настройках блока разрешено ограничение интервала передачи, функция DeleteTimer – уничтожать созданный таймер, функция CheckSendTimer – проверять, можно ли передать данные прямо сейчас (при этом она вернет TRUE) или нужно ждать заданный интервал времени (при этом она запустит таймер и вернет FALSE). В конструктор и деструктор класса тоже внесены изменения: в конструкторе новым полям присваиваются начальные значения, а в деструктор добавлен вызов функции уничтожения созданного таймера.

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

      // Создать таймер
      void TNetSendRcvData::CreateTimer(void)
      {
        if(Mode!=NETSRMODE_SENDER || // Приемнику таймер не нужен
           (!LimitSpeed) )           // Интервал передачи не ограничивается
          { DeleteTimer(); // Удаляем таймер, если он создан
            return;
          }
        if(Timer) // Таймер уже создан
          return;
        // Создаем таймер
        Timer=rdsSetBlockTimer(
                  NULL, // Создается новый таймер
                  0,    // Задержка задается при запуске
                  RDS_TIMERM_STOP | RDS_TIMERS_TIMER, // Однократный
                  FALSE);// Создается остановленным
      }
      //=========================================

      // Удалить таймер
      void TNetSendRcvData::DeleteTimer(void)
      {
        if(Timer) // Таймер есть
          { rdsDeleteBlockTimer(Timer);
            Timer=NULL;
          }
        WaitingForTimer=FALSE; // Сбрасываем флаг ожидания
      }
      //=========================================

В функции создания таймера CreateTimer сначала проверяется, нужен ли вообще этому блоку таймер. Если объект этого класса принадлежит блоку-приемнику (поле Mode не равно константе NETSRMODE_SENDER) или если ограничение интервала передачи выключено (поле LimitSpeed имеет значение FALSE), то таймер блоку не нужен: если он есть, он уничтожается вызовом DeleteTimer и работа функции завершается. Если же таймер блоку необходим, мы проверяем, нет ли уже идентификатора таймера в поле Timer. Если его значение – не NULL, значит, таймер уже был создан, и создавать его повторно не нужно. В противном случае вызовом сервисной функции RDS rdsSetBlockTimer создается новый таймер, который будет срабатывать однократно (флаг RDS_TIMERM_STOP) и, при срабатывании, вызывать модель блока в режиме RDS_BFM_TIMER (флаг RDS_TIMERS_TIMER).

Функция уничтожения таймера DeleteTimer удаляет созданный таймер вызовом rdsDeleteBlockTimer и присваивает полю Timer значение NULL, которое будет указывать на то, что таймер не создан. Кроме того, она сбрасывает логический флаг WaitingForTimer, поскольку теперь мы не ждем срабатывания таймера, и поступившие на вход блока данные нужно будет передавать на сервер немедленно.

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

      // Проверить, можно ли передать данные немедленно, и запустить
      // таймер, если нельзя
      BOOL TNetSendRcvData::CheckSendTimer(void)
      { DWORD interval;

        if(!Connected) // Нет связи с сервером
          return TRUE; // Разрешаем отправку, чтобы попытка отправки
                       // была зафиксирована в функции SendValue

        if(!LimitSpeed) // Интервал не ограничивается
          return TRUE;

        if(WaitingForTimer) // Уже запустили таймер и ждем срабатывания
          return FALSE;

        // Интервал передачи ограничен, таймер сейчас выключен

        interval=GetTickCount()-LastSendTime; // Время с прошлой отправки
        if(interval>=Delay) // Прошло много времени – можно передавать
          return TRUE;
        // С прошлой отправки прошло менее Delay мс
        // Нужно подождать (Delay-interval)
        WaitingForTimer=TRUE; // Взводим флаг ожидания таймера
        rdsRestartBlockTimer(Timer,Delay-interval); // Запускаем таймер
        return FALSE; // Передавать сейчас нельзя – ждем таймера
      }
      //=========================================

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

Если в настройках блока не ограничен интервал передачи данных (LimitSpeed равно FALSE), функция возвращает TRUE – данные нужно передать немедленно. Если таймер уже запущен, и мы ждем его срабатывания, чтобы передать данные (WaitingForTimer равно TRUE), функция возвращает FALSE – данные передавать нельзя. Если же ни одна из этих проверок не выполнилась, значит, в настройках блока задано ограничение интервала передачи, и таймер не запущен. В этом случае необходимо проверить, сколько времени прошло с момента последней передачи, и либо разрешить немедленную передачу данных, если прошло не менее Delay миллисекунд, либо запретить передачу и запустить таймер.

В переменную interval записывается интервал времени в миллисекундах, прошедший с момента последней передачи данных на сервер. Он вычисляется как разность между текущим значением времени, получаемым при помощи функции GetTickCount, и запомненным временем передачи LastSendTime. Если этот интервал больше или равен параметру Delay, функция возвращает TRUE – данные можно передавать немедленно. В противном случае нужно дождаться конца интервала: необходимо подождать еще Delay–interval миллисекунд. Таймер Timer программируется на это значение задержки вызовом rdsRestartBlockTimer, взводится флаг WaitingForTimer, и функция возвращает FALSE, запрещая передачу – данные будут переданы тогда, когда таймер сработает.

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

      // Передать данные
      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;

        // Запоминаем время последней передачи
        LastSendTime=GetTickCount();
      }
      //=========================================

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

      // Установка соединения
      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);

        // Создаем или уничтожаем таймер
        CreateTimer();
      }
      //=========================================

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

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

        if(Mode==NETSRMODE_SENDER) // Передатчик
          { // Создаем новую секцию
            rdsSetObjectStr(ini,RDS_HINI_CREATESECTION,0,"Timer");
            // Записываем параметры
            rdsINIWriteInt(ini,"On",LimitSpeed);
            rdsINIWriteInt(ini,"Delay",Delay);
          }
        // Сохраняем текст, сформированный объектом, как параметры блока
        rdsCommandObject(ini,RDS_HINI_SAVEBLOCKTEXT);
        // Удаляем вспомогательный объект
        rdsDeleteObject(ini);
      }
      //=========================================

Значения параметров LimitSpeed и Delay записываются только для блоков-передатчиков, поскольку в приемниках они не используются. Функция загрузки тоже меняется соответствующим образом:

      // Загрузка параметров блока
      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")) // Секция есть
          { // …
            // … без изменений …
            // …
          }

        if(Mode==NETSRMODE_SENDER && // Передатчик
           rdsINIOpenSection(ini,"Timer")) // Есть секция "[Timer]"
          { LimitSpeed=rdsINIReadInt(ini,"On",LimitSpeed)!=0;
            Delay=rdsINIReadInt(ini,"Delay",Delay);
          }
        // Удаляем вспомогательный объект
        rdsDeleteObject(ini);
        // Поскольку имя канала могло измениться, соединяемся с
        // сервером заново
        Disconnect(); // Разрываем старое соединение
        Connect();    // Создаем новое
      }
      //=========================================

Здесь мы читаем параметры из секции «[Timer]» только в том случае, если данный блок – передатчик, и такая секция есть в тексте параметров блока.

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

      // Настройка параметров блока
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      int TNetSendRcvData::Setup(char *title)
      { RDS_HOBJECT win; // Идентификатор вспомогательного объекта
        BOOL ok;         // Пользователь нажал "OK"
        // Создание окна
        win=rdsFORMCreate(FALSE,-1,-1,title);
        // Поле ввода имени канала
        rdsFORMAddEdit(win,0,1,RDS_FORMCTRL_EDIT,
            "Имя канала:",200);
        rdsSetObjectStr(win,1,RDS_FORMVAL_VALUE,ChannelName);

        if(Mode==NETSRMODE_SENDER)
          { // Для передатчика – ввод интервала
            rdsFORMAddEdit(win,0,2,
                RDS_FORMCTRL_EDIT | RDS_FORMFLAG_CHECK,
                "Интервал передачи, мс:",80);
            // Значение интервала
            rdsSetObjectInt(win,2,RDS_FORMVAL_VALUE,Delay);
            // Разрешающий флаг поля
            rdsSetObjectInt(win,2,RDS_FORMVAL_CHECK,LimitSpeed);
          }

        // Открытие окна
        ok=rdsFORMShowModalEx(win,NULL);
        if(ok)
          { // Пользователь нажал OK
            char *NewName=rdsGetObjectStr(win,1,RDS_FORMVAL_VALUE);
            if(ChannelName==NULL || strcmp(NewName,ChannelName)!=0)
              { // Имя канала изменилось – запоминаем новое
                rdsFree(ChannelName);
                ChannelName=rdsDynStrCopy(NewName);
              }
            // Флаг ограничения интервала и сам интервал
            LimitSpeed=rdsGetObjectInt(win,2,RDS_FORMVAL_CHECK)!=0;
            Delay=rdsGetObjectInt(win,2,RDS_FORMVAL_VALUE);
            // Устанавливаем соединение с сервером заново и создаем
            // или удаляем таймер, если необходимо
            Disconnect();
            Connect();
          }
        // Уничтожаем вспомогательный объект-окно
        rdsDeleteObject(win);
        // Возвращаемое значение
        return ok?RDS_BFR_MODIFIED:RDS_BFR_DONE;
      }
      //=========================================

Рис. 111. Настройка передатчика

В этой функции, как и во многих других примерах, окно для ввода параметров создается с помощью вспомогательного объекта RDS. Функция будет вызываться и для блока-передатчика, и для блока-приемника, поэтому внутри нее анализируется поле Mode: если в нем находится константа NETSRMODE_RECEIVER, в окне будет только поле ввода для имени канала, если NETSRMODE_SENDER – дополнительное поле для ввода интервала передачи Delay, совмещенное с разрешающим флагом LimitSpeed (рис. 111). Если пользователь закроет окно кнопкой «OK», строка имени канала ChannelName будет освобождена вызовом rdsFree, и новое имя будет скопировано из объекта в новую динамическую строку, созданную вызовом rdsDynStrCopy. Разрешающий флаг поля ввода интервала и значение этого поля будут записаны в поля класса LimitSpeed и Delay соответственно. Это будет сделано не только для блока-передатчика, но и для блока-приемника, которому эти поля не нужны – значение Mode здесь не проверяется. Хотя поля ввода с идентификатором 2, из которого читаются эти значения, в окне настроек приемника не будет, ничего страшного не случится: при попытке чтения данных из несуществующего поля будут возвращены нули.

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

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

            // Срабатывание таймера
            case RDS_BFM_TIMER:
              // Сбрасываем флаг ожидания таймера
              data->WaitingForTimer=FALSE;
              // Передаем значение входа блока
              data->SendValue(x);
              break;

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

Если таймер сработал, значит, интервал ожидания кончился, и значение входа блока необходимо передать на сервер. Для этого вызывается функция SendValue, а флаг ожидания таймера WaitingForTimer сбрасывается – теперь его можно запустить снова, если значение на входе изменится слишком быстро. В реакцию на такт расчета добавлен вызов функции проверки CheckSendTimer. Если с момента последней передачи прошло слишком мало времени, она запустит таймер и вернет FALSE – функция SendValue при этом вызвана не будет. Если интервал передачи отключен, или уже прошло достаточно времени, функция вернет TRUE, и значение входа блока будет отправлено на сервер немедленно.

Для проверки работы измененной модели соберем две схемы, подобных показанным на рис. 112, на разных машинах (сервером можно сделать одну из них, или запустить отдельный сервер на третьей). В настройках блока-передатчика зададим минимальный интервал передачи 250 мс.

(а)

(б)

Рис. 112. Передача с ограничением интервала: передающая (а) и принимающая (б) схемы

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

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

Создадим два блока для передачи и приема массива вещественных чисел произвольного размера. И на передающей, и на принимающей стороне можно будет подключать связи к отдельным элементам этого массива, таким образом один блок сможет передать в другую схему сразу несколько чисел за один раз. Мы не будем создавать эти блоки «с нуля», вместо этого изменим все тот же класс TNetSendRcvData, добавив в него функции для передачи и приема массива, и сделаем две новых функции модели, которые будут пользоваться этим классом.

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

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

          // … без изменений …

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

          void SendArray(void *input); // Передать массив в канал
          BOOL ReceiveArray(RDS_NETRECEIVEDDATA *rcv, // Реакция на
                            void *output);  // пришедшие данные
          int Setup(char *title); // Функция настройки блока

          // … без изменений …

      };
      //=========================================

В функции SendArray и ReceiveArray будут передаваться указатели на вход блока input и выход output соответственно. Выход и вход должны быть массивами чисел типа double (строка типа «MD»), указатели на них, в данном случае, передаются как универсальные указатели void*. В ReceiveArray, как и в ReceiveValue, будет также передаваться указатель rcv на структуру с принятыми данными.

Функция SendArray будет очень похожа на SendValue, только она будет отправлять не одно вещественное число, а несколько чисел сразу. Количество передаваемых чисел будет определяться текущим размером массива. Поскольку функция rdsNetBroadcastData одновременно с блоком двоичных данных может передавать целое число и строку, размер массива будет передаваться в этом целом числе.

      // Передать массив
      void TNetSendRcvData::SendArray(void *input)
      { int N;

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

        // Связь есть – определяем размер массива input
        N=RDS_ARRAYEXISTS(input)?
            (RDS_ARRAYROWS(input)*RDS_ARRAYCOLS(input)):0;
        if(N==0) // Массив пуст – передавать нечего
          return;

        // Передаем N чисел double всем блокам канала
        rdsNetBroadcastData(
            ConnId, // Соединение
            RDS_NETSEND_UPDATE|RDS_NETSEND_UDP, // Флаги
            N,      // Целое число – размер массива
            NULL,   // Строка не передается
            RDS_ARRAYDATA(input), // Начало данных массива
            N*sizeof(double));    // Размер массива в байтах
        // Сбрасываем флаг ожидания – мы только что передали данные
        DataWaiting=FALSE;
        // Запоминаем время передачи
        LastSendTime=GetTickCount();
      }
      //=========================================

Сначала мы, как и в SendValue, проверяем наличие связи с сервером, и, если ее нет, взводим флаг DataWaiting и завершаем функцию. Если связь есть, мы определяем размер массива. Для проверки массива на пустоту используется макрос RDS_ARRAYEXISTS, для определения числа строк и столбцов – RDS_ARRAYROWS и RDS_ARRAYCOLS (работа с массивами в RDS и использование этих макросов подробно рассмотрены в §2.5.3). Хотя массив в RDS представляет собой матрицу из одной строки, мы не можем гарантировать, что эта модель будет подключена к блоку, вход которого является именно массивом, а не матрицей вещественных чисел – оба они имеют строку типа «MD». На всякий случай, мы вычисляем число элементов массива N как произведение числа строк на число столбцов, в этом случае размер будет определен правильно, даже если вход будет матрицей.

Если в массиве 0 элементов, функция завершается, в противном случае вызывается rdsNetBroadcastData, передающая содержимое массива как блок двоичных данных и его размер как целое число. Указатель на самый первый элемент массива мы получаем с помощью макроса RDS_ARRAYDATA, а размер массива в байтах вычисляется как произведение числа элементов N на размер элемента sizeof(double). После передачи, как и в функции SendValue, сбрасывается флаг DataWaiting и запоминается время последней передачи LastSendTime.

Функция ReceiveArray тоже будет очень похожа на ReceiveValue:

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

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

        // Вычисляем число элементов в принятом массиве
        N=rcv->BufferSize/sizeof(double);
        // Вычисленное число элементов должно совпасть с переданным
        if(N!=rcv->Id)
          return FALSE;
        // Принято N чисел double – отводим массив под них
        if(!rdsResizeVarArray(output,1,N,FALSE,NULL))
          return FALSE;
        // Копируем принятые данные в отведенный массив выхода
        memcpy(RDS_ARRAYDATA(output),rcv->Buffer,rcv->BufferSize);
        return TRUE;
      }
      //=========================================

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

Если принятые данные прошли эту проверку, вычисляется число элементов в принятом массиве N: оно равно отношению размеру всего массива в байтах (rcv->BufferSize) к размеру одного элемента (sizeof(double)) – мы уже выяснили, что результат этого деления будет целым числом. Затем N сравнивается с принятым целым числом rcv->Id: если они не равны, значит, данные отправлены не блоком-передатчиком массива через функцию SendArray, в этом случае мы возвращаем FALSE – принятые данные не подходят нашему блоку. Если же два этих числа равны, мы даем выходу блока размерность 1×N при помощи функции rdsResizeVarArray, после чего копируем принятые данные в полученный массив функцией memcpy из стандартной библиотеки языка C. Для получения указателя на первый элемент массива, то есть начального адреса области, куда копируются данные, используется макрос RDS_ARRAYDATA. После копирования функция ReceiveArray возвращает TRUE – данные приняты успешно.

Теперь нужно написать модели блоков, которые будут передавать и принимать массивы. Они будут практически точными копиями моделей NetSend и NetReceive, за исключением того, что в них будут использоваться другие макроопределения и строка для проверки типа переменных (входы и выходы будут массивами, а не числами, как раньше), у них будут другие заголовки окон настройки, а везде, где вызывались функции SendValue и ReceiveValue, будут вызываться SendArray и ReceiveArray соответственно.

Структура переменных блока-передатчика массива будет следующей:

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

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

      // Блок-передатчик массива
      extern "C" __declspec(dllexport)
        int RDSCALL NetSendArray(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 pX ((void **)(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,"{SSMD}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

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

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

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

            // Срабатывание таймера
            case RDS_BFM_TIMER:
              // Сбрасываем флаг ожидания таймера
              data->WaitingForTimer=FALSE;
              // Передаем значение входа блока
              data->SendArray(pX);
              break;

            // Такт расчета
            case RDS_BFM_MODEL:
              if(data->CheckSendTimer()) // Можно передавать немедленно
                data->SendArray(pX);	// Передаем значение входа
              break;

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

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

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

Блоку-приемнику потребуется следующая структура переменных:

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

В параметрах этого блока тоже нужно будет задать запуск по сигналу. Его модель будет мало отличаться от NetReceive:

      // Блок-приемник массива
      extern "C" __declspec(dllexport)
        int RDSCALL NetReceiveArray(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 pY ((void **)(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,"{SSMD}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

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

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

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

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

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

(а)

(б)

Рис. 113. Передача массива по сети: передающая (а) и принимающая (б) схемы

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

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