The OpenNET Project / Index page

[ новости /+++ | форум | wiki | теги | ]

Введение в систему обмена сообщениями ZeroMQ

30.06.2010 06:22

Николя Пиeль (Nicolas Piël) опубликовал введение в технологию ZeroMQ (0MQ), позволяющую организовать быстрый асинхронный обмен сообщениями между высоконагруженными приложениями и интересную тем, что сетевое взаимодействие организовано через новый уровень сетевого стека, который может использовать в качестве транспорта TCP, PGM, IPC и т.п. API библиотеки напоминает обычные сокеты, поддерживается передача сообщений в направлениях точка-точка, издатель-подписчики, запрос-ответ, возможна параллельная рассылка. Система очень быстрая, тесты производительности показали способность обработать на обычном сервере более 8 млн. сообщений в секунду.

Ниже представлен перевод вводной статьи по ZeroMQ:

ZeroMQ - это библиотека обмена сообщениями (Messaging Queue, MQ), которая без особых усилий позволяет создавать сложные коммуникационные решения. Сначала эта программная компонента разрабатывалась как интерфейс для обмена сообщениями (messaging middleware), затем - как легкий коммуникационный протокол, основанный на TCP/IP, а в настоящее время ZeroMQ позиционируется как новая компонента в стеке сетевых протоколов.

Было не просто разобраться в ZeroMQ, даже на основании попытки сравнения MQ-систем, проделанной в компании Linder Research и, прежде всего потому, что ZeroMQ не является полноценной системой как, например RabbitMQ или ActiveMQ: полнофункциональная система, после развертывания и настройки - работает, и можно увидеть ее достоинства и недостатки. ZeroMQ - всего лишь достаточно простой программный интерфейс, позволяющий создать свою собственную MQ-систему.

Почему же нужно использовать ZeroMQ, а не просто стандартный интерфейс Berkeley-сокетов? Ответ, скорее всего, в компромиссе между сложностью реализации и высокой производительностью. Как правило, прикладная система, когда используется по назначению, работает эффективно, но любая попытка добавления функциональности или универсализации использования путем модификации базовых элементов системы приводит к ухудшению производительности. Это справедливо не только для MQ-систем. Поэтому в настоящее время принято использовать небольшие фреймворки, как это используется в web-технологиях.

Кажется, что в ZeroMQ успешно реализован компромисс между функциональностью и эффективностью и ниже перечисляются основные возможности этой библиотеки:

  • Производительность. ZeroMQ действительно работает существенно быстрее, чем большинство реализаций AMQP, и это достигается:

  • Простота использования. С помощью API ZeroMQ передача сообщения действительно проще, чем при использовании сокетов, где вам нужно, например, следить за длиной сокетного буфера, а в ZeroMQ - просто инициировать отправку сообщения, а дробление (или агрегация) и отправка делается API в отдельном потоке, асинхронно с выполнением пользовательского кода. Асинхронная природа методов ZeroMQ особенно удобна для реализации механизмов событийной обработки. Немаловажным удобством в ZeroMQ является отказ от типизации сообщений передаваемых интерфейсом - сообщения никак не интерпретируются интерфейсом и являются BLOB (областью памяти). Таким образом, через ZeroMQ можно передавать что угодно, например сообщения JSON или двоичные форматированные данные типа BSON, Protocol Buffers или Thrift, не чувствуя при этом никаких неудобств.

  • Масштабируемость. Являясь низкоуровневым интерфейсом, ZeroMQ, тем не менее, предоставляет множество опций, например сокет ZeroMQ может быть подключенным к нескольким адресатам и равномерно распределять нагрузку по сети. Другая возможность - это входное мультиплексирование, когда один сокет может получать сообщения от множества отправителей. В ZeroMQ реализована децентрализованная схема обмена сообщениями. Это, в комбинации с высокой производительностью, дает возможность построения распределенных систем любой сложности.

