Copyright (c) Prolog Development Center SPb
Обмен информацией между BackEnd и FrontEnd
Домен EDF_D как основа обмена информацией
Основным доменом, используемыми в BackEnd и FrontEnd близко к границе передачи данных является домен EDF_D (Exchange Data Format).
Определение домена и операции над термами домена определены в пакете EDF (SpbVipTools\Packs\Logic\edf).
Домен EDF_D по сути является комбинацей доменов VIP value, namedValue и namedValueList, при этом
имена термов сокращены до минимально приемлемого с целью экономии размера текста и представляется спецификацией
domains
edf_D =
n;
av(string,edf_D); <- аналог namedValue (аттрибут-значение)
bl(boolean LogicBoolean);
u(unsigned Value);
u64(unsigned64 Value);
i(integer Value);
i64(integer64 Value);
r(real Number);
c(char Value);
s(string Value);
s8(string8 Value);
b(binary Value);
bNA(binaryNonAtomic Value);
tg(gmtTimeValue Value);
tl(localTimeValue Value);
o(object VipObject);
a(edf_D* Values); <- список термов (массив)
rf(edf_D Reference);
err(integer ErrorCode,string ErrorText).
Пакет EDF содержит операции преобразования термов домена
EDF_D в термы доменов value, namedValue и обратно (для возможных
соответствий) .
Случай моно-приложения (BackEnd и FrontEnd в одном проекте-приложении)
В этом случае для BackEnd и FrontEnd родным форматом является EDF_D и они обмениваются данными именно в этом формате.
Случай обмена по http протоколу (BackEnd и FrontEnd в разных проектах-приложениях)
При обмене по http протоколу базовым форматом является JSON, но особенности передачи данных определяются деталями архитектуры.
Сама форма сообщения в формате Json выглядит в строковом представлении как
{
"id" : <идентификатор сообщения>,
"jsonrpc" : "2.0",
"method" : <метод>,
"params" : {
"transID" : <идентификатор транзакции>
"eventID" : <идентификатор операции>,
"dataformat" : <имя
формата>, <- формат передачи данных - либо EDF_D (значение
"edf"), либо JSON (значение "json"),
"evParams" : <параметры операции>
}
}
Ключевыми здесь являются параметры "dataformat" и "evParams".
Принято,
что BackEnd использует в качестве рабочего формата EDF_D, а FrontEnd
может использовать либо формат EDF_D, либо формат JSON
(например, если FrontEnd является клиентом на основе WebBrowser).
Поскольку
инициатива обмена данными в концепции RPC принадлежит клиенту, то
клиент при передаче данных указывает удобный для него формат передачи
данных.
Если это "edf", то клиент (FE) использует
в качестве рабочего EDF_D, а при передаче серверу (BE) преобразует
рабочий терм в строковое представление
операцией toString(<EDF_data>) и передает evParams
как строку. Сервер (BE), получив такое сообщение, преобразует строковое
представление в терм EDF_D простой операцией toTerm(<строковый
EDF-терм>) и далее использует этот терм как предписано операцией.
Если это "json", то параметры операции evParams
клиент передает как json-объект {...}. Сервер (BE) в этом случае,
получив данные в JSON формате, преобразует их в терм домена EDF_D.
Пакет EDF содержит операции прямого и обратного преобразования между форматами EDF_D и JSON.
Результат
операции всегда возвращается в том формате, который указал клиент, при
этом структура сообщения соответствует приведенной выше.
Формат данных, в котором FE осуществляет передачу, хранится в виде свойства exchangeDataFormat_P в объекте класса common\appFrontEnd\frontend
facts
...
exchangeDataFormat_P:string:="json". %Alternative is "edf"
и доступен через интерфейс frontEnd.i
Движение запросов FE и ответов BE
Общая схема обмена запросами и ответами приведена на следующем рисунке
Объекты FrontEnd обращаются к BackEnd путем вызова предиката класса fe_Connector:
request(<метод>,<идентификатор операции>,<параметры операции>)
Как видно, здесь не указыватся ни формат передачи данных, ни способ связи FE и BE.
Идентификатор
(код) операции является целым числом. И FE и BE должны "знать", что
обозначает этот код, то есть какая операция должна быть выполнена.
Механизм ядра как FE, так и BE исполъзуют подлюченный к каждому из них
файл SpbVipTools\Packs\Logic\appFrameSupport\coreIdentifiers.i.
Принято, что в пользовательских программах, также для тестирования должен быть использован файл Common\dataExchangeIdentifiers.i.
При этом выделены диапазоны значений кодов операций:
- для операций ядра 0-3000
- для тестовых операций 3001-5000
- для операций пользователя >5000
Параметры операции здесь передаются в виде термов домена EDF_D, как было описано выше.
Каждое
сообщение в механизме запрос-ответ рассматривается как одна транзакция,
которая может состоять либо из одного запроса и соответствующего
ответа, либо из одного запроса и множества ответов сервера. Для каждого
запроса клиент создает идентификатор транзакции (целое число). При
множественном ответе сервера (на один запрос) все ответы должны иметь
тот же идентификатор транзакции, что и первый запрос.
Сообщения
от Fe_Connector через систему передачи-приема с добавлением
идентификатора траназкции попадают в класс Fe_Requests, который вызывает
предикат fe_Request одного из модулей (Be_CoreTasks или Be_Tests или
пользовательского модуля (обозначены как Be_Tasks)).
Запрос обрабатывается с привлечением необходимых модулей, а затем ответ вызовом предиката класса Be_Connector
response(<идентификатор ответа>,<параметры операции>,<идентификатор транзакции>)
передается в механизм транспортировки данных с тем же идентификатором транзакции и достигает клиента (FrontEnd).
Там, в зависимости от кода (идентификатора) ответа, данные ответа попадают в соответствующий модуль обработки.
Виды запросов
FrontEnd может обращаться к BackEnd с запросами трех видов:
- methodDo
- methodRequest
- methodChain
Все
запросы и обработка ответов выполняются в стиле асинхронного обмена. То
есть клиент (FE) отправляет запрос вызовом предиката notify и "забывает
о нем". BackEnd после обработки запроса посылает во FrontEnd ответ вызовом предиката notify, сопровождая его кодом результата, который обрабатывается во FE самостоятельно.
methodDo
должен инициировать в BE операцию с соответствующими кодом
и параметрами. FE не ожидает никакого ответа от BE и предполагает,
что этот запрос должен безусловно выполниться.
methodRequest инициирует в BE операцию с заданным кодом, причем BE должен вернуть результат выполнения операции.
methodChain
инициирует в BE начало операции, которая может (и должна) возвратить во
FE множество ответов не в виде списка (что можно было бы сделать с
использованием метода methodRequest), а путем выработки самостоятельных сообщений. Цепь сообщений завершается, когда BackEnd посылает сообщение с кодом be_EndOfData_C.
Первичная
обработка запроса клиента (FE) осуществляется ядром. Ядро обеспечивает,
при необходимости, конвертацию формата данных (edf или json). Затем запрос поступает в модуль Common\AppBackEnd\be_Core\fe_Requests, где запрос перенаправляется в один из модулей обработки запроса, в зависимости от кода запроса RequestID
predicates
fe_Request:event3{integer CommandID, edf_D Parameters,object TaskQueue}::listener.
clauses
...
fe_Request(RequestID,RequestData,TaskQueue):-
RequestID<=3000,
be_CoreTasks():fe_Request(RequestID,RequestData,TaskQueue),
!.
fe_Request(RequestID,RequestData,TaskQueue):-
RequestID<=5000,
be_Tests():fe_Request(RequestID,RequestData,TaskQueue),
!.
fe_Request(RequestID,RequestData,TaskQueue):-
RequestID>5000,
be_Tasks():fe_Request(RequestID,RequestData,TaskQueue),
!.
В соответствующем модуле обработка может выглядеть следующим образом
fe_Request(RequestID,RequestData,TaskQueueObj):-
ResponseData=requestPerformer(RequestID) <- обработчик запроса
response(ResponseID,ResponseData ,TaskQueueObj).
Параметр TaskQueueObj используется для организации очереди передачи данных и должен присутствовать. Результат обработки ResponseData таким образом передается клиенту (FE). Рекомендуется коды операций RequestID и RequestID делать разными, но в пределах установленных диапазонов.
Первичная обработка ответа от сервера (BE)
осуществляется ядром. При этом ядро обеспечивает, при необходимости, конвертацию формата данных (edf или json) и поддержание целостности транзакций. Результаты обработки запроса сервером (BE)
могут быть получены несколькими способами.
Все ответы сначала поступают в обработчик события класса Common\AppFrontEnd\fe_Core\be_Responses
predicates
be_Response:event2{integer ResponseID, edf_D EdfData}::listener.
clauses
...
be_Response(ResponseID, EdfData):-
...
Если
не требуется никакой специальной обработки, то ответ направляется
в один из модулей обработки, в зависимости от кода ответа ResponseID
be_Response(ResponseID, EdfData):-
if ResponseID<= 3000 then
fe_CoreTasks(): tryHandleRespondedData(ResponseID, EdfData)
elseif ResponseID<=5000 then
fe_Tests(): tryHandleRespondedData(ResponseID, EdfData)
else
fe_Tasks(): tryHandleRespondedData(ResponseID, EdfData)
end if,
!.
be_Response(_Any, _Parameters). % - other noncontrolled responses.
Но, как указывалось выше, во-первых, тип запроса может иметь один из трех видов (methodDo,methodRequest и methodChain), а во-вторых, особенности функций приложения и его архитектура могут потребовать специальных способов обработки ответа.
Случай methodDo
В случае methodDo все выглядит триваильно.
Клиент может выполнить запрос как один из предикатов последовательности
...
request(methodDo,fe_SetFrontEndOptions_C, AttrValueList),
...
Сервер должен выполнить предписанную операцию fe_SetFrontEndOptions_C с данными AttrValueList.
Случай methodRequest
В случае methodRequest мы отправляем запрос к серверу и ожидаем получение данных, которые мы должны использовать после исполнения этого запроса.
request(methodRequest,RequestID,RequestData).
Сервер должен ответить результатом обработки (как уже было показано)
fe_Request(RequestID,RequestData,TaskQueueObj):-
ResponseData=requestPerformer(RequestID) <- обработчик запроса
response(ResponseID,ResponseData ,TaskQueueObj).
Если такой запрос предполагает единственный способ обработки ответа, то достаточно предусмотреть обработку ответа предикатом
tryHandleRespondedData(RequestID,RequestData):-
...
в соответствующем модуле обработки.
Если
же на один и тот же запрос в разных точках программы может быть
предусмотрена различная обработка ответа то следует либо как-то (с
использованием каких-то семафоров) программно указать, какой предикат
должен обработать именно данный запрос, либо тут же при запросе
показать как ответные данные по этому запросу обработать. Ядро
приложения позволяет это показать с использованием механизма promise-future.
foo(RequestID,RequestData):-
Future=be_Responses():createResponseReciever_async(ResponseID),
request(methodRequest,RequestID,RequestData ),
_NewF=Future:map(
{(ResponseData) = unit:-
responsePerformer(ResponseData)
}).
Если при этом должен направляться новый запрос к серверу, то он делается тут же.
Пример такого запроса и обработки его результата приведен ниже
clauses
initCoreDictionary(RequestData,CoreDictionary ):-
Future=be_Responses():createResponseReciever(be_TakeCoreDictionaryInitResponse_C),
request(methodRequest,fe_AddCoreDictionaryNameSpace_C, ReRequestData ),
_F=Future:map(
{(Response)=unit:-
if Response=edf::av("create-update-dictionaryFile",s(_DictionaryXmlFile)) then
ItemList=[av(ItemID,a([s(ItemString),s(Meaning)]))||CoreDictionary:getItem(ItemID,ItemString,Meaning)],
request(methodDo,fe_CreateCoreDictionary_C,edf::av(CoreDictionary:nameSpace_P,edf::av(CoreDictionary:fileName_P,edf::a(ItemList))))
end if,
request(methodRequest,fe_GetDictionary_C,edf::s(CoreDictionary:nameSpace_P))
}).
Перед вызовом предиката notify(...) вызывается стандартный для ядра предикат класса be_Responses
Future=be_Responses():createResponseReciever_async(be_TakeCoreDictionaryInitResponse_C),
Он устанавливает с каким идентификатором ожидается ответ (be_TakeCoreDictionaryInitResponse_C) и возвращает объект класса future, в котором будет содержаться результат обработки результата сервером (BE).
Пока ответа нет (отправка запроса к серверу, обработка запроса сервером, транспортировка ответа и т.д.) исполнение предиката
initCoreDictionary(RequestData,CoreDictionary)
на этом завершается и клиент (FE) может заниматься своими делами.
Исполнение вызова предиката
Future:map({...})
начнется лишь когда от сервера будет доставлен ответ с заданным идентификатором.
В
приведенном примере обработка не только выполняет простую операцию, но,
кроме того, содержит новые, зависящие от ответа,обращения к серверу.
Как видно, такая организация кода делает его представление удобным и для восприятия.
Случай methodChain
В случае methodChain мы отправляем запрос к серверу, а в ответ ожидаем поток ответов, который завершается ответом с кодом be_EndOfChain_C.
сообщением получение данных, которые мы должны использовать после исполнения этого запроса.
...
notify(methodChain,RequestID,RequestData).
...
Сервер должен ответить результатом обработки, например, как показано ниже:
fe_Request(RequestID,RequestData,TaskQueueObj):-
StringDataList=requestPerformer(RequestData),
foreach StringData in StringDataList do
response(fe_SomeResponseID_C, s(StringData),TaskQueueObj)
end foreach,
response(be_EndOfChain_C, i(be_SomeResponseID_C),TaskQueueObj).
Для приема ответов от сервера (BE) в модуле-обработчике должна быть предусмотрена конструкция
mapPromise(Future,be_SomeResponseID_C,BeResponces):-
!,
_NewF=Future:map(
{(ResponseData) = future::newUnit():-
responsePerformer(ResponseData),
_NewFuture=BeResponces:createResponseReciever_async(be_SomeResponseID_C)
}).
...
mapPromise(_Any,_AnyRequestID,_BeResponces).
Как видно, после приема очередного сообщения клиент подготавливает прием следующего сообщения вызовом предиката
_NewFuture=BeResponces:createResponseReciever_async(be_SomeResponseID_C)
Остается обеспечить прием самого первого сообщения из цепочки сообщений.
Это может сделано двумя способами:
Во-первых, перед отправкой запроса, как показано ниже
...
_NewFuture=BeResponces:createResponseReciever_async(be_SomeResponseID_C),
request(methodChain,be_SomeRequestID_C,RequestData).
...
Во-вторых,
каждый модуль, посылающий запрос и принимающий ответ, может представить
список сообщений, которые он предполагает обрабатывать с использованием
механизма promise-future. Этот список должен быть помещен в декларацию интерфейса или класса этого модуля в виде константы handleByTasks_C.
constants
handleByTasks_C : integer* =
[
...
be_SomeResponseID_C,
...
].
В последнем случае для всех элементов списка идентификаторов
ответов в момент старта приложения будут созданы объекты, ожидающие
сообщения от сервера.
Обработка исключений
При
возникновении исключительной ситуации у сервера, такая ситуация должна
быть обработана на стороне сервера и сервер должен послать ответ в форме
response(<ResponseID>, <ErrorData>,TaskQueueObj).
То
есть сообщение об ошибке ничем не отличается от любого другого
сообщения. На стороне клиента должна быть предусмотрена обработка
такого сообщения и и соотвествующая программная реакция.
Так, в ядре приложения принята следующая форма сообщения об ошибке
response(<ResponseID>, av("be_error",a(
[av("short",s(<Short_Error_Message>),
av("detailed",s(<Detailed_Error_Message>)),
av("params",a([
av(<ParameterName>,<ParameterValue>),
...
av(<ParameterName>,<ParameterValue>)
]))
])))
Значение ResponseID вместе с идентификацие ответа
определяет в какой модуль клиента это сообщение поступит.
Соответственно, в обрабатывающем модуле должна быть предусмотрена
процедура его обработки.
Так, одна из операций ядра выглядит следующим образом
getDictionary(TaskQueueObj):-
try
foreach NS_DictionaryItemList=be_Dictionary():getDictionary_nd() do
response(be_TakeDictionary_C, NS_DictionaryItemList,TaskQueueObj)
end foreach,
response(be_EndOfDictionaryList_C, edf::n,TaskQueueObj)
catch TraceID do
tuple(ShortInfo,DetailedInfo)=exceptionHandlingSupport::new():getExceptionInfo(TraceID),
log::write(log::error,ShortInfo),
response(be_Error_C, av("be_error",a([av("short",s(ShortInfo)),av("detailed",s(DetailedInfo))])),TaskQueueObj)
end try,
response(be_EndOfChain_C, edf::i(be_TakeDictionary_C),TaskQueueObj).
Как видно, при возникновении ошибки делается запись в лог-файл и клиенту посылается извещение об ошибке.
На стороне клиента такая ошибка обрабатывается нехитрым образом
tryHandleRespondedData(be_Error_C,av("be_error",a(DescrList))):-
av("short",s(ShortMessage)) in DescrList,
av("detailed",s(DetailedData)) in DescrList,
!,
log::write(log::error,"Be_Error",DescrList),
if convert(window,fe_AppWindow()):isShown then
spbExceptionDialog::displayMsg(convert(window,fe_AppWindow()),ShortMessage,DetailedData)
end if.
Здесь делается запись в лог-файл и (при возможности) вызывается диалог, сообщающий об ошибке.
При
обработке любого запроса на стороне сервера (BE) клиенту может быть
отправлено сообщение об ошибке. Даже при выполнении запроса типа messageDo.
Если
исключительная ситуация на стороне сервера не обработана и используется
режим связи по HTTP-протоколу, то внутренний механизм сервера
вырабатывает ошибку, данные о которой доставляются клиенту, а на
стороне клиента такая ошибка доставляется в модуль fe_CoreTasks с кодом be_RpcError_C. Детальную информацию об исключительной ситуации можно получить из содержимого лог-записи.
tryHandleRespondedData(be_RpcError_C,av("be_error",a(DescrList))):-
log::write(log::error,"RpcError ",DescrList),
if convert(window,fe_AppWindow()):isShown then
Message="The Rpc Error. Please see Details!",
convert(window,fe_AppWindow()):delayCall(3,{:-vpiCommonDialogs::note(Message)})
end if,
!.
TimeOut
При
отправке любого запроса со стороны клиента может потребоваться
наложение ограничения на время ожидания ответа. Для этих целей предикаты
request(<Method>,<RequestID>,<RequestData>).
и
Future=createResponseReciever_async(be_SomeResponseID_C)
имеют вторую форму вызова
request(<Method>,<RequestID>,<RequestData>, <TimeOut>).
и
Future=createResponseReciever_async(be_SomeResponseID_C,<TimeOut>)
Время ожидания устнавливается в секундах.
Если время ожидания превысило установленное значение, то:
a) в случае предиката request вырабатывается возможные ошибки
be_Non200Status_C или be_NonResponsiveServer_C. Которые обрабатываются в модуле fe_CoreTasks предикатом tryHandleRespondedData.
При этом флаг backEndAlive_P устанавливается в состояние false и все следующие обращения к серверу (кроме служебных) блокируются.
б) в случае предиката createResponseReciever_async
отсчет времени ожидания начинается от момента вызова этого предиката.
Если слующий за ним запрос не выполняется в течение установленного
времени, то вызывается предикат onTimeOut того модуля, который вызвал предикат createResponseReciever_async.
В частности в модуле обработка этого события выглядит так:
tryHandleRespondedData(be_Timeout_C,av("be_error",a(DescrList))):-
av("params",a(Params)) in DescrList,
av("requestid",i(RequestID)) in Params,
not(RequestID=fe_IsBackEndAlive_C), <- специальная проверка
av("short",s(ShortMessage)) in DescrList,
av("detailed",s(DetailedData)) in DescrList,
!,
log::write(log::error,"TimeOut Other then CheckAlive",DescrList),
if convert(window,fe_AppWindow()):isShown then
spbExceptionDialog::displayMsg(convert(window,fe_AppWindow()),ShortMessage,DetailedData)
end if.
Здесь проверка на отстутствие в списке данных идентификатора запроса fe_IsBackEndAlive_C
делается для того, чтобы отделить обычные запросы приложения или ядра
от специального запроса, определяющего активность сервера.
Проверка активности сервера
Если
клиент и сервер обмениваются данными по протоколу связи HTTP, то
клиент постоянно проверяет активность сервера с заданным
интервалом.
Ядро приложения при своем старте посылает серверу запрос с идентификатором fe_IsBackEndAlive_C
checkServerAlive():-
_F=be_Responses():createResponseReciever_async(be_Alive_C):map(
{(_Data)=unit:-
backEndAlive_P:=true,
fe_AppWindow():showBackEndStatus(true)
}
),
request(methodRequest,fe_IsBackEndAlive_C,edf::n).
Если сервер отвечает, флаг backEndAlive_P устанавливается в состояние true, и в окно приложения посылается сигнал о доступности сервера (showBackEndStatus(true)).
Если же сервер не отвечает, то сообщение об ошибке попадает в модуль fe_CoreTasks, как было описано ранее (с указанием причины), где флаг backEndAlive_P устанавливается в состояние false, а в окно приложения посылается сигнал о недоступности сервера showBackEndStatus(false).
Далее запросы об активности сервера checkServerAlive() посылаются периодически с интервалом, который устанавливается свойством checkAliveInterval_P модуля fe_CoreTasks.
Если сервер недоступен (флаг backEndAlive_P находится в состоянии false), то никакие запросы в направлении сервера не посылаются, кроме запроса, созданного предикатом checkServerAlive(). Таким образом, если сервер начинает отвечать на запросы, то флаг backEndAlive_P устанавливается в состояние true и приложение
может продолжать работать. Возможная рассинхронизация запросов и
ответов ядром приложения не контролируется и не исправляется. Об этом
должно позаботиться конкретное приложение.