我是Indy的新手,在本地局域网(没有互联网)上实现了2个pc之间非常基本的idTCPServer和IdTCPClient,但现在需要3台机器进行通话! (环境是Delphi 2010 / Win7 / DBExpress / MySQL)。 应用是控制实时航行事件。一个“主”pc和一个“奴隶”(现在需要2个奴隶!)。 slave pc使得水手能够注册他们的事件细节(存储在MySQL表中)。主控制器控制a)当注册屏幕打开/关闭时,b)向从属设备发送事件详细信息,c)将实时比赛倒计时/开始和比赛经过时间发送给他们必须显示和做出反应的对象(关闭注册屏幕等) 。师父需要知道新人何时登录或退出以更新其racelist。
目前我在master上使用IDTCPServer实现了(我的第一个Indy程序)。奴隶上的IdTCPClient在新的SignIn / Out时告诉master,并不断向服务器发送“time request”文本消息,因为我不知道如何从TCPServer发送消息!)。
我认为这不是最好的方法,现在俱乐部想要两个“登录”的奴隶我需要重新访问整个事情,所以我请你的意见...
哪种Indy组件最好使用?最好在“主”电脑上安装TCPServer吗?服务器应该向2个从站广播吗?并且(请!)是否有任何具有类似功能的示例我可以作为帮助我实现的基础? 非常感谢 克里斯
答案 0 :(得分:8)
在主服务器上使用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
集合必须将数据发送回客户端,事情仍然不能很好地发挥作用在服务器向这些客户端广播消息的同时响应来自客户端的命令。在客户端发送命令并尝试回读响应之后,它可能会接收到广播,并且稍后在发送另一个命令之后将收到实际响应。客户端必须检测广播,以便它能够继续阅读,直到它得到预期的响应。
您基本上要求两种不同的通信模型。一种模型允许客户端向服务器发送命令(SignIn / Out等),以及服务器向客户端发送实时广播的另一种模型。尝试通过单个连接管理这两个模型是可行的,但很棘手。一个简单的解决方案是将广播移动到仅发送广播的另一个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;