Top.Mail.Ru
Мастера DELPHI, Delphi programming community Титульная страница Поиск, карта сайта Написать письмо 
| Новости | Поиск | FAQ | Статьи | Книги | Новости VCL |
| Форумы ЧАТ | Орешник | KOL и MCK |
Форумы -» Основная («Начинающим»)/ Базы / WinAPI / Компоненты / Сети / Media / Игры / Corba и COM / KOL / FreePascal / .Net / Прочее / RSDN.ORG

« предыдущая ветвь | форум | следующая ветвь »

Как лучше реализовывать архитектуру клиент-сервер [D7, WinXP]


Страницы:   1 2 3

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 это весьма простая и не самая худшая реализация серверной транспортной логики.


Страницы:   1 2 3

Наверх

Титульная страница Поиск, карта сайта Написать письмо