1) Какой тип реализации сервера предпочтителен при большом (тысячи – десятки тысяч) и при малом (десятки) предполагаемом количестве клиентских подключений? (Варианты: multi-thread blocking synchronous или single-thread non-blocking asynchronous)? Т.е. как сделать, чтобы сервер не заглючил при большом кол-ве клиентов и как не сделать слишком навороченный сервер если клиентов раз два и обчелся?
2) Какой тип реализации клиента (blocking synchronous, non-blocking asynchronous) самый практичный с точки зрения производительности и простоты реализации?
Как в клиенте, так и в сервере я склоняюсь к асинхронному single-thread non-blocking (т.е. путем указания окна с помощью WSAAsyncSelect), как наиболее простому, универсальному и функциональному, насколько это обосновано?
3) В связи с этим вопрос по использованию WSAAsyncSelect: Не порекомендуете ли уже отработанную схему обработки сообщений FD_CONNECT, FD_ACCEPT, FD_READ, FD_WRITE, FD_CLOSE в клиенте и сервере (если у обоих asynchronous single-thread non-blocking, протокол TCP), в правильной последовательности, с правильными функциями и обработкой ошибок? Я не уверен, что до конца правильно понял то, что описано в MSDN, а проверить все возможные ситуации нет возможности (например, пакеты данных маленькие, но нельзя рассчитывать, что они будут обязательно приняты за один recv(), в этом случае нужно пододать следующего FD_READ, и т.д. и т.п.). К тому же я слышал, что у какой-то версии винды без сервис-пака есть баг с FD_WRITE. В общем, все это меня малость пугает .
Здравствуйте, Ignoramus, Вы писали:
I>1) Какой тип реализации сервера предпочтителен
Multithreadded + blocking проще в реализации, но реально не вынесет больше 500 сеансов.
Если требуется больше сеансов, то есть смысл завести один или несколько дочерних процессов и передавать новые сокеты туда... Такой "гибридный" подход использует Apache.
Этот сопособ хорош тем, что он прост как 5 копеек — 1 поток == 1 сеанс.
Все проблемы конкурентности отпадают сами собой.
Single-thread + non-blocking должен быть более производителен, но только при очень и очень аккуратном и грамотном программировании. Т.к. все расчеты ведутся в одном потоке, то программист сам должен писать "планировщик сеансов" (задача д-а-а-леко нетривиальная и чреватая всякими чудесами). И, так как сеанс постоянно прерывается другими сеансами, то надо определять специальную структуру в которой его состояние и логику работы с ней.
Если проект большой и ТЗ постоянно меняется, этот подход может привести к тому что код станет совершенно невозможно поддерживать. В общем, если на сервер выдано толковое ТЗ (которое не будет слишком сильно меняться "в процессе") и команда толковыя и аккуратная и, (самое главное) небольшая, то есть смысл пойти по этому пути.
____________________
God obviously didn't debug, hasn't done any maintenance, and no documentation can be found. Truly amateur work.
Здравствуйте, TepMuHyc, Вы писали:
TMH>Если проект большой и ТЗ постоянно меняется, этот подход может привести к тому что код станет совершенно невозможно поддерживать. В общем, если на сервер выдано толковое ТЗ (которое не будет слишком сильно меняться "в процессе") и команда толковыя и аккуратная и, (самое главное) небольшая, то есть смысл пойти по этому пути.
Ок, в моем случае последние условия выполняются .
Я с Вами согласен. У multi-thread реализации есть еще недостаток – потоки нельзя корректно прервать, если будет нужно, по крайней мере это не так просто. Синхронизация потоков также не тривиальная задача даже в сравнении с «планировщиком сеансов», если сервер/клиент не совсем уж примитивный. Про многопроцессное приложение я вообще молчу. А передача сообщений окну привычна и удобна для Windows-программистов. Если это решение к тому же и более scalable, то это именно то, что мне нужно.
На форумах я часто встречал вопросы от разных участников по примерам реализации асинхронных сокетов. Особенно проблематичным, в частности, оказывается порядок поступления сообщений в окно и их правильной обработки. Предлагаю обсудить предлагаемый ниже пример реализации. Спорные моменты отмечены фразами «я надеюсь» . Хотелось бы услышать комментарии профессионалов .
Итак, сервер и клиент используют non-blocking asynchronous single-thread сокеты, протокол TCP.
Общение между клиентом и сервером заключается в инициации запроса клиентом серверу и получении некоторого ответа, после чего сеанс связи завершается.
Пакеты данных состоят из заголовка и тела. Общий размер пакета данных известен и содержится в заголовке каждого пакета.
Действительно, в случае использования WSAAsyncSelect требуется организация «планировщика сеансов», который, как мне видится, будет представлять собой список, каждый элемент которого будет включать сокет, текущий режим сокета (прием/передача), количество уже принятых/переданных байт, и буфер, который первоначально имеет размер заголовка пакета.
Для сервера:
Сервер вызывает listen.
При появление incoming соединения возникает FD_ACCEPT. Вызываю accept(), который создает новый сокет. Создаю элемент списка, для которого выделяю буфер размером с заголовок, инициализирую указатель в буфере, устанавливаю режим приема. Заношу элемент в список.
После установления соединения вначале возникает FD_READ (а не FD_WRITE, я надеюсь!). По сокету идентифицируется элемент списка «планировщика сеансов». Вначале recv заполняет буфер заголовка. Если это не произойдет за один раз (что маловероятно, но все же), жду новых FD_READ и вызываю recv, передвигая указатель в буфере, пока весь заголовок не будет прочитан. Затем увеличиваю буфер до размера, указанного в заголовке пакета и продолжаю ловить FD_READ и вызывать recv, пока он не заполнится. При всех вызовах recv не должно возникнуть WSAEWOULDBLOCK, т.к. recv вызывается только один раз по FD_READ. Если последний кусок принят с WSAEMSGSIZE (впрочем, равно как и с любой другой ошибкой), то это неправильный пакет и его нужно выбросить, а сокет закрыть и удалить из списка. Когда весь пакет принят, он передается приложению, которое его обрабатывает и генерирует ответный пакет. Режим для данного сокета переключается на передачу, указатель в буфере сбрасывается.
Если в процессе приема приходит FD_WRITE, я считаю, что пакет закончился раньше времени, т.е. это неправильный пакет, его нужно выбросить, сокет закрыть и удалить из списка. В других случаях (я надеюсь!) FD_WRITE не приходит, пока прием не будет закончен.
Приходит «правильный» FD_WRITE (когда сокет в режиме передачи). Идентифицирую элемент «планировщика сеансов» по сокету. Вызываю send(), передавая весь буфер с пакетом. Опять же, WSAEWOULDBLOCK не должен возникнуть (я надеюсь), так как передача идет в момент FD_WRITE, т.е. когда можно передавать прямо сейчас. Единственное что может случиться – не все байты пакета могут быть переданы за один раз. Тогда нужно передвинуть указатель в буфере и ждать новых FD_WRITE, вызывая по ним send пока весь пакет не будет передан. Когда это произойдет, вызываю shutdown(SD_SEND) для «грациозного» закрытия сокета, сразу после этого closesocket.
Если приходит FD_CLOSE с ошибкой, сокет закрывается и удаляется из списка. Если приходит без ошибки (после shutdown со стороны клиента), ничего не происходит, если прием уже закончен. В противном случае (преждевременный shutdown), опять закрываю сокет и удаляю из списка.
Работа клиента во многом напоминает работу сервера кроме следующих моментов:
Вместо listen() вызывается connect()
Вместо FD_ACCEPT приходит FD_CONNECT, по которому пополняется список «планировщика сеансов».
Вместо FD_READ первым приходит FD_WRITE (я надеюсь). Передача осуществляется так же, как у сервера.
Когда передача закончена, вызывается shutdown(SD_SEND).
Приходит FD_READ. Прием осуществляется так же, как у сервера.
Если FD_READ или FD_WRITE приходят невовремя, сокет закрывается и удаляется из списка.
Приходит FD_CLOSE, вызываю closesocket. Если FD_CLOSE приходит невовремя, сокет закрывается и удаляется из списка.
Оставшиеся вопросы:
1) Есть ли гарантия, что все сообщения будут возникать именно в указанном порядке или последовательность может быть другая?
2) Правильная ли последовательность действий при обработке сообщений?
3) Off-topic: Как быть, если вызов, к примеру, recv два раза на одно событие FD_READ приводит к WSAEWOULDBLOCK? В следующий раз, когда придет FD_READ, по причине последнего recv, нужно ли вызывать recv еще раз? Если нет, то как определить сколько байт было принято?
Здравствуйте, Ignoramus, Вы писали:
I>После установления соединения вначале возникает FD_READ
Ты сначала должен вызывать WSAAsyncSelect с флагом FD_READ и ждать пока "тебя не позовут" —
т.е. покуда не прибудут данные от клиента.
I>(а не FD_WRITE, я надеюсь!).
Зря надеешься — FD_WRITE возникнет СРАЗУ ЖЕ — так как новый сокет УЖЕ ГОТОВ к отправке данных.
Ты не забывай — сокет он дуплексный, то есть он может передавать данные в обе стороны одновременно.
Короче, это будет выглядеть примерно так:
LRESULT YourWndProc(.........) {
case WM_ASYNCSELECT:
switch( WSAGETSELECTEVENT(lParam) )
{
case FD_ACCEPT://создание нового сеанса.
{
if( WSAGETSELECTERROR(lParam) != 0 )
{
//глобальная паника - серверный сокет сдох!!!!
}
if( wParam != listeningSocket )
{
//глобальная паника - пришел непонятный ACCEPT!!!!
}
SOCKET newSocket = accept(listeningSocket, .......);
SocketData* sockData = CreateControlStructureForSocket(newSocket);//создать управляющую структуру для сокета
WSAAsyncSelect(newSocket, hYourWnd, WM_ASYNCSELECT, FD_READ);//ждем "привет" от клиентаbreak;
}
case FD_READ://читаем данные из сокета
{
if( WSAGETSELECTERROR(lParam) != 0 )
{
//какой-то сеанс загнулся - убить управляющую структуру сокетаbreak;
}
SocketData* sockData = GetControlStructureForSocket(wParam);
if( sockData == NULL )
//глобальная паника - сокет без управляющей структуры!!!!char arrDataChunk[255];
int size = recv(wParam, arrDataChunk, 255, 0);
sockData->AddDataChunk(arrDataChunk, size);
if( sockData->EndOfDataDetected() )
{
//засекли что клиент закончил "посылку".
//значит надо отвечать...
WSAAsyncSelect(wParam, hYourWnd, WM_ASYNCSELECT, FD_WRITE);
sockData->AddDataToSend("Hello, poor bastard!");
} else {
//ответ клиента пришел не полностью - ждем дальше...
WSAAsyncSelect(newSocket, hYourWnd, WM_ASYNCSELECT, FD_READ);
}
break;
}
case FD_WRITE://пишем данные в сокет
{
if( WSAGETSELECTERROR(lParam) != 0 )
{
//какой-то сеанс загнулся - убить управляющую структуру сокетаbreak;
}
SocketData* sockData = GetControlStructureForSocket(wParam);
if( sockData == NULL )
//глобальная паника - сокет без управляющей структуры!!!!int size = sockData->GetDataToSendLength();
if( size > 0 )
{
// есть что посылать...
send(wParam, sockData->GetDataBuffer(), size, 0);
WSAAsyncSelect(wParam, hYourWnd, WM_ASYNCSELECT, FD_WRITE);
} else {
//все данные ушли - переходим в режим чтения
WSAAsyncSelect(newSocket, hYourWnd, WM_ASYNCSELECT, FD_READ); }
}
break;
}
case FD_CLOSE:
//сеанс закрылся - прибить управляющую структуру сокета
//и закрыть сокет
}
}
I>3) Off-topic: Как быть, если вызов, к примеру, recv два раза на одно событие FD_READ
приводит к WSAEWOULDBLOCK?
Не вызывай recv два раза подряд
I>В следующий раз, когда придет FD_READ, по причине последнего recv,
FD_READ приходит не потому что ты recv() вызвал, а потому, что данные в сокет пришли.
I>нужно ли вызывать recv еще раз?
Нет, не нужно.
I>Если нет, то как определить сколько байт было принято?
А MSDN почитать слабо?
ЗЫ: Оффтопик. Если хочешь чтобы у тебя все работало и голова не болела, делай все в следующей последовательности:
1) Внимательно читай документацию
2) Думай как применить полученные знания на практике
А не наоборот. И сразу же солнце станет светить ярче.... I>Заранее спасибо.
____________________
God obviously didn't debug, hasn't done any maintenance, and no documentation can be found. Truly amateur work.
Используйте порты завершения ввода-вывода применительно к сокетам. Работают исключительно хорошо при любом количестве подключений. Правда есть один минус — существуют исключительно под NT. В MSDN про это хорошо написано.
Спасибо, что нашли время на столь развернутый ответ
Примерно так, как Вы написали, и выглядит мой сервер . Единственно что мне не пришло в голову – вызывать WSAAsyncSelect при обработке одного из FD, меняя таким образом статус сокета с чтения на запись и наоборот. Вместо этого я вызываю ее единожды, перед listen(), а на каждое FD анализирую текущий статус клиентского сокета (хранимый в sockData) и игнорирую, если оно пришло невовремя. Я подумал над этим, и мне кажется, что мой способ все же более надежный, ведь не исключен случай, когда во время обработки одного FD в очередь поступает еще один FD, который нас уже не будет интересовать после обработки текущего сообщения, но тем не менее, будет принят. Что Вы об этом думаете?
С FD_WRITE я уже разобрался, действительно, он может возникнуть когда угодно. К счастью, анализируя статус sockData несвоевременные FD_WRITE можно отсеять.
По поводу повторного вызова recv() я спрашивал не потому, что я собираюсь это делать, а из спортивного интереса, так как в MSDN этот вопрос не освещен. Может быть, recv не очень удачный пример. Ну скажем, что будет, если на FD_CLOSE вызвать send() (Это, кстати, рекомендуется делать для graceful shutdown в MSDN, чтобы выслать remaining response data)? Если в данный момент передача невозможна, возникнет WSAEWOULDBLOCK, а когда станет возможна, появится FD_WRITE. Нужно ли при этом вызывать еще раз send, или он уже в процессе? Если да, то как узнать, сколько байт будет реально передано? Конечно, я буду избегать таких вариантов, но просто интересно . В MSDN среди прочего написано: “FD_WRITE:… After send or sendto fail with WSAEWOULDBLOCK, when send or sendto are likely to succeed” И что из этого следует?
И еще, в Вашем коде я не нашел вызовов shutdown. Как правильно организовать окончание сеанса связи? Напоминаю, что в моем случае сеанс связи всегда заканчивается сразу после ответа сервера (Ваш код, похоже, рассчитан на более длительный сеанс).
В MSDN (WinSock->”Graceful Shutdown, Linger Options, and Socket Closure”) написано, что нужно вызывать shutdown с обеих сторон, сервера и клиента. Мне, кстати, кажется неудачной идея отсылать ответ сервера как "remaining response data” на FD_CLOSE, как там написано (см. причину выше).
Вот и получается, что не все в MSDN написано так, чтобы не возникало вопросов. На самом деле я достаточно много времени провел в изучении матчасти . Кроме MSDN Winsock API (который, кстати, достаточно скупой на методику, есть только описание функций, а также чрезвычайно важных отличий Winsock 2 от WinSock 1 и от Berkeley) прочитал еще кучу всего www.sockets.com, www.tangentsoft.net/wskfaq/ и другие. Мои вопросы, в основном, касаются нюансов практической реализации. Авторы примеров, которые можно найти в сети, обычно не утруждают себя написанием действительно практически применимого кода, ограничиваясь, как правило фразой «sorry, my program is not smart enough to deal with this»).
Здравствуйте, masta, Вы писали:
M>Используйте порты завершения ввода-вывода применительно к сокетам. Работают исключительно хорошо при любом количестве подключений. Правда есть один минус — существуют исключительно под NT. В MSDN про это хорошо написано.
Спасибо, охотно верю. К сожалению, я мало знаком с completion port i/o, так что пришлось бы долго разбираться. К тому же NT ограничение достаточно весомое в моем случае.
Здравствуйте, Ignoramus, Вы писали:
I>Ну скажем, что будет, если на FD_CLOSE вызвать send()
Данные уйдут.
Кстати, в этом случае очень рекомендую перевести сокет в блокирующий режим, а только затем посылать данные — сильно упростишь себе работу. Данных, как правило, должно быть совсем немного — т.к. сеанс уже закрывается.
I>Если в данный момент передача невозможна, возникнет WSAEWOULDBLOCK, а когда станет возможна, появится FD_WRITE. Нужно ли при этом вызывать еще раз send, или он уже в процессе?
Вызывай send() тогда и только тогда когда приходит FD_WRITE. И только один раз. Когда сокет закончит с посылкой — он тебе скажет (пошлет FD_WRITE). То же самое с recv().
I>Если да, то как узнать, сколько байт будет реально передано?
send() закончит работу только тогда когда ВСЕ данные будут переданы. Тогда он опять пошлет FD_WRITE (типа, "я готов"). Кстати, буфер данных должен существовать и быть неизменным до тех пор пока все данные из него не уйдут. Поэтому, боже упаси располагать этот буфер на стеке.
I>В MSDN среди прочего написано: “FD_WRITE:… After send or sendto fail with WSAEWOULDBLOCK, when send or sendto are likely to succeed”
В переводе на русский: Ты попробовал послать данные — не получилось. Дождись FD_WRITE и пробуй еще.
I>И еще, в Вашем коде я не нашел вызовов shutdown.
Его и не надо.
Дело в том, что если не играться с опцией Linger, что-то похожее на "graceful shutdown" произодет автоматически на closesocket().
То есть: все данные переданные в функцию send() уйдут. Все данные еще не принятые функцией recv() будут потеряны. Так как программист вызывает closesocket() вполне сознательно, он должен понимать,
что после вызова этой функции любая каммуникация невозможна — следовательно, тем самым он согласен на "потерю" еще не приятых данных .
I>Как правильно организовать окончание сеанса связи?
Это зависит от протокола который ты реализуешь.
Проблемы graceful shutdown реально важны только для полностью дуплексных протоколов (т.е. когда inbound и outbound потоки данных независимы в той или иной степени). Вот здесь и надо делать полный Graceful Shutdown.
Большинство же Internet протоколов полудуплексные (т.е. клиент дал запрос, и ждет ответа сервера, дождался — опять запрос). В этом случае — graceful shutdown — излишество. Если клиент закрыл сокет, значит он уже не ждет от сервера ответа. Следовательно, возиться с оставшимися данными (и graceful shutdown) — просто лишняя работа.
I>Напоминаю, что в моем случае сеанс связи всегда заканчивается сразу после ответа сервера .
Тем более, никакого graceful shutdown'а и близко не надо.
____________________
God obviously didn't debug, hasn't done any maintenance, and no documentation can be found. Truly amateur work.
Здравствуйте, TepMuHyc, Вы писали:
TMH>В переводе на русский: Ты попробовал послать данные — не получилось. Дождись FD_WRITE и пробуй еще.
Я понял, действительно, “WSAEWOULDBLOCK” это ведь фактически сообщение об ОШИБКЕ, означающее «выполнение функции невозможно, поскольку это привело бы к блокированию потока, а сокет находится в не-блокирующем режиме». Именно поэтому нужно вызывать recv() и send() на FD_READ и FD_WRITE соответственно. Вопросы, подобные моему, вызваны тем фактом, что не во всех случаях сообщение WSAEWOULDBLOCK действительно свидетельствует об ошибке, например, connect() всегда возвращает WSAEWOULDBLOCK, тем не менее, функция выполняется и позднее генерирует FD_CONNECT (вызывать connect() еще раз не нужно, вызов завершен успешно). Такая, панимаешь, загогулина.
I>И еще, в Вашем коде я не нашел вызовов shutdown. TMH>Его и не надо. TMH>Дело в том, что если не играться с опцией Linger, что-то похожее на "graceful shutdown" произодет автоматически на closesocket().
Ок, слава богу. Снова, похоже я стал жертвой MSDN-а .
Все же, Вы не ответили на вопрос, лучше ли вызывать WSAAsyncSelect при обработке FD_, или единожды перед listen(), как это предложил я.
Весьма признателен за Ваши квалифицированные и исчерпывающие ответы. Приятно удивляет уровень профессионализма Ваш и форумов на этом сайте в целом.
Здравствуйте, Ignoramus, Вы писали:
I>Все же, Вы не ответили на вопрос, лучше ли вызывать WSAAsyncSelect при обработке FD_, или единожды перед listen(), как это предложил я.
На listening сокете достаточно вызвать один раз с FD_CONNECT.
На сеансовых сокетах лучше вызывать каждый раз перед сменой режима работы (т.е. чтение или запись)
с соответствующим флагом.
____________________
God obviously didn't debug, hasn't done any maintenance, and no documentation can be found. Truly amateur work.
Спасибо за помощь, теперь я уже на стадии отладки .
Проблема следующая: клиент не получает FD_READ, когда сервер отвечает на его запрос. Последовательность действий такая:
1) Клиент вызывает connect и получает FD_CONNECT.
2) Сервер в это время получает FD_ACCEPT и вызывает accept, создавая новый сокет.
3) Клиент получает FD_WRITE и отсылает свой пакет send. Функция возвращает размер пакета, т.е. успех.
4) Сервер получает FD_READ и принимает пакет recv. Функция также возвращает размер пакета, т.е. успех.
5) Сервер получает FD_WRITE и отсылает в сокет, созданный функцией accept, ответный пакет send. Функция возвращает размер пакета.
6) Сразу после этого сервер вызывает closesocket и закрывает сокет, созданный accept. (пробовал отключать эту команду, эффект тот же).
7) Клиент не получает FD_READ . Сокет остается висеть незакрытым.
Что делать? Какие могут быть причины? Какие Вы порекомендуете мне утилиты для контроля реального трафика в сети, чтобы локализовать ошибку?
...
>I>Если да, то как узнать, сколько байт будет реально передано? >TMH>send() закончит работу только тогда когда ВСЕ данные будут переданы. Тогда он опять пошлет FD_WRITE (типа, "я готов"). Кстати, буфер >данных должен существовать и быть неизменным до тех пор пока все данные из него не уйдут. Поэтому, боже упаси располагать этот буфер на стеке.
Не согласен с Вами, коллега. send() копирует пользовательский буфер в свой внутренний буфер стека. Это еще и Шнэйдер говорит, и просто здравый смысл.
Re: WinSock : муки выбора
От:
Аноним
Дата:
15.04.03 20:45
Оценка:
Здравствуйте, Ignoramus, Вы писали:
I> Какой тип реализации сервера предпочтителен при большом (тысячи – десятки тысяч) и при малом (десятки) предполагаемом количестве клиентских подключений?
— не совсем по существу вопроса (винсок), но рекомендую ознакомиться: