информационная безопасность
без паники и всерьез
 подробно о проектеRambler's Top100
Страшный баг в WindowsГде водятся OGRыЗа кого нас держат?
BugTraq.Ru
Русский BugTraq
 Анализ криптографических сетевых... 
 Модель надежности двухузлового... 
 Специальные марковские модели надежности... 
 Три миллиона электронных замков... 
 Doom на газонокосилках 
 Умер Никлаус Вирт 
главная обзор RSN блог библиотека закон бред форум dnet о проекте
bugtraq.ru / форум / programming
Имя Пароль
ФОРУМ
если вы видите этот текст, отключите в настройках форума использование JavaScript
регистрация





Легенда:
  новое сообщение
  закрытая нитка
  новое сообщение
  в закрытой нитке
  старое сообщение
  • Напоминаю, что масса вопросов по функционированию форума снимается после прочтения его описания.
  • Новичкам также крайне полезно ознакомиться с данным документом.
[C++] Маленькое уточнение 15.12.03 10:24  Число просмотров: 1976
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
1.*РЕАЛИЗАЦИЯ*

Поскольку по-большому разобраться не получается, попробуем по-маленькому. Я попробую конкретизировать ещё, для дополнительной «затравки», некоторые аспекты. Хотя они и не основные, но разобраться хочется.

В реализации клиента и сервера всё как бы обыденно. Обсуждать особо нечего. Может только некоторые детали ниже. И сервер, и клиент - на блокирующих сокетах. Интерфейс Беркли. Асинхронные штучки в реализации сокетов в Windows здесь не использую (пока). Сервер – многопоточный (каждого клиента обслуживаем в отдельном потоке).

1.1 Потоки клиентов.
Зная свою слабость к большому числу ошибок в коде, решил не оставлять потоки клиентов «безнадзорными». Кое-что здесь уже сделано. Кое-что не стал делать (для «спортивного интереса» было б слишком). Есть пул потоков, который можно контролировать из специального потока-свиппера «менеджера клиентских потоков». Для описания клиентского потока используем структуру CLIENTINFO:

#define MAXCLIENTS 512 #define MAX_CLIENTQUERYES 10 typedef struct _iclientquery { _DWORD hQuery; // дескриптор объекта доступа // к набору данных DBMS _DWORD status; // статус оъекта (создан/открыт/закрыт...) IQueryInterface* IQuery; // указатель на интерфейс объекта … … } IQUERY; typedef struct _clientinfo { HANDLE hThread; // дескриптор потока _BYTE ThreadState; // статус потока SOCKET sock; // клиентский сокет _DWORD addr; // ip клиента _BYTE *stream_buf; // буфер ввода/вывода клиента _DWORD cur_bufsize; // текущий размер буфера IQUERY querylist[MAX_CLIENTQUERYES]; _WORD nqueryes; … … } CLIENTINFO; typedef CLIENTINFO* PCLIENTINFO;

Каждый клиентский поток регистрируется при его создании (спящим). После того, как данные, необходимые клиенту заполнены (структура CLIENTINFO, указатель на которую уже передан потоку при его создании) – будим поток.

while((client_socket=accept(srv_socket, (sockaddr *) &client_addr, &client_addr_size))) { PCLIENTINFO sockinfo = allocinfo(); if ( !sockinfo ) { printf("\n\aAllocation error. Close connection."); byeclient( client_socket ); continue; } … … unsigned thID; uintptr_t hThread = _beginthreadex( NULL, 0, &ClientThread, sockinfo, CREATE_SUSPENDED, &thID ); if (! hThread) { printf("\n\aCreating client thread error. Close connection."); byeclient( client_socket ); freeinfo( sockinfo); continue; } sockinfo->sock = client_socket; sockinfo->addr = client_addr.sin_addr.S_un.S_addr; sockinfo->hThread = (HANDLE) hThread; sockinfo->ThreadState |= THS_CREATED | THS_SUSPENDED; if (ResumeThread( (HANDLE) hThread ) != -1L) sockinfo->ThreadState &= ~THS_SUSPENDED; … … }

Доступ потока-свиппера к пулу быстрый, без существенной блокировки потоков клиентов. Свиппер будет искать и чистить подвисшие потоки по такому сценарию. В критической секции (сама по себе быстрая) можно выполняем «моментальный снимок» - копию пула. Затем, «отпустив» критическую секцию, можно уже в копии пула искать процессы-зомби по некоторому критерию (скорее всего – некоторый таймаут клиентского потока, который по типу «жив/не жив» контролируется из клиентского потока), и если таковые найдутся, то в свиппер-потоке: приостановить клиентский поток, почистить его ресурсы (буфер ввода/вывода, открытые наборы данных DBMS, объекты доступа к данным DBMS) и потом уж - убить подвисший поток. Останется быстро вторым вхождением в критическую секцию почистить реальный пул с CLIENTINFO уже задавленного потока-зомби. Взаимных продолжительных блокировок клиентских потоков не будет. Клиентский поток обращается к пулу в критической секции только один раз и на очень короткое время – для освобождения своего дескриптора и саморазрегистрации (удаления себя из пула потоков) непосредственно перед завершением функции потока.
Как думаете нужен ли свиппер, и какой?
Здесь было сказано о буфере ввода/вывода клиентского потока, см. stream_buf в структуре CLIENTINFO выше. Вообще, на мой взгляд, это вопрос – как выделять память потоку для буфера ввода/вывода? В той реализации, которая сделана из «спортивного интереса» – сделано дубово. Память выделяется в одной, общей куче процесса. Какие идеи в части памяти, господа?

1.2 Буфер ввода/вывода клиентского потока.
Неожиданно наткнулся на вопрос, на который раньше не обращал внимания. Выше, в п. 1.1 сказано, что память для буфера ввода/вывода клиентского потока выделяется из одной кучи процесса. Сам по себе вопрос. Другой вопрос связан с тем, как выделяется? Проще и быстрее всего, с точки зрения доступа – realloc (поэтому-то и realloc). Алгоритм здесь, казалось бы, может быть совсем простым – вычитываем из заголовка «сообщения» его размер (размер заголовка плюс данные), и делаем realloc требуемого размера. Сомнения здесь связаны с возможной фрагментацией памяти из большого числа клиентских потоков, и как следствие с общим кирдык. В случае, когда каждый из клиентских потоков сервера будет «шинковать» память процесса для маленьких «сообщений», мне кажется, что возможна ситуация, когда память будет нарезана, как рулетик. Пока я сделал тупо – память выделяется блоками кратными 1024 и в два раза больше, чем было запрошено, если текущий буфер мал. При следующем запросе вначале проверяем текущий размер буфера, и если «сообщение» помещается, то realloc не делаем вовсе. Какие идеи, господа?
У меня – никаких нормальных.

1.3 Клиент.
В работе клиента есть одна особенность – возможно большие таймауты, когда пользователь пошёл покурить. Т.е. соединились, прочитали что надо, а дальше юзвер ничего не делает. Просто рвать соединение по таймауту, пусть даже достаточно большому, на мой взгляд неразумно. Молчание клиента может быть связано как с тем, что клиент «курит», так и с тем, что клиентская сторона подвисла. Именно подвисла, а не разрушила сокет, разорвала соединение и т.д., что серверная сторона заметит по клиентскому сокету на своей стороне. Другие патологические ситуации, когда клиент исчез, оставив полуоткрытое соединение на конце сервера, а сервер ожидает каких-либо данных от клиента, здесь я не рассматриваю. Хотя и это может быть. Единственное решение в этой ситуации, на мой взгляд – это пинги пингать. Опция TCP keepalive вряд ли поможет. Да и не рекомендуют её гуру в Host Requirements RFC. Не гуру, но практики на форумах так же замечают, что реальное время таумаута keepalive в различных реализациях и конфигурациях TCP может быть различным - 1,5 – 2 и более часов. Мутно всё это.
Таким образом, мне кажется, что в любом мало-мальски нормальном прикладном протоколе, рассчитанном на длительные соединения (имеется ввиду только TCP, а не соединения с DBMS) обрабатывать длительные таймауты следует самостоятельно. Не проблема это включить в обсуждаемый протокол. Не очень ясно другое, как это сделать в реализации сервера и клиента. Например, на клиенте придётся или делать это в отдельном потоке (все данные и получаем и отсылаем в отдельном потоке) – изврат, или использовать асинхронные сокеты Windows в сочетании с таймерами (для организации обычных таймаутов чтения/записи в сокет).
Какие идеи, господа?

2.*ДОПОЛНИТЕЛЬНЫЕ ВОПРОСЫ*
Совсем простые вопросы. Хочется узнать ваше мнение, уважаемые.

2.1 Сетевой порядок байт.
В простейшем случае запрос/отклик состоят из заголовка в 12 байт и возможно данных (если таковые необходимы). Формат заголовка запроса и отклика одинаков:


msg_size (32 bit) – общий размер «сообщения» | command ( 8 bit ) – код команды запроса | param ( 8 bit ) – параметр команды (пока у меня всегда 0) | checksum ( 16 bit ) – контрольная сумма | error ( 32 bit ) – код результата запроса |

typedef struct _appheader { _DWORD msg_size; /* 32 bit */ _BYTE command; /* 8 bit */ _BYTE param; /* 8 bit */ _WORD checksum; /* 16 bit */ _DWORD error; /* 32 bit */ // _BYTE reserved[HDRPADDING]; /* HDRPADDING bytes of trailing padding */ } APPHEADER; /* 12 bytes */

ООО!!! Неоднократно встречал на форумах, в том числе и в faq-ах от гуру, что, дескать, не забывайте, ламеры, про сетевой порядок байт. Зачем?!!! Наивные. Да у них (у гуру) крыша поехала. Это ж прикладной протокол. Какая здесь разница. Всё равно клиентов под разные платформы придётся компилировать отдельно. Там (на платформах отличных от Intel) и учесть можно – делать htons, или нет. Другие мнения есть?

2.2 Выравнивание заголовка по границе слова.
Имеется в виду то, обстоятельство, что если в будущем пришлось бы убрать из заголовка, например, _BYTE param (за ненадобностью), то хорошо б было добавить закомментированное выше reserved, см.заголовок выше.
Не вижу большого смысла, ибо напоминает это мне «мастерство» типа замены операции « a / 2 » побитовым «a >> 1». Хотя сомнения, конечно, есть небольшие. Заголовок вычитывается из сокета «первым». Здесь не так важно сколько читать – 11 байт, или 12. Другое дело, насколько эффективным будет следующий шаг – вычитывание «невыровненных» данных (что за заголовком) из буфера сокета.
Какие будут мнения, товарищи?

2.3 Выравнивание полей в заголовке.
Похоже на предыдущий пункт. И здесь логики больше. Хотя, тоже с учётом возможностей современных компиллеров, сомнительно. Речь идёт о том, что слово выравниваем по границе слова, а двойное слово – по границе двойного слова. Придерживаюсь пока правил. А по правде – насколько это актуально сейчас (чай не 80-е годы на дворе)?

2.4 Контрольная сумма.
Есть некое противоречие между потоковой природой сокетов TCP и желанием программиста работать с потоком, как с набором «сообщений». «Ненадёжность» здесь может быть связана с ошибками в коде сервера или клиента, когда, грубо говоря, будем читать «заголовок» следующего сообщения, не дочитав из потока ещё данные предыдущего сообщения. Подчёркиваю, речь не о том, что данные придут не в том порядке, неполностью или искажёнными (это невозможно, поскольку контрольные суммы и IP, и, главным образом, TCP – гаранты надёжности), а о только том, чтобы программер правильно «вычитавал» заголовок, а потом данные. Чтобы он не считал мусор – заголовком.
Здесь для повышения надёжности протокола можно было б вычислять контрольную сумму по заголовочной информации (без данных). Поле checksum, см. структуру заголовка APPHEADER выше, – это контрольная сумма. Пока вычисляю и использую. На стадии отладки кое-где помогало. Хочу с вами посоветоваться о нужности его вообще в прикладных протоколах, и в обсуждаемом протоколе в частности. Алгоритм вычисления – стандартный, как в IP, или в TCP/UDP. Как и в IP при вычислении контрольной суммы данные не учитываются (считаем только заголовочные данные). Однако в отличие от протокола IP, расчёт ведётся с учётом псевдохедера (как в TCP/UDP). Алгоритм и структура псевдозаголовка соответствует таковым в RFC (но в отличие от TCP/UDP данные не учитываются). Структура псевдохедера:

typedef struct { _DWORD saddr; /* 32 bit */ _DWORD daddr; /* 32 bit */ _DWORD len; /* 32 bit */ _BYTE zero; /* 8 bit */ _BYTE proto; /* 8 bit */ } PSEUDOHDR; /* 14 bytes */

Здесь при вычислении контрольной суммы в поле len помещаю значение поля msg_size реального заголовка. Поле proto – порт, который «слушает» сервер.

Вставляется эта проверка движением руки. Потери производительности – никакой. А сомнения здесь такого рода – хорошо «вылезанный» код не требует проверки checksum. На форумах вообще не слышал, чтобы кто-то так делал. Отсюда, думаю, - а не усложняю ли я понапрасну здесь всё. Единственное, что могу привести в свою защиту – так и в первых реализациях UDP не вычисляли контрольную сумму.
Как думаете вы, уважаемые?
<programming>
[C++] Спортивный интерес к сокетам 13.12.03 04:21  
Автор: void <Grebnev Valery> Статус: Elderman
Отредактировано 13.12.03 05:31  Количество правок: 1
<"чистая" ссылка> <обсуждение закрыто>
Недавно на одном форуме обсуждался вопрос – «Как лучше написать «сервисок»?» Автор - не я. Там я тоже задавал вопрос, на который не получил ответа – «А каким должен быть прикладной протокол, определяющий информационный обмен клиент/серверного приложения, написанного «на сокетах»?».

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

Вот схожие ситуации, где я бы тоже не отвечал с учётом их тривиальности:
1) читаем и пишем в таблицы, структура которых известна клиенту «заранее». При этом ещё лучше, если MEMO и BLOB – отсутствуют. Скорее всего, и клиента и сервер здесь проектирует один и тот же человек.
2) пишется протокол под «конкретную» задачу. Есть вполне определённая база. Клиент обращается к серверу не с SQL запросами, а с запросами типа – GET_FOLDERS, GET_DEPARTMENTS. Приблизительно так построено приложение «1C Архив» (если я правильно понял, промониторив сетефой трафик). Совершенно классное и эффективное Российское приложение.

Но мне хотелось выяснить: «Каким должен быть протокол, если предполагается решение немногим более сложных задач? Например, клиентские приложения работают по инет с DBMS так, как если б клиенты использовали DCOM в LAN c Windows-машинами. Задают SQL вопросы и получают достаточно быстро SQL ответы».

Я не занимался и не занимаюсь этой задачей всерьёз. Более того, я б отстаивал существующие промышленные решения, а не суррогат. В корпоративной интранет среде – тем более. Даже для корпоративных сетей, разделённых межсетевыми экранами, я сторонник DCOM, для Win, разумеется (здесь «сложности» с фильтрацией пакетов больше надуманны. Хотя маленько есть, конечно).
Так, что мой вопрос и всё, что ниже – из спортивного интереса и желания узнать, как бы такой «сокетный» протокол проектировали многоопытные .

Невозможно предметно обсуждать «ничего». Поэтому, в свободное от основной работы время я написал протокол, сервер и клиента. Сделал это для «затравки» обсуждения. Из спортивного интереса. Вот код (работающий), реализующий рассматриваемый «сокетный» протокол для «затравки» обсуждения:

_DWORD rc; _DWORD hQuery; if ( FAILED(rc = QueryCreate("MY_DB_ALIAS", &hQuery) )) return rc; if ( FAILED(rc = QueryOpen(hQuery, "select * from tbl1")) ) { QueryDelete( hQuery ); return rc; } int ival; LPTSTR sval; while ((rc = QueryEof( hQuery )) != QUERYEOF_S_EOF) { if ( FAILED( rc = QueryIntegerField( hQuery, "id", &ival) )| FAILED( rc = QueryStringField( hQuery, "txt", &sval) )) break; _tprintf(_TEXT("id = %d txt = %s\n"), ival, sval); if ( FAILED(rc = QueryNext( hQuery )) ) break; } if ( FAILED( rc )) OnErr ( rc ); return QueryDelete( hQuery );

В LAN – это работает весьма эффективно. Имеются в виду ощущения пользователя клиентской программы. Для сравнения вот результаты тестирования выполнения того же кода и использованием ODBC и ADO – время выполнения кода, что выше в цикле 100 раз (без _tprintf, разумеется):

1) «На сокетах», с использованием ODBC на сервере ~ 6 сек.
2)Обычное приложение, использующее ODBC и DSN к удалённой базе на том же сервере ~ 5 – 7 сек.
3) Тоже, что и в п.2, но с использованием ADO ~ 1 – 3 сек.

ВНИМАНИЕ ВОПРОС:

Каким должен быть протокол, если это не LAN, а инет?

Ред. 1 раз.
[C++] Последнее маленькое уточнение 18.12.03 08:56  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
Я обещал в конце обсуждения публично расписаться в своей программеро-импотенции. Выполняю;))) Только вначале замечу, что часто в таких задачах кивают в сторону XML & SOAP. И да и нет. И в начале про это. Потом распишусь;)))

1.*ПРО «ПРЕИМУЩЕСТВА» SOAP*

Сравним на примере два подхода при использовании техники «вычитывания данных по полям», как было показано в самом начале. Один подход - на сокетах с использованием самодельного протокола, другой – с использованием SOAP.


1.1. На сокетах

В начале топика был фрагмент кода клиента, в котором был такой вызов:

if ( FAILED( rc = QueryIntegerField( hQuery, "id", &ival) ) )
{


}

Этот вызов реализует запрос клиента размером в 18-байт (12-байт на заголовок, 4-байта на дескриптор hQuery, 2-байта на имя поля «id» ) в виде:

заголовок
msg_size = 18 байт
command = UC_QUERYINTEGER
param = 0
checksum = 21980
error = 0

данные
hQuery = 301989888
FieldName = «id»

Здесь макро UC_QUERYINTEGER (числовой код команды) определено, например, #define UC_QUERYINTEGER 16. Весь этот буфер и записывается в сокет.

Сервер знает, каков формат «сообщения по протоколу» для запросов клиента с полем в заголовке command = UC_QUERYINTEGER. Поэтому, сделав «свою работу» он возвращает отклик размером 16-байт (заголовок + результат) или 12-байт (только заголовок), в зависимости от значения ошибки error, см. выше. Например, он может вернуть клиенту отклик:

заголовок
msg_size = 16 байт
command = UC_QUERYINTEGER
param = 0
checksum = 14962
error = QUERYINTEGER_S_RECORDSET

данные
FieldValue = 1

Здесь макро QUERYINTEGER_S_RECORDSET определено по аналогии с COM, например, так:

#define FACILITY_ITFPROTO FACILITY_ITF
#define SCODE_BASE 0x200
//
// Клиент запросил целое поле (UC_QUERYINTEGER) набора данных объекта IQueryInterface.
//
#define QUERYINTEGER_E_RECORDSET MAKE_HRESULT(1, FACILITY_ITFPROTO, SCODE_BASE + 115)
#define QUERYINTEGER_S_RECORDSET MAKE_HRESULT(0, FACILITY_ITFPROTO, SCODE_BASE + 115)

Этот отклик читает клиент из сокета (за два раза). Как было сказано – в LAN работает быстро. В инет – с практической точки зрения работать не будет (имеется в виду эффективность)

1.2. С использованием SOAP

Я не спец в XML и SOAP. Могу наделать ошибок. Но по существу, надеюсь, там правильно.

Запрос клиента, аналогичный тому, что в п.1.1, может выглядеть так:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<SOAP-ENV:Envelope ...>
<SOAP-ENV:Body ...>
<SOAPSDK1:QueryInteger ...>
<hQuery>301989888</hQuery>
<fieldname>ID</fieldname>
</SOAPSDK1:QueryInteger>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Соответствующий ответ сервера - так:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<SOAP-ENV:Envelope ...>
<SOAP-ENV:Body ...>
<SOAPSDK1:QueryIntegerResponse ...>
<HRESULTResponse>262771</HRESULTResponse>
<Result>1</Result>
</SOAPSDK1:QueryIntegerResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Здесь код запроса (поле error) помещён непосредственно в soap:body в качестве возвращаемого значения, а не в fault:

<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>Query failed</faultstring>

<detail>
<mserror:errorInfo ...>

</detail>
</SOAP-ENV:Fault>

Это связано с теми же проблемами, которые присутствуют в COM программировании в среде VB. (если клиенту необходимо анализировать HRESULT с не установленным битом ошибки, то это следует делать путём передачи HRESULT кода в параметрах метода).

Возникает вопрос, а чем лучше то, что «на сокетах», п.1., или то, что «на soap»? Ни лучше, ни хуже, если речь идёт о коде клиента, который приведён в начале топика и инет. Это просто не будет работать и для SOAP, и для протокола «на сокетах».

1.3. Почему это не работает

Код (а вернее техника), который приведён в самом начале, - совершенно нормальный для LAN. По такому же «сценарию» строят клиентские приложения в LAN и с использованием ODBC, и ADO, и BDE. И не важно, что строчки кода будут другими, если программер будет использовать не обёрточное API (как в коде выше), а непосредственно обращаться к методам интерфейсов COM-объектов, скажем, ADO. Стереотип в LAN в том, что данные обычно вычитывают «порциями» и при этом ещё обращаются к серверу с «промежуточными» запросами, типа QueryNext, QueryEof. При этом набор данных держится открытым на сервере до тех пор, пока клиент с ним работает. Клиент делает запрос QueryEof, сервер отвечает - «нет». Тогда клиент делает следующий запрос QueryInteger, сервер отвечает откликом со значением требуемого поля. Потом клиент просит переместить указатель текущей записи набора данных на серверной стороне – QueryNext, сервер – выполняет. И т.д.

Не сложно показать, что при такой технике, даже если узлы расположены на «расстоянии» 2-3 хопов, и, предполагая любое разумное минимальное RTT, – клиентское приложение будет ждать «вечность». Работать, конечно, будет. Только захочется закрыть приложение раньше, чем заполнится какой-то ListBox.

2.*КАК МОЖНО БЫЛО БЫ СДЕЛАТЬ*

В принципе решение на поверхности. Следует любые результаты возвращать сразу, а не по частям. Это означает, что на запрос клиента QueryOpen( hQuery, «select * from Spr» ) сервер должен возвращать весть набор данных сразу, в одном отклике. Это делает архитектуру клиента в принципе отличной от того, что обычно принято делать в LAN. Клиент должен прочитать все данные из сокета в буфер, заполнить массив неких структур CRecordset так, чтобы из него можно было б извлекать данные в коде, как показано в самом начале. Другими словами все танцы с бубном вокруг QueryEof, QueryNext и т.д. должны происходить на стороне клиента, а не сервера. И это не представляется очень простой задачей. Кстати для сервера в этом случае есть некоторое облегчение. После записи в сокет всего массива он может закрыть открытые наборы данных, освободив драгоценные ресуры.

2.1. На сокетах

2.1.1) Пользователь перечисляет все поля в инструкции SELECT.
Здесь та ситуация, когда пользователь использует инструкции типа select fld1, fld2, …from tbl. Соответствующее «расширение» рассматриваемого протокола легко построить. Клиент знает, сколько полей он хочет получить. Серверу останется только передать в области данных (после основного заголовка) дополнительный заголовок, описывающий структуру полей и число записей. В принципе, за основу можно было бы взять вообще формат аналогичный DBF (было время, когда для DOS вообще писал DBF «руками», на С – и ничего страшного). Можно было б ограничится и более простыми заголовками, в которых перечисляются только длины в байтах каждого поля. Дело клиента – как интерпретировать последовательность байт для каждого из полей. В этом случае, правда, придётся заменить доступ к полю записи «по имени» доступом «по номеру» поля.
Но одна вот проблема – как быть в этом случае с MEMO и с BLOB? Не знаю. Если б не это – задача не особо сложная.

2.1.2) Пользователь не перечисляет поля в инструкции SELECT.
Здесь та ситуация, когда пользователь использует инструкции типа «select * from tbl». Задача аналогична п.3.3.1, но сложнее. Если нет MEMO и BLOB – может помочь дополнительный заголовок «DBF» в области данных. С учетом того, что MEMO и BLOB используют почти всегда– ситуация тупиковая.


2.2. С использованием SOAP

Не вижу здесь (из-за того, что не знаю) особых облегчений, даже с учётом того, что SOAP имеет встроенную поддержку для передачи массивов сложных структурированных данных. Маперы SOAP не помогут, поскольку предполагают описания вполне конкретных структур данных в WSDL-документах. Т.е. в схеме

<types>
<schema ...>
...
<complexType name ='Recordset'>
...
</complexType>
</schema>
</types>

нужно писать чего-то вполне определённое. Это не относится к рассматриваемой задаче, поскольку SELECT принципиально предполагает, что структура данных может быть любой. Ну, не генерировать же WSDL-документы каждый раз, после очередного запроса клиента, и не переинициализировать же объекты SOAP-сервера и SOAP- клиента. Впрочем, не знаю. Я здесь – не разбираюсь.

2.3. С использованием XML

Здесь немного лучше, поскольку мы имеет возможность разметки отклика сервера «на лету». Например, сервер, мог бы возвратить такой отклик на запрос клиента «select * from tbl»:

<QuerySelectResponse>
<HRESULTResponse>262453</HRESULTResponse>
<Record>
<field …>
<INTEGER>1</INTEGER>
</field>

<field …>
<FLOAT>1.0</FLOAT>
</field>

<field …>
<МЕМО>Привет мир</МЕМО>
</field>
</Record>
<Record>


</Record>


</QuerySelectResponse >

Я не рассматриваю здесь то, что часто говорят о снижении производительности для XML. В рассматриваемом XML-случае будет не снижение, а повышение производительности в 100 и более раз по сравнению с тем, что «на сокетах» в п.1. Там и причины. Правда, неясно, что мы получим в плане производительности для BLOB-полей с бинари. Имеется ввиду кодирование/декодирование base64.
Напоследок скажу, что даже «неправильный» XML-документ можно легко встроить в рассматриваемый «сокетный протоко». Всё равно парсить будем в сугубо индивидуальных целях. Получится некий гибрид.

Уродец? Как думаете?
[C++] Немного реабилитирую варианты на сокетах и SOAP 18.12.03 13:23  
Автор: Ktirf <Æ Rusakov> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
По поводу API для работы с большими издержками в расчете на вызов (например через инет). Обычно используют Prepare-Fetch схему: сначала SQL-запрос препарируется сервером и подготавливается все необходимое для его быстрого выполнения. После этого возможен дополнительный этап Execute, который часто объединяется с Prepare: сервер выполняет запрос и в этом случае клиент может даже получить число записей, которое он может прочитать. Дальше идет самое интересное: клиент в цекле делает Fetch (забирание) определенного числа записей и обрабатывает их сообразно своим надобностям. Таким образом достигается компромисс между издержками в расчете на один вызов и нежеланием получать миллион записей, если ты после первой тысячи поймешь, что больше тебе не нужно. Я бы посоветовал для общего образования взглянуть на Oracle Call Interface (OCI) - "родной" С-интерфейс к базам Oracle. Многие вопросы отпадут.
Что касается SOAP. Сразу говорю, что я тоже еще тот знаток SOAP, поэтому все ниже может оказаться полным бредом. В случае сравнительно низкоуровневого программирования SOAP, на мой взгляд, малая подмога, потому что этот протокол, наоборот, предназначен для высокоуровневого использования API. Просто чтобы не писать кучу вспомогательного кода (каждый раз очень похожего), если тебе нужно дернуть пару функций. В данном конкретном случае, если работать через SOAP, я бы возвращал результирующий набор записей в виде инкапсулированного XML. Никаких описаний в этом случае не надо, на выходе ты получаешь просто здоровенную строку. И только ты знаешь, что на самом деле, если все аккуратненько разэскейпить, то в этой строке находится XML. Хотя лично мне такой подход категорически не нравится - не для того XML придумывался, чтобы инкапуслировать себя в своей же строке.
В общем, вариант с XML при таком выборе мне кажется наиболее разумным и удобным для дальнейшей работы. Хотя, если писать клиент и сервер, скажем, не на C/C++, а на Java, то многие сетевые заморочки и проблемы с передачей по сети динамических массивов динамических же структур отпадают сами собой.
В принцине, в последнем посте я скромно на это и намекал... 19.12.03 01:46  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> Prepare-Fetch схему: сначала SQL-запрос препарируется
> сервером и подготавливается все необходимое для его
> быстрого выполнения. После этого возможен дополнительный
> этап Execute, который часто объединяется с Prepare: сервер
> выполняет запрос и в этом случае клиент может даже получить
> число записей, которое он может прочитать.

В принцине, в последнем посте я скромно на это и намекал. Конечно, для рассматриваемого случая так делать и надо.

> самое интересное: клиент в цекле делает Fetch (забирание)
> определенного числа записей и обрабатывает их сообразно
> своим надобностям. Таким образом достигается компромисс
> между издержками в расчете на один вызов и нежеланием
> получать миллион записей, если ты после первой тысячи
> поймешь, что больше тебе не нужно.

Здесь есть два момента, на мой взгляд:

1) если " .... если ты после первой тысячи
> поймешь, что больше тебе не нужно.....". В этом случае, разумеется, схема блочного получения всего набора очень даже оправдана.

2) Мы просто хотим получить весь набор данных. Что даст больший оверхед - передача, скажем, 100 записей размером 1024 за 5 раз, или за один раз? С одной стороны, передача меньшими блоками может уменьшить вероятность повторных прередач. С другой стороны, практически все реализации TCP поддерщивают механизм избирательных, селективных подтверждений (SACK). Даже Windows. Кто быстрее доедет - вопрос.

> Я бы посоветовал для
> общего образования взглянуть на Oracle Call Interface (OCI)
> - "родной" С-интерфейс к базам Oracle. Многие вопросы
> отпадут.

Спасибо за совет. Посмотрю.

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

А вообще - это идея. И возможно, что очень даже клёвая.

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

Да, надо бы Java мне поизучать. В некоторых кругах чувствую себя, если не идиотом, то уж изгоем это точно. ;))

Спасибо, Лёшка. А про XML в SOAP - клёво. По крайней мере на первый взгляд - точно.
"маленькое"? ну-ну ;-) 18.12.03 11:34  
Автор: LLL <Алексей> Статус: Member
<"чистая" ссылка> <обсуждение закрыто>
[C++] Маленькое уточнение 15.12.03 10:24  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
1.*РЕАЛИЗАЦИЯ*

Поскольку по-большому разобраться не получается, попробуем по-маленькому. Я попробую конкретизировать ещё, для дополнительной «затравки», некоторые аспекты. Хотя они и не основные, но разобраться хочется.

В реализации клиента и сервера всё как бы обыденно. Обсуждать особо нечего. Может только некоторые детали ниже. И сервер, и клиент - на блокирующих сокетах. Интерфейс Беркли. Асинхронные штучки в реализации сокетов в Windows здесь не использую (пока). Сервер – многопоточный (каждого клиента обслуживаем в отдельном потоке).

1.1 Потоки клиентов.
Зная свою слабость к большому числу ошибок в коде, решил не оставлять потоки клиентов «безнадзорными». Кое-что здесь уже сделано. Кое-что не стал делать (для «спортивного интереса» было б слишком). Есть пул потоков, который можно контролировать из специального потока-свиппера «менеджера клиентских потоков». Для описания клиентского потока используем структуру CLIENTINFO:

#define MAXCLIENTS 512 #define MAX_CLIENTQUERYES 10 typedef struct _iclientquery { _DWORD hQuery; // дескриптор объекта доступа // к набору данных DBMS _DWORD status; // статус оъекта (создан/открыт/закрыт...) IQueryInterface* IQuery; // указатель на интерфейс объекта … … } IQUERY; typedef struct _clientinfo { HANDLE hThread; // дескриптор потока _BYTE ThreadState; // статус потока SOCKET sock; // клиентский сокет _DWORD addr; // ip клиента _BYTE *stream_buf; // буфер ввода/вывода клиента _DWORD cur_bufsize; // текущий размер буфера IQUERY querylist[MAX_CLIENTQUERYES]; _WORD nqueryes; … … } CLIENTINFO; typedef CLIENTINFO* PCLIENTINFO;

Каждый клиентский поток регистрируется при его создании (спящим). После того, как данные, необходимые клиенту заполнены (структура CLIENTINFO, указатель на которую уже передан потоку при его создании) – будим поток.

while((client_socket=accept(srv_socket, (sockaddr *) &client_addr, &client_addr_size))) { PCLIENTINFO sockinfo = allocinfo(); if ( !sockinfo ) { printf("\n\aAllocation error. Close connection."); byeclient( client_socket ); continue; } … … unsigned thID; uintptr_t hThread = _beginthreadex( NULL, 0, &ClientThread, sockinfo, CREATE_SUSPENDED, &thID ); if (! hThread) { printf("\n\aCreating client thread error. Close connection."); byeclient( client_socket ); freeinfo( sockinfo); continue; } sockinfo->sock = client_socket; sockinfo->addr = client_addr.sin_addr.S_un.S_addr; sockinfo->hThread = (HANDLE) hThread; sockinfo->ThreadState |= THS_CREATED | THS_SUSPENDED; if (ResumeThread( (HANDLE) hThread ) != -1L) sockinfo->ThreadState &= ~THS_SUSPENDED; … … }

Доступ потока-свиппера к пулу быстрый, без существенной блокировки потоков клиентов. Свиппер будет искать и чистить подвисшие потоки по такому сценарию. В критической секции (сама по себе быстрая) можно выполняем «моментальный снимок» - копию пула. Затем, «отпустив» критическую секцию, можно уже в копии пула искать процессы-зомби по некоторому критерию (скорее всего – некоторый таймаут клиентского потока, который по типу «жив/не жив» контролируется из клиентского потока), и если таковые найдутся, то в свиппер-потоке: приостановить клиентский поток, почистить его ресурсы (буфер ввода/вывода, открытые наборы данных DBMS, объекты доступа к данным DBMS) и потом уж - убить подвисший поток. Останется быстро вторым вхождением в критическую секцию почистить реальный пул с CLIENTINFO уже задавленного потока-зомби. Взаимных продолжительных блокировок клиентских потоков не будет. Клиентский поток обращается к пулу в критической секции только один раз и на очень короткое время – для освобождения своего дескриптора и саморазрегистрации (удаления себя из пула потоков) непосредственно перед завершением функции потока.
Как думаете нужен ли свиппер, и какой?
Здесь было сказано о буфере ввода/вывода клиентского потока, см. stream_buf в структуре CLIENTINFO выше. Вообще, на мой взгляд, это вопрос – как выделять память потоку для буфера ввода/вывода? В той реализации, которая сделана из «спортивного интереса» – сделано дубово. Память выделяется в одной, общей куче процесса. Какие идеи в части памяти, господа?

1.2 Буфер ввода/вывода клиентского потока.
Неожиданно наткнулся на вопрос, на который раньше не обращал внимания. Выше, в п. 1.1 сказано, что память для буфера ввода/вывода клиентского потока выделяется из одной кучи процесса. Сам по себе вопрос. Другой вопрос связан с тем, как выделяется? Проще и быстрее всего, с точки зрения доступа – realloc (поэтому-то и realloc). Алгоритм здесь, казалось бы, может быть совсем простым – вычитываем из заголовка «сообщения» его размер (размер заголовка плюс данные), и делаем realloc требуемого размера. Сомнения здесь связаны с возможной фрагментацией памяти из большого числа клиентских потоков, и как следствие с общим кирдык. В случае, когда каждый из клиентских потоков сервера будет «шинковать» память процесса для маленьких «сообщений», мне кажется, что возможна ситуация, когда память будет нарезана, как рулетик. Пока я сделал тупо – память выделяется блоками кратными 1024 и в два раза больше, чем было запрошено, если текущий буфер мал. При следующем запросе вначале проверяем текущий размер буфера, и если «сообщение» помещается, то realloc не делаем вовсе. Какие идеи, господа?
У меня – никаких нормальных.

1.3 Клиент.
В работе клиента есть одна особенность – возможно большие таймауты, когда пользователь пошёл покурить. Т.е. соединились, прочитали что надо, а дальше юзвер ничего не делает. Просто рвать соединение по таймауту, пусть даже достаточно большому, на мой взгляд неразумно. Молчание клиента может быть связано как с тем, что клиент «курит», так и с тем, что клиентская сторона подвисла. Именно подвисла, а не разрушила сокет, разорвала соединение и т.д., что серверная сторона заметит по клиентскому сокету на своей стороне. Другие патологические ситуации, когда клиент исчез, оставив полуоткрытое соединение на конце сервера, а сервер ожидает каких-либо данных от клиента, здесь я не рассматриваю. Хотя и это может быть. Единственное решение в этой ситуации, на мой взгляд – это пинги пингать. Опция TCP keepalive вряд ли поможет. Да и не рекомендуют её гуру в Host Requirements RFC. Не гуру, но практики на форумах так же замечают, что реальное время таумаута keepalive в различных реализациях и конфигурациях TCP может быть различным - 1,5 – 2 и более часов. Мутно всё это.
Таким образом, мне кажется, что в любом мало-мальски нормальном прикладном протоколе, рассчитанном на длительные соединения (имеется ввиду только TCP, а не соединения с DBMS) обрабатывать длительные таймауты следует самостоятельно. Не проблема это включить в обсуждаемый протокол. Не очень ясно другое, как это сделать в реализации сервера и клиента. Например, на клиенте придётся или делать это в отдельном потоке (все данные и получаем и отсылаем в отдельном потоке) – изврат, или использовать асинхронные сокеты Windows в сочетании с таймерами (для организации обычных таймаутов чтения/записи в сокет).
Какие идеи, господа?

2.*ДОПОЛНИТЕЛЬНЫЕ ВОПРОСЫ*
Совсем простые вопросы. Хочется узнать ваше мнение, уважаемые.

2.1 Сетевой порядок байт.
В простейшем случае запрос/отклик состоят из заголовка в 12 байт и возможно данных (если таковые необходимы). Формат заголовка запроса и отклика одинаков:


msg_size (32 bit) – общий размер «сообщения» | command ( 8 bit ) – код команды запроса | param ( 8 bit ) – параметр команды (пока у меня всегда 0) | checksum ( 16 bit ) – контрольная сумма | error ( 32 bit ) – код результата запроса |

typedef struct _appheader { _DWORD msg_size; /* 32 bit */ _BYTE command; /* 8 bit */ _BYTE param; /* 8 bit */ _WORD checksum; /* 16 bit */ _DWORD error; /* 32 bit */ // _BYTE reserved[HDRPADDING]; /* HDRPADDING bytes of trailing padding */ } APPHEADER; /* 12 bytes */

ООО!!! Неоднократно встречал на форумах, в том числе и в faq-ах от гуру, что, дескать, не забывайте, ламеры, про сетевой порядок байт. Зачем?!!! Наивные. Да у них (у гуру) крыша поехала. Это ж прикладной протокол. Какая здесь разница. Всё равно клиентов под разные платформы придётся компилировать отдельно. Там (на платформах отличных от Intel) и учесть можно – делать htons, или нет. Другие мнения есть?

2.2 Выравнивание заголовка по границе слова.
Имеется в виду то, обстоятельство, что если в будущем пришлось бы убрать из заголовка, например, _BYTE param (за ненадобностью), то хорошо б было добавить закомментированное выше reserved, см.заголовок выше.
Не вижу большого смысла, ибо напоминает это мне «мастерство» типа замены операции « a / 2 » побитовым «a >> 1». Хотя сомнения, конечно, есть небольшие. Заголовок вычитывается из сокета «первым». Здесь не так важно сколько читать – 11 байт, или 12. Другое дело, насколько эффективным будет следующий шаг – вычитывание «невыровненных» данных (что за заголовком) из буфера сокета.
Какие будут мнения, товарищи?

2.3 Выравнивание полей в заголовке.
Похоже на предыдущий пункт. И здесь логики больше. Хотя, тоже с учётом возможностей современных компиллеров, сомнительно. Речь идёт о том, что слово выравниваем по границе слова, а двойное слово – по границе двойного слова. Придерживаюсь пока правил. А по правде – насколько это актуально сейчас (чай не 80-е годы на дворе)?

2.4 Контрольная сумма.
Есть некое противоречие между потоковой природой сокетов TCP и желанием программиста работать с потоком, как с набором «сообщений». «Ненадёжность» здесь может быть связана с ошибками в коде сервера или клиента, когда, грубо говоря, будем читать «заголовок» следующего сообщения, не дочитав из потока ещё данные предыдущего сообщения. Подчёркиваю, речь не о том, что данные придут не в том порядке, неполностью или искажёнными (это невозможно, поскольку контрольные суммы и IP, и, главным образом, TCP – гаранты надёжности), а о только том, чтобы программер правильно «вычитавал» заголовок, а потом данные. Чтобы он не считал мусор – заголовком.
Здесь для повышения надёжности протокола можно было б вычислять контрольную сумму по заголовочной информации (без данных). Поле checksum, см. структуру заголовка APPHEADER выше, – это контрольная сумма. Пока вычисляю и использую. На стадии отладки кое-где помогало. Хочу с вами посоветоваться о нужности его вообще в прикладных протоколах, и в обсуждаемом протоколе в частности. Алгоритм вычисления – стандартный, как в IP, или в TCP/UDP. Как и в IP при вычислении контрольной суммы данные не учитываются (считаем только заголовочные данные). Однако в отличие от протокола IP, расчёт ведётся с учётом псевдохедера (как в TCP/UDP). Алгоритм и структура псевдозаголовка соответствует таковым в RFC (но в отличие от TCP/UDP данные не учитываются). Структура псевдохедера:

typedef struct { _DWORD saddr; /* 32 bit */ _DWORD daddr; /* 32 bit */ _DWORD len; /* 32 bit */ _BYTE zero; /* 8 bit */ _BYTE proto; /* 8 bit */ } PSEUDOHDR; /* 14 bytes */

Здесь при вычислении контрольной суммы в поле len помещаю значение поля msg_size реального заголовка. Поле proto – порт, который «слушает» сервер.

Вставляется эта проверка движением руки. Потери производительности – никакой. А сомнения здесь такого рода – хорошо «вылезанный» код не требует проверки checksum. На форумах вообще не слышал, чтобы кто-то так делал. Отсюда, думаю, - а не усложняю ли я понапрасну здесь всё. Единственное, что могу привести в свою защиту – так и в первых реализациях UDP не вычисляли контрольную сумму.
Как думаете вы, уважаемые?
Мысли по поводу 15.12.03 12:10  
Автор: Ktirf <Æ Rusakov> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> 1.1 Потоки клиентов.
> Зная свою слабость к большому числу ошибок в коде, решил не
> оставлять потоки клиентов «безнадзорными». Кое-что здесь
> уже сделано. Кое-что не стал делать (для «спортивного
> интереса» было б слишком). Есть пул потоков, который можно
> контролировать из специального потока-свиппера «менеджера
> клиентских потоков».
Хм, насколько я знаю, для надежной работы пула потоков (в том числе в системах реального времени) нонче модно использовать так называемый watchdog, который регулярно извещается самим потоками о том, что эти потоки живы. И я, чесговоря, не вижу, почему это может быть плохо. Порядок действий в этом случае такой: рабочий поток произвольным образом (например, через UDP) извещает следящий процесс о том, что этот рабочий поток жив. Если следящий процесс в течение определенного времени не получает сигнала от рабочего потока, он прибивает рабочий поток (скажем, через kill(SIGKILL)), и перезапускает новый поток - преемника убитого. Такой способ быстрее, чем предложенный, и не требует взаимной синхронизации за исключением механизма посылания сообщения.

> Здесь было сказано о буфере ввода/вывода клиентского
> потока, см. stream_buf в структуре CLIENTINFO выше. Вообще,
> на мой взгляд, это вопрос – как выделять память потоку для
> буфера ввода/вывода? В той реализации, которая сделана из
> «спортивного интереса» – сделано дубово. Память выделяется
> в одной, общей куче процесса. Какие идеи в части памяти,
> господа?
Ну а какие тут могут быть идеи... Не использовать же thread local storage. А раз не использовать, то из многочисленных соображений (фрагментация памяти, производительность выдачи и освобождения памяти и проч.) лучше написать свой менеджер памяти, который и будет заниматься организацией всего этого безобразия. Алгоритм работы менеджера целиком и полностью зависит от конкретного приложения и даже от конкретных условий работы этого приложения.

> тем, как выделяется? Проще и быстрее всего, с точки зрения
> доступа – realloc (поэтому-то и realloc).
Не могу сказать, что realloc - это самое быстрое, что можно использовать в нашем случае. См. выше.

> 1.3 Клиент.
> В работе клиента есть одна особенность – возможно большие
> таймауты, когда пользователь пошёл покурить...
> ...
> Таким образом, мне кажется, что в любом мало-мальски
> нормальном прикладном протоколе, рассчитанном на длительные
> соединения (имеется ввиду только TCP, а не соединения с
> DBMS) обрабатывать длительные таймауты следует
> самостоятельно. Не проблема это включить в обсуждаемый
> протокол. Не очень ясно другое, как это сделать в
> реализации сервера и клиента. Например, на клиенте
> придётся или делать это в отдельном потоке (все данные и
> получаем и отсылаем в отдельном потоке) – изврат, или
> использовать асинхронные сокеты Windows в сочетании с
> таймерами (для организации обычных таймаутов чтения/записи
> в сокет).
> Какие идеи, господа?
В отдельном-то потоке зачем? Ставим select, а для keep-alive-сигналов либо создаем специальный формат сообщения, который будет отлавливаться еще до передачи в рабочие потоки, либо открываем дополнительное соединение, по которому только кипалайвы и шлются.

> 2.1 Сетевой порядок байт.
> ООО!!! Неоднократно встречал на форумах, в том числе и в
> faq-ах от гуру, что, дескать, не забывайте, ламеры, про
> сетевой порядок байт. Зачем?!!! Наивные. Да у них (у гуру)
> крыша поехала. Это ж прикладной протокол. Какая здесь
> разница. Всё равно клиентов под разные платформы придётся
> компилировать отдельно. Там (на платформах отличных от
> Intel) и учесть можно – делать htons, или нет. Другие
> мнения есть?
Есть. Зачем выбирать, делать или не делать htons, если можно всегда его делать?

> 2.2 Выравнивание заголовка по границе слова.
> ...
> Заголовок вычитывается из сокета «первым». Здесь не так
> важно сколько читать – 11 байт, или 12. Другое дело,
> насколько эффективным будет следующий шаг – вычитывание
> «невыровненных» данных (что за заголовком) из буфера
> сокета.
> Какие будут мнения, товарищи?
По-моему, это не имеет большого значения. А поле reserved лучше иметь всегда.

> 2.3 Выравнивание полей в заголовке.
> Похоже на предыдущий пункт. И здесь логики больше. Хотя,
> тоже с учётом возможностей современных компиллеров,
> сомнительно. Речь идёт о том, что слово выравниваем по
> границе слова, а двойное слово – по границе двойного слова.
> Придерживаюсь пока правил. А по правде – насколько это
> актуально сейчас (чай не 80-е годы на дворе)?
Актуально, но не в нашем случае. Это интересно для выжимания производительности на локальной машине, а когда появляется сеть и/или доступ к БД... Характерные времена уже не те, чтобы это было заметно. ИМХО.

> 2.4 Контрольная сумма.
> ...
> Вставляется эта проверка движением руки. Потери
> производительности – никакой. А сомнения здесь такого рода
> – хорошо «вылизанный» код не требует проверки checksum.
Требует. "Нам не дано предугадать", в каком месте наш софт слетит. И не стоит уповать на вылизанность кода, тем более что потери производительности действительно никакой.
В очередной раз спасибо, Лёшка. 16.12.03 02:32  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
В очередной раз спасибо, Лёшка.

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

Признаюсь, я просто не смотрел сабдж. И раз ты говоришь, что это хорошо, то я соглашусь, ибо прислушиваюсь к "старшим" ;)) (без иронии. я серьёзно)

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

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

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

Это было б сложно для того контекста ("из спортивного интереса"), который здесь. А если честно - мне и недоступно. Программист я - сам знаешь какой. Но всё равно интересно;)).


> В отдельном-то потоке зачем? Ставим
> select, а для keep-alive-сигналов
> либо создаем специальный формат сообщения, который будет
> отлавливаться еще до передачи в рабочие потоки, либо
> открываем дополнительное соединение, по которому только
> кипалайвы и шлются.

Дык, и мне не хочется в отдельном потоке;)) Я ж пишу - изврат. Да, может и на selecte. Всё равно, там надо думать.
А про формат keepalive сообщения - дык это уже сделано в том протоколе "для затравки";))) Всё пока правда криво, ибо делалось в несколько дней "на скоряк".

> > компилировать отдельно. Там (на платформах отличных
> от
> > Intel) и учесть можно – делать htons, или нет. Другие
> > мнения есть?
> Есть. Зачем выбирать, делать или не делать htons, если
> можно всегда его делать?

Обоснуй. Это ж не смтп и фтп, т.е. то, что оговорено RFC. Какой смысл соблюдать сетевой порядок байт только в заголовке прикладного протокола. Тогда и все числовые значения, что длиной более одного байта, и которые следуют за заголовком прикладного протокола - придётся в сетевом порядке представлять.
Пожалуйста, обоснуй. То, что на сетевом и транспортных уровнях так - нет вопросов. Но я не пойму, зачем это в прикладном протоколе для DBMS?

> По-моему, это не имеет большого значения. А поле
> reserved лучше иметь всегда.

Зачем тогда гуру обманывают начинающих, как я? ;)))

> Актуально, но не в нашем случае. Это интересно для
> выжимания производительности на локальной машине, а когда
> появляется сеть и/или доступ к БД... Характерные времена
> уже не те, чтобы это было заметно. ИМХО.

Про производительность согласен. Но! Если компиллер не вставит дополнительный код (за спиной программера) при доступе к невыровненным данным, то на некоторых процеках это приводит к ошибкам, как говорят гуру. Сам не встречал. Как думаешь?


> checksum.
> Требует. "Нам не дано предугадать", в каком месте наш софт
> слетит. И не стоит уповать на вылизанность кода, тем более
> что потери производительности действительно никакой.

Вот блин! Хоть это я сделал правильно;))
И здесь мне приятно, что ты поддержал меня, Лёшка. А то "импортные" только обзываться умеют. ;)))
Да ладно. 16.12.03 03:54  
Автор: Ktirf <Æ Rusakov> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> Признаюсь, я просто не смотрел сабдж. И раз ты говоришь,
> что это хорошо, то я соглашусь, ибо прислушиваюсь к
> "старшим" ;)) (без иронии. я серьёзно)
Да ладно, какой из меня "старший"... Просто так получилось, что те несколько production-систем, которые я видел и на которых вставала подобная задача, использовали именно такую систему контроля пула потоков. Видимо, чем-то она действительно хороша, раз ею пользуются. Ее минус состоит в том, что она интрузивна по отношению к рабочим потокам, но в нашем случае это неважно.

> Не могу согласится (сразу) только с тем, что в описанном
> алгоритме будет долго. Там ж не мьютексов, не хрютексов.
> Критическая секция очень быстрая (по сравнению с
> остальным). В ней пара операций. Дёрнули и отпустили. Хотя
> ... может я и не прав.
В твоей схеме есть еще дополнительные издержки на копирование моментального снимка состояния потоков.

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

> > В отдельном-то потоке зачем? Ставим
> > select, а для keep-alive-сигналов
> > либо создаем специальный формат сообщения, который будет
> > отлавливаться еще до передачи в рабочие потоки, либо
> > открываем дополнительное соединение, по которому только
> > кипалайвы и шлются.
> Дык, и мне не хочется в отдельном потоке;)) Я ж пишу -
> изврат. Да, может и на selecte. Всё равно, там надо думать.
> А про формат keepalive сообщения - дык это уже сделано в
> том протоколе "для затравки";))) Всё пока правда криво, ибо
> делалось в несколько дней "на скоряк".
А, я, честно говоря, не обратил внимания. Ну и хорошо. А над select'ом особо думать не надо, с ним только работать муторно, зато код пишется ровно один раз.

> > > компилировать отдельно. Там (на платформах отличных
> > > от Intel) и учесть можно – делать htons, или нет. Другие
> > > мнения есть?
> > Есть. Зачем выбирать, делать или не делать htons, если
> > можно всегда его делать?
>
> Обоснуй. Это ж не смтп и фтп, т.е. то, что оговорено RFC.
> Какой смысл соблюдать сетевой порядок байт только в
> заголовке прикладного протокола. Тогда и все числовые
> значения, что длиной более одного байта, и которые следуют
> за заголовком прикладного протокола - придётся в сетевом
> порядке представлять.
Да, придется, и это правильно. Ибо это переносимо, не нужно заморачиваться с платформами, отличающимися порядком байтов. Каков будет порядок байт при пересылке, действительно не имеет значения. Но зато можно быть твердо уверенным, что каждая машина может использовать свой порядок байт, и никаких проблем с перестраиванием порядка байт при передаче данных не возникнет.
Другим вариантом является жесткое прошивание во всем тракте порядка байтов, не только в самих клиенте и сервере, но еще и в агентах, с которыми они взаимодействуют. То есть если ты работаешь с базой данных - в ней должен быть фиксированный порядок байтов, не плавающий от платформы к платформе. То же самое и на клиенте - ты должен быть уверен, что не только сам клиент, но и все приложения, которые им пользуются, имеют фиксированный порядок байтов, неизменный от платформы к платформе. Если ты можешь гарантировать подобную определенность (то есть фактически независимость порядка байтов на любом из узлов от платформы), то ты можешь и не пользоваться промежуточным преобразованием в сетевой порядок байт, а передавать байты как тебе удобно. Этот же вариант ты можешь смело использовать, если все поддерживаемые тобой платформы имеют одинаковый порядок байт, и ты не боишься, что этот статус-кво в ближайшем будущем изменится.
Надеюсь, я не слишком запутал ситуацию? :)

> > По-моему, это не имеет большого значения. А поле
> > reserved лучше иметь всегда.
>
> Зачем тогда гуру обманывают начинающих, как я? ;)))
Ну так им же обидно, что их тоже когда-то обманули :) А если серьезно, то чаще выравнивание все-таки полезно. Другой вопрос, что в случае сетевых приложений однозначного ответа нет: в зависимости от характера данных это может быть желательно, не важно или даже вредно (например, когда размер передаваемых данных находится где-то в районе размера пакета).

> Про производительность согласен. Но! Если компиллер не
> вставит дополнительный код (за спиной программера) при
> доступе к невыровненным данным, то на некоторых процеках
> это приводит к ошибкам, как говорят гуру. Сам не встречал.
> Как думаешь?
Хм, ну да, чтобы, скажем, обратиться ко второму байту в слове, нужен дополнительный код %-) Ну так блин, если компилятор его когда надо не вставляет - на свалку такой компилятор, он же неработающий код генерит. Что же касается производительности, то да, этот дополнительный код ее снизит, поэтому, если нет насущной необходимости все уложить максимально компактно, то лучше данные выравнивать. См. выше мои слова про размер данных и размер пакета.

> > "Нам не дано предугадать", в каком месте наш софт
> > слетит. И не стоит уповать на вылизанность кода, тем более
> > что потери производительности действительно никакой.
>
> Вот блин! Хоть это я сделал правильно;))
Ну почему "хоть это" - основная идея была заложена вполне прогрессивная, имхо :)

Да, кстати. Весьма серьезным моментом при выходе на уровень Интернет, будет параноидальность при обработке полученных пакетов. В LAN обычно еще можно (хотя уже нехорошо) заложиться на то, что к серверу всегда будет присоединяться "хороший" клиент, а не хакер с неткатом. А в Интернет надо проверять все по полной. Поэтому вопросы производительности по сравнению с вопросами безопасности нередко отходят (должны отходить) на второй план.
Ну, в прынципе да ;))) 16.12.03 06:41  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> В твоей схеме есть еще дополнительные издержки на
> копирование моментального снимка состояния потоков.

Ну, в прынципе да ;)))

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

Если есть линки под рукой, кинь, плз. Только желательно, для таких "одарённых" как я;))) Шоб шо нибудь там понял;)))

> над select'ом особо думать не надо, с ним только работать
> муторно, зато код пишется ровно один раз.

Ды там и так всё, что читается из потока - вычитывается стандартно по select селекту:
...
rc = select(0, &readfds, 0, &exceptfds, &tv);

if(rc == SOCKET_ERROR|FD_ISSET(s, &exceptfds))
return SOCKET_ERROR;

if(FD_ISSET(s, &readfds))
{
rc = recv(s, ( char FAR *) buf + total, bytesleft, 0);
...
...

Как же без него. Я там о другом - придётся мне не функции чтения из сокета переделывать, а перелапачивать всего хозяйства клиента, чтобы при получении сообщений keepalive отвечать "ack". Да в принципе, думаю, не беда там.

> Да, придется, и это правильно. Ибо это переносимо, не нужно
> заморачиваться с платформами, отличающимися порядком
> байтов. Каков будет порядок байт при пересылке,
> действительно не имеет значения. Но зато можно быть твердо
> уверенным,
...
> Надеюсь, я не слишком запутал ситуацию? :)

Согласен. По крайней мере буду знать, что делать "так", всёж нужно;)))
Ситуацию - не запутал.


> Хм, ну да, чтобы, скажем, обратиться ко второму байту в
> слове, нужен дополнительный код %-) Ну так блин, если
> компилятор его когда надо не вставляет - на свалку такой
> компилятор, он же неработающий код генерит.

Легко. Раньше, вспомни Турбо С, можно было устанавливать( не устанавливать) опции выравнивания для компиллера. Хотя может это я уже не помню.;))

> Ну почему "хоть это" - основная идея была заложена вполне
> прогрессивная, имхо :)

До основной идеи (где я прокосячил) мы ещё не дошли. Я чуть позже публично распишусь в программеро-импотенции ;))

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

И да и нет. На том форуме, из-за чего я и завёлся, тоже отчасти обсуждались и вопросы безопасности. И тебе SSL, и тебе куки, и тебе - вообщем разное. Я не встрявал по некоторым соображениям. Несчастные и наивные. Я могу, конечно заблуждаться, но считаю, что если программеры моего уровня и могут упражняться в несложных задачах, то к вопросам инф. безопасности - БЛИЗКО НЕЛЬЗЯ ПОДПУСКАТЬ.
Моё мнение следующее. В России есть классные решения, например, информзащиты или инфотекса. Да и другие конторы есть. Возьми, например, дионис. Их и нужно использовать для создания "наложенных сетей" (как один из вариантов) в существующей сетевой инфраструктуре и д.р. В России, в госконторах - только такое и может быть. Я могу сильно заблуждаться в программировании (поскольку таковым не являюсь). Но здесь ты уж мне поверь;)))
Ну, в прынципе да ;))) 16.12.03 12:56  
Автор: Ktirf <Æ Rusakov> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> Если есть линки под рукой, кинь, плз. Только желательно,
> для таких "одарённых" как я;))) Шоб шо нибудь там понял;)))
Боюсь, что ничего толкового посоветовать не смогу, кроме разве что Элджера ("Язык программирования C++"). У Страуструпа тоже было по этому добру кое-что.

> > Хм, ну да, чтобы, скажем, обратиться ко второму байту
> в
> > слове, нужен дополнительный код %-) Ну так блин, если
> > компилятор его когда надо не вставляет - на свалку
> такой
> > компилятор, он же неработающий код генерит.
>
> Легко. Раньше, вспомни Турбо С, можно было устанавливать(
> не устанавливать) опции выравнивания для компиллера. Хотя
> может это я уже не помню.;))
Э-э, насколько я помню, в Турбо С (как и в других компиляторах) можно было либо выравнивать, либо не выравнивать. Я имел в виду случай, когда не выравнивать и не генерить код для обращения к невыровненному %-) В общем, бред, забей :)
Короче говоря, программист заказывает музыку: выравнивать или нет. Можно это делать выборочно, если очень хочется. Мое скромное мнение состоит в том, что обычно лучше выравнивать, но при передаче данных по сети надо внимательно смотреть, что и насколько выравнивается.
А вообще в одной умной книжке сказано следующее: оптимизацией надо заниматься только тогда, когда тебе об этом скажет профайлер. Не раньше.

> На том форуме, из-за чего я и завёлся, тоже
> отчасти обсуждались и вопросы безопасности. И тебе SSL, и
> тебе куки, и тебе - вообщем разное. Я не встрявал по
> некоторым соображениям. Несчастные и наивные. Я могу,
> конечно заблуждаться, но считаю, что если программеры моего
> уровня и могут упражняться в несложных задачах, то к
> вопросам инф. безопасности - БЛИЗКО НЕЛЬЗЯ ПОДПУСКАТЬ.
Я не говорю сейчас об информационной безопасности, это отдельная большая тема, и конечно, не нам, простым программерам, ей заниматься. Но код, торчащий наружу (т.е. реализующий какой-то API любого уровня и вида) обязан быть параноидальным rather than производительным.

> Моё мнение следующее. В России есть классные решения,
> например, информзащиты или инфотекса. Да и другие конторы
> есть. Возьми, например, дионис. Их и нужно использовать для
> создания "наложенных сетей" (как один из вариантов) в
> существующей сетевой инфраструктуре и д.р. В России, в
> госконторах - только такое и может быть. Я могу сильно
> заблуждаться в программировании (поскольку таковым не
> являюсь). Но здесь ты уж мне поверь;)))
Верю :)
ОК ;)) 17.12.03 01:13  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> Я имел в виду случай, когда не выравнивать и
> не генерить код для обращения к невыровненному %-) В общем,
> бред, забей :)

ОК ;))

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

Золотые слова ;))

> Но код, торчащий наружу (т.е.
> реализующий какой-то API любого уровня и вида) обязан быть
> параноидальным rather than производительным.

Оооо!!! Хорошо с тобой беседовать;))) Серьёзно. А то в моём окружении, есть товарисчи, которые меня параноиком и считают ;)))
Впрочем, на них я забил;)))
Нечто подобное я для себя делал. Только там и клиент был... 15.12.03 11:12  
Автор: ukv Статус: Незарегистрированный пользователь
<"чистая" ссылка> <обсуждение закрыто>
> И сервер, и клиент - на блокирующих сокетах. Интерфейс
> Беркли. Асинхронные штучки в реализации сокетов в Windows
> здесь не использую (пока). Сервер – многопоточный (каждого
> клиента обслуживаем в отдельном потоке).
>

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

Многопоточная схема с блокирующими сокетами - конечно, выглядит просто (с точки зрения разработки), но расчитывать на ее работоспособность при количестве клиентов порядка 512 я бы не стал. По крайней мере без предварительной проверки.
Спасибо за мнение. 16.12.03 01:51  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
Спасибо за мнение.

> Нечто подобное я для себя делал. Только там и клиент был
> многопоточный и его код отличался от серверного наличием
> пользовательского интерфейса.

Когда присутствует гуи, очень даже и хорошо иметь иметь отдельный поток для медленного ввода/вывода.

> Многопоточная схема с блокирующими сокетами - конечно,
> выглядит просто (с точки зрения разработки),

Это для таких, как я. Сложное - не умею;))

> на ее работоспособность при количестве клиентов порядка 512
> я бы не стал. По крайней мере без предварительной проверки.

Думаю, да. Ты прав. И ты знаешь, мне смешно читать гуру-ассемблировщиков, что их сервера "тащат" до 5000 потоков легко. Я не хочу с апломбом говорить о том, чего не знаю. Только догадываюсь, что при большем числе потоков (~512) нужно б другие решения искать. Может, как их моднецки называют - "гетерогенные", балансируюшие нагрузку между процессами и потоками. Не знаю, но что-то мелькало - вроде как Апач так работает.
Про потоки — есть в NT облегчённый их собрат, fiber называется... 16.12.03 06:58  
Автор: HandleX <Александр М.> Статус: The Elderman
Отредактировано 16.12.03 07:02  Количество правок: 1
<"чистая" ссылка> <обсуждение закрыто>
> Думаю, да. Ты прав. И ты знаешь, мне смешно читать
> гуру-ассемблировщиков, что их сервера "тащат" до 5000
> потоков легко. Я не хочу с апломбом говорить о том, чего не
> знаю. Только догадываюсь, что при большем числе потоков
> (~512) нужно б другие решения искать. Может, как их
> моднецки называют - "гетерогенные", балансируюшие нагрузку
> между процессами и потоками. Не знаю, но что-то мелькало -
> вроде как Апач так работает.

Причём ты сам царь и бог в плане их переключения — соль subj в том, что ядро их не shedul'ит. Ну и вот тебе пожалуйста — реализовывай балансирование нагрузки и проч...

Объект ядра fiber имеет только свой «приватный» стек, регистры и некий FiberData в лице DWORD.
Кстати, их можно использовать в MS SQL, настраивается как параметр сервера... Говорят когда много клиентов, он крутится гораздо живее с использованием subj...
Да, спасибо за Ваше замечание. Я не трогал фиберы в связи с... 16.12.03 08:51  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> Причём ты сам царь и бог в плане их переключения — соль
> subj в том, что ядро их не shedul'ит. Ну и вот тебе
> пожалуйста — реализовывай балансирование нагрузки и проч...
>
> Объект ядра fiber имеет только свой «приватный» стек,
> регистры и некий FiberData в лице DWORD.
> Кстати, их можно использовать в MS SQL, настраивается как
> параметр сервера... Говорят когда много клиентов, он
> крутится гораздо живее с использованием subj...

Да, спасибо за Ваше замечание. Я не трогал фиберы в связи с их специфичностью в Windows.
Если можно, киньте, пожалуйста, ссылку про MS SQL. Сервер хороший для небольших баз. Но иногда немного тормознутый (хотя может и по моей вине;))))

Спасибо.
Ну как бы промышленный сервер считается ;-) 16.12.03 12:20  
Автор: HandleX <Александр М.> Статус: The Elderman
<"чистая" ссылка> <обсуждение закрыто>
> Да, спасибо за Ваше замечание. Я не трогал фиберы в связи с
> их специфичностью в Windows.
> Если можно, киньте, пожалуйста, ссылку про MS SQL. Сервер
> хороший для небольших баз. Но иногда немного тормознутый
> (хотя может и по моей вине;))))
О сорри, со мной можно «на ты».
Тем более, что я первый начал ;-)
Если не против, конечно...

На самом деле там много наворотов, сейчас под рукой его у меня нет.
Я про него напомнил только из-за того, что использование fibers там можно явно указать. На вкладке «Параметры производительности» можно закрыжить галочку типа «Use fibers as worker threads» или что-то в этом роде.

Кста, последний их релиз вроде MS SQL 2000 там же несколько версий... Есть Desktop Engine (который с тормозами), а есть Enterprise Edition навороченный... В тестах Microsoft на их платформе это самый быстрый и мощный сервер...
Они затихли и готовят нечто грандиозное... Много будет заточек под XML и вообще, они там что-то начали с древовидными структурами мутить... Т.е. может сразу вестись работа не с таблицами, а с деревьями данных...

Посмотрим, что из этого получится ;-)
А точно, есть такая партия: 17.12.03 01:03  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
> Я про него напомнил только из-за того, что использование
> fibers там можно явно указать. На вкладке «Параметры
> производительности» можно закрыжить галочку типа «Use
> fibers as worker threads» или что-то в этом роде.

А точно, есть такая партия:
MyServer->Properies->Processor->галка Use Windows NT fibers ;)))
Раньше никогда не обращал внимания;)))

> Кста, последний их релиз вроде MS SQL 2000 там же несколько
> версий... Есть Desktop Engine (который с тормозами), а есть
> Enterprise Edition навороченный... В тестах Microsoft на их
> платформе это самый быстрый и мощный сервер...
> Они затихли и готовят нечто грандиозное... Много будет
> заточек под XML и вообще, они там что-то начали с
> древовидными структурами мутить... Т.е. может сразу вестись
> работа не с таблицами, а с деревьями данных...

ООБД? Типа КешЭ?
Да-да. А не подкинешь линк на «КешЭ», чтобы познакомится? Первый раз слышу просто ;-) 17.12.03 07:19  
Автор: HandleX <Александр М.> Статус: The Elderman
<"чистая" ссылка> <обсуждение закрыто>
Линк до InterSystems Corporation 17.12.03 10:27  
Автор: void <Grebnev Valery> Статус: Elderman
<"чистая" ссылка> <обсуждение закрыто>
http://www.intersystems.ru/
1  |  2 >>  »  




Rambler's Top100
Рейтинг@Mail.ru


  Copyright © 2001-2024 Dmitry Leonov   Page build time: 1 s   Design: Vadim Derkach