Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Управление транзакциями. Разработка распределенных приложений в .NET
В этой статье описывается, как выполнять локальные и распределенные транзакции в приложениях Microsoft .NET.

Содержание

Введение

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

У транзакции есть начало и конец, определяющие ее границы (transaction boundaries), внутри которых транзакция может охватывать различные процессы и компьютеры. Все ресурсы, используемые в ходе данной транзакции, считаются участвующими в этой транзакции. Для поддержания целостности используемых ресурсов транзакция должна обладать свойствами ACID: Atomicity (атомарность), Consistency (целостность), Isolation (изоляция) и Durability (отказоустойчивость). Подробнее об основах обработки транзакций см. Processing Transactions (EN) в Microsoft .NET Framework SDK и Transaction Processing в Microsoft Platform SDK.

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

Локальные и распределенные транзакции

Локальной называется транзакция, областью действия которой является один ресурс, поддерживающий транзакции, — база данных Microsoft® SQL Server™, очередь сообщений MSMQ и др. Например, отдельно взятая СУБД может вводить в действие правила ACID, когда в ее распоряжении имеются все данные, участвующие в транзакции. В SQL Server предусмотрен внутренний диспетчер транзакций (transaction manager), предоставляющий функциональность для фиксации (commit) и отката (rollback) транзакций.

Распределенные транзакции могут использовать гетерогенные ресурсы, поддерживающие транзакции, включать самые разнообразные операции, например выборку информации из базы данных SQL Server, считывание сообщений Message Queue Server и запись в другие базы данных. Программирование распределенных приложений упрощается программным обеспечением, способным координировать фиксацию и откат, а также восстановление данных, хранящихся в различных ресурсах. Одной из таких технологий является DTC (Microsoft Distributed Transaction Coordinator). DTC реализует протокол двухфазной фиксации (two-phase commit protocol), гарантирующий непротиворечивость результатов транзакции во всех ресурсах, участвующих в этой транзакции. DTC поддерживает только приложения, в которых реализуются совместимые с ним интерфейсы управления транзакциями. Эти приложения называются диспетчерами ресурсов (Resource Managers) (дополнительную информацию по этой теме см. в Distributed Transactions (EN) в .NET Framework Developer’s Guide). В настоящее время существует довольно много таких приложений — MSMQ, Microsoft SQL Server, Oracle, Sybase и др.

Транзакции баз данных

Вызов хранимой процедуры (stored procedure), которая заключает необходимые операции в операторы BEGIN TRANSACTION и COMMIT/ROLLBACK TRANSACTION, дает наилучшую производительность, позволяя выполнить транзакцию с разовым обменом данными с сервером (single round-trip). Кроме того, транзакции баз данных могут быть вложенными, т. е. внутри активной транзакции можно начать выполнение новой транзакции.

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

CREATE PROCEDURE Proc1
…
AS
   -- Начинаем транзакцию
   BEGIN TRANSACTION
   -- Выполняем операции транзакции
   …
   -- Проверяем наличие ошибок
   If @@Error <> 0
      -- Откатываем транзакцию
      ROLLBACK TRANSACTION
   …
   -- Фиксируем транзакцию
   COMMIT TRANSACTION

Показанная ниже хранимая процедура принимает в качестве входного параметра XML-представление информации о заказе (order). Для выполнения соответствующих вставок в таблицы Orders и OrderDetails хранимая процедура загружает и анализирует XML с помощью системной хранимой процедуры sp_xmlpreparedocument. Как видно из исходного кода, все операции хранимой процедуры включены в явно выполняемую транзакцию, поэтому при неудачном завершении любой операции все внесенные изменения отменяются (откатываются).

Заметьте, что процедура устанавливает флаг XACT_ABORT в ON, указывая, что SQL Server должен автоматически откатить транзакцию, если выполнить какой-нибудь оператор не удастся.

CREATE PROCEDURE InsertOrder 
@Order  NVARCHAR(4000) = NULL
, @OrderId int Output
AS
   SET NOCOUNT ON
   DECLARE @hDoc INT
   DECLARE @PKId  INT
   -- Указываем, что SQL Server должен автоматически откатывать текущую 
   -- транзакцию, если оператор Transact-SQL генерирует ошибку периода
   -- выполнения (run-time error).
   SET XACT_ABORT ON
   -- Начинаем транзакцию
   BEGIN TRANSACTION
   -- Загружаем и анализируем содержимое входного XML-представления 
   -- информации о заказе, а затем помещаем его в XMLDocument
   EXEC sp_xml_preparedocument @hDoc OUTPUT, @Order
   -- Выбираем заголовок заказа из XMLDocument-узла Orders
   -- и вставляем его в таблицу Orders
   INSERT Orders(CustomerId,
                  OrderDate,
                  ShipToName,
                  ShipToAddressId,
                  OrderStatus)
SELECT CustomerId, CONVERT(DateTime,OrderDate), ShipToName,
 ShipToAddressId, OrderStatus
   FROM OPENXML(@hDoc, '/NewDataSet/Orders') 
   WITH ( CustomerId int 'CustomerId',
          OrderDate nvarchar(23) 'OrderDate',
          ShipToName nvarchar(40) 'ShipToName',
          ShipToAddressId int 'ShipToAddressId',
          OrderStatus  int 'OrderStatus')
   -- Выбираем OrderId заказа, только что вставленного в таблицу Orders
   -- для использования при вставке позиций заказа (order details)
   SELECT @PKId = @@IDENTITY
   -- Выбираем позиции заказа из XMLDocument-узла Details 
   -- и вставляем их в таблицу OrderDetails
   INSERT OrderDetails (OrderId,
                       ItemId,
                       UnitPrice,
                       Quantity) 
   SELECT @PKId as OrderId, ItemId, UnitPrice, Quantity
   FROM OPENXML(@hDoc, '/NewDataSet/Details') 
   WITH (ItemId int 'ItemId',
         UnitPrice money 'UnitPrice',
         Quantity int 'Quantity')
   -- Присваиваем значение выходному параметру
   Select @OrderId = @PKId 
   -- Фиксируем транзакцию
   COMMIT TRANSACTION
   EXEC sp_xml_removedocument @hDoc 
   RETURN 0
GO

Хотя такой подход обеспечивает хорошую производительность, при его использовании приходится программировать на Transact SQL, а это сложнее, чем на языке, совместимом с .NET.

Транзакции вручную

Транзакции, выполняемые вручную (manual transactions), позволяют явно управлять границами транзакции с помощью команд начала и конца транзакции. В этой модели также поддерживаются вложенные транзакции, т. е. вы можете начинать новую транзакцию в рамках активной транзакции. Расплата за возможность такого управления заключается в том, что на вас ложится бремя включения в транзакцию нужных ресурсов данных и их координация. Встроенной поддержки распределенных транзакций нет, поэтому управление распределенной транзакцией вручную потребует взять на себя массу обязанностей. Вам придется управлять каждым подключением к ресурсу и его использованием, а также реализовать поддержку свойств ACID в транзакции.

Транзакции ADO.NET, выполняемые вручную

Транзакции вручную поддерживают оба провайдера данных Microsoft ADO.NET, которые предоставляют набор объектов, позволяющих создавать соединение с хранилищем данных, начинать транзакцию, фиксировать или откатывать ее и, наконец, закрывать соединение. В своих примерах мы будем использовать управляемый ADO.NET-провайдер SQL (ADO.NET SQL managed provider).

Для выполнения операций в рамках единой транзакции нужно создать объект SQLTransaction, начать транзакцию с помощью объекта SQLConnection, добиться, чтобы все операции над базой данных проходили в этой транзакции, и зафиксировать или отменить транзакцию. Объект SQLTransaction предоставляет целый ряд свойств и методов для управления транзакцией. При успешном выполнении всех операций транзакции вы можете зафиксировать изменения в базе данных методом Commit. Для отката изменений применяется метод Rollback объекта SQLTransaction.


