Модераторы: LSD, AntonSaburov

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Nio, select, длиные запросы, сохранение канала, Как реализовать оптимально? 
:(
    Опции темы
JavaCraft
Дата 9.2.2007, 14:03 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Извиняюсь за многословность...

При обмене данными посредством nio и службы select, данные из входного потока поступают порциями, которые могут не соотвествовать размерам передаваемого байтового массива (сериализованного объекта в произвольном формате(бинарный, xml, и т.д.)). Очевидно, получаемые куски нужно ЯВНО копировать в буферы накопители для каждого живого ключа и както в будущем заканчивать это накопление. Стандартный алгоритм, где используется один небольшой перезаписываемый буфер, не подходит по вышеписанной причине.
ЯВНОЕ копирование означает, что придется отказаться от оптимизации чтения из системного буфера, при котором реального копирования не происходит. Иначе мы не сможем слепить целевой массив.

Вопрос первый: как лучше всего реализовать данный алгоритм? Может быть есть стандартные готовые решения?

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

Вопрос: Как "сказать" сокету, каналу или селектору сохранить канал и закрыть его если на нем не будет активности начиная с текущего момента и до некоторого таймаута. Да так сказать, чтобы он не заблокировался на этот срок. Я пробовал найти свойство у сокета, канала, в котором зафиксировано время его открытия, но не нашел. Это решило бы проблему. Наследовать класс и добавить свойство не получится, ибо объект входящего соединения создается системой, а не приложением:

Socket              s  = myServerSocket.accept();
SocketChannel c  = s.getChannel();

Подведу итог.
Я предполагаю, что для каждого канала нужны, как минимум, еще два свойства
1) Время открытия
2) Буфер накопитель

также я предполагаю, что придется парсить входящие порции на предмет наличия в них управляющих команд, реализующих некий оригинальный протокол приема массивов.
ТОгда зачем всё это, может быть эффективнее использовать стандартные протоколы типа RMI, CORBA

Можно ли всё это реализовать по другому, как-нибудь поизящнее?

PM MAIL   Вверх
COVD
Дата 9.2.2007, 17:25 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Начнем с конца.

Цитата

ТОгда зачем всё это..?


NIO труднее программировать, но работать ( возможно ) будет быстрее ( неблокирующий ввод/вывод одним потоком, прямое размещение буферов в памяти ).

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

Так что, ответа нет.

Теперь по сути.

Цитата

данные из входного потока поступают порциями


Они могут поступать по одному байту! Могут поступать с паузами (и отсылаться они могут точно также) . Поэтому при чтении из буфера сообщение должно где-то накапливаться.  Приемная сторона должна знать длину сообщения. "Иначе мы не сможем слепить целевой массив".

Цитата

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


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

Цитата

Я предполагаю, что для каждого канала нужны, как минимум, еще два свойства
1) Время открытия -> Время последней активности
2) Буфер накопитель


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

Мне кажется, изящнее ничего пока нет.




Это сообщение отредактировал(а) COVD - 9.2.2007, 17:41
PM MAIL   Вверх
JavaCraft
Дата 10.2.2007, 13:36 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Согласен.

Значит вердикт такой.
СокетСервер в своем потоке принимает сообщения и без обработки передает их соседнему потоку-Монитору, работающему в режиме событийного цикла, который ведет буферизацию, учет времени и состояния готовности для каждого канала. Быстро передав сообщение, СокетСервер читает текущее состояние (команду) текущего канала из переменной в Мониторе. Если состояние="ОТВЕТ ГОТОВ", то СокетСервер берет от Монитора байтовый массив с "ОТВЕТОМ" и передает его в поток вывода, если "ТАЙМАУТ" или "ЗАКРЫТЬ", то закрывает канал, если другая команда, то выполняет ее, если нет команд то ничего не делает и переходит к другому каналу.

Монитор определяет длину сообщения, накапливает массивы до определенной длины или таймаута, меняет переменную состояния, определяет тип данных и по готовности передает их новому потоку Исполнителю. Потоки Исполнители открываются по необходимости и закрываются по готовности. В потоках Исполнителях вызываются тяжелые методы сериализации/десериализации, парсинга, запросы к базе данных, вычисления.

Итого от 2 до 2+X потоков.

Вроде бы не сложно.



Это сообщение отредактировал(а) JavaCraft - 10.2.2007, 13:41
PM MAIL   Вверх
COVD
Дата 10.2.2007, 17:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата

Вроде бы не сложно.


Не принижайте  задачу smile .  Это - сложно. 

Я под потоком - монитором подразумевал поток, занимаюшийся исключительно "отстрелом" мертвых соединений, у которых пауза в активности превысила таймаут. И ничем более. А определять момент окончание приема сообщения может основной поток (ввод/вывод, который с селектором работает). Он же может и отправлять принятое сообщение на обработку потокам - Исполнителям.

Действительно, на приемной стороне надо 2 потока (ввод/вывод + монитор/"санитар") + N потоков - исполнителей (worker threads), которые обычно организуются в виде пула.


Это сообщение отредактировал(а) COVD - 10.2.2007, 17:22
PM MAIL   Вверх
JavaCraft
Дата 13.2.2007, 19:45 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



В общем что-то такое сваял. И вот в чем вопрос!
Сервер принимает пакеты sc.read(buffer) и передает их Монитору, который их склеивает в буферах накопителях.
Монитор готовит ответы(довольно длинные) и помещает их в буфера.
Когда сервер видит, что ответ для данного канала готов, он отправляет его клиенту через тот же самый сокет, методом sc.write(buffer).
Очевидно, если буфер большой, сервер не перейдет к обработке другого канала, пока не передаст буфер по этому каналу. Это потенциально, может перечеркнуть все выгоды асинхронного обмена.

В хелпе Eclipse я обнаружил после
sc.write(buffer);
вызов метода (сжатие путем сдвига текущей позиции в начало)
buffer.compact(); и пояснение "Если буфер передан не полностью.."

Как так не полностью... Как метод write решает когда прекратить передачу?
Как установить критерий прерывания, если меня не устраивают критерии по умолчанию?
Например, уменьшить размер порции или увеличить. И надо ли это делать?

и зачем вообще для этого compact(), который копирует данные? Вместо этого можно просто оставить состояние position буфера как есть. Тогда при следующей попытке write должен продолжить с текущей позиции. Или я не прав?

All, что скажете, по моим вопросам?

PM MAIL   Вверх
COVD
Дата 13.2.2007, 22:18 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



1. Поскольку обмен асинхронный, то ByteBuffer -ов надо два - один для read, другой для write.

2. 
Цитата

Когда сервер видит, что ответ для данного канала готов, он отправляет его клиенту через тот же самый сокет, методом sc.write(buffer).
Очевидно, если буфер большой, сервер не перейдет к обработке другого канала, пока не передаст буфер по этому каналу. Это потенциально, может перечеркнуть все выгоды асинхронного обмена.


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

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

Предположим, что "ответ" готов и его надо отослать. Для этого "ответ" копируется в buffer. Вовсе не обязательно, что в buffer есть место для полного "ответа". Копируется по мере освобождения места в буфере.

Таким образом один поток заполняет буфер данными, а другой поток (который с селектором работает) его опустошает (sc.write(buffer) + buffer.compact()).

Между прочим, при чтении все то же самое, но наоборот.


Это сообщение отредактировал(а) COVD - 13.2.2007, 22:23
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 00:22 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Цитата(COVD @ 13.2.2007,  22:18)
1. Поскольку обмен асинхронный, то ByteBuffer -ов надо два - один для read, другой для write.

Обмен не полностью асинхронный. Если его разрешить полностью асинхронным, то для выполнения запросов, содержащихся в Целых сообщениях, потребуется открывать всё больше и больше потоков-Исполнителей. Предыдущие потоки-Исполнители не будут успевать завершаться. В результате производительность упадет до предела. Такой сервер легко забить запросами и "повесить на собственом галстуке".

У меня сервер сначала асинхронно принимает запрос, затем асинхронно отвечает на него, через один и тот же канал, но фазы приема и передачи синхронны. "Прислал запрос, жди ответ и занимайся своими делами". В таком режиме работают множество каналов, поэтому в целом процесс асинхронный. 

Это сообщение отредактировал(а) JavaCraft - 14.2.2007, 01:07
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 01:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Цитата(COVD @  13.2.2007,  22:18 Найти цитируемый пост)
buffer - фиксированной длины и обычно allocateDirect. "Ответ" в общем случае может быть длиннее буфера. Следовательно,  данные могут передаваться по частям. 


Не совсем точно. Это системный буфер фиксированной длины, а мой buffer может быть любым. Я разобрался с этой проблемой. Прерывание, точнее Исключение, генерируется службой select  в момент заполнения системного буфера и блокировки канала в процессе записи из пользовательского буфера. Служба мониторит эти исключения и передает управление на другие каналы. Поэтому данные и передаются по частям.

Цитата(COVD @  13.2.2007,  22:18 Найти цитируемый пост)
Для этого "ответ" копируется в buffer

Собственно "Ответ" и "Буфер" у меня синонимы, т.к. я имею в виду пользовательский буфер. В любой литературе, примерах и у меня под buffer понимается пользовательский буфер, а не системный. Описаний системного буфера я не встречал. Знаю только что он существует где-то глубоко в потрохах nio, а как он называется там внутри, кто его знает!

Цитата(COVD @  13.2.2007,  22:18 Найти цитируемый пост)
Таким образом один поток заполняет буфер данными, а другой поток (который с селектором работает) его опустошает (sc.write(buffer) + buffer.compact()).

