Руководство программиста
Глава 2. Создание моделей блоков
§2.15. Обмен данными по сети
Рассматриваются способы обмена данными между схемами, работающими на разных машинах. Приводятся примеры моделей блоков, осуществляющих такой обмен.
§2.15.1. Общие принципы обмена данными по сети в RDS
Рассматривается механизм обмена данными по сети между блоками RDS. Описываются основные сервисные функции и структуры и способ их использования.
Поскольку модели блоков в RDS являются просто функциями в составе динамически подключаемой библиотеки (DLL), им доступны все функции Windows API, и они могут самостоятельно создавать сетевые соединения и обмениваться данными друг с другом, как это делают обычные программы. Тем не менее, в RDS включен специализированный механизм сетевого обмена, облегчающий работу программистам, которые не хотят писать сетевые функции самостоятельно. Конечно, этот механизм менее гибок, чем универсальные функции API, но свою задачу он выполняет: с его помощью блок может передать произвольные данные одному или нескольким блокам в схемах, запущенных на других машинах.
В RDS используется клиент-серверная модель обмена данными. Одна из машин в сети (точнее, одна из запущенных на ней копий «rds.exe») должна стать сервером, через который будет проходить весь поток данных. Все остальные будут передавать ей данные и получать их от нее. RDS может работать в режиме выделенного сервера, то есть заниматься только организацией обмена данными между клиентами (для этого нужно запустить «rds.exe» с параметром командной строки «/server default» или «/server номер_порта») или обслуживать работу какой-либо схемы, выполняя при этом еще и функции сервера. Если в сети есть свободная машина, можно использовать первый вариант, если нет – второй. Технически, первый вариант предпочтительнее, поскольку во втором варианте критические ошибки в моделях блоков загруженной схемы могут привести к аварийному завершению RDS, при этом вся сеть лишится сервера.
Для того, чтобы блоки схемы могли обмениваться данными с другими схемами, в сетевых настройках RDS (рис. 108) должен быть разрешен сетевой обмен – в противном случае все попытки установить соединение с другими машинами или запустить сервер будут блокироваться. Разумеется, все это относится только к встроенному механизму сетевого обмена RDS: если модель блока установит соединение с помощью функций Windows API, никакие настройки RDS не смогут ей помешать. В настройках также обычно указывается имя или IP-адрес и порт сервера по умолчанию. Эти параметры будут использованы клиентом при создании соединения с сервером, если адрес сервера и порт не будут указаны моделью блока. Номер порта по умолчанию также используется при запуске RDS в режиме выделенного сервера с параметром командной строки «/server default». Для обмена данными между клиентом и сервером используются протоколы TCP и UDP: служебные данные всегда передаются по протоколу TCP, а данные, которыми обмениваются блоки, могут передаваться как по TCP, так и по UDP, в зависимости от настроек RDS (использование UDP должно быть разрешено) и флагов при вызове сервисных функций.
Рис. 108. Окно настроек RDS: вкладка «»
Обмен данными между блоками происходит через так называемые каналы передачи данных сервера, каждый из которых имеет текстовое имя, уникальное на данном сервере. Когда блок создает сетевое соединение с сервером при помощи сервисной функции RDS, он указывает IP-адрес и порт сервера (или разрешает использовать значения по умолчанию из настроек RDS), имя канала, с которым нужно установить соединение, а также хочет ли он получать данные из этого канала, или будет только передавать их. После этого блок может передавать в этот канал двоичные данные произвольного размера. Если при передаче данных не указана машина и блок, которые должны их получить, то сервер, приняв эти данные, отправляет их на все машины, блоки которых сообщили о желании получать данные из этого канала. Копии RDS, работающие на этих машинах, принимают данные от сервера и вызывают модели блоков-получателей, передавая им полученные данные. Если же при передаче данных указан конкретный блок-получатель на конкретной машине, сервер передаст данные только на эту машину, и на ней будет вызвана модель только одного блока.
Для установки соединения с каким-либо каналом сервера модель блока должна вызвать сервисную функцию rdsNetConnect:
int RDSCALL rdsNetConnectA( RDSCSTR Host, // IP-адрес или имя сервера, или NULL для // сервера по умолчанию (UTF8) int Port, // Порт сервера или -1 для порта по умолчанию RDSCSTR Channel, // Имя канала передачи данных сервера (UTF8) BOOL Receive); // Будет ли блок получать данные int RDSCALL rdsNetConnectW( RDSWCSTR Host, // IP-адрес или имя сервера, или NULL для // сервера по умолчанию (UTF16) int Port, // Порт сервера или -1 для порта по умолчанию RDSWCSTR Channel,// Имя канала передачи данных сервера (UTF16) BOOL Receive); // Будет ли блок получать данные // Функция-псевдоним int RDSCALL rdsNetConnect( RDSXCSTR Host, // IP-адрес или имя сервера, или NULL для // сервера по умолчанию (кодировка по умолчанию) int Port, // Порт сервера или -1 для порта по умолчанию RDSXCSTR Channel,// Имя канала передачи данных сервера (кодировка по умолчанию) BOOL Receive); // Будет ли блок получать данные
В параметре Host передается строка с IP-адресом сервера (четыре числа, разделенные точками, например «192.168.1.1») или его именем. Если вместо строки будет передано значение NULL, адрес сервера будет взят из сетевых настроек RDS (см. рис. 108). В параметре Port передается номер порта, используемого сервером RDS для обмена данными, при передаче значения −1 номер порта будет взят из сетевых настроек. Лучше всего использовать именно значения по умолчанию, то есть NULL и −1 – в этом случае при переносе сервера на другую машину достаточно будет изменить сетевые настройки RDS на каждой клиентской машине. Если же адрес сервера и номер порта будут храниться в настройках блока, при изменении адреса или порта сервера потребуется изменить настройки всех блоков на всех клиентских машинах. Тем не менее, если схеме необходимо устанавливать соединения с двумя разными серверами, адрес и порт одного из них придется хранить в настройках блоков.
В параметре Channel передается имя канала передачи данных, с которым устанавливается соединение. Имя канала может быть произвольной строкой символов. В параметрах сервера каналы никак не настраиваются, они создаются автоматически при первом запросе на подключение к каналу с указанным именем. Типа данных у канала тоже нет, блоки передают в него двоичные данные произвольного размера, и сервер пересылает их получателям без какой-либо обработки. Чаще всего имя канала хранят в настройках блока, чтобы пользователь мог ввести его самостоятельно, указывая, какие группы блоков связываются друг с другом через этот канал. Для обеспечения уникальности можно добавлять к имени, введенному пользователем или жестко указанному в программе, какой-нибудь префикс – например, имя библиотеки, в которой находится модель блока. Логический параметр Receive определяет, должен ли блок, вызвавший функцию, получать данные из канала (TRUE) или он будет только передавать их (FALSE). Если в модели блока не предусмотрен прием данных по сети, а в параметре Receive передано значение TRUE, ничего страшного не случится – при поступлении данных модель блока будет вызвана, но, поскольку в ней нет соответствующей реакции, принятые данные будут проигнорированы. Тем не менее, в блоках, которые только передают данные, лучше указывать в параметре Receive значение FALSE: это не только предотвратит потери процессорного времени на лишние вызовы модели блока, но и снизит нагрузку на сеть. Если в схеме на клиентской машине нет ни одного блока, получающего данные из канала, сервер вообще не будет передавать данные этого канала на эту машину.
Функция rdsNetConnect возвращает целое число, являющееся уникальным идентификатором созданного соединения. Это число в дальнейшем используется в сервисных функциях передачи данных и в реакциях блока на получение данных из сети. Если сетевое соединение установить невозможно (например, если обмен данными по сети запрещен в настройках RDS), функция вернет −1.
Если нужно не подключиться к другому серверу, а включить функции сервера в RDS на данной машине, необходимо вызвать функцию rdsNetServer. Она одновременно запускает сервер RDS, если он еще не запущен, и устанавливает соединение с указанным каналом в нем:
int RDSCALL rdsNetServerA( int Port, // Порт сервера или -1 для порта по умолчанию RDSCSTR Channel, // Имя канала передачи данных сервера (UTF8) BOOL Receive); // Будет ли блок получать данные int RDSCALL rdsNetServerW( int Port, // Порт сервера или -1 для порта по умолчанию RDSWCSTR Channel, // Имя канала передачи данных сервера (UTF16) BOOL Receive); // Будет ли блок получать данные // Функция-псевдоним int RDSCALL rdsNetServer( int Port, // Порт сервера или -1 для порта по умолчанию RDSXCSTR Channel, // Имя канала передачи данных сервера (кодировка по умолчанию) BOOL Receive); // Будет ли блок получать данные
Параметры этой функции совпадают с параметрами rdsNetConnect, за исключением отсутствующего параметра Host: поскольку сервер запускается на той же машине, на которой загружена схема с вызвавшим эту функцию блоком, адрес сервера, очевидно, указывать не нужно. Для запуска сервера достаточно одного блока, вызвавшего эту функцию, все остальные блоки схемы могут использовать как rdsNetConnect, так и rdsNetServer. Функция rdsNetServer тоже возвращает уникальный номер соединения, которым можно пользоваться при приеме и передаче данных.
После успешного соединении с сервером (не важно, на этой же машине, или на другой) модель блока вызывается в режиме RDS_BFM_NETCONNECT, при этом в параметре ExtParam передается указатель на структуру RDS_NETCONNDATA:
typedef struct { RDSINT32 ConnId; // Уникальный идентификатор соединения RDSCSTR HostA; // Сервер (только для клиента, UTF8) RDSWCSTR HostW; // Сервер (только для клиента, UTF16) // RDSXCSTR Host; // Сервер (только для клиента, поле-псевдоним) RDSINT32 Port; // Порт сервера RDSCSTR ChannelA; // Имя канала (UTF8) RDSCSTR ChannelW; // Имя канала (UTF16) // RDSXCSTR Channel; // Имя канала (поле-псевдоним) BOOL ByServer; // Соединение разорвано сервером (только при // разрыве соединения) } RDS_NETCONNDATA; typedef RDS_NETCONNDATA *RDS_PNETCONNDATA;
Поля этой структуры повторяют параметры функций rdsNetConnect и rdsNetServer (кроме поля ByServer, которое в режиме вызова RDS_BFM_NETCONNECT не используется). Этот вызов информирует блок о том, что соединение с сервером успешно установлено, и теперь можно передавать данные в указанный канал, используя уникальный номер соединения ConnId.
Для передачи данных всем блокам канала используется сервисная функция rdsNetBroadcastData, возвращающая TRUE при успешной передаче:
BOOL RDSCALL rdsNetBroadcastDataA( int ConnId, // Идентификатор соединения DWORD Flags, // Флаги RDS_NETSEND_* int id, // Передаваемое целое число RDSCSTR string, // Передаваемая строка или NULL (UTF8) LPVOID buf, // Указатель на передаваемый блок данных // (или NULL) DWORD bufsize); // Размер передаваемого блока данных BOOL RDSCALL rdsNetBroadcastDataW( int ConnId, // Идентификатор соединения DWORD Flags, // Флаги RDS_NETSEND_* int id, // Передаваемое целое число RDSWCSTR string,// Передаваемая строка или NULL (UTF16) LPVOID buf, // Указатель на передаваемый блок данных // (или NULL) DWORD bufsize); // Размер передаваемого блока данных // Функция-псевдоним BOOL RDSCALL rdsNetBroadcastData( int ConnId, // Идентификатор соединения DWORD Flags, // Флаги RDS_NETSEND_* int id, // Передаваемое целое число RDSXCSTR string,// Передаваемая строка или NULL (кодировка по умолчанию) LPVOID buf, // Указатель на передаваемый блок данных // (или NULL) DWORD bufsize); // Размер передаваемого блока данных
В параметре ConnId передается уникальный номер соединения с каналом сервера, возвращенный функцией rdsNetConnect или rdsNetServer при создании этого соединения. Функция позволяет одновременно передать в канал целое число id, строку текста string и блок двоичных данных buf произвольного размера bufsize. Целое число передается всегда, а строку и блок данных можно не передавать – в этом случае в соответствующем параметре следует указать NULL. Разбиение передаваемых данных на три части сделано для удобства программиста: в принципе, строки и числа тоже можно передавать в виде двоичных данных. Однако, во многих случаях приходится передавать данные сложного формата, и тут отдельная передача целых чисел и строк облегчает жизнь программисту, ликвидируя необходимость разбирать принятые данные. Например, число id можно использовать в качестве типа передаваемых двоичных данных или номера фрагмента, если данные передаются последовательными блоками; строка string может содержать имя файла, содержимое которого передается в двоичном блоке, и т.д.
Способом передачи данных серверу управляют битовые флаги в параметре Flags. Можно использовать сочетание следующих флагов:
- RDS_NETSEND_UDP – использовать для передачи данных протокол UDP, если его использование разрешено в настройках RDS. По умолчанию все данные передаются по протоколу TCP. С использованием протокола UDP данные, как правило, передаются быстрее, но не всегда настройки и политика сети позволяют использовать этот протокол. Если при использовании этого флага данные по каким-либо причинам не могут быть переданы по протоколу UDP, они будут переданы по TCP – никаких дополнительных действий для этого не потребуется.
- RDS_NETSEND_UPDATE – можно заменять еще не отправленные данные новыми. Для передачи данных на сервер в RDS используется очередь: при вызове rdsNetBroadcastData данные ставятся в ее конец, а данные, находящиеся в ее начале, отправляются на сервер с той скоростью, которую может обеспечить сеть. Если блоки будут передавать данные быстрее, чем они будут уходить в сеть, очередь будет расти. В результате этого в некоторых случаях в очереди могут оказаться устаревшие данные, которые можно было бы и не передавать. Допустим, например, что в одной из схем данные в канал передачи поступают через какой-либо блок, берущий эти данные со своего вещественного входа. Если значение входа изменится, блок поставит это новое значение в очередь на передачу, вызвав rdsNetBroadcastData. Если затем, через очень небольшой промежуток времени, значение снова изменится, блок снова поставит в очередь новое значение. Если сеть перегружена и очередь достаточно длинная, в очереди окажется два числа для передачи в один и тот же канал: устаревшее (поставленное в очередь первым) и актуальное (поставленное вторым). Если при передаче использовать флаг RDS_NETSEND_UPDATE, устаревшее значение будет автоматически выбрасываться из очереди при поступлении новых данных для этого канала, таким образом, нагрузка на сеть несколько снизится (устаревшими будут считаться ранее поставленные в очередь данные с тем же значением Id). Без этого флага передаются все поставленные в очередь данные – чаще всего, повторная передача данных в канал не означает, что предыдущая устарела, обычно это просто новая порция данных, которые должен обработать принимающий блок.
- RDS_NETSEND_NOWAIT – не ждать ответа от сервера, передавать следующие данные немедленно. По умолчанию после передачи данных на сервер клиент ждет от него сигнала о получении этих данных, и, пока он не придет, не передает следующую порцию данных из очереди. Это позволяет несколько снизить нагрузку на сеть, но уменьшает скорость передачи почти в два раза – к задержке при передаче данных на сервер добавляется задержка ответа от сервера. Если при передаче использовать этот флаг, для передаваемой порции данных сервер не будет отправлять подтверждение приема, а клиент не будет ждать этого подтверждения.
- RDS_NETSEND_SERVREPLY – получив данные, сервер должен сообщить об этом передавшему их блоку (нельзя использовать вместе с флагом RDS_NETSEND_NOWAIT). Если этот флаг включен, то, как только клиент получит подтверждение приема данных сервером, модель блока, передавшего данные, будет вызвана в режиме RDS_BFM_NETDATAACCEPTED. Это позволит, при необходимости, организовать в модели блока собственную очередь передачи данных, отправляя новую порцию на сервер только после подтверждения приема предыдущей.
При вызове модели в режиме RDS_BFM_NETDATAACCEPTED в параметре ExtParam передается указатель на структуру RDS_NETACCEPTDATA:
typedef struct { RDSINT32 ConnId; // Идентификатор соединения RDSCSTR HostA; // Адрес сервера (только для клиента, UTF8) RDSWCSTR HostW; // Адрес сервера (только для клиента, UTF16) // RDSXCSTR Host; // Адрес сервера (только для клиента, поле-псевдоним) RDSINT32 Port; // Порт сервера RDSCSTR ChannelA; // Канал передачи (UTF8) RDSWCSTR ChannelW; // Канал передачи (UTF16) // RDSXCSTR Channel; // Канал передачи (поле-псевдоним) RDSINT32 Id; // Целое число (id) из принятого блока } RDS_NETACCEPTDATA; typedef RDS_NETACCEPTDATA *RDS_PNETACCEPTDATA;
В полях ConnId, Host (HostA, HostW), Port и Channel (ChannelA, ChannelW), как и в структуре RDS_NETCONNDATA, содержатся идентификатор соединения, адрес сервера, порт и имя канала соответственно. В поле Id записано целое число, которое было передано в параметре id при вызове rdsNetBroadcastData. Таким образом, реагируя на вызов в режиме RDS_BFM_NETDATAACCEPTED, модель блока сможет понять, прием какой именно порции данных подтвердил сервер.
Получив данные для какого-либо канала передачи, сервер рассылает их всем подключенным к нему клиентам, блоки которых создали соединение с этим каналом и изъявили желание получать данные (параметр Receive при вызове rdsNetConnect или был равен TRUE). Приняв данные, клиент вызывает модели всех этих блоков в режиме RDS_BFM_NETDATARECEIVED, передавая в параметре ExtParam указатель на структуру RDS_NETRECEIVEDDATA:
typedef struct { // Параметры соединения RDSINT32 ConnId; // Идентификатор соединения RDSCSTR HostA; // Адрес сервера (только для клиента, UTF8) RDSWCSTR HostW; // Адрес сервера (только для клиента, UTF16) // RDSXCSTR Host; // Адрес сервера (только для клиента, поле-псевдоним) RDSINT32 Port; // Порт сервера RDSCSTR ChannelA; // Имя канала передачи данных (UTF8) RDSWCSTR ChannelW; // Имя канала передачи данных (UTF16) // RDSXCSTR Channel; // Имя канала передачи данных (поле-псевдоним) // Принятые данные RDSINT32 Id; // Принятое целое число RDSCSTR StrA; // Принятая строка (UTF8) RDSWCSTR StrW; // Принятая строка (UTF16) // RDSXCSTR Str; // Принятая строка (поле-псевдоним) LPVOID Buffer; // Указатель на буфер с принятыми двоичными // данными или NULL DWORD BufferSize; // Размер принятого буфера // Идентификаторы отправителя RDS_NETSTATION SenderStation; // Идентификатор передавшей машины RDS_NETBLOCK SenderBlock; // Идентификатор передавшего блока } RDS_NETRECEIVEDDATA; typedef RDS_NETRECEIVEDDATA *RDS_PNETRECEIVEDDATA;
Первые поля структуры, как всегда, описывают параметры соединения, по которому пришли данные, для обработки которых вызвана модель. Если блок установил сразу несколько соединений, по этим параметрам можно понять, какое из них приняло данные. В поле Id содержится принятое целое число, переданное другим блоком в параметре id при вызове rdsNetBroadcastData. В поле Str – указатель на принятую строку во внутренней памяти RDS (ей соответствует параметр string в вызове rdsNetBroadcastData). Этот указатель никогда не будет равен NULL: если строка не передавалась, поле String будет указывать на пустую строку. Наконец, поле Buffer указывает на принятый блок двоичных данных (при этом в BufferSize записан размер этого блока), или содержит NULL, если двоичные данные не передавались. Таким образом, есть однозначное соответствие между параметрами функции rdsNetBroadcastData и полями структуры RDS_NETRECEIVEDDATA: то, что один блок передал в параметрах сервисной функции, другой блок получает в полях структуры при вызове модели в режиме RDS_BFM_NETDATARECEIVED.
Два последних поля структуры, SenderStation и SenderBlock, содержат уникальные идентификаторы машины и блока в схеме на этой машине, отправившего данные в канал. Эти поля могут использоваться для того, чтобы получивший данные блок мог передать ответ только блоку, пославшему эти данные, а не всем блокам, подключенным к каналу передачи. Для передачи данных единственному блоку используется сервисная функция rdsNetSendData:
BOOL RDSCALL rdsNetSendDataA( int ConnId, // Идентификатор соединения DWORD Flags, // Флаги RDS_NETSEND_* int id, // Передаваемое целое число RDSCSTR string, // Передаваемая строка или NULL (UTF8) LPVOID buf, // Указатель на передаваемый блок данных или NULL DWORD bufsize, // Размер передаваемого блока данных RDS_NETSTATION station, // Машина-получатель RDS_NETBLOCK block); // Блок-получатель BOOL RDSCALL rdsNetSendDataW( int ConnId, // Идентификатор соединения DWORD Flags, // Флаги RDS_NETSEND_* int id, // Передаваемое целое число RDSWCSTR string,// Передаваемая строка или NULL (UTF16) LPVOID buf, // Указатель на передаваемый блок данных или NULL DWORD bufsize, // Размер передаваемого блока данных RDS_NETSTATION station, // Машина-получатель RDS_NETBLOCK block); // Блок-получатель // Функция-псевдоним BOOL RDSCALL rdsNetSendData( int ConnId, // Идентификатор соединения DWORD Flags, // Флаги RDS_NETSEND_* int id, // Передаваемое целое число RDSXCSTR string,// Передаваемая строка или NULL (кодировка по умолчанию) LPVOID buf, // Указатель на передаваемый блок данных или NULL DWORD bufsize, // Размер передаваемого блока данных RDS_NETSTATION station, // Машина-получатель RDS_NETBLOCK block); // Блок-получатель
Первые шесть параметров функции в точности совпадают с параметрами rdsNetBroadcastData – они определяют передаваемые данные и соединение, через которое их нужно передать. Последние два параметра указывают машину-клиент, на которую нужно передать эти данные, и блок в схеме на этой машине, который должен их получить. Эти два идентификатора блок-отправитель данных может узнать, только приняв данные из структуры RDS_NETRECEIVEDDATA, поэтому функцию rdsNetSendData можно использовать только для ответа на переданные данные. Нет никакой возможности узнать идентификатор машины-клиента и блока, не приняв от них какие-либо данные. Обычно это и не требуется: если нужно передавать данные конкретному блоку конкретной схемы, лучше всего выделить ему отдельный канал передачи данных, в котором он будет единственным получателем.
Таким образом, вызывая функции rdsNetBroadcastData и rdsNetSendData и реагируя на вызовы в режиме RDS_BFM_NETDATARECEIVED, блоки на разных машинах могут обмениваться произвольными данными до тех пор, пока сетевое соединение не будет разорвано. Для завершения сетевого соединения модель блока должна вызвать сервисную функцию rdsNetCloseConnection, принимающую единственный параметр: уникальный идентификатор соединения, которое нужно закрыть. После этого, как только соединение будет разорвано, модель блока будет вызвана в режиме RDS_BFM_NETDISCONNECT, и в параметре ExtParam ей будет передан указатель на уже знакомую нам структуру RDS_NETCONNDATA, содержащую параметры конкретного разорванного соединения. В этом режиме модель вызывается не только при самостоятельном завершении соединения, но и при его разрыве по инициативе сервера, из-за отключения сети или по другим причинам. При разрыве соединения из-за отключения сервера в поле ByServer будет содержаться значение TRUE. В этом случае, а также при разрыве соединения по любым причинам, отличным от вызова rdsNetCloseConnection, RDS будет пытаться самостоятельно восстановить связь до тех пор, пока эти попытки не увенчаются успехом, или пока для этого соединения не будет вызвана функция rdsNetCloseConnection.
В случае возникновения ошибок при приеме или передаче данных модель блока вызывается в режиме RDS_BFM_NETERROR, при этом в параметре ExtParam передается указатель на структуру ошибки RDS_NETERRORDATA. Поскольку RDS самостоятельно восстанавливает оборванные соединения, заново посылает пропавшие данные и решает прочие возникающие проблемы, модели редко реагируют на этот вызов. Чаще всего реакция на RDS_BFM_NETERROR используется при отладке моделей блоков, когда обмен данными по сети не работает, и нужно выяснить, почему именно.