Для выполнения SQL-команды в транзакции свойство Transaction объекта Command необходимо установить на уже начатую транзакцию.

Visual Basic .NET

Dim conn as SQLConnection
Dim cmd as SQLCommand
Dim txn As SQLTransaction
conn = New SQLConnection("ConnString")
cmd = New SQLCommand
' Открываем соединение
conn.Open()
' Начинаем транзакцию
txn = conn.BeginTransaction()
' Настраиваем свойство Transaction на транзакцию, где выполняется
' SQL-команда
cmd.Transaction = Txn

Visual C# .NET

SQLConnection Conn = New SQLConnection("ConnString");
SQLCommand Cmd = New SQLCommand;
// Открываем соединение
Conn.Open();
// Начинаем транзакцию
SQLTransaction Txn = Conn.BeginTransaction();
// Настраиваем свойство Transaction на транзакцию, где выполняется
// SQL-команда
Cmd.Transaction = Txn;

В примере, представленном ниже, мы выполняем в рамках транзакции две SQL-команды. Первая вставляет заголовок заказа (order header) в таблицу Orders и возвращает OrderId только что созданного заказа. Этот OrderId используется во второй команде, которая вставляет позиции этого заказа в таблицу OrderDetails. Транзакция отменяется, если хотя бы одна из двух команд терпит неудачу; при этом строки в базу данных не добавляются.

Visual Basic .NET

Dim conn As SqlConnection
Dim cmd As SqlCommand
Dim tran As SqlTransaction
' Создаем новое соединение
conn = New SqlConnection("ConnString")
' Открываем соединение
conn.Open()
' Создаем объект Command
cmd = New SqlCommand()
' Создаем транзакцию
tran = conn.BeginTransaction
' Настраиваем свойство Transaction на транзакцию, где выполняется
' SQL-команда
cmd.Transaction = tran
Try
  ' Вставляем заголовок заказа.
  ' Настраиваем свойства Command
   With cmd
      .CommandType = CommandType.StoredProcedure
      .CommandText = "InsertOrderHeader"
      .Connection = conn
      ' Добавляем входные и выходные параметры
      .Parameters.Add("@CustomerId", SqlDbType.Int)
      .Parameters("@CustomerId").Direction = ParameterDirection.Input
      …
      ' Устанавливаем значения параметров
      .Parameters("@CustomerId").Value = 1
      …
      ' Выполняем команду
      .ExecuteNonQuery()
      ' Получаем OrderId добавленного заголовка заказа
      OrderId = .Parameters("@OrderId").Value
      ' Очищаем параметры для следующей команды
      .Parameters.clear()
   End With

   ' Вставляем позиции заказа
   ' Настраиваем свойства Command
   With cmd
      .CommandType = CommandType.StoredProcedure
      .CommandText = "InsertOrderDetail"
      .Connection = conn
      ' Добавляем параметры
      .Parameters.Add("@OrderId", SqlDbType.Int)
      .Parameters("@OrderId").SourceColumn = "OrderId"
      .Parameters("@OrderId").Direction = ParameterDirection.Input
      …
      ' Устанавливаем значения параметров
      .Parameters("@OrderId").Value = OrderId
      .Parameters("@ItemId").Value = 100
      …
      ' Выполняем команду
      .ExecuteNonQuery()
      ' Повторяем показанные выше строки для каждой позиции заказа
   End With
   
   ' Фиксируем транзакцию
   tran.Commit()
Catch
   ' Откатываем транзакцию
   tran.Rollback()
Finally
   ' Код очистки.
   ' Закрываем соединение.
   conn.Close()
End Try

Как видите, две команды выполняются как часть одной транзакции. Если одна из них терпит неудачу, транзакция отменяется, и любые изменения в базе данных откатываются. Заключив код в блок try/catch/finally, вы гарантируете корректное выполнение транзакции: она фиксируется в самом конце блока try после успешного выполнения обеих SQL-команд. Любое исключение перехватывается в блоке catch, где транзакция отменяется и изменения, внесенные в ходе этой транзакции, откатываются.

