![]() |
| ||
|
|
Новости |
Поиск |
FAQ |
Статьи |
Книги |
Новости VCL
| | Форумы ЧАТ |
Орешник |
|
| |||
| |||
« предыдущая ветвь | форум | следующая ветвь »
Как лучше реализовывать архитектуру клиент-сервер [D7, WinXP]
bobah © (2006-07-12 16:43:00)Здравствуйте.
Начну с описания. Существует у меня три вида программ. 1 - БД , 2 - сервер опроса приборов (клиент БД), 3 - клиентское ПО (ПО с которым взаимодействует пользователь). Это не трехзвенная модель в полном понимании. Программа 3 допустим вставляет запись в БД, при этом обращается непосредственно в БД. В программе 3 есть журнал событий, котором отображаются изменения БД. Для того чтобы сообщить всем клиентам об изменениях прога 3 взаимодействует с 2 с помощью сокетов. То есть по сокетам передаются сообщения типа refresh. Клиент посылает серверу опроса сообщение, а сервер опроса рассылает это же сообщение всем остальным клиентам.
Теперь конкретнее. Я использую WinSock 2.2 асинхронную модель на событиях. С клиентом всё ясно, а вот с сервером не всё. Важно чтобы сообщения, приходящие на сервер, уходили на другие клиенты в том порядке, в котором они приходят на сервер. Как это обеспечить?
На данный момент серверная часть работает в одном отдельном потоке. Причем события read от всех клиентов обрабатываются в одной функции WSAWaitForMultipleEvents.
bobah (2006-07-14 15:26:00) [21]Таа-к начались разногласия. Ну ничего я всё поясню.
1) Сервер, кроме обновлений, уведомляет юзеров о ходе процесса опроса приборов.
2) В клиентском присутствует журнал событий, который сообщает об события проихсодящих в системе, в том числе и об изменениях в БД. Обновлять БД кажду секунду или каждые 5 сек (неважно), мне показалось не лучшей идеей.
3) > тем болле не понятно, что тебя заставило задать вопрос в WinApi.
Согласен, с дуру, в то время мозг был напарен и прыгал с одной проблемы на другую. Кроме того, в последний раз когда я здесь бывал задавал вопрос не видел этой конференции, не заметил, моя вина.
4) > ... ты концептуально "плаваешь" в вопросах работы в многопользовательской СУБД...
Ошибка подтверждения транзакции известит 1-го юзера в т.ч. и о том, что другой юзер успел изменить ту самую запись, пока 1-й юзер телился с подтверждением. Первая же мысль нормального юзера - выполнить рифреш НД или конкретной записи в нем.
Пнервая же мысль ненормального программера - заср.ть 1-гоь юзера нотификациями о том, что кто-то его опередил.
Несколько не плаваю, если бы мне этого было достаточно, я бы так и сделал.
Механизм транзакций, уровни изоляций транзакций мне хорошо известны. И проблемы синхронизации при взаимодействии с БД здесь ни причем...
Теория реляционных баз данных клинент-серверных мне также мною уже давно была проштудирована.
> заср.ть 1-гоь юзера нотификациями о том, что кто-то его опередил.
Нет у меня нотификаций.
Прежде чем начинать наезжать можно и спросить.
P.S. Обновление в сокетах, пажалуй, только как дополнение к основной работе сокетов - управление приборам через серевер и GSM модем, который связан с приборами.
Сергей М. (2006-07-14 15:01:00) [22]
> Я вообще-то его в WinApi задал
тем болле не понятно, что тебя заставило задать вопрос в WinApi.
Суть вопроса, как выясняется, не имеет к WinApi никакого отношения - ты концептуально"плаваешь" в вопросах работы в многопользовательской СУБД.
Сергей М. (2006-07-14 14:58:00) [23]
> Что-то не пойму причем здесь транзакции.
Все с тобой понятно.
Дальнейший разговор бесплоден.
До встречи в Базах.
Сергей М. (2006-07-14 14:57:00) [24]Зачем вообще другим юзерам эти извещения ?
Юзер редактирует некую запись, постит ее и подтверждает транзакцию.
Ошибка подтверждения транзакции известит 1-го юзера в т.ч. и о том, что другой юзер успел изменить ту самую запись, пока 1-й юзер телился с подтверждением. Первая же мысль нормального юзера - выполнить рифреш НД или конкретной записи в нем.
Пнервая же мысль ненормального программера - заср.ть 1-гоь юзера нотификациями о том, что кто-то его опередил.
bobah (2006-07-14 14:55:00) [25]> Ты сочиняешь механизм для ЛВС или для глоб.сети ?
Для ЛВС
bobah (2006-07-14 14:55:00) [26]> Проблему и решать не надо, ибо она надумана по причине плохой твоей ориентации в механизме транзакций.
Что-то не пойму причем здесь транзакции.
> Вопрос по сути д.б. задан не в Сетях, а в Базах.
Я вообще-то его в WinApi задал:)
Сергей М. (2006-07-14 14:52:00) [27]Зачем вообще тебе те самые сокеты ?
Ты сочиняешь механизм для ЛВС или для глоб.сети ?
Сергей М. (2006-07-14 14:49:00) [28]
> сокеты в моей системе нужны чтобы своевременно обновлять
> данные в клиентах
Понятно.
Выбрось затею из головы - дурная она.
> Есть решение?
>
Проблему и решать не надо, ибо она надумана по причине плохой твоей ориентации в механизме транзакций.
Вопрос по сути д.б. задан не в Сетях, а в Базах.
bobah (2006-07-14 14:10:00) [29]> atruhin
MS SQL Server 2000.
bobah (2006-07-14 14:08:00) [30]Хорошо с Send() разобрались. Код в процессе написания... Завтра полужу его сюда. Спасибо за помощь.
Цель задачи: сокеты в моей системе нужны чтобы своевременно обновлять данные в клиентах. Т.е. один клиент скажем добавил запись в БД и передал сообщение серверу, чтобы последний разослал это сообщение это остальным клиентам. Мне бы хотелось чтоб в этих сообщениях передовалась точная информация об изменениях в БД, чтоб клиент менял у себя в контролах только нужные записи.
> Но я не вижу в этом никакой проблемы.
Есть решение?
Хотя, в последнее время я больше склоняюсь к передаче маркера, сигнализирующего о том что надо обновить данные из БД, в этом случае вообще можно не обращать внимание на строгую последовательность сообщений + ко всему вероятность появления таких ситуаций мала + сообщения в этом случае не более 3 - х байт.
Однако, вариант со строгой последовательностью стал мне ПРИНЦИПИАЛЬНО интересен.
Что думаем об этом?
atruhin (2006-07-14 14:03:00) [31]А с какой БД работаешь? Вроде все многопользовательские БД поддерживают отправку event. И все уже реализованно до тебя.
Сергей М. (2006-07-14 10:46:00) [32]
> как такая ситуация может возникнуть?
Очень просто.
Ф-ция Send() в действительности не занимается фактической передачей данных, ее задача - поместить указанные данных в некую внутреннюю очередь передачи (буфер передачи), которую неявно создает ядро winsock. Очередь индивидуальна для каждого сокета, размер ее фиксирован в каждый момент времени и по умолчанию равен 8К (зависит от ряда иных важных общесистемных параметров). Очередь освобождается с головы по мере фактической передачи имеющихся в ней данных. Попытка поместить в хвост очереди данные общего размера (т.е. при однократным или многократном последовательном вызове ф-ции send), превышающего свободное место в очереди, приводит к отказу самого последнего по времени вызова ф-ции send, при котором был достигнут лимит.
Если возникает такая ситуация, winsock сообщает об этом кодом отказа WSAGetLastError = WSAEWOULDBLOCK и впоследствии обязательно известит о фактическом освобождении очереди событием FD_WRITE. Кр.того, событие FD_WRITE возникает однократно всякий раз при успешном установлении соединения, сигнализируя о готовности сокета к передаче.
> Для этого необхадимо создать дополнительный буффер для отправки
> запоздавших сообщений? Или смещать буффер SBuff на Sindex
> какой-нибудь?
Самым простым и очевидным решением будет использование не буфера (тем более фиксированного размера), а стрима (например, TMemoryStream), фактически представляющего "резиновую" (варьируемого размера) локальную очередь.
Данные, которые требуется передать, записываются в хвост стрима в любом количестве в порядке поступления (очередь растет). Данные, которые передаются на вход ф-ции Send(), извлекаются из головы стрима порциями размером не более размера внутреннего буфера передачи winsock (см. GetSockOpt(SO_SNDBUF)) (очередь сокращается при успешном выполнении Send()
> Такое может случиться?
Да, может.
Но я не вижу в этом никакой проблемы.
bobah (2006-07-14 09:47:00) [33]Так всё становится яснее.
> следует повторить неудачную передачу позднее
Для этого необхадимо создать дополнительный буффер для отправки запоздавших сообщений? Или смещать буффер SBuff на Sindex какой-нибудь?
Да, кстате, а как такая ситуация может возникнуть?
И вот теперь насчет синхронизации можно нормально сформулировать вопрос.
1)Допустим наступил FD_READ от ClientSock1, началась обработка пакета функция RecievePacket(). Во время выполнения RecievePacket() от ClientSock1, взевелись события FD_READ от ClientSock2 и ClientSock3. После выполнения RecievePacket() от ClientSock1. Мы имеем в WSAWaitForMultipleEvent два FD_READ. И вот собственно вопрос, можно ли при такой ситуации гарантировать что данные прочитаются в RecievePacket()
в том порядке, в коком они пришли, т.е. сначала пакет от ClientSock2 и только потом от ClientSock3?
2)Допустим ситуация следующая от ClientSock1 пришел ну очень большой пакет за один присест данные в RecievePacket() прочитаться не смогли. Во время обработки данных от ClientSock1 в RecievePacket(), взевлось событие FD_READ от ClientSock2 с ну очень маленьким пакетом. В функции WSAWaitForMultipleEvents наступает событие от ClientSock2, в результате пришедшие данные от ClientSock2 обрабатываются в RecievePacket() за один присест. За тем приходит оставшаяся часть данных от ClientSock1.
Таким образом на все клиенты приходят данные следующего вида:
1/2*ClientSock1 - > 1*ClientSock2 -> 1/2*ClientSock1
Вопрос: Такое может случиться?
Сергей М. (2006-07-14 08:22:00) [34]
> TimeBuff = SBuff вынес из процедуры
Уже лучше.
> В случае если произошла ошибка Send закрываем соответсвующий
> сокет
> Что я ещё упустил?
Упустил самое главное - не проанализировал причинуошибки и тут же ничтоже сумняшеся бросился закрывать соединение.
А ведь ошибка эта вполне могла быть связана с достижением лимита размера буфера передачи !
Цитата из справки:
Return Values
If no error occurs, send returns the total number of bytes sent. (Note that this can be less than the number indicated by len.) Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError.
В ситуации с заполненностью буфера передачи WSAGetLastError вернет код WSAEWOULDBLOCK. При этом следует повторить неудачную передачу позднее, но не ранее возникновения события FD_WRITE.
bobah (2006-07-13 13:43:00) [35]Я переписал код.
type
PClientSock = ^TClientSock;
TClientSock = record
Socket: TSocket;
SockAddr: TSockAddrIn;
RBuff: Array[0..255] of Byte;
SBuff: Array[0..255] of Byte;
SendReady: Boolean;
RIndex: Integer;
UserID: Byte;
SockEvent: TWSAEvent;
end;
---------------------------------
procedure TSocketServThread.RecievePacket(ClientSock: PClientSock; ClientIndex: Integer);
var RecvBytes,SendBytes: Integer;
begin
with ClientSock^ do
begin
if UserID = 0 then // Если этот клиент не авторизован
begin
// Если клиент не подключен может придти только запрос на авторизацию
// поэтому читаем только три байта
RecvBytes := Recv(Socket,RBuff,3,0);
if RecvBytes <> SOCKET_ERROR then
begin
Move(RBuff,SBuff[RIndex],RecvBytes);
if SBuff[0] = RecvBytes + RIndex then
begin
// Пакет полностью получен
RIndex := 0;
SBuff[2] := Byte(SetUserID(ClientSock,SBuff[2])); // 1 - OK
SendBytes := Send(Socket,SBuff,3,0);
if SendBytes <> SOCKET_ERROR then
begin
//if SendBytes < 3 then
end else
begin
// Будем обробатывать ошибки потом
// CloseSocket...
end;
end else
begin
// Пакет получен не полностью
RIndex := RecvBytes + Rindex;
end;
end;
end else // Если это авторизованный пользователь
begin
RecvBytes := Recv(Socket,RBuff,SOCK_RBUFF_LEN,0);
Move(RBuff,SBuff,RecvBytes);
// Сколько сервер данных прочитал столько и отправил другим
// клиентам. Сам клиент приведет в порядок пакеты.
for i:=0 to ClientConnects.Count-1 do
begin
if (i = ClientIndex) or (PClientSock(ClientConnects[i])^.UserID = 0) then Continue;
SendBytes := Send(PClientSock(ClientConnects[i])^.Socket,SBuff,RecvBytes,0);
if SendBytes <> SOCKET_ERROR then
begin
//if SendBytes < RecvBytes then;
end else
begin
// Будем обробатывать ошибки потом
// CloseSocket...
end;
end;
end;
end;
-----------------------------------
procedure TSocketServThread.Execute;
var Sync: Cardinal;
NetworkEvents: TWSANetworkEvents;
ClientIndex: Integer;
ClientSock: PClientSock;
begin
if FOpened then
begin
Events[0]:=Self.CloseEvent;
Events[1]:=ServSockEvent;
CountEvents := 2;
// Асициируем событие Accept, переводимся в неблокирующий режим
WSAEventSelect(ServSocket,Events[1],FD_ACCEPT);
while not Terminated do
begin
sync := WSAWaitForMultipleEvents(CountEvents, @Events, false, INFINITE, false);
case sync of
WSA_WAIT_EVENT_0 : break; // close event
WSA_WAIT_EVENT_0+1: // Accept
begin
WSAEnumNetworkEvents(ServSocket,Events[1],@NetworkEvents);
if NetworkEvents.lNetworkEvents and FD_ACCEPT>0 then
if NetworkEvents.iErrorCode[FD_ACCEPT_BIT]=0 then
begin
NewClientSock;
end else
begin
// Error := 'Ошибка подключения клиента';
end;
end;
WSA_WAIT_EVENT_0+2..WSA_WAIT_EVENT_0+WSA_MAXIMUM_WAIT_EVENTS-1: // Клиентские сокеты
begin
ClientIndex := Sync - WSA_WAIT_EVENT_0;
ClientSock := PClientSock(ClientConnects[ClientIndex-2]);
WSAEnumNetworkEvents(ClientSock^.Socket,Events[ClientIndex],
@NetworkEvents);
if NetworkEvents.lNetworkEvents and FD_READ>0 then
if NetworkEvents.iErrorCode[FD_READ_BIT]=0 then
begin
// Пришли данные, которые надо прочитать
RecievePacket(ClientSock,ClientIndex-2);
end else
begin
// Произошла ошибка. Надо сообщить о ней и завершить нить
//CloseSocket(S);
end;
if NetworkEvents.lNetworkEvents and FD_WRITE>0 then
if NetworkEvents.iErrorCode[FD_WRITE_BIT]=0 then
begin
// Данные успешно записались
end else
begin
// Ошибки при записи произошли
// CloseSocket...
end;
if NetworkEvents.lNetworkEvents and FD_CLOSE>0 then
begin
// Связь разорвана
if NetworkEvents.iErrorCode[FD_CLOSE_BIT]=0 then
begin
// Свзяь закрыта корректно
end else
begin
// Связь разорвана в результате сбоя сети
end;
// В любом случае надо закрыть сокет и завершить нить
CloseClientSock(ClientIndex);
end
end;
end; // case
end;
end; // FOpened
end;
Функцию Send() проштудировал.
TimeBuff = SBuff вынес из процедуры, теперь существует она пока существует сокет.
В случае если произошла ошибка Send закрываем соответсвующий
сокет. Пока конкретные ошибки не рассматриваю, рассмотрю их поже.
Что я ещё упустил? Нужно ли ещё как-то следить за Send()?
Сергей М. (2006-07-13 10:51:00) [36]
> bobah © (13.07.06 10:16) [11]
А каков тогда смысл статической переменно RIndex, если она после возврата из RecievePacket() содержит индекс элемента уже несуществующего массива ?
> Можно подробнее?
Справку по Send() ты уже проштудировал ?
> Мой код имеет ли право на жизнь для моих целей?
Конечно имеет, но его еще доводить и доводить до ума.
bobah (2006-07-13 10:16:00) [37]> 1. Аккумулирующий буфер приема TimeBuff не должен быть локальным - время жизни лок.переменной равно времени жизни процедуры.
А нам больше и не нужно. TimeBuff наполняется в RecievePacket испоьлзуется ProccessRecvPacket... А-А-А или вы хотите сказать, что при вызове Send в неблокирующем режиме TimeBuff уже может не существовать? Если так то не хорошо. А как можно бороться?
> 2. Отсутствует анализ результата вызова ф-ции Send(), в то время как он обязателен !
В смысле на ошибки. Можно подробнее?
P.S. Мой код имеет ли право на жизнь для моих целей?
Сергей М. (2006-07-13 10:02:00) [38]
> bobah © (13.07.06 09:41) [9]
Отложи на время свои сомнения по поводу порядка.
В коде как минимум 2 серьезных ошибки.
1. Аккумулирующий буфер приема TimeBuff не должен быть локальным - время жизни лок.переменной равно времени жизни процедуры.
2. Отсутствует анализ результата вызова ф-ции Send(), в то время как он обязателен!
bobah (2006-07-13 09:41:00) [39]Ну вобщем этот для начала код привел. Чтоб пальцем в облака не тыкать...
Вот оставшиеся функции... (возможно они еще сыроваты)
procedure TSocketServThread.RecievePacket(ClientSock: PClientSock; ClientIndex: Integer);
var RecvBytes: Integer;
TimeBuff: Array[0..SOCK_RBUFF_LEN] of Byte;
begin
with ClientSock^ do
begin
RecvBytes := Recv(Socket,RBuff,SOCK_RBUFF_LEN,0);
if RecvBytes <> SOCKET_ERROR then
begin
Move(RBuff,TimeBuff[RIndex],RecvBytes);
if TimeBuff[0] = RecvBytes + RIndex then
begin
// Пакет полнстью собран
ProccessRecvPacket(ClientSock,TimeBuff,ClientIndex);
RIndex := 0;
end else
begin
// Пакет не полный
RIndex := RecvBytes;
end;
end;
end;
end;
------------------------------------
procedure TSocketServThread.ProccessRecvPacket(ClientSock: PClientSock; Buff: Array of Byte; ClientIndex: Integer);
var SBuff: Array[0..SOCK_RBUFF_LEN] of Byte;
i: Integer;
begin
// Здесь по идее надо разбирать пакет
// но в случае сервера просто смотрим, если код 01 (байт 2)
// то это запрос на USER ID и над вернуть ответ с код OK - 1
// c кодом ERRROR - 0;
// байт 1 - 3 - длинна пакета
// байт 2 - 1 - код пакета
// байт 3 - STATE - байт состояния
if Buff[1] = $01 then // Запрос User ID
begin
SBuff[0] := $03;
SBuff[1] := $01;
SBuff[2] := Byte(SetUserID(ClientSock,Buff[2])); // 1 - OK
Send(ClientSock^.Socket,SBuff,3,0);
end else
begin
// Оставшиеся сообщения раздаем всем остальным клиентам
for i:=0 to ClientConnects.Count-1 do
begin
if (i = ClientIndex) or (PClientSock(ClientConnects[i])^.UserID = 0) then Continue;
Send(PClientSock(ClientConnects[i])^.Socket,Buff,Buff[0],0);
end;
end;
end;
-------------------------------
procedure TSocketServThread.NewClientSock;
var NewClient: PClientSock;
SizeSockAddr: Integer;
begin
// Принимаем подключение
New(NewClient);
with NewClient^ do
begin
SizeSockAddr := SizeOf(SockAddr);
Socket := Accept(ServSocket,@SockAddr,@SizeSockAddr);
// Проверяем
if Socket = INVALID_SOCKET then
begin
Shutdown(Socket,SD_SEND);
KillClientSock(Socket);
Dispose(NewClient);
exit;
end;
// Создаем событие для данного клиентского сокета
SockEvent := WSACreateEvent;
if SockEvent = 0 then
begin
Shutdown(Socket,SD_SEND);
KillClientSock(Socket);
Dispose(NewClient);
exit;
end;
// Инициализируем параметры
RIndex := 0;
UserID := 0;
if CountEvents >= WSA_MAXIMUM_WAIT_EVENTS then
begin
// Превышен лимит подключений
Shutdown(Socket,SD_SEND);
KillClientSock(Socket);
Dispose(NewClient);
exit;
end else
begin
// Если подключений мало
Events[CountEvents] := SockEvent;
WSAEventSelect(Socket,Events[CountEvents],FD_READ or FD_CLOSE);
inc(CountEvents);
end;
ClientConnects.Add(NewClient);
end;
end;
Как можно видеть из последней функции крах на 64 ивэнта не происходит.
Логику TCP я под сомнение не ставлю.
> Если передатчик отправил данные в порядке A -> B -> C, то приемник их получит точно в таком же порядке временного следования.
С этим я согласен. Не сомневаюсь что данные придут на сервер в том же порядке. Я сомневаюсь при моем коде данные уйдут на клиенты в том же порядке в коком пришли.
> Посмотри как реализован сервер в компоненте TServerSocket (scktcomp.pas)
Посмотрю
Сергей М. (2006-07-13 09:10:00) [40]
> Как лучше реализовывать архитектуру клиент-сервер
Посмотри как реализован сервер в компоненте TServerSocket (scktcomp.pas), в режиме stThreadBlocking это весьма простая и не самая худшая реализация серверной транспортной логики.