Легенда:
новое сообщение
закрытая нитка
новое сообщение
в закрытой нитке
старое сообщение
|
- Напоминаю, что масса вопросов по функционированию форума снимается после прочтения его описания.
- Новичкам также крайне полезно ознакомиться с данным документом.
[C++] Маленькое уточнение 15.12.03 10:24 Число просмотров: 1975
Автор: 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 не вычисляли контрольную сумму.
Как думаете вы, уважаемые?
|
|
|