Пример использования ZeroMQ

Приведем пример кода, использующего интерфейс ZeroMQ. В примере используется библиотека Python PyZMQ для ZeroMQ, разработанная Брайаном Грэнджером (Brian Granger).

Первый шаг - выбор транспорта из четырех вариантов, предоставляемых ZeroMQ:

  • INPROC - передача сообщений внутри процесса, между потоками.

  • IPC - передача сообщений между процессами.

  • MULTICAST - широковещательная сетевая передача сообщений с гарантированной доставкой, реализованная посредством инкапсуляции прикладных данных непосредственно в IP-пакет (pgm) или с использованием стандартного UDP-протокола (epgm).

  • TCP - стандартная однонаправленная сетевая передача данных с использованием TCP-протокола.

Второй шаг - разработка инфраструктуры распределенной системы обмена сообщениями. Это, фактически ответ на вопрос "что с чем связано". Ответ на вопрос "как связываются компоненты системы между собой" получен в предыдущем пункте. Компоненты инфраструктуры, которые всегда подключены к сети и выполняют роль серверов, должны выполнить BIND для выбранных типов соединений, а те компоненты, которые будут подключаться к этим серверам как клиенты, выполняют CONNECT, в котором также указывается тип соединения.

Для компонентов распределенной системы, которые подключаются динамически, в ZeroMQ предусмотрен другой набор примитивов:

  • QUEUE - механизм интерактивного взаимодействия, типа "запрос-ответ"

  • FORWARDER - механизм "подписки/публикации" сообщений (publish/subscribe)

  • STREAMER - механизм потокового обмена сообщениями

Для перечисленных методов ZeroMQ, и клиент, и сервер распределенной системы обмена сообщениями подключаются к агенту, у которого открыта пара сокетов на каждое соединение.

Третий шаг - разработка механизма обмена сообщениями. В ZeroMQ предусмотрены следующие типы обмена:

  • REQUEST/REPLY - двусторонняя связь между программами-абонентами распределенной MQ-системы: одна программа-клиент может взаимодействовать с одной или несколькими программами-серверами. Каждое отправленное сообщение предусматривает уведомление о доставке. Уведомление о доставке однозначно идентифицирует получателя сообщения.

  • PUBLISH/SUBSCRIBE - опубликовать сообщение для множества подписчиков. От предыдущего метода отличается тем, что программа-отправитель не получает уведомлений о получении сообщений программами-подписчиками. В данном методе, однако, тоже имеется механизм регулирования: определяется некоторое, пороговое количество сообщений, которое может оставаться в очереди не полученным подписчиком и при попытке опубликовать очередное сообщение, система сбрасывает попытку с соответствующим уведомлением отправителя.

  • UPSTREAM/DOWNSTREAM или Pipeline - этот метод используется для иерархической рассылки сообщений, DOWNSTREAM используется для рассылки вниз по иерархии, а UPSTREAM - наоборот. Программа-отправитель не получает уведомлений о доставке и также, как и в случае PUBLISH/SUBSCRIBE имеется ограничение на количество не полученных сообщений.

  • PAIR - взаимодействие только между клиентом и сервером. Данный тип взаимодействия не предполагает маршрутизации сообщений и не содержит уведомлений о доставке

Рассмотрим эти типы взаимодействия практически

RequestReply. Такой тип взаимодействия программ в сети является типичным, например так работают протоколы HTTP, POP, или IMAP - когда за запросом следует ответ. В ZeroMQ для такого обмена сообщениями клиентская программа использует сокет типа REQ. Серверная программа использует сокет REP. ZeroMQ позволяет через один сокет взаимодействовать с любым количеством парных программ.

Фрагмент кода на Python, который "слушает" TCP-порт 5000 и возвращает отправителю полученные сообщения:



import zmq
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://127.0.0.1:5000")
 
while True:
    msg = socket.recv()
    print "Got", msg
    socket.send(msg)

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