Так-то оно так, но ткните меня носом, где в литературе сказано, что чтение и запись выполняется в один и тот-же системный буфер(СБ). У чтения и записи разная логика работы. Запись в СБ выполняется до тех пор пока в нем есть место, после чего блокируется и СБ "выталкивается" в сеть.  При автоматическом режиме выталкивания, СБ не выталкивается в сеть пока не будет заполнен до конца. 
Если в нем находятся пакеты для чтения, зачем их выталкивать в сеть???!!!

Запись извне в буфер (OP_READ) для Чтения выполняется до тех пор пока в буфере есть место, после чего блокируется. Нигде не упоминается, что чтение не может быть выполнено при нулевом заполнении буфера, а также что оно автоматически инициируется при его заполнении. Этого нет. Буфер просто блокируется и вызывается исключение. Нигде не упоминается, что записанный(OP_WRITE) пакет не нужно читать селектом, и не упоминается как такие пакеты распознавать. Характеристики OP_READ и OP_WRITE являются постоянными параметрами зарегистрированного канала, а не пакетов.

Из всего этого можно сделать вывод, что существуют два системных буфера - для чтения СБ1() и для записи СБ2, скрытые от нас под методами read() и write(). Соотвественно, когда и как мы их будем заполнять или опустошать, в одном потоке или в двух, не принципиально. Ведь это делается независимо! Или я что-то напутал??? Поправте плиз!

Наверное поэтому в примерах в литературе не объясняется почему в одном цикле Select выполняется последовательное read, а потом после обработки, write для одного канала в одном потоке. А надо было бы объяснить!
Ведь при одном потоке у "писателя" просто не будет шансов поиметь свободное место в системном буфере и вставить туда хотя бы 1 байт (OP_WRITE), его постоянно будут заполнять поступающие извне пакеты (OP_READ), которые поступили ПОСЛЕ read(), но ДО write(). Это было бы именно так, если бы был один буфер, но это не так, не случайно в литературе нигде об этом нюансе не предупреждается. Этого просто не происходит.

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

И всё равно не понял зачем использовать compact(), когда лучше обойтись без него. После каждой фазы буфер-накопитель очищается полностью, а в процессе записи достаточно счетчика текущей позиции.
Например, если "Ответ" весит 1Мб, а средний сегмент равен к примеру 128 байт, то данный мегабайт, постепенно уменьшаясь, будет перезаписан compact() примерно 8192 раза, в то время, как можно было бы обойтись без перезаписывания. А если "Ответ" больше 1М, к примеру 2D/3D матрица, как результат численного моделирования физических процессов может достигать, например, 10M? И это вполне законные запросы, которые нельзя отбросить как "слишком большие" - не поймут заказчики.



Это сообщение отредактировал(а) JavaCraft - 14.2.2007, 16:34
PM MAIL   Вверх
COVD
Дата 14.2.2007, 16:21 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата

Обмен не полностью асинхронный. Если его разрешить полностью асинхронным, то для выполнения запросов, содержащихся в Целых сообщениях, потребуется открывать всё больше и больше потоков-Исполнителей. Предыдущие потоки-Исполнители не будут успевать завершаться. В результате производительность упадет до предела. Такой сервер легко забить запросами и "повесить на собственом галстуке".

У меня сервер сначала асинхронно принимает запрос, затем асинхронно отвечает на него, через один и тот же канал, но фазы приема и передачи синхронны. "Прислал запрос, жди ответ и занимайся своими делами". В таком режиме работают множество каналов, поэтому в целом процесс асинхронный. 


Концепцию "Прислал запрос, жди ответ и занимайся своими делами" я отношу все же к синхронному обмену. А то, что обслуживается одновременно много клиентов - это многопоточность сервера.

Добавлено @ 16:29 
Цитата(JavaCraft @ 14.2.2007,  01:06)

Цитата(COVD @  13.2.2007,  22:18 Найти цитируемый пост)
Для этого "ответ" копируется в buffer

Собственно "Ответ" и "Буфер" у меня синонимы, т.к. я имею в виду пользовательский буфер. В любой литературе, примерах и у меня под buffer понимается пользовательский буфер, а не системный. Описаний системного буфера я не встречал. Знаю только что он существует где-то глубоко в потрохах nio, а как называется не важно.


Я буфером называл обьект ByteBuffer, который участвует в операциях read/write. Вы назначаете его размер и он обычно постоянен, в том смысле, что не меняется в зависимости от длины передаваемого сообщения ("Ответа") . А размер ответа может быть любой.

Цитата

Так-то оно так, но ткните меня носом, где в литературе сказано, что чтение и запись выполняется в один и тот-же системный буфер(СБ). 


Я не знаю про "системный" буфер и ничего про него не говорил. Программисту доступен буфер - обьект класса ByteBuffer, который он сам создает и этого достаточно.

В неблокирующем режиме write копирует байты из этого ByteBuffer (столько, сколько сможет в данный момент) на отправку (по вашей терминологии - в "системный" буфер). И все. По какой причине не скопировано или сколько скопировано - не наша забота. 

Важно то, что если N байтов скопировано, то position увеличится на N. А скопированные байты так и останутся на своем месте в ByteBuffer .  Но мы имеем полное право их удалить и для этого делаем compact().

Это сообщение отредактировал(а) COVD - 14.2.2007, 16:52
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 16:37 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Я отредактировал свой пост.
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 16:57 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Цитата(COVD @  14.2.2007,  16:21 Найти цитируемый пост)
Я буфером называл обьект ByteBuffer, который участвует в операциях read/write. Вы назначаете его размер и он обычно постоянен, в том смысле, что не меняется в зависимости от длины передаваемого сообщения ("Ответа") . А размер ответа может быть любой.


У меня копирование Из "Ответа" в "ByteBuffer" исключено, поэтому это одно и тоже. И у меня ByteBuffer переменной длины, в зависимости от длины Ответа, содержащегося в нем. Длина устанавливается один раз, в момент определения длины сообщения Запроса или Ответа. ByteBuffer передается в read() и write(), без копирования, без обнуления позиции и без компактификации. В общем, как я и говорил ранее, у меня Буфер и Ответ это одно и тоже. Я думаю мы поняли, кто, что имеет в виду.
PM MAIL   Вверх
COVD
Дата 14.2.2007, 17:02 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата

Из всего этого можно сделать вывод, что существуют два системных буфера - для чтения СБ1() и для записи СБ2, скрытые от нас под методами read() и write(). Соотвественно, когда и как мы их будем заполнять или опустошать, в одном потоке или в двух, не принципиально. Ведь это делается независимо! Или я что-то напутал??? Поправте плиз!


Нам начихать на эти системные буферы и мы даже не подозреваем о их существовании. Это проблема селектора. Это он знает какой канал готов к чтению, а какой к записи. И выдает нам пачку каналов готовых к операциям ввода вывода по нашему запросу. И мы это можем делать независимо, но обычно это делается одним потоком. Сначала чтение, потом запись. Наверное, можно наоборот. Но наша ответственность - предоставлять ByteBuffers для этого.

Добавлено @ 17:09 
Цитата

у меня Буфер и Ответ это одно и тоже


Раз у вас буфер одноразовый, то и чистить его, действительно, не надо. Поэтому у вас и возникает постоянно вопрос блокировки. Мне кажется это плохо вписывается в идею неблокирующего IO.  
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 17:09 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Цитата(COVD @  14.2.2007,  16:21 Найти цитируемый пост)
В неблокирующем режиме write копирует байты из этого ByteBuffer (столько, сколько сможет в данный момент) на отправку (по вашей терминологии - в "системный" буфер). И все. По какой причине не скопировано или сколько скопировано - не наша забота. 


Ну так понятно по какой причине. "Системный буфер" заполнился и это вызвало исключение.
Это не я придумал "Системный буфер", может он по другому называется, но он реализован в nio или еще глубже. Вопрос в том, является он парой буферов(массивов) - чтения и записи, или пакеты чтения и записи перемешаны в одном буфере(массиве). Я думаю, что имеет место первый случай.

Добавлено @ 17:17 
Цитата(COVD @  13.2.2007,  22:18 Найти цитируемый пост)
Таким образом один поток заполняет буфер данными, а другой поток (который с селектором работает) его опустошает (sc.write(buffer) + buffer.compact()).


Цитата(COVD @  14.2.2007,  17:02 Найти цитируемый пост)
Нам начихать на эти системные буферы и мы даже не подозреваем о их существовании.


Дык который заполняется, который опустошается? 
Из Вашего первого поста можно сделать вывод, что буфер один единственный и операции чтения и записи конкурируют между собой за доступ к нему. Хотя на самом деле никакой конкуренции нет.
Если буфер чтения заполнен и не читается во время, то это не помешает нам записать данные в буфер записи. Хотя мы даже не знаем о их существовании...
PM MAIL   Вверх
COVD
  Дата 14.2.2007, 17:29 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата(COVD @  14.2.2007,  16:21 Найти цитируемый пост)
В неблокирующем режиме write копирует байты из этого ByteBuffer (столько, сколько сможет в данный момент) на отправку (по вашей терминологии - в "системный" буфер). И все. По какой причине не скопировано или сколько скопировано - не наша забота. 


Цитата

Ну так понятно по какой причине. "Системный буфер" заполнился и это вызвало исключение.


Да нет, вроде. Не должно быть исключения. Просто канал не будет готов к записи и не будет селектирован селектором. Со временем "системный буфер" рассосется и селектор селектирует каналы для write.

Добавлено @ 17:17 
Цитата(COVD @  13.2.2007,  22:18 Найти цитируемый пост)
Таким образом один поток заполняет буфер данными, а другой поток (который с селектором работает) его опустошает (sc.write(buffer) + buffer.compact()).


Цитата(COVD @  14.2.2007,  17:02 Найти цитируемый пост)
Нам начихать на эти системные буферы и мы даже не подозреваем о их существовании.


Цитата

Дык который заполняется, который опустошается? 
Из Вашего первого поста можно сделать вывод, что буфер один единственный и операции чтения и записи конкурируют между собой за доступ к нему. Хотя на самом деле никакой конкуренции нет.
Если буфер чтения заполнен и не читается во время, то это не помешает нам записать данные в буфер записи. Хотя мы даже не знаем о их существовании...


Я имел в виду что каждого канала есть два постоянных ByteBuffer - один для read, другой для write. А у вас одноразовые буферы. Вы их создаете под каждое сообщение и выбрасываете. Отсюда и путаница smile

Это сообщение отредактировал(а) COVD - 14.2.2007, 17:31
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 17:30 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Цитата(COVD @  14.2.2007,  17:02 Найти цитируемый пост)
Это проблема селектора. Это он знает какой канал готов к чтению, а какой к записи. И выдает нам пачку каналов готовых к операциям ввода вывода по нашему запросу.


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

Эти Условия не конкурируют между собой и не противоречат друг другу:

(key.readyOps() & SelectionKey.OP_READ)==SelectionKey.OP_READ
(key.readyOps() & SelectionKey.OP_WRITE)==SelectionKey.OP_WRITE

Биты готовности к чтению и к записи устанавливаются и маскируются независимо.

А метод key.readyOps() вовсе не говорит о том что имеется в виду:
1) Зарегистрированный "интерес" канала, то в чем канал "заинтересован"
2) или Текущая готовность, которая может быть не равна зарегистрированному "интересу".

Остается только гадать.

Это сообщение отредактировал(а) JavaCraft - 14.2.2007, 17:51
PM MAIL   Вверх
COVD
Дата 14.2.2007, 17:42 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата

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


Я не спорю. Наверное, два. Но я эту информацию никак не использую. 

Мне достаточно того, что селектор по моему запросу выдает мне список каналов, готовых к операции read. И это означает , что каждый канал из списка имеет хотя бы один байт, доступный для чтения в данный момент. Точно так же селектор мне выдает список каналов, готовых для write. Это означает, что минимум один байт можно послать (вот насчет того в каждом канале или по совокупности - не уверен).  

Все достаточно тупо: 1."Готов к чтению? - Читай"  2. Прочитал (неважно сколько). 3. "Следующий !"



Это сообщение отредактировал(а) COVD - 14.2.2007, 17:55
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 18:00 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Я понял так, что, хотя существуют две формы селекта, но обычно selector.select() находится в заблокированном состоянии до тех пор пока в любом из каналов не появится хотя бы один байт на чтение, или один байт как часть запроса на соединение или хотябы один из каналов "заинтересованных в записи" оказался с пустым буфером записи (по факту очередной проверки во внутреннем цикле селектора).

Любой из каналов может одновременно находиться во всех трех состояниях. Т.е. все три бита установлны.
PM MAIL   Вверх
COVD
Дата 14.2.2007, 18:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Да. Каналам назначаются операции, которые они должны выполнять - read,write,accept,..
А селектор блокируется пока хотя бы один из каналов не готов выполнить хотя бы одну заявленную операцию. Можно селектировать все операции вместе, можно порознь. И никаких "системных буферов". 

Это сообщение отредактировал(а) COVD - 14.2.2007, 18:08
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 18:10 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Цитата
А селектор блокируется пока хотя бы один из каналов не готов выполнить заявленную операцию. 


Может с точностью до наоборот? до тех пор пока хотя бы один канал не станет готовым хотя бы по одной из заявленных операций! ))

Цитата

И никаких "системных буферов".

А куда же write() пишет? прям в сеть что-ли?
А откуда read() читает? Из сети?
 smile 

Это сообщение отредактировал(а) JavaCraft - 14.2.2007, 18:18
PM MAIL   Вверх
COVD
Дата 14.2.2007, 18:28 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата(JavaCraft @ 14.2.2007,  18:10)
Цитата
А селектор блокируется пока хотя бы один из каналов не готов выполнить заявленную операцию. 


Может с точностью до наоборот? до тех пор пока хотя бы один канал не станет готовым хотя бы по одной из заявленных операций! ))

Цитата

И никаких "системных буферов".

А куда же write() пишет? прям в сеть что-ли?
А откуда read() читает? Из сети?
 smile

Честно говоря, не вижу разницы в этих фразах. Смысл один.

Я специально упростил. Конечно, пишется и читается все в "системный буфер", в сетевую карту. Это спрятано в реализации nio. 
PM MAIL   Вверх
JavaCraft
Дата 14.2.2007, 18:33 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 139
Регистрация: 8.2.2007

Репутация: нет
Всего: 1



Кстати, насчет заявленных операций OP_READ и OP_WRITE.
Многих примерах асинхронного однопоточного сервера, вообще не указывается OP_WRITE, хотя запись в канал выпоняется! Т.е. получается их заявлять вообще не обязательно.
Код

 if(skey.isAcceptable()) {
            ch = ssc.accept();
            System.out.println(
              "Accepted connection from:" + ch.socket());
            ch.configureBlocking(false);
            ch.register(sel, SelectionKey.OP_READ);
          } else {
            ch = (SocketChannel)skey.channel();
            ch.read(buffer);
            CharBuffer cb = cs.decode(
              (ByteBuffer)buffer.flip());
            String response = cb.toString();
            System.out.print("Echoing : " + response);
            ch.write((ByteBuffer)buffer.rewind());
            if(response.indexOf("END") != -1) ch.close();
            buffer.clear();
          }



и в таком духе, сплош и рядом.

Добавлено @ 18:44 
Цитата(COVD @  14.2.2007,  18:28 Найти цитируемый пост)
в сетевую карту

Разве nio не на Java написано? разве Java имеет непосредственный доступ к оборудованию?

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

PM MAIL   Вверх
COVD
Дата 14.2.2007, 19:51 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата

Кстати, насчет заявленных операций OP_READ и OP_WRITE.
Многих примерах асинхронного однопоточного сервера, вообще не указывается OP_WRITE, хотя запись в канал выпоняется! Т.е. получается их заявлять вообще не обязательно.


Заявлять операции важно для селектора, чтобы он знал какие каналы вам возвращать по запросу.  Если вы не используете селектор для нахождения каналов, то и заявлять ничего не надо. Если канал всего один (что весьма вероятно в клиентском приложении, у которого всего одно соединение с одним сервером), то и селектор в общем-то не нужен.  

В приведенном вами примере канал гарантированно готов к read. Готов ли он к write, авторы кода не интересуются. Если и не готов, то ничего страшного не произойдет - оперция write вхолостую сработает и все. 

Про сетевые карты и "системные буферы" ничего не могу сказать - не знаю. smile 
PM MAIL   Вверх
iLoveJava
Дата 30.7.2007, 02:07 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 49
Регистрация: 29.7.2007

Репутация: нет
Всего: нет



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

Добавлено через 5 минут и 34 секунды
а при каких нагрузках стоит использовать nio? допустим у меня паралельно 1000 подключений, ну будет 1001+1-2 поток ну да много, но в принципе не так уж много, стоит мне использовать в таком случае nio? или имеет смисл его использовать для 10000 подключений, тогда понятно что 10000 потоков не выйдет...
PM MAIL   Вверх
COVD
Дата 30.7.2007, 15:40 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата

а при каких нагрузках стоит использовать nio? допустим у меня паралельно 1000 подключений, ну будет 1001+1-2 поток ну да много, но в принципе не так уж много, стоит мне использовать в таком случае nio? или имеет смисл его использовать для 10000 подключений, тогда понятно что 10000 потоков не выйдет... 


когда нио разрабатывали, то имели в виду тысячи подключений. Если у вас 1000 и все работает и нет никаких перспектив увеличения нагрузки, то может и нет смысла. НИО более сложна в программировании и если остановится поток, обслуживающий селектор, то все клиенты пострадают. А в схеме 1 клиент=1поток нет такой опасности. 
PM MAIL   Вверх
iLoveJava
Дата 30.7.2007, 23:39 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 49
Регистрация: 29.7.2007

Репутация: нет
Всего: нет



спасибо за оперативный ответ

но можете еще вот это прокоментировать

Цитата

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


В данном проете действительно
Цитата

когда нио разрабатывали, то имели в виду тысячи подключений. Если у вас 1000 и все работает и нет никаких перспектив увеличения нагрузки, то может и нет смысла. НИО более сложна в программировании и если остановится поток, обслуживающий селектор, то все клиенты пострадают. А в схеме 1 клиент=1поток нет такой опасности. 


Но на будущее хочу всеравно данную техннологию изучить думаю еще пригодится.
PM MAIL   Вверх
Vermut
Дата 10.2.2009, 19:33 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 19
Регистрация: 26.12.2008

Репутация: 1
Всего: 0



А тема то интересная.

При обмене данными посредством nio и службы select, данные из входного потока поступают порциями, которые могут не соотвествовать размерам передаваемого байтового массива (сериализованного объекта в произвольном формате(бинарный, xml, и т.д.))

При сериализации больших объектов, а возможно и не больших они будут переданы несколькими порциями байтов в результате так просто связку ObjectInputStream - ByteArrayInputStream использовать неполучиться, плюс проблема что в одной порции байтов может оказаться конец одного объекта и начало другого.

В случае если клиент использует NIO, то проблема чтения на клиенте решается просто все полученные байты можно скармливать в PipedOutputStream и в другой нити восстанавливать объекты  через связку ObjectInputStream - PipedInputStream, таким образом сервер может не заботиться о том чтобы клиент знал длину сереализованного объекта в байтах и гнать объекты сплошным потоком, лишняя нить на клиенте не страшно.

В случае чтения объектов на сервере мы уже так поступить не можем так как любой InputStream 
предполагает отдельную нить выполнения. Таким образом чтобы подготовить массив байт для десериализации сервер должен предварительно знать размер этого массива.

Как на клиенте подготовить байтовый буфер для записи в сокет:
1 Создаём ByteArrayOutputStream
2  Выводим в него любое произвольное число типа int (как будет видно дальше это позволит избежать двойного копирования буферов).
2 Сереализуем объект в тот же ByteArrayOutputStream
3 Получаем методов  toByteArray() массив байт BUF.
4 Получаем длину BUF.length вычитаем 4 и сохраняем в переменной len типа  int;
5 Первые 4 байта(длина int) массива BUF заменяем len
Всё массив BUF готов для отправки через NIO

При чтении на сервере сначала узнаётся длина сереализованного объекта, Выделяется ByteArray необходимого размера. Дальнейшее восстановление объекта дело техники.
Также необходимо учитывать тот факт, что не только сам объект может фрагментироваться, но длина объекта предаётся 4-мя байтами(тип int), следовательно и она при определённых условиях может быть фрагментирована на 2 пакета байт, к примеру в одном пакете 1 байт во втором 3. Тут даже COVD заметил что данные могут идти и по 1-му байту, так что необходимо предусмотреть все пограничные ситуации. 

PM MAIL   Вверх
COVD
Дата 12.2.2009, 18:18 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата(Vermut @ 10.2.2009,  19:33)
В случае чтения объектов на сервере мы уже так поступить не можем так как любой InputStream 
предполагает отдельную нить выполнения. Таким образом чтобы подготовить массив байт для десериализации сервер должен предварительно знать размер этого массива.

Несколько неожиданный вывод, что при неблокирующей обработке (NIO) надо слать размер, а при блокирующей (IO) - не надо.

Относительно того, что 
Цитата

на клиенте решается просто все полученные байты можно скармливать в PipedOutputStream и в другой нити восстанавливать объекты  через связку ObjectInputStream - PipedInputStream, таким образом сервер может не заботиться о том чтобы клиент знал длину сереализованного объекта в байтах и гнать объекты сплошным потоком
.
Я сомневаюсь, что можно "просто..гнать объекты сплошным потоком". После каждого обьекта возможно надо делать то, что делает метод reset() из ObjectOutputStream, т.е. разделять обьекты понятным для ObjectInputStream  способом, чтобы ObjectInputStream смог их восстановить.    

А тема интересная. Правда, появились публикации, что NIO не всегда эффективнее IO. Если интересно, то начать можно здесь Суть, как я понял, в том, что накладные расходы на переключения потоков в современных ОС снизились. Поэтому переключения потоков возможно менее накладны чем переключения селектора и архитектура "каждому клиенту  - поток" не так уж плоха. В пользу NIO один автор высказался, что его проще отлаживать   smile  , потому что там один поток. 

Наверное, когда много соединений и они не активны постоянно, т.е. большую часть времени простаивают, то NIO эффективней. А вот если все соединения одновременно загружают поток данных (живая видеотрансляция, например), то, возможно, проще и эффективней IO.   

Это сообщение отредактировал(а) COVD - 12.2.2009, 18:22
PM MAIL   Вверх
Platon
Дата 12.2.2009, 18:40 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1801
Регистрация: 25.4.2006

Репутация: 3
Всего: 40



Цитата(COVD @  12.2.2009,  19:18 Найти цитируемый пост)
живая видеотрансляция, например

COVD, Как обычно, полезный поток информации ))) Поподробней об этом и в своем блоге.
PM MAIL ICQ   Вверх
COVD
Дата 12.2.2009, 19:43 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Platon, спасибо за доброе слово. В местном блоге неудобно редактировать и защита от спамеров там, похоже, не работает.  smile 
А видеотрансляцию я только в качестве наглядного на мой взгляд  примера помянул. 

Тут гипотеза такая. Сервер имеет канал связи с фиксированной пропускной способностью ( и другие его ресурсы естественно тоже ограничены ). 

Если обмен носит эпизодический характер, то канал связи позволяет обслуживать предположим 10,000 соединений. В том случае лучше, наверное, NIO. 

Если же обмен есть живая трансляция, т.е. всем клиентам отсылается поток данных постоянной интенсивности, то канал связи позволит обслужить например только 100 соединений. И в этой ситуации, возможно, на сервере предпочтительнее использовать IO (обслуживать каждое соединение в отдельном потоке). На самом деле, конечно, клиенты находятся не в равных условиях (разные компьютеры, разный интернет-доступ ) и их способность загружать поток меняется во времени. Поэтому некоторая вынужденная "эпизодичность" в активности соединений во втором сценарии тоже будет иметь место.   

Это просто предположение.

Это сообщение отредактировал(а) COVD - 12.2.2009, 20:34
PM MAIL   Вверх
Vermut
Дата 13.2.2009, 15:09 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 19
Регистрация: 26.12.2008

Репутация: 1
Всего: 0



Цитата(COVD @  12.2.2009,  18:18 Найти цитируемый пост)
Несколько неожиданный вывод, что при неблокирующей обработке (NIO) надо слать размер, а при блокирующей (IO) - не надо.


Ничего не понял из заявления COVD, речь шла про сериализации а не про обмен пакетами неизвестной структуры.

Цитата(COVD @  12.2.2009,  18:18 Найти цитируемый пост)
Суть, как я понял, в том, что накладные расходы на переключения потоков в современных ОС снизились. 


Они и 10 лет назад были невилики. То что время отклика на один запрос многопоточной модели всегда меньше, чем селекторной известно давно.Однако это справедливо для потоков изменяющих относительно независимые данные Вы привели отличный этому пример - видеотрансляции. Чуть коснись дело соблюдения причинно следственных связей в реальном времени и многопоточная модель посыпется. Так как потоки будут переключаться уже не только в ожиданиях операций ввода-вывода, но и начнут блокировать друг друга в борьбе за доступ к общим структурам данных и java.util.concurent не всегда сможет прийти на помощь. Экономия на переключении потоков при операциях ввода-вывода не такой уж сильный аргумент в пользу NIO, как экономия на синхронизации. 

Цитата(COVD @  12.2.2009,  18:18 Найти цитируемый пост)
Наверное, когда много соединений и они не активны постоянно, т.е. большую часть времени простаивают, то NIO эффективней. А вот если все соединения одновременно загружают поток данных (живая видеотрансляция, например), то, возможно, проще и эффективней IO. 


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

PM MAIL   Вверх
COVD
Дата 13.2.2009, 17:10 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Завсегдатай
Сообщений: 1655
Регистрация: 26.7.2005

Репутация: 11
Всего: 43



Цитата

Ничего не понял из заявления COVD, речь шла про сериализации а не про обмен пакетами неизвестной структуры.


Пока писал вам ответ, сообразил, в чем дело. Наверное, отослать размер - самое простое решение. Потому, что ObjectInputStream наверное не имеет неблокирующего read() ?  

Цитата

Они и 10 лет назад были невилики. То что время отклика на один запрос многопоточной модели всегда меньше, чем селекторной известно давно.Однако это справедливо для потоков изменяющих относительно независимые данные Вы привели отличный этому пример - видеотрансляции. Чуть коснись дело соблюдения причинно следственных связей в реальном времени и многопоточная модель посыпется. Так как потоки будут переключаться уже не только в ожиданиях операций ввода-вывода, но и начнут блокировать друг друга в борьбе за доступ к общим структурам данных и java.util.concurent не всегда сможет прийти на помощь. Экономия на переключении потоков при операциях ввода-вывода не такой уж сильный аргумент в пользу NIO, как экономия на синхронизации.


Да, это то вы верно отметили про борьбу за доступ.

Цитата

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


Согласен, причин много. А сделать два варианта и сравнивать в боевых условиях - трудноподьемная задача.

Это сообщение отредактировал(а) COVD - 13.2.2009, 17:14
PM MAIL   Вверх
Страницы: (3) [Все] 1 2 3 
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Java"
LSD   AntonSaburov
powerOn   tux
  • Прежде, чем задать вопрос, прочтите это!
  • Книги по Java собираются здесь.
  • Документация и ресурсы по Java находятся здесь.
  • Используйте теги [code=java][/code] для подсветки кода. Используйтe чекбокс "транслит", если у Вас нет русских шрифтов.
  • Помечайте свой вопрос как решённый, если на него получен ответ. Ссылка "Пометить как решённый" находится над первым постом.
  • Действия модераторов можно обсудить здесь.
  • FAQ раздела лежит здесь.

Если Вам помогли, и атмосфера форума Вам понравилась, то заходите к нам чаще! С уважением, LSD, AntonSaburov, powerOn, tux.

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Java: Работа с сетью | Следующая тема »


 




[ Время генерации скрипта: 0.0915 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.