Управление транзакциями через объекты ADO.NET приводит к менее эффективному блокированию, чем при использовании явных транзакций в хранимых процедурах. Причина в том, что при транзакциях ADO.NET, выполняемых вручную, требуется как минимум столько же двусторонних обменов данными с СУБД, сколько операций выполняется в транзакции плюс два обмена в ее начале и конце. Блокировки удерживаются в течение всего времени передачи вызовов из кода ADO.NET на сервер базы данных и обратно.

Транзакции MSMQ, выполняемые вручную

.NET Framework предусматривает два вида поддержки транзакций MSMQ: внутреннюю (для транзакций вручную) и внешнюю (для автоматических транзакций). В первом случае в рамках транзакции возможен прием или передача нескольких сообщений. Во втором — сообщения участвуют в транзакциях DTC (Distributed Transaction Coordinator).

Транзакции MSMQ, выполняемые вручную, поддерживаются классом MessageQueueTransaction и обрабатываются исключительно ядром MSMQ. Подробности см. в статье Дункана Мак-Кензи (Duncan Mackenzie) Reliable Messaging with MSMQ and .NET (EN).

Автоматические транзакции

Поддержка автоматических транзакций в .NET Framework опирается на службы MTS/COM+. COM+ использует DTC в качестве диспетчера и координатора транзакций при выполнении транзакций в распределенной среде. Это позволяет приложениям .NET выполнять транзакции, охватывающие разнообразные операции над множеством ресурсов, например вставку заказа в базу данных SQL Server, запись сообщения в очередь MSMQ (Microsoft Message Queue), отправку сообщения электронной почты и считывание информации из базы данных Oracle.

Предоставляя модель программирования на основе декларативных транзакций (declarative transactions), COM+ резко упрощает выполнение транзакций, в которых участвуют гетегрогенные ресурсы. Но учтите, что за это приходится расплачиваться снижением производительности, связанным с издержками взаимодействия DTC и COM; кроме того, поддержка вложенных транзакций отсутствует.

Страницы ASP.NET, методы Web-сервисов и .NET-классы можно помечать как транзакционные, присваивая им атрибут декларативной транзакции (declarative transaction attribute).

ASP.NET

<@ Page Transaction="Required">
Web-сервис ASP.NET
<%@ WebService Language="VB" Class="Class1" %>
<%@ assembly name="System.EnterpriseServices" %>
…
Public Class Class1
   Inherits WebService
   <WebMethod(TransactionOption := TransactionOption.RequiresNew)> _
Public Function Method1()
…

Для участия в автоматических транзакциях .NET-класс должен наследовать от System.EnterpriseServices.ServicedComponent, который обеспечивает выполнение класса в COM+. Если вы сделаете именно так, COM+, взаимодействуя с DTC, создаст распределенную транзакцию и подключит к ней все необходимые ресурсы без вашего участия. Кроме того, вам нужно присвоить классу атрибут декларативной транзакции, чтобы определить его поведение при выполнении транзакции.

Visual Basic .NET

<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent

Visual C# .NET

[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
…
}

Транзакционный атрибут класса принимает одно из следующих значений:

  • Disabled. Указывает, что объект никогда не создается в транзакции COM+. Для поддержки транзакций объект может обращаться к DTC напрямую.
  • NotSupported. Указывает, что объект никогда не создается в транзакции.
  • Supported. Указывает, что объект выполняется в контексте транзакции своего создателя. Если объект сам является корневым или если его создатель не выполняется в транзакции, объект создается вне транзакции.
  • Required. Указывает, что объект выполняется в контексте транзакции своего создателя. Если объект сам является корневым или если его создатель не выполняется в транзакции, при создании такого объекта создается новая транзакция.
  • RequiresNew. Указывает, что объекту нужна транзакция и что при его создании создается новая транзакция.

В следующем коде содержится .NET-класс, настроенный на выполнение в COM+. Кроме того, атрибутам сборки присваиваются значения, необходимые для конфигурирования свойств COM+-приложения.

Visual Basic .NET

