(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Учебный пример: DataSnap XE - Callbacks (механизмы обратного вызова)

Источник: embarcadero
Vsevolod Leonov

В ходе выполнение учебного примера, посвященного функциям обратного вызова в DataSnap, будет показан самый простой способ реализации callback-ов. И клиент, и сервер представляют собой приложения Delphi VCL Forms. Здесь мы рассмотрим публикацию в канал и запуск обратных вызовов уведомлениями.

Введение

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

В ходе выполнения примера мы будем использовать Delphi XE для создания простой системы, состоящей из клиента и сервера, для демонстрации работы функций обратного вызова. Серверное приложение будет играть роль коммуникационного центра (communication hub) для нескольких клиентских приложений, запущенных в сети. Это более реалистичный сценарий в сравнении с прямой посылкой уведомления из серверного приложения в клиентское. В большинстве случаев серверное приложение не имеет интерфейса пользователя, так что функции обратного вызова (callbacks) представляют собой отличный механизм взаимодействия между клиентами.

Первым шагом будет создание нового серверного приложения DataSnap с использованием мастера "DataSnap Server".

Механизм обмена сообщениями

Наиболее общим механизмом обмена сообщениями в системах клиент/сервер является "запрос-ответ". Одно приложение ("клиент") посылает сообщение ("запрос") другому приложению в сети ("сервер"), а сервер посылает обратно сообщение ("ответ").

Во многих реальных приложениях бывает полезно иметь и обратный механизм, когда серверное приложение посылает сообщение ("уведомление" - "notification") клиенту. Серверное приложение может сообщить клиенту, что произошло что-то интересное на сервере. Это называется "callback" ("обратный вызов") - ситуация, когда сервер делает обратный (т.е. от сервера к клиенту в нарушение классического принципа) вызов клиента.

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

Возможность сервера асинхронно посылать уведомления одному или нескольким клиентам является очень полезной в различных сценариях.

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

Возможность сервера асинхронно посылать уведомления одному или нескольким клиентам является очень полезной в различных сценариях.

 

Обратные вызовы каналы в DataSnap

Для того чтобы использовать механизм обратных вызовов в DataSnap, нужно определить класс функции обратного вызова, который является производным от класса "TDBXCallback" и переопределяет его виртуальный абстрактный метод "Execute". Вызов метода производится на сервере, а выполняется он на клиенте. Класс "TDBXCallback" определен в модуле "DBXJSON", как показано ниже (некоторые члены выброшены для большей читабельности):

unit DBXJSON;

interface

// …

type
  TDBXCallback = class abstract
  public
    function Execute(const Arg: TJSONValue): TJSONValue; overload; virtual; abstract;
    function Execute(Arg: TObject): TObject; overload; virtual; abstract;
    // …
  end;

В предыдущей версии DataSnap, который поставлялся с RAD Studio 2010, можно было использовать так называемые "лёгкие" ("lightweight") функции обратного доступа. Экземпляр функции обратного вызова передавался в метод сервера, который затем выполнялся продолжительное время. Экземпляр, поступая в серверный метод в качестве параметра, использовался на сервере для вызова его метода "Execute" на клиенте в процессе работы серверного метода, например, для уведомления клиента о прогрессе долго длящейся операции.

В RAD Studio XE, самой последней на текущей момент версии, появились "тяжелые" ("heavyweight" - тяжелые, солидные, мощные в противовес "lightweight" - лёгким, лёгковесным) механизмы обратного вызова. Они могут быть использованы в течение всего жизненного цикла клиентского приложения, а не только в течение работы серверного метода. Это открывает новые возможности для создания приложений различных типов. В оставшейся части этого учебного примера мы собираемся сфокусироваться на "тяжёлых" обратных вызовах, а для простоты будем ссылаться на них как на просто "обратные вызовы".

В архитектуре DataSnap обратные вызовы ассоциируются с "каналами" ("channels"). В целом, может существовать несколько приложений, соединенных с сервером, причем каждый клиент может не содержать или содержать несколько функций обратного вызова. Сервер может "вещать" в канал так, что все функции обратного вызова каждого клиента, зарегистрированные в отдельном канале, получают это уведомление. Также возможно вызвать определенную функцию обратного вызова с использованием уникального идентификатора, заданного при регистрации функции обратного вызова на сервере. Таким образом можно организовать коммуникационную модель "peer-to-peer" (пиринговое взаимодействие, одноранговое взаимодействие, "точка-точка").

Мы испытаем оба подхода: "вещание" в канал и использование отдельного callback-а.

Серверное приложение вызовет метод "Execute" на клиенте асинхронно. Это очень важно понять. Каждое приложение Delphi VCL Forms имеет главный поток выполнения, а в случае многопоточного приложения любые вызовы из других потоков, которые манипулируют графическим интерфейсом пользователя приложения, должны быть синхронизированы. Это именно та ситуация, когда используются обратные вызовы. Методы "Execute" обратного вызова вызывается в потоке, отличном от главного. Существуют различные способы синхронизации вызовов, но, возможно, самым простым путем является использование классового метода "TThread.Queue", который асинхронно вызывает блок кода в рамках главного потока.

 

Реализация обратного вызова на стороне сервера

Наше серверное приложение будет тривиально простым. Функциональность обратного вызова встроена в компонент "DSServer", который является краеугольным камнем любого приложения DataSnap. В этом демонстрационном примере нам не нужно создавать серверные методы, т.к. мы собираемся осуществить взаимодействие между клиентскими приложениями с использованием функций обратного вызова.

Первым шагом является создание нового приложения сервера DataSnap с использованием мастера "DataSnap Server".

Выберите "File -> New -> Other" и в диалоге "New Items" кликните два раза на иконке "DataSnap Server" в категории "Delphi Projects -> DataSnap Server".

На первой странице оставьте опцию "Project type" по-умолчанию как "VCL Forms Application".

На второй странице мы оставим "TCP/IP" в качестве коммуникационного протокола, но снимем "галочку" для генерации "Server method class", т.к. нам этого ненужно для простой демонстрации обратных вызовов. Если вы оставите эту опцию для генерации серверных методов, то проблем не будет. Просто мы не будем их использовать.

На третьей странице оставим значение порта TCP/IP по-умолчанию "211". Желательно всегда проверять, свободен ли порт, нажатием на "Test Port".

Т.к. ранее мы отменили опцию генерации серверного класса, нам не показана страница с просьбой выбрать базовый класс для нашего класса серверных методов.

Кликните на "Finish", и мастер создаст нам новый проект с двумя модулями: главной формой и серверным контейнером. В данном случае не будет модуля с серверными методами.

Кликните "File -> Save All".

Создайте новый папку для всех файлов данного примера, например, "C:\DataSnapLabs\SimpleCallbacks".

Сохраните главную форму приложения как "FormServerUnit" и оставьте имена по-умолчанию для модуля серверного контейнера, обычно, "ServerContainerUnit1".

Сохраните проект как "SimpleCallbackServer".

Выберите главную форму и в инспекторе объектов измените свойство "Name" на "FormServer", а свойство "Caption" на "Delphi Labs: DataSnap XE - Simple Callbacks - Server".

Серверная форма выглядит следующим образом:

Откройте модуль серверного контейнера и проверьте, что на нем всего два компонента: "DSServer1" и "DSTCPServerTransport1".

Вот и всё! Наш сервер готов, и нам не нужно реализовывать ничего особого на стороне сервера, т.к. механизм обратных вызовов встроен в компонент "DSServer1". У нас также есть компонент для транспорта, так что внешние клиенты могут взаимодействовать с экземпляром "DSServer1".

"Save All", "Run without Debugging" и минимизируйте серверное приложение. Оно должно быть запущено до окончания данного примера.

 

Создание клиентского приложения

Теперь настало время реализовать клиентское приложение. Просто кликните на проектную группу в менеджере проектов и выберите "Add New Project".

В диалоге "New Items" выберите "VCL Forms Application" из категории "Delphi Projects".

Кликните на "ОК". Новый проект автоматически добавится в проектную группу.

Кликните на "File -> Save All".

Выберите папку, куда был сохранен проект, и сохраните модуль главной формы клиента как "FormClientUnit", а новый проект как "SimpleCallbacksClient" и всю проектную группу как "SimpleCallbacksGrp".

 

Реализация обратного вызова

Следующим шагом является создание нового класса для обратного вызова, производного от "TDBXCallback", и реализации его метода "Execute". Этот метод будет вызываться асинхронно со стороны сервера для посылки уведомления клиенту.

Добавьте модуль "DBXJSON" в раздел "uses" модуля "FormClientUnit", так как именно здесь и будет находиться класс "TDBXCallback".

Создайте класс "TMyCallback" и переопределите его виртуальный абстрактный метода "Execute". Существуют два варианта для переопределении метода "Execute". Один принимает и возвращает "TObject", а второй принимает и возвращает "TJSONValue". Я собираюсь использовать второй вариант, т.к. оба метода для передачи значения используют формат представления JSON.

На этой стадии исходный код клиента выглядит так:

unit FormClientUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, DBXJSON;

type
  TMyCallback = class(TDBXCallback)
  public
    function Execute(const Arg: TJSONValue): TJSONValue; override;
  end;

  TFormClient = class(TForm)
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormClient: TFormClient;

implementation

{$R *.dfm}

{ TMyCallback }

function TMyCallback.Execute(const Arg: TJSONValue): TJSONValue;
begin
  // ...
end;

end.

А что же происходит, когда вызывается метод "Execute"? Это реально зависит от программиста и логики приложения. Чтобы не усложнять пример, мы добавим компонент "memo" на клиентскую форму, а когда вызывается метод обратного вызова "Execute", мы будем добавлять строчку текста в "memo" с содержимым параметра "Arg", преобразованного в строку.

Давайте зададим открытый метод для класса формы и назовем его "LogMsg", который получит строковый параметр с сообщением для отображения в "memo". Также мы добавим "штамп времени".

Поместим компонент "TMemo" на клиентскую форму. В инспекторе объектов измените его название на "MemoLog".

Добавьте к классу "TFormClient" открытую процедуру "LogMsg(const s: string)" и реализуйте её следующим способом:

procedure TFormClient.LogMsg(const s: string);
begin
  MemoLog.Lines.Add(DateTimeToStr(Now) + ': ' + s);
end;

А вот теперь более интересный трюк. Нам нужно обеспечить потоково-безопасный вызов процедуры "TFormClient.LogMsg" из нашего метода "TMyCallback.Execute".

Давайте создадим потоково-безопасную версию нашего метода "LogMsg", которая будет вызываться в другом потоке.

procedure TFormClient.QueueLogMsg(const s: string);
begin
  TThread.Queue(nil,
    procedure
    begin
      LogMsg(s)
    end
  );
end;

Синтаксис использованного анонимного метода может показаться экзотичным на первый взгляд, но пусть это будет куском кода, который передается как переменная. Мы просто передаем блок кода как второй параметр в метод "TThread.Queue". Этот метод является "классовым" методом класса "TThread", так что нам не нужно инстанцировать (создавать) объект "TThread" для того чтобы вызвать его метод.

Теперь мы можем вызвать потоково-безопасную версию метода "LogMsg" непосредственно из метода "TMyCallback.Execute".

function TMyCallback.Execute(const Arg: TJSONValue): TJSONValue;
begin
  FormClient.QueueLogMsg(Arg.ToString);
  Result := TJSONTrue.Create;
end;

Мы можем возвращать любые данные из метода "Execute", а поскольку эти данные могут быть любыми, мы просто вернем JSON-значение "true".

Теперь нам нужно зарегистрировать обратный вызов на сервере, так чтобы он знал, что вызывать "в обратную сторону".

Для управления обратными вызовами существует специальный класс "TDSClientCallbackChannelManager". Он объявлен в модуле "DSHTTPCommon".

Поместите компонент "TDSClientCallbackChannelManager" на форму и задайте его свойства в инспекторе объектов.

Нам нужно определить имя канала на сервере, с которым будет ассоциирован конкретный обратный вызов. Зададим его как "DelphiLabsChannel".

Также нам нужно задать свойства "CommunicationProtocol", "DSHostname" и "DSPort".

Следующее, что мы должны сделать, это очистить свойство "ManagerId", т.к. мы собираемся сгенерировать это значение в процессе работы (runtime).

Это - очень важный момент. Нам нужно, чтобы каждый экземпляр клиентского приложения был по-разному идентифицирован на сервере. Значение "ManagerID" используется на сервере для идентификации клиентов, так что это значение должно отличаться для каждого экземпляра клиента.

Мы собираемся использовать метода "TDSClientCallbackChannelManager.RegisterCallback" для регистрации экземпляра обратного вызова на сервере. Этот метод принимает два параметра: название обратного вызова, который уникально идентифицирует его на сервере, и ссылку на экземпляр обратного вызова. В нашем случае это "FMyCallback".

Если посмотреть в конструктор класса "TDSClientCallbackChannelManager", то можно увидеть, что значение "ManagerId" генерируется на основе вызова метода "TDSTunnelSession.GenerateSessionId", который возвращает случайную строку из трех чисел. Мы будем использовать эту функциональность для генерации случайных имен наших экземпляров обратного вызова.

Добавьте закрытое поле "FCallbackName: string" к классу формы и добавьте код для инициализации его в событии "OnCreate". Нам потребуется добавить модуль "DSService" в раздел "uses", т.к. именно в нем определен класс "TDSTunnelSession".

Нам также нужно добавить код для инициализации свойства "DSClientCallbackChannelManager1.ManagerId".

uses DSService; // for "TDSTunnelSession"

// … 

procedure TFormClient.FormCreate(Sender: TObject);
begin
  DSClientCallbackChannelManager1.ManagerId := 
    TDSTunnelSession.GenerateSessionId;

  FMyCallbackName := 
    TDSTunnelSession.GenerateSessionId;

  DSClientCallbackChannelManager1.RegisterCallback(
    FMyCallbackName,
    TMyCallback.Create
  );
end;

"DSClientCallbackChannelManager1" владеет ссылкой на обратный вызов, которую мы передаем в метод "RegisterCallback", поэтому нам не нужно как-то по-особому хранить ее.

Вот теперь мы готовы принимать обратные вызовы. На следующем шаге мы реализуем функциональность "вещания в канал". Все вызовы, зарегистрированные в рамках определенного канала, будут реализовывать уведомления с сервера.

 

Вещание в канал

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

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

Первое, что нужно сделать, это добавить компонент "TSQLConnection" на форму для связи с сервером. Возможно, самый простой путь для этого - воспользоваться сервисом "IDE Insight". Просто нажмите F6 и начните вводить "TSQLConnection" для поиска, а затем для добавления его на форму.

Задайте свойство "Driver" компонента "SQLConnection1" как "DataSnap".

Задайте свойство "LoginPrompt" как "False".

Установите свойство "Connected" в "True" для проверки, сможет ли клиент соединиться с сервером.

Согласно типовому сценарию на данной стадии нам нужно сгенерировать код клиентвкого прокси DataSnap, чтобы появилась возможность вызывать серверные методы. Однако в данном случае, этот шаг не является необходимым, т.к. у нас нет пользовательских методов на сервере! Генератор клиентского прокси DataSnap использует класс "TDSAdminClient" в качестве базового для классов клиентских прокси. Этот класс уже содержит значительную часть функциональности, которая готова к использованию, включая вещание в канал и уведомления посредством обратных вызовов. Мы будем использовать класс "TDSAdminClient" напрямую как средство взаимодействия с сервером.

Нам нужно немного расширить интерфейс пользователя клиентского приложения для поддержки вещания в канал.

Добавьте компонент "TButton" на форму. Установите его свойство "Name" в "ButtonBroadcast", а свойство "Caption" в "Broadcast to Channel".

Добавьте компонент "TEdit". Установите его свойство "Name" в "EditMsg" и введите в него произвольное сообщение.

Также можно добавить метку к полю ввода сообщения, чтобы обозначить место для ввода сообщений.

Кликните два раза на кнопке и добавьте следующий код для посылки сообщений методом вещания в канал. Заметьте, что мы можем посылать любые сложные данные в JSON-формате, соответственно, сообщением может быть и сложнее, чем просто строка.

uses DSProxy; // <- for "TDSAdminClient"

// …

procedure TFormClient.ButtonBroadcastClick(Sender: TObject);
var AClient: TDSAdminClient;
begin
  AClient := TDSAdminClient.Create(SQLConnection1.DBXConnection);
  try
    AClient.BroadcastToChannel(
      DSClientCallbackChannelManager1.ChannelName,
      TJSONString.Create(EditMsg.Text)
    );
  finally
    AClient.Free;
  end;
end;

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

Запустите второй экземпляр клиентского приложения, и вы увидите, что сообщения, посланные от одного приложения, будут получать все клиенты!

Это - нереально красиво! Мы теперь можем транслировать любые данные, которые закодированы с помощью JSON, различным приложениям, запущенным в сети.

А как насчет чистого взаимодействия по типу "точка-точка"? Может быть, нам не нужно транслировать все сообщения в канал?

Будет гораздо лучше, если определенный клиент посылает сообщение другому за счет обращения только к одному экземпляру клиентского callback-а.

Это также возможно, но нам придется расширить наше клиентское приложение для поддержки вызовов только заданных callback-ов.

 

Запуск обратных вызовов уведомлением

Остановите обоих клиентов, но оставьте сервер запущенным.

Класс "TDSAdminClient" также содержит метод "NotifyCallback", который может быть использован для реализации модели "точка-точка". У этого метода следующая сигнатура:

function TDSAdminClient.NotifyCallback(ChannelName: string;
ClientId: string;
CallbackId: string; Msg: TJSONValue;
out Response: TJSONValue): Boolean;

Параметр "ChannelName" задает имя коммуникационного канала, с которым ассоциирован экземпляр обратного вызова целевого клиента. "ClientID" и "CallbackID" представляют собой значения, которые передаются в метод "RegisterCallback" компонента "DSClientCallbackChannelManager1" в целевом клиенте. Они оба генерируются случайным образом. "Msg" представляет собой JSON-значение, которое содержит информацию, которую мы хотим послать в целевой callback, а "Response" является возвращаемым параметром (out-параметром) и содержит JSON-значение с закодированным ответом.

Существует также "TDSAdminClient.NotifyObject", который принимает схожие параметры, но вместо использования значения "TJSONValue" для ввода и вывода параметров, он использует потомки TObject, которые автоматически сериализуются и десериализуются из представления в виде JSON.

Процесс вызова индивидуального callback-а уведомлением подразумевает немного ручного труда, так как придется копировать и вставлять значения "ClientID" и "CallbackID" из одного запущенного экземпляра в другой.

Давайте добавим к форме нашего клиентского приложения четыре компонента "TEdit", четыре "TLabel" и четыре "TButton".

Измените свойство "Caption" кнопки на "Notify Callback" и переименуйте поля ввода на: "EditLocalClientId", "EditLocalCallbackId", "EditDestinationClientId", "EditDestinationCallbackId".

В событие "OnCreate" клиентской формы добавьте код, инициализирующий поля ввода:

EditLocalClientId.Text := DSClientCallbackChannelManager1.ManagerId;
EditLocalCallbackId.Text := FMyCallbackName;
EditDestinationClientId.Text := '';
EditDestinationCallbackId.Text := '';

Дважды кликните на кнопку "Notify Callback" и введите следующий код для уведомления удаленного callback-а:

procedure TFormClient.ButtonNotifyClick(Sender: TObject);
var AClient: TDSAdminClient; aResponse: TJSONValue;
begin
  AClient := TDSAdminClient.Create(SQLConnection1.DBXConnection);
  try
    AClient.NotifyCallback(
      DSClientCallbackChannelManager1.ChannelName,
      EditDestinationClientId.Text,
      EditDestinationCallbackId.Text,
      TJSONString.Create(EditMsg.Text),
      aResponse
    );
  finally
    AClient.Free;
  end;
end;

Теперь запустите два или более клиентских приложений и скопируйте "ClientId" и "CallbackId" из клиента, в какой вы хотите послать уведомление, и вставьте в поля "destination" клиента, который должен послать уведомление.

Вот оно и работает! Мы только что осуществили коммуникацию "точка-точка" между клиентскими приложениями DataSnap!

 

Расширяя горизонты

Основной идеей обучающего примера было показать всё максимально просто.

Также возможно использовать обратный вызовы через протокол HTTP в дополнение к TCP/IP. Мы использовали в данном примере архитектуру DataSnap DBX, однако обратные вызовы осуществимы и для DataSnap REST.

RAD Studio XE предлагает очень интересный демонстрационный проект, который показывает все эти возможности.

Вы можете загрузить этот проект непосредственно из IDE с использованием Subversion.

Выберите "File -> Open from Version Control…", а затем введите следующий URL в поле "URL or Repository…":

В поле "Destination" введите папку, в которую вы хотите загрузить демо-проект. В моем случае я создал локальную папку "C:\DataSnapLabs\CallbackChannelsDemo".

Просто нажмите ОК и наберитесь терпения. Сначала вы увидите пустое окно с заголовком "Updating…". После неутомительного ожидания оно отобразит называние всех файлов, которые сгружаются с публичного репозитория на SourceForge.

Здесь три проекта. "ChannelsServerProject" является главным серверным приложением. "DBXClientChannels" и "RESTClientChannels" представляют собой два клиентских приложения. Одно базируется на архитектуре DataSnap DBX, а другое - на новой архитектуре DataSnap REST, появившейся в RAD Studio XE.

Выберите серверный проект и нажмите на кнопку ОК для открытия его в IDE.

Кликните на "ОК" для закрытия окна "Updating". На этой стадии только серверный проект является открытым в IDE.

Теперь нам нужно добавить оба клиентских проекта к проектной группе так, чтобы все три демонстрационных проекта были доступны в IDE.

Кликните правой кнопкой мыши на узле "Project Group" в окне "Project Manager", выберите "Add Existing Project", и выберите проект "DBXClientChannels".

Кликните правой кнопкой снова на "Project Group", выберите "Add Existing Project" и в этот раз выберите проект "RESTClientChannels".

Выберите "File -> Save All" или просто кликните на иконке "Save All".

Дайте проектной группе название. Я назвал её "CallbackChannelsDemo".

На этой стадии мой "Project Manager" выглядит так:

А дальше я предлагаю вам самим проработать с данным примером и в удобном для вас темпе ознакомиться с возможностями DataSnap XE.

Итог

В этом учебном примере мы использовали Delphi XE для создания системы, состоящей из сервера и клиента как приложений Win32, которые взаимодействуют друг с другом посредством протокола TCP/IP и используют обратные вызовы.

Механизм обратных вызовов представляет собой очень полезную альтернативу традиционной системе обмена сообщениями по типу "запрос/ответ" в распределенных приложениях.

С использованием callback-ов серверное приложение может посылать асинхронные уведомления одному или многим экземплярам классов обратных вызовов, находящихся внутри клиентских приложений.

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 01.08.2011 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Delphi Professional Named User
Enterprise Connectors (1 Year term)
DevExpress / ASP.NET Subscription
JIRA Software Commercial (Cloud) Standard 10 Users
WinRAR 5.x 1 лицензия
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
CASE-технологии
Новые материалы
Мастерская программиста
Windows и Office: новости и советы
Новые программы для Windows
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100