import zmq
context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect("tcp://127.0.0.1:5000")
socket.connect("tcp://127.0.0.1:6000")
 
for i in range(10):
    msg = "msg %s" % i
    socket.send(msg)
    print "Sending", msg
    msg_in = socket.recv()

В примере сделано, что клиент работает с двумя серверами (TCP-порты 5000 и 6000), на которые отправляются сообщения равномерно, по одному открытому сокету REQ, вывод сообщений этой клиентской программы будет такой:



Sending msg 0
Sending msg 1
Sending msg 2
Sending msg 3
Sending msg 4
Sending msg 5
Sending msg 6
Sending msg 7
Sending msg 8
Sending msg 9

в то время как сервер, "слушающий" соединение по TCP-порту 5000 напечатает:



Got msg 0
Got msg 2
Got msg 4
Got msg 6
Got msg 8

а вывод консоли второго сервера, на порту 6000 будет



Got msg 1
Got msg 3
Got msg 5
Got msg 7
Got msg 9

Метод Publish subscribe стал популярным относительно недавно, его наиболее часто используют в механизмах рассылки сообщений, например XMPP или webhooks. Рассылка и получение сообщения не связаны напрямую или говоря по-другому, когда программе нужно отправить сообщение не заботясь, получат ли его - в не компьютерной тематике наиболее близко работе радиостанции: отправленное сообщение доходит только до слушателей, настроенных на волну вашей радиостанции или, возвращаясь к компьютерной тематике - программы, подписавшиеся (subscribed) на сообщения такого типа.

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

Следующий фрагмент кода показывает как создать сервер рассылки, скажем, футбольных новостей:



import zmq
from random import choice
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://127.0.0.1:5000")
 
countries = ['netherlands','brazil','germany','portugal']
events = ['yellow card', 'red card', 'goal', 'corner', 'foul']
 
while True:
    msg = choice( countries ) +" "+ choice( events )
    print ' ->' ,msg
    socket.send( msg )

Сервер постоянно будет "вещать" в сокет типа PUB случайными событиями:



-> portugal corner
-> portugal yellow card
-> portugal goal
-> netherlands yellow card
-> germany yellow card
-> brazil yellow card
-> portugal goal
-> germany corner
…

Клиент-подписчик (через сокет SUB) будет получать только "интересующую его" информацию:



import zmq
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://127.0.0.1:5000")
socket.setsockopt(zmq.SUBSCRIBE, "netherlands")
socket.setsockopt(zmq.SUBSCRIBE, "germany")
 
while True:
    print  socket.recv()
Вывод полученного таким клиентом-подписчиком будет таким: [[PRE] netherlands red card netherlands goal netherlands red card germany foul netherlands yellow card germany foul netherlands goal netherlands corner germany foul netherlands corner …

Метод Pipeline похож на RequestReply, что вместо полудуплексного способа передачи данных (каждый запрос сопровождается ответом), организуется два независимых потока сообщений - от клиента к серверу и - наоборот (UPSTREAM и DOWNSTREAM). Данный способ передачи сообщений может быть удобным для, например, потоковой обработки: данные от клиента передаются серверу, а сервер пересылает результат дальше по цепочке. Как и в описанных ранее методах возможна организация несколько потоков данных через один сокет.

Метод Paired Sockets похож на Berkeley-сокеты: каждому отправителю соответствует один получатель и наоборот - установленное логическое соединение симметрично. Данный метод наименее применим в системах рассылки сообщений и разработан, скорее всего для полноты интерфейса. Пару программ взаимодействующую таким образом можно представить очень просто:

Клиент



import zmq
context = zmq.Context()
socket = context.socket(zmq.PAIR)
socket.bind("tcp://127.0.0.1:5555")

Сервер



import zmq
context = zmq.Context()
socket = context.socket(zmq.PAIR)
socket.connect("tcp://127.0.0.1:5555")

Дальнейшее развитие ZeroMQ

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

К ZeroMQ реализовано 15 языковых интерфейсов (Ada, C, C++, Common Lisp, Erlang, Go, Haskell, Java, Lua, .NET, OOC, Perl, PHP, Python и Ruby) и, таким образом, он может служить универсальной шиной в неоднородной распределенной прикладной системе. Наверняка можно придумать и другие применения для ZeroMQ.

  1. Главная ссылка к новости (http://nichol.as/zeromq-an-int...)
Автор новости: vr13
Лицензия: CC BY 3.0
Короткая ссылка: https://opennet.ru/27137-zeromq
Ключевые слова: zeromq, message
При перепечатке указание ссылки на opennet.ru обязательно


Обсуждение (20) Ajax | 1 уровень | Линейный | +/- | Раскрыть всё | RSS
  • 1.1, klalafuda (?), 10:35, 30/06/2010 [ответить] [﹢﹢﹢] [ · · · ]  
  • +1 +/

    Любопытно. Нужно будет поковырять на досуге.
     
  • 1.2, Аноним (-), 10:38, 30/06/2010 [ответить] [﹢﹢﹢] [ · · · ]  
  • +1 +/
    Сразу скажу, что в питоновских примерах очень не очевиден вариант с подпиской: setsockopt(zmq.SUBSCRIBE, str(messagetype)), в котором messagetype, судя по всему (документации пока нет),- есть первое слово в msg. В PyroES по крайней мере можно швырять произвольные объекты (понятно, с синхронизацией кода объектов на серверах и клиентах, иначе - никак), в которых как хочешь задавай messagetype, хоть через свойство, хоть через object.__class__.__name__ дергай.
     
     
  • 2.4, Omniton (?), 13:06, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +1 +/
    PyroES работает только python-объектами, что сильно ограничивает область использования. На самом деле отстутвие типа это как раз сильная сторона ZeroMQ. Как было описано можно использовать любой формат данных. С учетом, что ZeroMQ перетендует на самую скоростную реализацию обмена сообщениями, до кучи стоит сразу рассмотреть наиболее скоростной формат для обмена данными - MessagePack (http://msgpack.org/), который декларируется как в 4 раза более быстрый чем Protocol Biffers от Google. ZeroMQ+MessagePack = SuperJetMQ  :)
    Просто как раз над этим и занимась, статья в тему.
     
  • 2.9, oxyum (ok), 18:31, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +2 +/
    в SUBSCRIBE передаётся бинарная строка с которой должно начинаться сообщение.

    Само сообщение в этом случае должно быть разделено нулевым байтом на 2 части:

    <"NAME">\x00<"BODY">

    Если в SUBSCRIBE передать строку с завершающим нулём, то будет полное соответствие, если без - то по началу строки. Если на примере, то примерно так:

    возьмём N сообщений:
    "/queue/1\x00body"
    "/queue/2\x00body"
    ...
    "/queue/10\x00body"

    И маски:
    "/queue/" - получит все сообщения,
    "/queue/1" - получит 1 и 10е сообщения
    "/queue/1\x00" - получит только 1е сообщение

     

  • 1.5, const_cast (?), 14:31, 30/06/2010 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    Ну не все так радужно, как написано, в смысле производительности. Вот тут что-то люди сравнивали http://mnb.ociweb.com/mnb/MiddlewareNewsBrief-201004.html. Хотя наверно все зависит от того чего хочешь достичь меряя производительность разных систем. :)
     
     
  • 2.6, Crazy Alex (??), 15:12, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +/
    Действительно, не всё так радужно в данном случае. С другой стороны, у конкурентов я что-то не вижу поддержки UDP и мультикаста, да и реализация на полутора десятков языковых интерфейсов - тоже жирный плюс. Кроме того, тот же DDS использует Corba... Лично мне её разворачивать не очень хочется. В общем, штука интересная, буду иметь в виду.
     
     
  • 3.8, klalafuda (?), 16:06, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +/
    > Кроме того, тот же DDS использует Corba... Лично мне её разворачивать не очень хочется. В общем, штука интересная, буду иметь в виду.

    ..кто юзал TAO тот в цирке не смеется? Это да, есть такое :)

     
     
  • 4.12, const_cast (?), 18:50, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +/
    В TAO есть и UDP и мултикаст, и еще несколько протоколов (shared memory, UNIX sockets, etc), но смеяться не будем, грех. :)
     
  • 3.10, oxyum (ok), 18:32, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +3 +/
    На замену CORBA можно попробовать ZeroC Ice - мне в свое время понравилось. Не без своих проблем мидлварь конечно, но куда приятнее и понятнее корбы.
     
     
  • 4.13, Crazy Alex (??), 20:12, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +/
    Да мне замена корбы как-то без надобности. Я гляжу в сторону чего-то легковеснго и и просто натстраиваемого. В этом плане ZeroMQ приятно вполне. Ну и полтора десятка биндингов, включая перл и эрланг - радуют.
    А айс... Я как слышу слово "платформа", сразу понимаю - что-то здоровое, неудобопонятное и навязывающее свои правила игры. Языков маловато, примерчиков мелких не видно - верный признак, что не просто использовать. Возможносте масса - только обычно нужно что-то гораздо более простое.
     
     
  • 5.15, oxyum (ok), 20:23, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +1 +/
    >Да мне замена корбы как-то без надобности. Я гляжу в сторону чего-то
    >легковеснго и и просто натстраиваемого. В этом плане ZeroMQ приятно вполне.
    >Ну и полтора десятка биндингов, включая перл и эрланг - радуют.

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

    >А айс... Я как слышу слово "платформа", сразу понимаю - что-то здоровое,

    Ну не такое оно и здоровое. Ядро весьма компактно, по сравнению с CORBA и вовсе миниатюрно! :)

    >неудобопонятное и навязывающее свои правила игры. Языков маловато,

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

    Примеры простые вроде были. Сложности возникали обычно при попытках скрестить с каким-нить Qt, у которого свой mail-loop, но тут проблемы и с ZeroMQ будут примерно такие же на самом-то деле.
    > Возможносте масса - только обычно нужно что-то гораздо более простое.

    разные решения, для разных задач.

     
     
  • 6.16, Crazy Alex (??), 20:32, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +1 +/
    >>Да мне замена корбы как-то без надобности. Я гляжу в сторону чего-то
    >>легковеснго и и просто натстраиваемого. В этом плане ZeroMQ приятно вполне.
    >>Ну и полтора десятка биндингов, включая перл и эрланг - радуют.
    >
    >Я писал на обоих, у каждого свои плюсы, надо неплохо изучить оба,
    >чтобы суметь сделать правильный выбор для конкретной задачи.
    >

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

    >>А айс... Я как слышу слово "платформа", сразу понимаю - что-то здоровое,
    >
    >Ну не такое оно и здоровое. Ядро весьма компактно, по сравнению с
    >CORBA и вовсе миниатюрно! :)

    А по сравнению с Asio/ZeroMQ - большое весьма.

    >Примеры простые вроде были. Сложности возникали обычно при попытках скрестить с каким-нить
    >Qt, у которого свой mail-loop, но тут проблемы и с ZeroMQ
    >будут примерно такие же на самом-то деле.

    Примеров не нашел, правда, больше 5 минут не искал. А main-loop... Хорошо только fd комбинируются, всё остальное с чудесами :-)

    >разные решения, для разных задач.

    Святая правда.

     
  • 4.19, klalafuda (?), 09:56, 01/07/2010 [^] [^^] [^^^] [ответить]  
  • +/

    Есть только одна мелочь: модель лицензирования. GPL или коммерческая. Как в свою бытность - Qt. Первое отпадает как класс, второе - как договоритесь. А так - да, наверное, приятная вещь.
     

  • 1.7, gkv311 (ok), 15:25, 30/06/2010 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    Что-то забыли про лицензию написать - LGPL она. Любопытно - примеры выпущены под лицензией GPL, что несколько странно и необычно (хотя я эти примеры не видел ещё).
     
  • 1.11, const_cast (?), 18:44, 30/06/2010 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    > Кроме того, тот же DDS использует Corba...

    DDS конкурирующая технология и может взаимодействовать с CORBA, но реализация у нее независимая (по крайней мере в тех реализациях которые я знаю). В общем и целом DDS немного другой взгляд на вещи на которые раньше смотрели через CORBA.

     
     
  • 2.14, Crazy Alex (??), 20:13, 30/06/2010 [^] [^^] [^^^] [ответить]  
  • +/
    >> Кроме того, тот же DDS использует Corba...
    >
    >DDS конкурирующая технология и может взаимодействовать с CORBA, но реализация у нее
    >независимая (по крайней мере в тех реализациях которые я знаю). В
    >общем и целом DDS немного другой взгляд на вещи на которые
    >раньше смотрели через CORBA.

    Конкретно openDDS использует Corba - http://www.opendds.org/faq.html#DDS_and_CORBA

     

  • 1.17, аноним (?), 23:30, 30/06/2010 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    Чем это лучше spread?
     
     
  • 2.18, oxyum (ok), 01:11, 01/07/2010 [^] [^^] [^^^] [ответить]  
  • +1 +/
    >Чем это лучше spread?

    Для начала это просто разные решения. spread я не использовал, но доку почитал.

    spread - это судя по всему миддлварь, zmq - это теперь просто библиотека которая даёт по сути обычный socket api к более хитрым вариантам сетевого взаимодействия.

    PS: Ну а если вы хотите знать какой-то конкретный плюс, то в FAQ написано, что spread может посылать сообщения размером до приблизительно 100kb, у zmq таких ограничений нет.
    PPS: у zmq больше биндингов к разным языкам, у spread python bindings давно протухли.
    PPPS: а так же разработка spread по сути дела издохла, а zmq активно развивается.

     

  • 1.20, klalafuda (?), 10:01, 01/07/2010 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    > PPS: у zmq больше биндингов к разным языкам, у spread python bindings давно протухли.

    Ну само по себе абсолютное количество биндингов - десять! двадцать! сто!!! - это сферический конь в том самом. Это все равно что абстрактное 'количество поддерживаемых платформ NetBSD'. Ага. А вот как начнешь работать с конкретным нужным биндингом там и полезут все косяки и грабли. Хотя в релизе типа 'поддерживаем'. Это так, мысли в слух.

     
     
  • 2.21, oxyum (ok), 10:15, 01/07/2010 [^] [^^] [^^^] [ответить]  
  • +/
    >> PPS: у zmq больше биндингов к разным языкам, у spread python bindings давно протухли.
    >
    >Ну само по себе абсолютное количество биндингов - десять! двадцать! сто!!! -
    >это сферический конь в том самом. Это все равно что абстрактное
    >'количество поддерживаемых платформ NetBSD'. Ага. А вот как начнешь работать с
    >конкретным нужным биндингом там и полезут все косяки и грабли. Хотя
    >в релизе типа 'поддерживаем'. Это так, мысли в слух.

    Дык это всё понятно, но товарищ аноним хотел что-то узнать, я ему что-то и ответил! :)

     

     Добавить комментарий
    Имя:
    E-Mail:
    Текст:



    Партнёры:
    PostgresPro
    Inferno Solutions
    Hosting by Hoster.ru
    Хостинг:

    Закладки на сайте
    Проследить за страницей
    Created 1996-2024 by Maxim Chirkov
    Добавить, Поддержать, Вебмастеру