Imports System
Imports System.Runtime.CompilerServices
Imports System.EnterpriseServices
Imports System.Reflection

' Детали регистрации.
' Имя COM+-приложения в том виде, в каком оно присутствует
' в каталоге COM+
<Assembly: ApplicationName("Class1")> 
' Строгое имя (strong name) для сборки (assembly)
<Assembly: AssemblyKeyFileAttribute("class1.snk")> 
<Assembly: ApplicationActivation(ActivationOption.Server)> 

<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent
    Public Sub Example1()
      …
    End Sub
End Class

Visual C# .NET

using System;
using System.Runtime.CompilerServices;
using System.EnterpriseServices;
using System.Reflection;

// Детали регистрации.
// Имя COM+-приложения в том виде, в каком оно присутствует
// в каталоге COM+
[Assembly: ApplicationName("Class1")]
// Строгое имя для сборки
[Assembly: AssemblyKeyFileAttribute("class1.snk")]
[Assembly: ApplicationActivation(ActivationOption.Server)]

[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
       [AutoComplete]
    public void Example1() 
{
        …
    }
}

<Assembly: ApplicationName(“Class1”)> указывает имя COM+-приложения, в которое устанавливаются компоненты сборки. <Assembly: ApplicationActivation(ActivationOption.Server)> определяет, является ли это приложение сервером или библиотекой. Когда вы указываете ApplicationActivation(ActivationOption.Server), сборку необходимо установить в GAC (global assembly cache) с помощью утилиты командной строки gacutil (GacUtil.exe).

Для преобразования сборки в библиотеку типов, регистрации библиотеки типов и ее установки в заданное COM+-приложение можно использовать утилиту командной строки Regsvcs.exe. Кроме того, эта утилита настраивает свойства, добавленные в сборку программным способом. Например, если в сборке указано ApplicationActivation(ActivationOption.Server), утилита создаст серверное приложение. Если вызванная сборка еще не установлена в COM+, исполняющая среда создаст и зарегистрирует библиотеку типов, а затем установит ее в COM+. COM+-приложение, созданное для сборки, можно просмотреть и настроить в оснастке Component Services.

Процесс создания, регистрации и использования обслуживаемых компонентов (serviced components) подробно рассматривается в разделе Writing Serviced Components (EN) руководства .NET Framework Developer’s Guide.

В следующем коде показывается транзакционный класс, сконфигурированный для запуска под управлением COM+, в котором в рамках транзакции выполняются две SQL-команды. Первая вставляет заголовок заказа в таблицу заказов и возвращает OrderId добавленного заказа. Этот OrderId используется второй командой при вставке позиций заказа в таблицу OrderDetails. Транзакция отменяется, если не удалось выполнить хотя бы одну из двух команд; при этом записи в базу данных не добавляются.

Visual Basic .NET

<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent
    
 Public Sub Example1()
        …
        Try
            ' Создаем новое соединение
            conn = New SqlConnection("ConnString")
            ' Открываем соединение
            conn.Open()
            ' Создаем новый объект Command
            cmd = New SqlCommand()
            ' Вставляем заголовок заказа
            ' Присваиваем значения свойствам Command
            With cmd1
                .CommandType = CommandType.StoredProcedure
                .CommandText = "InsertOrderHeader"
                .Connection = conn
                ' Добавляем входные и выходные параметры
                .Parameters.Add("@CustomerId", SqlDbType.Int)
                …
               .ExecuteNonQuery()
               ' Очищаем параметры для следующей команды
               .Parameters.clear()
            End With
            
            ' Вставляем позиции заказа
            ' Настраиваем свойства Command
            With cmd
                .CommandType = CommandType.StoredProcedure
                .CommandText = "InsertOrderDetail"
                .Connection = conn
                ' Добавляем параметры
                .Parameters.Add("@OrderId", SqlDbType.Int)
                …
                ' Выполняем команду
               .ExecuteNonQuery()
                ' Повторяем эти строки для каждой позиции заказа
            End With
            
            ' Фиксируем транзакцию
            ContextUtil.SetComplete()
        Catch
            ' Откатываем транзакцию
            ContextUtil.SetAbort()
        Finally
            ' Код очистки
        End Try
    End Sub

Используя класс System.EnterpriseServices.ContextUtil, можно получить информацию о контексте COM+-объекта. Этот класс предоставляет методы SetComplete и SetAbort, позволяющие явным образом фиксировать и откатывать транзакцию. Легко догадаться, что метод ContextUtil.SetComplete вызывается в самом конце блока try, когда все операции выполнены успешно и нужно зафиксировать транзакцию. Все исключения перехватывается в блоке catch, где транзакция отменяется с помощью ContextUtil.SetAbort.

Кроме того, с помощью класса-атрибута (attribute class) System.EnterpriseServices.AutoComplete можно добиться, чтобы обслуживаемый компонент автоматически определял, фиксировать или откатывать транзакцию. Компонент «голосует» за фиксацию транзакции, если вызов метода завершился успешно. Если вызов метода привел к генерации исключения, транзакция автоматически отменяется; явный вызов ContextUtil.SetAbort не нужен. Чтобы воспользоваться этой возможностью, вставьте атрибут <AutoComplete> перед методом класса:

Visual Basic .NET

<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent
    <AutoComplete()> Public Sub Example1()
      …
    End Sub
End Class

Visual C# .NET

[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
       [AutoComplete]
    public void Example1() 
{
        …
    }
}

Атрибут <AutoComplete> предлагает самый простой способ программирования транзакций, позволяя обходиться без явных фиксации и отката транзакций. Вы получите точно такую же функциональность, что и в предыдущем примере, где для отмены транзакции явно вызывался метод ContextUtil.SetAbort в блоке catch. Недостаток этого способа в том, что выполнение транзакции неочевидно и при сопровождении кода о ней можно забыть. Кроме того, нет возможности вывода дружественного к пользователю сообщения, если транзакция терпит неудачу. В таких случаях следует явно перехватывать все исключения, вызывать ContextUtil.SetAbort и показывать требуемое сообщение.

В системах, где нужно выполнять транзакции, использующие MSMQ и другие ресурсы, единственно возможный выбор — применение транзакций DTC или COM+. DTC координирует все диспетчеры ресурсов, участвующие в распределенной транзакции, а также управляет деятельностью, связанной с транзакциями. Пример распределенной транзакции MSMQ и SQL Server см. в статье Дункана Мак-Кензи Reliable Messaging with MSMQ and .NET (EN).

Заключение

При использовании каждой из технологий работы с транзакциями приходится идти на компромис между производительностью приложения и удобством сопровождения кода. Запуск реализованной в хранимой процедуре транзакции базы данных обеспечивает наилучшую производительность, так как требуется лишь один двусторонний обмен информацией с базой данных. Кроме того, обеспечивается гибкость управления транзакциями, поскольку явно указываются начало и завершение транзакции. Но, хотя этот способ дает хорошую производительность и гибкость, приходится программировать на Transact SQL, что не так легко, как на языках .NET.

Транзакции, выполняемые вручную с помощью транзакционных объектов ADO.NET, просты в программировании. Благодаря явному применению инструкций начала и завершения транзакции, возможно гибкое управление границами транзакции. Платой за эту легкость и гибкость является снижение производительности за счет дополнительных двусторонних обменов информацией с базой данных при выполнении транзакции.

Автоматическая транзакция — единственно возможный выбор, когда транзакция использует несколько диспетчеров ресурсов, поддерживающих транзакции, например базы данных SQL Server, очереди сообщений MSMQ и т. д. Они значительно упрощают разработку приложений и предъявляют более низкие требования к квалификации программиста. Однако из-за того, что всю работу по координации выполняет служба COM+, возможны дополнительные издержки.


Может пригодится:


Автор: Прийя Дхаван
Прочитано: 5366
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (1)

Прислал: Илья
А исходники западло было выложить? Кажому тепрь собирать куски, раскиданные среди пустого ля-ля?

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру