СТАТЬЯ |
17.05.01
|
Распределенные вычисления и технологии Inprise
Наталия Елманова
Компьютер Пресс - CD, 1999, N 2
© Copyright N.Elmanova & ComputerPress Magazine.
Эта статья была размещена на сайте www.citforum.ru
Помимо функциональности, связанной с доступом к базам данных, можно использовать Entera для реализации любой другой функциональности, то есть использовать RPC (Remote Procedure Calls - вызовы удаленных процедур) для удаленного доступа к функциям, осуществляющим любые другие действия (например, расчеты).
Отметим, что RPC являются основой механизма обмена данными между клиентом и сервером в рассмотренных выше примерах. Генерация этих вызовов осуществляется неявно при использовании компонента TEnteraProvider.
Ниже будут рассмотрены примеры явного вызова RPC (так называемые Simple RPC Calls). В этом случае следует создать stub-код для сервера и клиента; напомним, что вызовы удаленных процедур осуществляются за счет обмена пакетами данных (эта процедура иногда называется маршалингом или маршрутизацией) между двумя stub-объектами в адресных пространствах сервера и клиента (подробнее об этом рассказано в первой статье данного цикла).
Рис. 9. Осуществление вызовов удаленных процедур
С целью иллюстрации этой процедуры создадим простейший TCP-сервер с помощью Entera 3.2.
Рассмотрим какую-либо функцию, написанную на Pascal, например, для вычисления синуса путем разложения в ряд:
function sin1(x: Double): Double; VAR I:INTEGER; R:DOUBLE;SL:DOUBLE;XX:DOUBLE; CONST DELTA=0.0001 ; begin SL:=X; I:=1; R:=0; XX:=X*X; WHILE (ABS(SL)>DELTA) DO BEGIN R:=R+SL; SL:=-(SL*XX/(2*I))/(2*I+1); inc(i); END; result:=R; end;
Реализация ее с помощью C++ выглядит так:
double sin1(double x) { int ii; double xx,r,sl,f,delta=0.0001; sl=x; ii=1; r=0; xx=x*x; f= fabs(sl); while (f>delta) { r=r+sl; sl=-(sl*xx/(2*ii))/(2*ii+1); f=fabs(sl); ii=ii+1 ; } return(r); }
Создадим простейшее приложение для тестирования этой функции:
Рис. 10. Приложение, подлежащее разбиению на клиента и сервер функциональности
unit SINTST; interface uses Windows, Messages, SysUtils, Classes,Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, TeEngine, Series, ExtCtrls, TeeProcs, Chart,a1; type TForm1 = class(TForm) Chart1: TChart; Series1: TFastLineSeries; BitBtn1: TBitBtn; procedure BitBtn1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} function sin1(x: Double): Double; VAR I:INTEGER; R:DOUBLE;SL:DOUBLE;XX:DOUBLE; CONST DELTA=0.0001 ; begin SL:=X; I:=1; R:=0; XX:=X*X; WHILE (ABS(SL)>DELTA) DO BEGIN R:=R+SL; SL:=-(SL*XX/(2*I))/(2*I+1); inc(i); END; result:=R; end; {sin1} procedure TForm1.BitBtn1Click(Sender: TObject); VAR I:INTEGER;X:DOUBLE;Y:DOUBLE; begin FOR I:=1 TO 270 DO BEGIN X:=0.1*I; //Y:=SIN1(I); Y:=sin1(X); CHART1.SERIES[0].ADDXY(X,Y,FLOATTOSTR(X),clwHITE); END end; end.
Предположим, нам нужно разделить это приложение на сервер функциональности, вычисляющий синус, и "тонкого" клиента, рисующего график этой функции. В этом случае создание сервера функциональности следует начать с определения его интерфейса, для чего следует создать DEF-файл с описанием интерфейса этой функции на языке IDL (Interface Definition Language); вспомним, что IDL фактически представляет собой стандарт описания интерфейсов, и все его диалекты похожи друг на друга.
[ uuid(cdf19a00-22c8-11d2-a36c-008048eb72de), version(1.0) ] interface a1{ double sin1( [in] double x); }
Этот код присваивает серверу уникальный код UUID (Universal Unique Identifier), который можно сгенерировать с помощью алгоритма, определенного Open Software Foundation и реализованного в ряде утилит и функций (например, есть функция Windows API CoCreateGUID, реализующая этот алгоритм). В случае TCP-сервера в действительности он не используется, так же как и номер версии сервера, но используется в случае DCE-сервера. Затем объявляется интерфейс сервера, в котором можно описать несколько функций (в нашем случае она одна).
При написании DEF-файлов следует учитывать соответствие типов данных в DEF-файлах и используемых для создания серверных и клиентских частей языков программирования:
Объявления в DEF-файле | Входные параметры Pascal | Выходные параметры Pascal |
Short x | x: Smallint | var x: Smallint |
long x | x: Longint | var x: Longint |
int x | x: Integer | var x: Integer |
float x | x: Single | var x: Single |
double x | x: Double | var x: Double |
char x | x: Char | var x: Char |
Для создания серверной и клиентской части распределенной системы на базе Entera следует сгенерировать stub-код ("заглушку"), превращающую простые вызовы функций в вызовы удаленных процедур. В частности, при использовании удаленной функции следует сообщить компилятору о том, что ее реализация находится не в клиентском приложении, а на удаленном сервере. "Заглушка" нужна для того, чтобы компилятор мог найти нужную функцию, а ее реализация представляет собой код, осуществляющий вызов удаленных процедур.
Генерация stub-кода для сервера и клиента осуществляется автоматически с помощью утилиты rcmake.exe. Ee параметры в случае любой 32-разрядной версии Delphi выглядят так:
rpcmake -d myserv.def -c delphi2.0 -s delphi2.0
В случае С или С++ параметры rpcmake выглядят следующим образом:
rpcmake -d myserv.def -c c -s c
Здесь параметр -d - имя DEF-файла, -c - язык, для которого должен быть сгенерирован клиентский stub-код, -s - язык, для которого должен быть сгенерирован серверный stub-код (eстественно, эти языки могут быть разными).
В каталоге Entera\TCP\BIN имеется также утилита rpcmgui.exe, представляющая собой GUI-оболочку для rpcmake.exe.
Рис. 11. Утилита RPCMGUI.EXE из комплекта поставки Entera 3.2 для Windows NT.
Наиболее близким описанному выше процессу генерации кода из знакомых Windows-программистам процедур является, пожалуй, генерация stub- и proxy- кода Microsoft Visual C++ для динамически загружаемых библиотек, используемых в COM-сервере и COM-клиенте, с помощью компилятора MIDL на основании IDL-описания интерфейсов сервера. Отметим, что автоматическая генерация stub-кода на основании описания интерфейсов является сейчас общепринятым процессом при организации распределенных вычислений; создание такого кода "вручную" сейчас используется довольно редко.
В результате использования утилиты rpcmake.exe с парамерами, соответствующими выбору Delphi в качестве языка для сервера, получим следующие файлы: a1_c.pas (stub-код, который можно встраивать в клиентское приложение), a1_s.dpr (исходный текст консольного приложения для сервера функциональности), a1.pas - файл-заготовка, используемая при создании сервера, в который разработчику следует добавить код реализации функций (примерно так, как пишутся обработчики событий), a1.inc - код, содержащий объявления функций без их реализации (секция интерфейса).
Код для сервера a1_s.dpr, сгенерированный этой утилитой, выглядит следующим образом:
{$APPTYPE CONSOLE} program a1_s; uses SysUtils, ODET3020, a1; procedure rpc_sin1(dce_table: PTable; socket: Integer); var rv : Integer; x : Double; begin x := dce_pop_double(dce_table,'x'); dce_push_double(socket,'dce_result',sin1(x)); end; procedure rpc_handle(func: PChar; table: PTable; socket: Integer); begin if (StrComp(func,'sin1')=0) then rpc_sin1(table,socket) else dce_unknown_func(func, table, socket); end; {Constants and global variables follow} const VARLEN = 100; var ode_file : PChar; ode_server : PChar; dce_func : PChar; argarr : array[0..5] of array[0..VARLEN] of Char; argptrs : array[0..5] of PChar; argv : PChar; argc : Integer; dce_table : PTable; called_init_func : Integer; socket, rsocket, i, rv : Integer; msgstr : String; begin {main} GetMem(ode_file,VARLEN); GetMem(ode_server,VARLEN); GetMem(dce_func,VARLEN); called_init_func := 0; FillChar(argarr,SizeOf(argarr),#0); FillChar(argv,SizeOf(argv),#0); for i := 0 to ParamCount + 1 do begin StrCopy(argarr[i],PChar(ParamStr(i))); argptrs[i] := @argarr[i]; end; {for} argc := ParamCount + 1; argv := @argptrs; rv := parse_args(argc, argv, ode_file); if (rv=0) then begin Writeln('Env flag (-e) not set'); if ParamCount > 1 then StrCopy(ode_file,PChar(ParamStr(ParamCount))); end; if (dce_setenv(ode_file,NIL,NIL) = 0) then begin msgstr := 'Set env '+ode_file^+' failed'; WriteLn(msgstr); msgstr := 'Reason: '+dce_errstr; Writeln(msgstr); Halt(1); end; ode_server := dce_servername('a1'); dce_checkver(2, 0); socket := dce_init_server(ode_file,ode_server); if (socket <= 0) then begin Writeln('setup server failed'); Writeln('Reason: '+dce_errstr); dce_set_exit; end; while(True=True) do begin dce_table := dce_waitfor_call(socket,dce_func); if (Boolean(dce_should_exit) or Boolean(dce_err_is_fatal)) then Exit else begin if (Boolean(dce_server_is_ded)) then begin dce_spawn(socket,argc,argv,ode_file,ode_server); socket := dce_retsocket; (* save for future *) end; rsocket := dce_retsocket(); (* (old socket closed) *) rpc_handle(dce_func,dce_table,rsocket); dce_send(rsocket,dce_func); dce_recv_conf(rsocket); dce_release; dce_table_destroy(dce_table); if (dce_server_is_ded=0) then dce_close_socket(rsocket); end; {else} end; {while} FreeMem(ode_file,VARLEN); FreeMem(ode_server,VARLEN); FreeMem(dce_func,VARLEN); dce_close_socket(rsocket); dce_table_destroy(dce_table); end.
Секция интерфейса a1.inc выглядит следующим образом:
function sin1(x: Double): Double;
Код реализации функций a1.pas имеет следующий вид (жирным шрифтом выделены строки, которые следует добавить разработчику):
unit a1; interface uses SysUtils, ODET3020; {$include a1.inc} implementation function sin1(x: Double): Double; VAR I:INTEGER; R:DOUBLE;SL:DOUBLE;XX:DOUBLE; CONST DELTA=0.0001 ; begin SL:=X; I:=1; R:=0; XX:=X*X; WHILE (ABS(SL)>DELTA) DO BEGIN R:=R+SL; SL:=-(SL*XX/(2*I))/(2*I+1); inc(i); END; result:=R; end; {sin1} end. {unit}Cервер можно скомпилировать из среды разработки или из командной строки, вызвав компилятор Pascal: Dcc32 -b a1_s.dpr
Перед компиляцией проекта файл ODET3020.pas (интерфейс к ODET3020.DLL - библиотеке, содержащей Entera API) следует поместить в тот же каталог, что и компилируемый проект. Исполняемый файл также требует наличия этой библиотеки в каком-либо доступном каталоге.
Полученный сервер функциональности, как обычно, представляет собой консольное приложение.
В результате использования утилиты rpcmake.exe (или rpcmgui.exe) с парамерами, соответствующими выбору C или C++ в качестве языка для сервера, получим следующие файлы: a1_c.c (stub-код, который можно встраивать в клиентское приложение), a1_s.c (исходный текст консольного приложения для сервера функциональности), a1_s.h - h-файл для myserv_s.c, a1.h - h-файл, содержащий объявления функций без их реализации (используется в клиентском приложении). Реализацию этих функций (a1.c) следует создать разработчику.
Код для сервера a1.с, сгенерированный этой утилитой, выглядит следующим образом:
/*######################## # Server Proxy Procedure Code # generated by rpcmake version2.0 # on Thursday, December 31, 1998 at 19:17:39 # # interface: a1 # ######################## # server stub routines # ########################*/ #ifdef __mpexl #include "dceinc.h" #else #include <dceinc.h> #endif #include <stdio.h> #include <stdlib.h> #include <string.h> #ifdef __cplusplus extern "C" { #endif /* RPC stub and stub handle definitions */ void rpc_handle (char *, struct table *, int); void rpc_sin1 (struct table *, int); #ifdef __cplusplus } #endif void rpc_sin1 (struct table *dce_table,int Socket) { double x; int _i; double sin1(double); x = dce_pop_double(dce_table,"x"); dce_push_double(Socket,"dce_result", sin1(x)); } int main(int argc,char **argv) { char *ode_file = NULL,*ode_server = NULL; char dce_func[VARLEN]; struct table *dce_table; int called_init_func = 0; int socket,rsocket; if (!parse_args(&argc, argv,&ode_file)) { printf ("Env flag (-e) not set\n"); ode_file = argc > 1 ? argv[argc-1] : (char *) NULL; } if (dce_setenv(ode_file,NULL,NULL) == 0) { fprintf(stderr,"Set env %s failed\n", ode_file); fprintf (stderr,"Reason: %s\n", dce_errstr()); exit(1); } ode_server = dce_servername("a1"); dce_checkver(2, 0); if ((socket = dce_init_server( ode_file,ode_server)) <= 0) { fprintf (stderr,"setup server failed\n"); fprintf (stderr,"Reason: %s\n", dce_errstr()); dce_set_exit(); } while(1) { dce_table = dce_waitfor_call(socket,dce_func); if (dce_should_exit() || dce_err_is_fatal() ) {break; } else{ if (dce_server_is_ded()) { dce_spawn(socket,argc,argv,ode_file,ode_server); socket = dce_retsocket(); /* save for future */ } rsocket = dce_retsocket(); /* (old socket closed) */ rpc_handle(dce_func,dce_table,rsocket); dce_send(rsocket,dce_func); dce_recv_conf(rsocket); dce_release(); dce_table_destroy(dce_table); if (!dce_server_is_ded()) { dce_close_socket(rsocket); } } } dce_close_socket(rsocket); dce_table_destroy(dce_table); return(0); } void rpc_handle(char *func,struct table *dce_table, int Socket) { if (strcmp(func,"sin1")==0) (void)rpc_sin1(dce_table,Socket); else (void)dce_unknown_func(func, dce_table, Socket); }
H-файл a1_s.h выглядит следующим образом:
/************************************* * * Server Header for a1 * Generated by rpcmake version 3.0 * on Thursday, December 31, 1998 at 19:17:39 * **************************************/ #ifdef __cplusplus extern "C" { #endif extern double sin1(double ); #ifdef __cplusplus } #endif
Код реализации функции следует создать разработчику. Он должен иметь примерно следующий вид:
USEUNIT("A1_s.c"); USELIB("odet30.lib"); //----------------------------------------------------------------- double sin1(double x) { int ii; double xx,r,sl,f,delta=0.0001; sl=x; ii=1; r=0; xx=x*x; f= fabs(sl); while (f>delta) { r=r+sl; sl=-(sl*xx/(2*ii))/(2*ii+1); f=fabs(sl); ii=ii+1 ; } return(r); }Отметим, что все функции, связанные с выделением памяти в обычном С-коде, следует заменить на соответствующие функции c префиксом dce_ (например, dce_malloc) из Entera API.
Для тестирования сервера следует создать для него env-файл с описанием переменных окружения:
DCE_BROKER=elmanova,16000 DCE_DEBUGLEVEL=DEBUG,DEBUG DCE_LOG=server.log
Далее следует запустить Entera Broker (если он еще не запущен), создав предварительно конфигурационный файл broker.env:
start broker -e broker.env
Затем следует создать и запустить командный файл для запуска сервера:
set odedir = c:\OpenEnv\Entera\TCP start "IT IS A SERVER" a1_s -e server.envТолько после этого можно запускать или отлаживать клиентское приложение.
Код, сгенерированный утилитой rpcmake для клиента Delphi (a1_c.pas), имеет следующий вид:
unit a1_c; interface uses SysUtils, Classes, ODET3020; function sin1(x: Double): Double; implementation function sin1(x: Double): Double; var dce_table : PTable; socket : Integer; rv : Integer; begin dce_table := nil; dce_checkver(2,0); socket := dce_findserver('a1'); if (socket > -1) then begin dce_push_double(socket,'x',x); dce_table := dce_submit('a1','sin1',socket); end; sin1 := dce_pop_double(dce_table,'dce_result'); dce_table_destroy(dce_table); end; end.
Этот код заставляет клиентское приложение обращатьcя к удаленной функции как к локальной. В действительности этот код представляет собой серию вызовов удаленных процедур. Все интерфейсы функций, к которым обращается клиент, содержатся в файле odet30.pas (dceinc.h), а их реализация - в файле odet30.dll.
Создадим клиентское приложение для тестирования созданного сервера. Создадим новый проект, добавим в него сгенерированный модуль a1_c.pas (a1_c.c в случае С++Builder) и сошлемся на него и на odet3020.pas в модуле, связанном с главной формой приложения. На форму поместим интерфейсные элементы, необходимые для тестирования сервера (примерно те же, что и в тестовом примере, расмотренном выше; можно сделать копию этого проекта и внести в нее необходимые изменения).
Создадим обработчики событий, связанных с нажатием на кнопки, а также с созданием и уничтожением формы:
unit sin_cln1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, TeEngine, Series, ExtCtrls, TeeProcs, Chart; type TForm1 = class(TForm) Chart1: TChart; Series1: TFastLineSeries; BitBtn1: TBitBtn; procedure BitBtn1Click(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation uses a1_c, odet3020; {$R *.DFM} procedure TForm1.BitBtn1Click(Sender: TObject); VAR I:INTEGER;X:DOUBLE;Y:DOUBLE; begin FOR I:=1 TO 270 DO BEGIN X:=0.1*I; Y:=sin1(X); CHART1.SERIES[0].ADDXY(X,Y,FLOATTOSTR(X),clwHITE); END; end; procedure TForm1.FormCreate(Sender: TObject); Var rv : integer; msg : array [0 .. 200] of char; begin rv := dce_setenv ('client.env', nil, nil); if (rv = 0) then begin dce_error (msg); MessageDlg('TCP Error: ' + msg, mtInformation, [mbOK], 0); PostMessage(Handle, WM_QUIT, 0, 0); end; end; procedure TForm1.FormDestroy(Sender: TObject); begin dce_close_env; end; end.Отметим, что при создании главной формы приложения следует вызвать процедуру dce_setenv из библиотеки odet3020.dll, указав имя конфигурационного файла client.env в качестве параметра. К моменту запуска клиента этот файл должен существовать и иметь примерно следующий вид:
DCE_BROKER=elmanova, 16000 DCE_LOG=CLIENT.LOG DCE_DEBUGLEVEL=D,D
По окончании работы приложения следует вызвать процедуру dce_close_env, уничтожающую запущенные с помощью dce_setenv сервисы Entera.
Код, сгенерированный утилитой rpcmake для клиента C/C++ (a1_c.c), имеет следующий вид:
/*######################## # Client Proxy Procedure Code # generated by rpcmake version 3.0 # on Thursday, December 31, 1998 at 19:17:39 # # interface: a1 # */ #include <stdio.h> #if defined __mpexl || defined _MACINTOSH_ #include "dceinc.h" #else #include <dceinc.h> #endif double sin1(double x) { double rv = 0; int Socket; struct table *dce_table = NULL; dce_checkver(2, 0); if ((Socket = dce_findserver("a1")) >= 0) { dce_push_double(Socket,"x",x); dce_table = dce_submit("a1", "sin1", Socket); } rv = dce_pop_double(dce_table,"dce_result"); dce_table_destroy(dce_table); return(rv); }
H-файл для него имеет следующий вид:
**************************************/ * * Client Header for a1 * Generated by rpcmake version 3.0 * on Thursday, December 31, 1998 at 19:17:39 * **************************************/ #ifdef __cplusplus extern "C" { #endif extern double sin1(double ); #ifdef __cplusplus } #endif
Код клиентского приложения, аналогичный приведенному выше для Delphi, в случае С++Builder выглядит следующим образом:
//----------------------------------------- #include "dceinc.h" #include "myserv.h" USEUNIT("a1_c.c"); USELIB("odet30.lib"); //----------------------------------------- void __fastcall TForm1::FormCreate(TObject *Sender) { dce_setenv("client.env",NULL,NULL); } //--------------------------------------------------------------------------- void __fastcall TForm1::FormDestroy(TObject *Sender) { dce_release(); } //--------------------------------------------------------------------------- void __fastcall TForm1::BitBtn1Click(TObject *Sender) { int i; double x1,y; for (i=1;i<271;i++) { x1=0.1*float(i); y=sin1(x1); Chart1->Series[0]->AddXY(x1,y,FloatToStr(x1),clWhite); } } //-----------------------------------------
Отметим, что клиентское приложение может быть создано с помощью любых версий и разновидностей Delphi (начиная с 1.0 и включая Standard-версии), любых версий C++Builder и вообще любых компиляторов С++. Помимо этого, для создания клиентских приложений можно использовать и другие средства разработки. В качестве примера рассмотрим Visual Basic for Applications.
Для начала создадим клиентский stub-код для Visual Basic, создав и выполнив командный файл вида:
set ODEDIR=F:\OPENENV\ENTERA\TCP PATH=%ODEDIR%\BIN;%PATH% rpcmake.EXE -d myserv.def -c bas
В результате получим файл a1_c.vb вида:
Function sin1# (x#) dim dce_table as long, Socket as integer call dce_checkver(2,0) Socket = dce_findserver("a1") If (Socket > -1) Then Call dce_push_double(Socket,"x",x) dce_table = dce_submit("a1","sin1",Socket) End If sin1 = dce_pop_double(dce_table,"dce_result") Call dce_table_destroy(dce_table) End function
Теперь создадим новый документ MS Word 97 (или MS Excel 97), сделаем видимой панель инструментов Visual Basic, войдем в режим конструктора и выведем на экран панель интерфейсных элементов. Далее поместим в документ кнопку:
Рис. 12. Создание клиента Entera c помощью VBA
Затем дважды щелкнем на созданной кнопке и перейдем в редактор Visual Basic. Добавим к документу форму UserForm1, поместим на ней несколько меток.
Рис. 13. Создание формы клиента Entera
Теперь создадим обработчик события, связанный с нажатием на кнопку CommandButton1 в документе (его прототип уже имеется в редакторе кода):
Private Sub CommandButton1_Click() x = sin1#(0.25) UserForm1.Label1.Caption = x x = sin1#(0.5) UserForm1.Label2.Caption = x x = sin1#(0.75) UserForm1.Label3.Caption = x x = sin1#(1#) UserForm1.Label4.Caption = x x = sin1#(1.25) UserForm1.Label5.Caption = x x = sin1#(1.5) UserForm1.Label6.Caption = x x = sin1#(1.75) UserForm1.Label7.Caption = x x = sin1#(2#) UserForm1.Label8.Caption = x x = sin1#(2.25) UserForm1.Label9.Caption = x x = sin1#(2.5) UserForm1.Label10.Caption = x x = sin1#(2.75) UserForm1.Label11.Caption = x x = sin1#(3#) UserForm1.Label12.Caption = x UserForm1.Show End Sub
После обработчика события добавим stub-код, содержащийся в сгенерированном файле a1_c.vb.
И, наконец, экспортируем в проект c помощью пункта меню Файл/Экспорт файла модуль odet30.bas из комплекта поставки Entera.
Создадим файл client.env в каталоге, содержащем документ. Возможно, потребуется отредактировать присоединенный к проекту модуль, изменив параметры процедуры dce_setenv, указав путь к файлу client.env:
rv = dce_setenv("client.env", "", "")
Отметим, что библиотека odet30.dll должна быть доступна нашему приложению, так как из нее им производятся вызовы функций.
Теперь можно вернуться в документ из среды разработки Visual Basic for Applications, выйти из режима конструктора и нажать кнопку в документе. На экране появится форма с результатами вызова удаленных процедур примерно следующего вида:
Рис. 14. Клиент Entera на этапе выполнения
Напомним, что перед запуском приложения следует убедиться, что брокер и сервер запущены.
Продолжение статьи будет опубликовано в течение недели.
Обсудить на форуме Borland
Отправить ссылку на страницу по e-mail
Interface Ltd.Отправить E-Mail http://www.interface.ru |
|
Ваши замечания и предложения отправляйте автору По техническим вопросам обращайтесь к вебмастеру Документ опубликован: 17.05.01 |