Использование TIdTCPServer
на ведущем устройстве и TIdTCPClient
на ведомых устройствах - правильный путь.
Один из способов отправки сообщений с сервера клиентам - использование свойства сервера Threads
(Свойство Indy 9 и более ранних версий) или Contexts
(Indy 10) для доступа к списку подключенных в данный момент клиентов.С каждым клиентом связан объект TIdTCPConnection
для связи с этим клиентом.При необходимости вы можете заблокировать список клиентов сервера, пройтись по нему, записав свое сообщение каждому клиенту, а затем разблокировать список:
Indy 9:
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Threads.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdPeerThread(List[I]).Connection.WriteLn(S);
except
end;
end;
finally
IdTCPServer1.Threads.UnlockList;
end;
end;
Indy 10:
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Contexts.LockList;
try
for I := 0 to List.Coun-1 do
begin
try
TIdContext(List[I]).Connection.IOHandler.WriteLn(S);
except
end;
end;
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
Однако у этого есть некоторые недостатки.
Одним из недостатков является то, что сообщения сериализуются, поэтому, если один клиент замерзнет, последующие клиенты не получат свои сообщения своевременно.Другая проблема заключается в том, что клиенты работают в своих собственных потоках на сервере, поэтому при отправке данных клиенту из нескольких потоков в одно и то же время вам потребуется предоставить собственный механизм блокировки для каждого подключения (например, критический раздел или мьютекс).вокруг каждого доступа на запись к соединению, чтобы избежать наложения данных и повреждения ваших коммуникаций
Чтобы избежать этих ловушек, лучше дать каждому клиенту исходящую очередь сообщений, а затем позволить OnExecute
сервера даже отправлятьсообщения в очереди по собственному расписанию.Таким образом, несколько клиентов могут получать сообщения параллельно, а не последовательно:
Indy 9:
uses
..., IdThreadSafe;
procedure TMaster.IdTCPServer1Connect(AThread: TIdPeerThead);
begin
AThread.Data := TIdThreadSafeStringList.Create;
end;
procedure TMaster.IdTCPServer1Disconnect(AThread: TIdPeerThead);
begin
AThread.Data.Free;
AThread.Data := nil;
end;
procedure TMaster.IdTCPServer1Execute(AThread: TIdPeerThead);
var
Queue: TIdThreadSafeStringList;
List: TStringList;
Tmp: TStringList;
I: Integer;
begin
...
Queue := TIdThreadSafeStringList(AThread.Data);
List := Queue.Lock;
try
if List.Count > 0 then
begin
Tmp := TStringList.Create;
try
Tmp.Assign(List);
List.Clear;
except
Tmp.Free;
raise;
end;
end;
finally
Queue.Unlock;
end;
if Tmp <> nil then
try
AThread.Connection.WriteStrings(Tmp, False);
finally
Tmp.Free;
end;
...
end;
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Threads.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdThreadSafeStringList(TIdPeerThread(List[I]).Data).Add(S);
except
end;
end;
finally
IdTCPServer1.Threads.UnlockList;
end;
end;
Indy 10:
uses
..., IdThreadSafe;
procedure TMaster.IdTCPServer1Connect(AContext: TIdContext);
begin
AContext.Data := TIdThreadSafeStringList.Create;
end;
procedure TMaster.IdTCPServer1Disconnect(AContext: TIdContext);
begin
AContext.Data.Free;
AContext.Data := nil;
end;
procedure TMaster.IdTCPServer1Execute(AContext: TIdContext);
var
Queue: TIdThreadSafeStringList;
List: TStringList;
Tmp: TStringList;
I: Integer;
begin
...
Queue := TIdThreadSafeStringList(AContext.Data);
List := Queue.Lock;
try
if List.Count > 0 then
begin
Tmp := TStringList.Create;
try
Tmp.Assign(List);
List.Clear;
except
Tmp.Free;
raise;
end;
end;
finally
Queue.Unlock;
end;
if Tmp <> nil then
try
AContext.Connection.IOHandler.Write(Tmp, False);
finally
Tmp.Free;
end;
...
end;
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Contexts.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdThreadSafeStringList(TIdContext(List[I]).Data).Add(S);
except
end;
end;
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
Даже при наличии очередиЧтобы решить проблемы многопоточности, еще один недостаток заключается в том, что все еще не очень хорошо работает, если событие OnExecute
вашего сервера или коллекция CommandHandlers
должны отправить данные обратно клиенту в ответ на команду от клиента в то же время, что и сервер.Сервер передает сообщения тем же клиентам.После того, как клиент отправит команду и попытается прочитать ответ, он может вместо этого получить широковещательную рассылку, и реальный ответ будет получен после отправки другой команды позже.Клиенту придется обнаруживать широковещательные сообщения, чтобы он мог продолжать чтение, пока не получит ожидаемый ответ.
По сути, вы запрашиваете две отдельные модели связи.Одна модель позволяет клиенту отправлять команды на сервер (вход / выход и т. Д.), А другая модель - когда сервер отправляет клиентам широковещательные сообщения в режиме реального времени.Попытка управлять этими двумя моделями по одному соединению выполнима, но сложна.Простым решением было бы перенести трансляции на другой TIdTCPServer
, который просто отправляет трансляции и больше ничего не делает.Ведущий может иметь два работающих компонента TIdTCPServer
, которые прослушивают разные порты, а затем на каждом подчиненном устройстве могут работать два компонента TIdTCPClient
, один для отправки команд и один для приема широковещательных сообщений.Недостатком является то, что каждому ведомому устройству придется поддерживать 2 соединения с ведущим, что может потреблять пропускную способность сети, если одновременно подключено много ведомых устройств.Но это делает ваше кодирование довольно простым с обеих сторон.
Indy 9:
procedure TMaster.IdTCPServer1Execute(AThread: TIdPeerThread);
var
S: String;
begin
S := AThread.Connection.ReadLn;
if S = 'SignIn' then
...
else if S = 'SignOut' then
...
else
...
end;
procedure TMaster.SendBroadcast(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer2.Threads.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdPeerThread(List[I]).Connection.WriteLn(S);
except
end;
end;
finally
IdTCPServer2.Threads.UnlockList;
end;
end;
.
procedure TSlave.Connect;
begin
IdTCPClient1.Connect;
IdTCPClient2.Connect;
end;
procedure TSlave.SignIn;
begin
IdTCPClient1.SendCmd('SignIn');
...
end;
procedure TSlave.SignOut;
begin
IdTCPClient1.SendCmd('SignOut');
...
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient2.InputBuffer.Size = 0 then
IdTCPClient2.ReadFromStack(True, 0, False);
while IdTCPClient2.InputBuffer.Size > 0 do
begin
S := IdTCPClient2.ReadLn;
... handle broadcast ...
end;
except
on E: EIdException do
IdTCPClient2.Disconnect;
end;
end;
Indy 10:
procedure TMaster.IdTCPServer1Execute(AContext: TIdContext);
var
S: String;
begin
S := AContext.Connection.IOHandler.ReadLn;
if S = 'SignIn' then
...
else if S = 'SignOut' then
...
else
...
end;
procedure TMaster.SendBroadcast(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer2.Contexts.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdContext(List[I]).Connection.IOHandler.WriteLn(S);
except
end;
end;
finally
IdTCPServer2.Contexts.UnlockList;
end;
end;
.
procedure TSlave.Connect;
begin
IdTCPClient1.Connect;
IdTCPClient2.Connect;
end;
procedure TSlave.SignIn;
begin
IdTCPClient1.SendCmd('SignIn');
...
end;
procedure TSlave.SignOut;
begin
IdTCPClient1.SendCmd('SignOut');
...
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient2.IOHandler.InputBufferIsEmpty then
IdTCPClient2.IOHandler.CheckForDataOnSource(0);
while not IdTCPClient2.IOHandler.InputBufferIsEmpty do
begin
S := IdTCPClient2.IOHandler.ReadLn;
... handle broadcast ...
end;
except
on E: EIdException do
IdTCPClient2.Disconnect;
end;
end;
Если по каким-либо причинам использование отдельных соединений для команд и широковещательных рассылок не представляется возможным, то вам, по сути, необходимо настроить асинхронный протокол связи, то есть клиент может отправить командусервер и не ждать ответа, чтобы вернуться сразу же.Клиент должен будет выполнить все свои чтения из таймера / потока, а затем определить, является ли каждое полученное сообщение широковещательной рассылкой или ответом на предыдущую команду, и действовать соответственно:
Indy 9:
procedure TSlave.IdTCPClient1Connect(Sender: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient1Disconnect(Sender: TObject);
begin
Timer1.Enabled := False;
end;
procedure TSlave.PostCmd(const S: String);
begin
IdTCPClient1.WriteLn(S);
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient1.InputBuffer.Size = 0 then
IdTCPClient1.ReadFromStack(True, 0, False);
while IdTCPClient1.InputBuffer.Size > 0 do
begin
S := IdTCPClient1.ReadLn;
if (S is a broadcast) then
... handle broadcast ...
else
... handle a command response ...
end;
except
on E: EIdException do
IdTCPClient1.Disconnect;
end;
end;
Indy 10:
procedure TSlave.IdTCPClient1Connect(Sender: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient1Disconnect(Sender: TObject);
begin
Timer1.Enabled := False;
end;
procedure TSlave.PostCmd(const S: String);
begin
IdTCPClient1.IOHandler.WriteLn(S);
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient1.IOHandler.InputBufferIsEmpty then
IdTCPClient1.IOHandler.CheckForDataOnSource(0);
while not IdTCPClient1.IOHandler.InputBufferIsEmpty do
begin
S := IdTCPClient1.IOHandler.ReadLn;
if (S is a broadcast) then
... handle broadcast ...
else
... handle a command response ...
end;
except
on E: EIdException do
IdTCPClient1.Disconnect;
end;
end;