Введение в Microsoft® RPC

Microsoft® Remote Procedure Call (MS RPC) для языков программирования C и C++ представляет совокупность трех моделей программирования распределенных вычислений: обычная модель разработки приложений на C путем написания процедур и библиотек; модель, которая использует мощные компьютеры в качестве сетевых серверов, выполняющих специфические задачи для своих клиентов; и модель клиент-сервер, в которой клиент обычно управляет интерфейсом пользователя, в то время как сервер занимается выборкой, манипулированием и хранением данных.

Программная модель.

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

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

Процедурно-ориентированные языки программирования обеспечивают простые механизмы определения и описания процедур. Например, ANSI стандарт прототипа функции C – конструкция, используемая для определения имени процедуры, типа возвращаемого результата (если есть), а также числа, последовательности, и типов параметров. Использование функционального прототипа – формальный способ определить интерфейс между процедурами. Далее термин «процедура» используется как синоним термина «подпрограмма» для обозначения любой последовательности компьютерных команд, выполняющих функциональную цель. Термин «функция» обозначает процедуру, которая возвращает значение.

Связанные процедуры часто группируются в библиотеках. Так, библиотека может включать набор процедур, которые выполняют задачи в какой-либо общей области, например, математические операции с плавающей запятой, форматируемый ввод / вывод,  сетевые функции. Библиотека процедур – следующий уровень упаковки, облегчающий разработку прикладных программ. Библиотеки могут быть разделены среди многих прикладных программ. Библиотеки, разработанные на C, обычно сопровождаются файлами заголовка. Каждая программа, которая использует библиотеку,  компилируется с файлами заголовка, которые формально определяют интерфейс к процедурам библиотеки.

Инструментальные средства Microsoft RPC представляют общий подход, который позволяет библиотекам процедур, написанным на C, выполняться на других компьютерах. Фактически, прикладная программа может компоноваться с библиотеками, реализованными с использованием RPC, без уведомления пользователя об использовании RPC.

Модель клиент-сервер.

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

Сетевые системы поддерживают разработку приложений клиент-сервер с использованием средств межпроцессных взаимодействий (interprocess communication – IPC), которые позволяют клиенту и серверу связываться и координировать их работу. Например, клиент может использовать механизм IPC, чтобы послать код операции и данные на сервер, запрашивая вызов специфической процедуры. Сервер получает и декодирует запрос, затем вызывает соответствующую процедуру, выполняет все вычисления, необходимые для удовлетворения запроса, затем возвращает результат пользователю. Приложения клиент-сервер обычно разрабатываются с целью минимизации количества  передаваемый по сети данных. В дополнение ко всем возможным ошибкам, которые могут происходить на одиночном компьютере, сеть имеет и собственные условия возникновения ошибок. Например, соединение можно потерять, сервер может исчезать из сети, сетевая служба безопасности может отвергать доступ к ресурсам системы, пользователи могут конкурировать при использовании ресурсов. Поскольку состояние сети всегда изменяется, прикладная программа может терпеть неудачу новыми и неожиданными способами, которые трудно воспроизвести. По этим причинам, каждая прикладная программа должна строго обрабатывать все возможные условия ошибок. Итак, при написании приложения клиент-сервер Вы должны обеспечить уровень кода, который управляет сетевой связью. Преимущество использования Microsoft RPC в том, что инструментальные средства RPC сами обеспечивают этот уровень для Вас. RPC почти устраняет потребность писать специфический для сети код, делая проще процесс разработки распределенных приложений. Инструментальные средства RPC управляет многими деталями в отношении сетевых протоколов и связи, позволяет Вам сосредотачиваться на деталях приложения.

Модель сервера-вычислителя.

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

Компоненты Microsoft RPC

Реализация Microsoft RPC совместима со стандартом DCE (distributed computing environment – распределенная вычислительная среда) консорциума OSF (Open Software Foundation) с небольшими различиями. Клиентские или серверные приложения, написанные с использованием Microsoft RPC, могут эксплуатироваться совместно с любыми DCE RPC серверами или клиентами, чьи библиотеки времени выполнения работают над поддерживаемым протоколом (список поддержанных протоколов см. ниже).

Продукт Microsoft RPC включает следующие главные компоненты:

В модели RPC, Вы можете формально определять интерфейс к удаленным процедурам, используя язык, разработанный для этой цели. Это так называемый язык определения интерфейсов, или IDL (Interface Definition Language). Реализация Microsoft этого языка называется MIDL. После того, как Вы создали интерфейс, Вы должны пропустить его через компилятор MIDL. Компилятор генерирует стабы (stub), которые транслируют локальные вызовы процедуры в удаленные. Стабы являются заместителями функций и в свою очередь производят вызовы функций библиотеки времени выполнения, выполняющих удаленный вызов процедур. Преимущество такого подхода в том, что сеть становится почти полностью прозрачной для вашего распределенного приложения. Ваша клиентская программа как будто бы вызывает локальные процедуры; работа по превращению их в удаленные вызовы выполняется для Вас автоматически. Весь код, который транслирует данные, обращается к сети, и возвращает результаты,  генерируется компилятором MIDL, и невидим для вашей прикладной программы.

Как работает RPC

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

Сервер выполняет следующие шаги, чтобы вызвать удаленную процедуру:

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

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

Для Microsoft Windows и Microsoft Windows NT, библиотеки времени выполнения  состоят из двух частей: импортируемой библиотеки, компонуемой с прикладной программой; и RPC-библиотеки времени выполнения, которая реализована как динамическая (DLL). Серверное приложение содержит вызовы функций серверной библиотеки времени выполнения, которые регистрируют интерфейс сервера и позволяют серверу принимать удаленные вызовы. Серверное приложение также содержит и сами специфические удаленные процедуры, предназначенные для вызова клиентскими приложениями.

Учебный пример распределенного приложения

Это введение в RPC представлено очень простой прикладной программой, концентрирующей внимание на различиях между автономными программами на C и распределенными приложениями, использующими RPC. Этот пример, конечно, не является исчерпывающим показом возможностей RPC, богатых средств языка определения интерфейсов Microsoft (MIDL). Это всего лишь рабочий пример, который быстро демонстрирует создание распределенного приложения. Приложение печатает слова «Привет, мир». Клиентский компьютер делает удаленный вызов процедуры на сервере, а сервер печатает слова на устройстве стандартного вывода.

Это распределенное приложение требует двух различных исполняемых программ: одну для клиента и одну для сервера. Подобно другим программам на C, эти исполняемые программы будут основаны на исходных файлах языка C, написанных разработчиком. Однако некоторые из исходных файлов будут автоматически сгенерированы RPC инструментом:  компилятором MIDL.

Чтобы сделать это приложение распределенным, Вы создадите файл, включающий функциональный прототип удаленной процедуры. Прототип связан с атрибутами, которые описывают, каким образом данные для удаленной процедуры должны быть переданы через сеть. Атрибуты, данные и функциональные прототипы описывают интерфейс между клиентом и сервером. Интерфейс ассоциируется с уникальным идентификатором, который отличает этот интерфейс от всех других. Вы создадите файл, который объявляет переменную – дескриптор связывания, который используют клиент и сервер для представления их логического соединения через этот интерфейс. Вы будете также писать главные программы клиента и сервера, которые вызывают RPC функции времени выполнения для установки интерфейса. Не волнуйтесь, если Вы не понимаете каждый из атрибутов, файлов или понятий, упомянутых ниже. Цель этого раздела состоит в том, чтобы обеспечить быстрый краткий обзор всего процесса. Реализовав пример, Вы будете видеть все файлы и все процедурные шаги реализации любого распределенного приложения.

Программа клиента

Автономная программа, которая может быть выполнена на одном компьютере, состоит из вызова одной функции, называемой HelloProc:

 

/* file: helloc.c (stand-alone) */

 

void HelloProc(unsigned char * pszString);

 

void main(void)

{      unsigned char * pszString = "Hello, world";

HelloProc(pszString);

}

Функция HelloProc вызывает библиотечную C функцию printf, чтобы отобразить текст "Привет, мир":

/* file: hellop.c */

 

#include <stdio.h>  

 

void HelloProc(unsigned char * pszString)

{

    printf("%s\n", pszString);

}

 

HelloProc определена в собственном исходном файле HELLOP.C, так что может компилироваться и компоноваться как с автономной прикладной программой, так и с распределенным приложением.

Файл на языке определения интерфейсов

Первый шаг при создании распределенного приложения должен обеспечить возможность клиенту и серверу найти друг друга и связываться через сеть. Для этого определяется формальный интерфейс с  использованием языка определения интерфейсов Microsoft (MIDL). Интерфейс состоит из типов данных, функциональных прототипов, атрибутов, и информации интерфейса. Определение интерфейса сохраняется в собственном файле с расширением IDL. Для удобства этот пример использует то же самое имя, HELLO, как для IDL файла и файла языка C. Вы можете использовать и различные имена для этих двух файлов:

/* file: hello.idl */

 

[ uuid (6B29FC40-CA47-1067-B31D-00DD010662DA),

  version(1.0)

]

interface hello

{

void HelloProc([in, string] unsigned char * pszString);

}

Конструкции IDL файла отличаются от конструкций в исходных файлах языка C, но они станут легки в использовании, как только Вы ознакомитесь с ними. Информация, обеспечиваемая в IDL файле, заменяет огромное количество сетевого программирования, и фактически все,  что Вы должны сделать для координирования приложений клиента и сервера.

IDL файл состоит из двух частей: заголовка интерфейса и тела интерфейса. Заголовок интерфейса включает информацию относительно интерфейса в целом, такую как его идентификатор и номер версии. Он состоит из спецификации, заключенной в квадратные скобки и заканчивающейся ключевым словом interface и именем интерфейса. Заголовок интерфейса в этом примере включает ключевые слова uuid, version и interface. Тело интерфейса содержит данные и функциональные прототипы. Тело интерфейса заключено в фигурные скобки.

UUID - универсально уникальный идентификатор (universally unique identifier), строка из пяти групп шестнадцатеричных цифр, отделяемых дефисами. Эти пять групп содержат восемь цифр, четыре цифры, четыре цифры, четыре цифры, и 12 цифр, соответственно. Например, "6B29FC40-CA47-1067-B31D-00DD010662DA" – правильный UUID. В среде Microsoft Windows NT UUID также известен как GUID, или глобально уникальный идентификатор (globally unique identifier). UUID интерфейса сгенерирован утилитой uuidgen, которая генерирует уникальные идентификаторы в требуемом формате.

Тело интерфейса содержит С-подобные определения типов данных и функциональных прототипов, к которым добавлены атрибутами. Атрибуты приводятся в квадратных скобках и описывают, каким образом данные должны быть переданы через сеть. В данном примере тело интерфейса содержит функциональный прототип HelloProc. Одиночный параметр pszString, обозначен как параметр in, что означает необходимость его передачи от клиента к серверу. Параметры могут также быть обозначены как out, если они передаются от сервера клиенту, или in, out , если передаются в обоих направлениях. Эти атрибуты сообщают библиотекам времени выполнения, как передавать данные между клиентом и сервером. Атрибут string указывает, что параметр - символьный массив.

Компилируйте IDL файл, используя компилятор MIDL, который сгенерирует файлы на языке C для клиентского и серверного стабов и файл заголовка. Файл заголовка, произведенный из файла определения интерфейса HELLO.IDL по умолчанию именуется HELLO.H и содержит включение (#include) заголовочного файла RPC.H , а также функциональные прототипы из IDL файла. Файл заголовка RPC.H определяет данные и функции, используемые сгенерированным файлом заголовка:

/* file: hello.h (fragment) */

 

#include <rpc.h>

 

void HelloProc(unsigned char * pszString);

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

/* file: helloc.c (distributed version) */

 

#include <stdio.h>

#include "hello.h"    // header file generated by the MIDL compiler

 

void main(void)

{

    char * pszString = "Hello, world";

    ...

    HelloProc(pszString);

    ...

}

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

Файл конфигурации приложения

Как определено стандартом распределенной вычислительной среды (DCE), Вы должны также определить файл конфигурации приложения или ACF (application configuration file), который обрабатывается компилятором MIDL вместе с IDL файлом. ACF содержит данные и атрибуты RPC, не имеющие отношения к передаваемым данным. Например, объект данных, называемый дескриптором связывания, представляет соединение между приложениями клиента и сервера. Клиент вызывает функции времени выполнения, чтобы установить имеющий силу дескриптор связывания, который может затем использоваться функциями времени выполнения всякий раз, когда клиент вызывает удаленную процедуру. Дескриптор связывания не является частью функционального прототипа и не передается через сеть, поэтому он определяется в ACF.

ACF для программы "Hello , world" имеет следующий вид:

/* file: hello.acf */

 

[ implicit_handle(handle_t hello_IfHandle)

]interface hello

{

}

Формат ACF подобен формату IDL файла. Атрибуты приводятся в квадратных скобках, со следующим за ними ключевым словом interface и именем интерфейса. Имя интерфейса, определенное в ACF, должно соответствовать имени, определенному в IDL файле. ACF содержит атрибут implicit_handle, указывающий, что дескриптор – глобальная переменная, т.е. доступная функциям библиотеки времени выполнения. Ключевое слово implicit_handle связано с типом дескриптора и именем дескриптора; в этом примере, дескриптор имеет MIDL тип данных handle_t. Имя hello_IfHandle дескриптора, специфицированное в ACF, определено в сгенерированном файле заголовка HELLO.H. Этот дескриптор будет использоваться при вызовах функций клиентской библиотеки времени выполнения.

Добавление функций запросов RPC

Библиотеки времени выполнения RPC часто вызывают функции, реализованные пользователем. Этот подход позволяет компилятору MIDL генерировать C код автоматически, в то же время позволяя Вам управлять непосредственно выполнением операции. Эти реализованные пользователем функции включают функции с фиксированными именами и именами, которые являются основанными на атрибутах или типах данных IDL-файла. Например, всякий раз, когда библиотеки времени выполнения распределяют или освобождают память, они вызывают реализованные пользователем функции midl_user_allocate и midl_user_free. В примере "Привет, мир" приложение не нуждается в сложном управлении памятью, так что эти функции просто реализованы в терминах C-библиотечных функций malloc и free:

void __RPC_FAR * __RPC_USER midl_user_allocate(size_t len)

{

    return(malloc(len));

}

 

void __RPC_USER midl_user_free(void __RPC_FAR * ptr)

{

    free(ptr);

}

Вызов функций клиентского API

RPC поддерживает два типа связывания: автоматическое и программно-управляемое. При использовании автоматического связывания, Вы не должны определять дескриптор связывания, а также делать какие-либо вызовы клиентских функций времени выполнения для объявления (регистрации) дескриптора. Когда ваше приложение управляет дескриптором, оно должно делать эти определения и вызовы. В этой примере клиент управляет соединением с сервером. Чтобы связаться с сервером, клиент должен вызвать функции времени выполнения перед вызовом удаленной процедуры и должен отсоединиться по завершении всех удаленных вызовов. Структура программы клиента, которая управляет собственным соединением с сервером, описана в следующем фрагменте кода. Удаленные вызовы процедуры вставлены между вызовами этих функций клиентской библиотеки времени выполнения:

/* file: helloc.c (fragment) */

 

#include "hello.h"    // header file generated by the MIDL compiler

 

void main(void)

{

    RpcStringBindingCompose(...);

    RpcBindingFromStringBinding(...);

 

    HelloProc(pszString);    // make remote calls

 

    RpcStringFree(...);

    RpcBindingFree(...);

}

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

Функции Microsoft RPC используют структуры данных, которые представляют связывание, интерфейс, последовательность протоколов, и пункт назначения (endpoint). Связывание – это соединение между клиентом и сервером; интерфейс – совокупность типов данных и процедур, реализованных сервером; последовательность протоколов – спецификация базового сетевого транспорта, который нужно использовать для передачи данных по сети; пункт назначения – сетевое имя, которое является специфическим для последовательности протоколов. Данный пример использует именованные каналы (конвейеры, named pipe) в качестве последовательности протоколов, и пункт назначения \pipe\hello. В приводимом далее примере кода функция RpcStringBindingCompose комбинирует последовательность протоколов, сетевой адрес (имя сервера), пункт назначения (имя канала), и другие строковые элементы в форму, требующуюся для следующей функции, RpcBindingFromStringBinding. RpcStringBindingCompose также распределяет память для символьной строки, достаточную для размещения данных. RpcBindingFromStringBinding использует строку для генерации дескриптора, который представляет связывание между сервером и клиентом. После того, как удаленные вызовы процедур выполнены, RpcStringFree освобождает память, которая была распределена RpcStringBindingCompose для структуры данных строки связывания. RpcBindingFree освобождает дескриптор.

Строка связывания (string binding), таким образом, является ключевым понятием при реализации соединения программно-управляемого типа, поскольку содержит всю необходимую для этого информацию. Строка связывания состоит из нескольких подстрок, представляющих UUID интерфейса, последовательность протоколов, сетевой адрес, пункт назначения и его опции. Последовательность протоколов, в свою очередь, представляет сетевой RPC протокол, а также определяет соглашения о соответствующем формате сетевых адресов и об именовании пунктов назначения. Например, последовательность протоколов ncacn_ip_tcp определяет протокол с установлением соединения, в терминах NCA (Network Computing Architecture), над TCP/IP. При этом требуется соответствующий формат представления сетевого адреса или имени сервера, а пункт назначения обозначает серверный коммуникационный порт.

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

Заметим, что наиболее сложные распределенные приложения для получения дескриптора связывания должны использовать функции сервиса имен вместо строк связывания. Функции сервиса имен позволяют серверу регистрировать интерфейс, UUID, сетевой адрес и пункт назначения под одним логическим именем. Эти функции обеспечивают независимость расположения компонентов приложения и легкость администрирования.

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

#include <stdlib.h>

#include <stdio.h>

#include <ctype.h>

#include "hello.h"    // header file generated by the MIDL compiler

 

void Usage(char * pszProgramName)

{   fprintf(stderr, "Usage:  %s\n", pszProgramName);

    fprintf(stderr, " -p protocol_sequence\n");

    fprintf(stderr, " -n network_address\n");

    fprintf(stderr, " -e endpoint\n");

    fprintf(stderr, " -o options\n");

    fprintf(stderr, " -s string\n");

    exit(1);

}

 

void main(int argc, char **argv)

{   RPC_STATUS status;

    unsigned char * pszUuid             = NULL;

    unsigned char * pszProtocolSequence = "ncacn_np";

    unsigned char * pszNetworkAddress   = NULL;

    unsigned char * pszEndpoint         = "\\pipe\\hello";

    unsigned char * pszOptions          = NULL;

    unsigned char * pszStringBinding    = NULL;

    unsigned char * pszString           = "Hello, world";

    unsigned long ulCode;

    int i;

 

    /* Allow the user to override settings with command line switches */

    for (i = 1; i < argc; i++) {

         if ((*argv[i] == '-') || (*argv[i] == '/')) {

             switch (tolower(*(argv[i]+1))) {

                case 'p':  // protocol sequence

                    pszProtocolSequence = argv[++i]; break;

                case 'n':  // network address

                    pszNetworkAddress = argv[++i]; break;

                case 'e':  // endpoint

                    pszEndpoint = argv[++i]; break;

                case 'o':  // options

                    pszOptions = argv[++i]; break;

                case 's':  // string

                    pszString = argv[++i]; break;

                case 'h':

                case '?':

                default: Usage(argv[0]);

             }

         }

         else Usage(argv[0]);

    }

    /* Use a convenience function to concatenate the elements of */

    /* the string binding into the proper sequence               */

    status = RpcStringBindingCompose(

                             pszUuid,

                             pszProtocolSequence,

                             pszNetworkAddress,

                             pszEndpoint,

                             pszOptions,

                             &pszStringBinding);

    printf("RpcStringBindingCompose returned 0x%x\n", status);

    printf("pszStringBinding = %s\n", pszStringBinding);

    if (status) exit(status);

 

    /* Set the binding handle that will */

    /* be used to bind to the server  */

    status = RpcBindingFromStringBinding(

                               pszStringBinding,

                               &hello_IfHandle);

    printf("RpcBindingFromStringBinding returned 0x%x\n", status);

    if (status) exit(status);

 

    printf("Calling the remote procedure 'HelloProc'\n");

    printf("Print the string '%s' on the server\n", pszString);

 

    RpcTryExcept {

        HelloProc(pszString);  // make call with user message

        printf("Calling the remote procedure 'Shutdown'\n");

 

        Shutdown();  // shut down the server side

    }

    RpcExcept(1) {

        ulCode = RpcExceptionCode();

        printf("Runtime reported exception 0x%lx \n", ulCode);

    }

    RpcEndExcept

 

    /*  The calls to the remote procedures are complete. */

    /*  Free the string and the binding handle           */

    status = RpcStringFree(&pszStringBinding); 

    printf("RpcStringFree returned 0x%x\n", status);

    if (status) exit(status);

    status = RpcBindingFree(&hello_IfHandle);

    printf("RpcBindingFree returned 0x%x\n", status);

    if (status) exit(status);

 

    exit(0);

}

void __RPC_FAR * __RPC_USER midl_user_allocate(size_t len)

{   return(malloc(len));

}

void __RPC_USER midl_user_free(void __RPC_FAR * ptr)

{   free(ptr);

}

 

Сборка приложения клиента

Распределенное приложение требует, чтобы Вы перед компиляцией и компоновкой исходного текста на C сделали дополнительный подготовительный шаг: компиляцию IDL и ACF файлов с использование компилятора MIDL. Вы должны быть внимательными при идентификации операционной системы, которая будет строить приложение, операционной системы (систем), которая выполнит программы клиента и сервера, и сетевой последовательности протоколов, которая будет использоваться. Эти условия определят версии используемых MIDL и C компиляторов, версии заголовочных файлов, включаемых в ваши приложения, и версии RPC-библиотек времени выполнения, компонуемые с вашими приложениями. Для простоты, мы примем, что этот первый пример использует ту же самую операционную систему – Microsoft Windows NT – как для построения, так и в качестве платформы клиента и сервера, и что пример использует именованные каналы в качестве последовательности протоколов.

MIDL компиляция

IDL файл HELLO.IDL и файл конфигурации приложения HELLO.ACF компилируется, с использованием компилятора MIDL:

# makefile fragment

midl hello.idl

Компилятор MIDL генерирует файл заголовка HELLO.H и файл клиентского стаба на языке C HELLO_C.C. (Компилятор MIDL также производит файл серверного стаба HELLO_S.C, но мы пока его игнорируем.)

Компиляция C

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

# makefile fragment

# CC refers to the C compiler

# CFLAGS refers to C compiler switches

# CVARS refers to variables that control #ifdef directives

#

$(CC) $(CFLAGS) $(CVARS) helloc.c

$(CC) $(CFLAGS) $(CVARS) hello_c.c

Обратите внимание: приведенные команды предполагают наличие определенной конфигурации программного обеспечения, которая состоит из утилиты nmake, компилятора Microsoft C и операционной системы Microsoft Windows NT.

Компоновка

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

# makefile fragment

# LINK refers to the linker

# CONFLAGS refers to flags for console apps

# CONLIBS refers to libraries for console apps

#

client.exe : helloc.obj hello_c.obj

$(LINK) $(CONFLAGS) -out:client.exe helloc.obj hello_c.obj \

     $(CONLIBS) rpcrt4.lib

Заметьте, что команды компоновщика и параметры могут измениться в зависимости от вашей компьютерной конфигурации.

Сборка клиента, резюме

Следующий фрагмент MAKEFILE для утилиты nmake показывает зависимости между файлами, используемыми для построения приложения клиента.

# makefile fragment

 

client.exe : helloc.obj hello_c.obj

 ...

hello.h hello_c.c : hello.idl hello.acf

 ...

helloc.obj : helloc.c hello.h

 ...

hello_c.obj : hello_c.c hello.h

 ...

Пример использовал заданные по умолчанию имена файла, которые произведены компилятором MIDL. Заданное по умолчанию имя для файла клиентского стаба сформировано из имени IDL файла (без расширения) и символов _C.C. Если имя (без расширения) имеет длину более шести символов, некоторые файловые системы могут не принимать имя файла стаба. Файлы стаба не должны использовать заданные по умолчанию имена. Вы можете изменить имя клиентского стаба, воспользовавшись ключом  /cstub компилятора MIDL:

midl hello.idl -cstub mystub.c

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

Программа сервера

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

Вызов функций серверного API

В следующем примере сервер вызывает функции RpcServerUseProtseqEp и RpcServerRegisterIf, чтобы сделать информацию связывания доступной клиенту. Затем сервер вызывает функцию RpcServerListen, чтобы указать, что он ожидает запросы клиента:

/* file: hellos.c (fragment) */

 

#include "hello.h"  // header file generated by the MIDL compiler

 

void main(void)

{

    RpcServerUseProtseqEp(...);

    RpcServerRegisterIf(...);

    RpcServerListen(...);

}

RpcServerUseProtseqEp идентифицирует пункт назначения (endpoint) сервера и последовательность сетевых протоколов. RpcServerRegisterIf регистрирует интерфейс, а RpcServerListen инструктирует сервер начать прослушивание запросов клиента.

Программа сервера должна также включить две функции, вызываемые серверными стабами, midl_user_allocate и midl_user_free. Эти функции распределяют и освобождают память на сервере, когда удаленная процедура должна передать параметры. В следующем примере, midl_user_allocate и midl_user_free осуществлены с использованием C‑библиотечных функции malloc и free:

void __RPC_FAR * __RPC_API midl_user_allocate(size_t len)

{

    return(malloc(len));

}

 

void __RPC_API midl_user_free(void __RPC_FAR * ptr)

{

    free(ptr);

}

Полный код для HELLOS.C выглядит следующим образом:

#include <stdlib.h>

#include <stdio.h>

#include <ctype.h>

#include "hello.h"    // header file generated by the MIDL compiler

 

void Usage(char * pszProgramName)

{   fprintf(stderr, "%s", PURPOSE);

    fprintf(stderr, "Usage:  %s\n", pszProgramName);

    fprintf(stderr, " -p protocol_sequence\n");

    fprintf(stderr, " -e endpoint\n");

    fprintf(stderr, " -m maxcalls\n");

    fprintf(stderr, " -n mincalls\n");

    fprintf(stderr, " -f flag_wait_op\n");

    exit(1);

}

 

void main(int argc, char * argv[])

{   RPC_STATUS status;

    unsigned char * pszProtocolSequence = "ncacn_np";

    unsigned char * pszSecurity         = NULL;

    unsigned char * pszEndpoint         = "\\pipe\\hello";

    unsigned int    cMinCalls           = 1;

    unsigned int    cMaxCalls           = 20;

    unsigned int    fDontWait           = FALSE;

    int i;

 

/* Allow the user to override settings with command line switches */

  for (i = 1; i < argc; i++) {

     if ((*argv[i] == '-') || (*argv[i] == '/')) {

        switch (tolower(*(argv[i]+1))) {

            case 'p':  // protocol sequence

                pszProtocolSequence = argv[++i]; break;

            case 'e': // endpoint

                pszEndpoint = argv[++i]; break;

            case 'm': // max concurrent calls

                cMaxCalls = (unsigned int) atoi(argv[++i]); break;

            case 'n': // min concurrent calls

                cMinCalls = (unsigned int) atoi(argv[++i]); break;

            case 'f': // flag

                fDontWait = (unsigned int) atoi(argv[++i]); break;

            case 'h':

            case '?':

            default: Usage(argv[0]);

        }

     }

     else Usage(argv[0]);

  }

 

    status = RpcServerUseProtseqEp(

                           pszProtocolSequence,

                           cMaxCalls,   

                           pszEndpoint,

                           pszSecurity);  // Security descriptor

    printf("RpcServerUseProtseqEp returned 0x%x\n", status);

    if (status) exit(status);

 

    status = RpcServerRegisterIf(

                         hello_ServerIfHandle,

                         NULL,   // MgrTypeUuid

                         NULL);  // MgrEpv; null means use default

    printf("RpcServerRegisterIf returned 0x%x\n", status);

    if (status) exit(status);

 

    printf("Calling RpcServerListen\n");

    status = RpcServerListen(

                       cMinCalls,

                       cMaxCalls,

                       fDontWait);

    printf("RpcServerListen returned: 0x%x\n", status);

    if (status) exit(status);

 

    if (fDontWait) {

            printf("Calling RpcMgmtWaitServerListen\n");

            status = RpcMgmtWaitServerListen();  // wait operation

            printf("RpcMgmtWaitServerListen returned: 0x%x\n", status);

            if (status) exit(status);

    }

} 

 

/* MIDL allocate and free */

void __RPC_FAR * __RPC_API midl_user_allocate(size_t len)

{   return(malloc(len));

}

 

void __RPC_API midl_user_free(void __RPC_FAR * ptr)

{   free(ptr);

}

Сборка серверного приложения

Сборка серверного приложения совершенно аналогична сборке приложения клиента.

MIDL компиляция

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

# makefile fragment

midl hello.idl

Компиляция C

Исходные файлы на C, содержащие вызовы серверных RPC-функций и удаленные процедуры,  компилируются с исходным файлом стаба, сгенерированным компилятором MIDL:

$(CC) $(CFLAGS) $(CVARS) hellos.c

$(CC) $(CFLAGS) $(CVARS) hellop.c

$(CC) $(CFLAGS) $(CVARS) hello_s.c

Обратите внимание: приведенные команды предполагают наличие определенной конфигурации программного обеспечения, которая состоит из утилиты nmake, компилятора Microsoft C и операционной системы Microsoft Windows NT.

Компоновка

Как только исходные файлы на C откомпилированы, они компонуются с серверной библиотекой времени выполнения и стандартными библиотеками C времени выполнения для данной платформы, последовательности протоколов (именованным каналом) и модели памяти:

# makefile fragment

# LINK refers to the linker

# CONFLAGS refers to flags for console apps

# CONLIBS refers to libraries for console apps

#

server.exe : hellos.obj hellop.obj hello_s.obj

$(LINK) $(CONFLAGS) -out:server.exe hellos.obj hellop.obj

 hello_s.obj $(CONLIBS) rpcrt4.lib

Заметьте, что команды компоновщика и параметры могут измениться в зависимости от вашей компьютерной конфигурации.

Сборка сервера, резюме

Следующий фрагмент MAKEFILE для утилиты nmake показывает зависимости между файлами, используемыми для построения приложения сервера. Исполняемая программа собирается из исходных файлов сервера и файла серверного стаба. Все исходные файлы сервера ссылаются на файл заголовка HELLO.H:

# makefile fragment

 

server.exe : hellos.obj hellop.obj hello_s.obj

    ...

hello.h hello_s.c : hello.idl hello.acf

    ...

hellos.obj : hellos.c hello.h

    ...

hellop.obj : hellop.c hello.h

    ...

hello_s.obj : hello_s.c hello.h

    ...

Остановка серверного приложения

Робастная серверная программа при завершении работы должна остановить прослушивание клиентов и удалить интерфейс из реестра. Две основных функции станции, используемые в этих целях – RpcMgmtStopServerListening и RpcServerUnregisterIf. Серверная функция RpcServerListen не возвращает вызвавшей программе управление до тех пор, пока не происходит исключение или пока сервер не вызовет RpcMgmtStopServerListening. В Microsoft RPC клиент не может непосредственно вызывать эту функцию, останавливающую прослушивание. Однако, Вы можете разработать программу сервера так, чтобы пользователь управлял ею как сервисом и заставлял другой поток серверного приложения вызвать RpcMgmtStopServerListening. Или Вы можете позволить клиентскому приложению останавливать серверное,  делая удаленный вызов процедуры на сервере, которая в свою очередь вызывает RpcMgmtStopServerListening. Следующий пример использует последний подход. Новая удаленная функция Shutdown добавлена к HELLOP.C:

/* hellop.c fragment */

 

#include "hello.h" //header file generated by the MIDL compiler

 

void Shutdown(void)

{

    RPC_STATUS status;

 

    status = RpcMgmtStopServerListening(NULL);

     ...

    status = RPCServerUnregisterIf(NULL, NULL, FALSE);

     ...

}

Один параметр со значением NULL указывает RpcMgmtStopServerListening, что локальное приложение должно остановить прослушивание удаленных вызовов процедур. Два NULL‑параметра для RpcServerUnregisterIf указывают, что никакие интерфейсы не зарегистрированы. Параметр FALSE указывает, что интерфейс должен быть удален из реестра немедленно.

Процедура Shutdown, поскольку она является удаленной, должна также быть добавлена в секцию тела интерфейса IDL файла:

/* file: hello.idl */

 

[ uuid (6B29FC40-CA47-1067-B31D-00DD010662DA),

  version(1.0)

]

interface hello

{

void HelloProc([in, string] char * pszString);

void Shutdown(void);

}

Наконец, программа клиента должна добавить вызов функции Shutdown:

/* helloc.c (fragment) */

 

#include "hello.h"    // header file generated by the MIDL compiler

 

void main(void)

{

    char * pszString = "Hello, world";

 

    RpcStringBindingCompose(...);

    RpcBindingFromStringBinding(...);

 

    HelloProc(pszString);

    Shutdown();

 

    RpcStringFree(...);

    RpcBindingFree(...);

}

Особенности сборки RPC приложений в среде визуального программирования

Рассмотренная выше последовательность операций иллюстрирует процесс сборки распределенного приложения с использованием простых программных средств, сопровождающих поставку компилятора Microsoft C: компилятора MIDL, утилиты nmake, компоновщика, библиотекаря и т.д. Те же шаги можно проделать с использованием широко распространенных сред визуального программирования на C++: Microsoft Visual C++ или Borland C++ Builder, в состав поставки которых входит компилятор MIDL.

Если не ставить цели интегрировать процесс компиляции IDL-файлов в среду разработки, компилятор MIDL можно запускать просто из командной строки, как показано в предыдущих разделах. При этом необходимо помнить, что компилятор MIDL реализован как динамическое приложение и может потребовать установки определенных библиотек DLL. Полученные исходные файлы для стабов и заголовочный файл затем необходимо добавить в соответствующие программные проекты для продолжения работы в среде визуального программирования.

Так рассматриваемый в данном руководстве пример реализуется в виде двух консольных приложений: клиентского и серверного. Компиляция примера может потерпеть неудачу из-за смены версий компилятора MIDL, в результате чего изменяются соглашения относительно генерируемых им идентификаторов. Например, MIDL из поставки Visual C++ 6.0 изменяет имя структуры данных регистрируемого на сервере интерфейса по сравнению с использованным в нашем примере идентификатором hello_ServerIfHandle. Затруднения при компоновке проектов могут возникать из-за отсутствия указаний относительно библиотечных файлов RPC-библиотек времени выполнения. Например, для поставки Visual C++ 6.0, необходимо добавить в установки проекта (окно «Project Settings», вкладка «Link») имя библиотечного модуля rpcrt4.lib. Для C++ Builder, возможно, потребуется указать путь к библиотечным модулям RPC (окно «Project Options», вкладка «Directories/Conditionals»). Например, при стандартной установке C++ Builder 5.0 эти модули помещаются в каталог \Lib\PSDK .

Заключение: основные шаги при разработке RPC приложений

Распределенное приложение состоит из исполняемых программ на стороне клиента и на стороне сервера.

Процесс разработки с использованием RPC включает, в дополнение к стандартному процессу разработки, два шага. Вы должны определить интерфейс для удаленного вызова процедуры в IDL и ACF файлах, которые компилируются MIDL. Компилятор MIDL производит исходные файлы на C и файлы стабов. Далее следует обычный для любого приложения процесс разработки: компилируйте файлы языка C, и компонуйте объектные модули с библиотеками для создания исполняемых программ.

Для распределенного приложения «Привет, мир», разработчик создает следующие исходные файлы:

·        HELLOC.C

·        HELLOS.C

·        HELLOP.C

·        HELLO.IDL

Компилятор MIDL использует файлы HELLO.IDL и HELLO.ACF для генерации исходного файла клиентского стаба HELLO_C.C, исходного файла серверного стаба HELLO_S.C и файл заголовка HELLO.H, который в свою очередь включает заголовочный файл RPC.H.

Исполняемая программа клиента строится из клиентской библиотеки времени выполнения и следующих файлов на языке C, заголовочного и исходных:

·        HELLO.H

·        HELLOC.C

Исполняемая программа сервера строится из серверной библиотеки времени выполнения и следующих файлов на языке C, заголовочного и исходных:

·        HELLO.H

·        HELLOS.C

·        HELLOP.C

Ниже перечислены задачи, выполняемые в процессе разработки с использованием Microsoft RPC:

1.        Создание файла на языке определения интерфейсов, который определяет идентификацию интерфейса, типы данных и функциональные прототипы для удаленных процедур.

2.        Создание файла конфигурации приложения.

3.        Компиляция определения интерфейса с использованием MIDL. Компилятор MIDL генерирует файлы на языке C для стабов и заголовочные файла для клиента и сервера.

4.        Включение (include) заголовочных файлов, сгенерированных компилятором MIDL в программы сервера и клиента.

5.        Написание исходного текста программы сервера, которая вызывает функции RPC, чтобы сделать информацию связывания доступной клиенту, затем вызывает RpcServerListen для начала прослушивания клиентских запросов. Обеспечение метода остановки сервера.

6.        Компоновка клиента с файлом клиентского стаба и клиентской RPC-библиотекой времени выполнения.

7.        Компоновка сервера с файлом серверного стаба, удаленными процедурами и серверной RPC-библиотекой времени выполнения.