Indy 10 TCP服务器

6
经过一番搜索,我认为Indy TCP服务器是我正在开发的即时通讯服务器上使用的最佳选择。目前唯一遇到的问题就是如何将消息广播和转发到其他连接的客户端。发送响应给同一客户端似乎很好并且不会影响其他客户端的活动,但是通过使用 aContext.locklist 并在连接列表之间迭代以找到接收数据的客户端连接的机制来转发消息到其他客户端。

我认为这里的问题是它会冻结列表并阻止处理其他客户端请求,直到调用 unlocklist。那么这是否会损害服务器的性能?在转发每条消息时 (因为这经常发生在即时通讯中),锁定列表并在连接之间进行迭代。是否有更好的方法来解决这个问题呢?

我正在使用 Indy 10 和 Delphi 7。

广播的代码:

Var tmpList: TList;
    i: Integer;
Begin
tmpList := IdServer.Contexts.LockList;

For i := 0 to tmpList.Count Do Begin
  TIdContext(tmpList[i]).Connection.Socket.WriteLn('Broadcast message');
End;
IdServer.Contexts.UnlockList;

转发消息的代码:

Var tmpList: TList;
  i: Integer;
Begin
  tmpList := IdServer.Contexts.LockList;

  For i := 0 to tmpList.Count Do Begin
    If TIdContext(tmpList[i]).Connection.Socket.Tag = idReceiver Then
      TIdContext(tmpList[i]).Connection.Socket.WriteLn('Message');
  End;
  IdServer.Contexts.UnlockList;

你指望我们能够心灵感应地调试你的代码吗? - Barmar
抱歉,我实际上认为这是一个普通问题,而不是代码问题。 - Junaid Noor
1个回答

12

是的,您需要遍历Contexts列表以向多个客户端广播消息。但是,您不应该在循环内部执行实际的写操作。一方面,如您已经注意到的那样,将列表锁定一段时间会影响服务器性能。另一方面,它也不是线程安全的。如果您的循环在另一个线程同时写入同一连接时向连接写入数据,则两次写入将相互重叠并破坏与该客户端的通信。

我通常会实现每个客户端的出站队列,使用TIdContext.Data属性或TIdServerContext派生类来保存实际的队列。当您需要从客户端的OnExecute事件之外向客户端发送数据时,请将数据放入该客户端的队列中。该客户端的OnExecute事件可以在安全时将队列的内容发送给客户端。

例如:

type
  TMyContext = class(TIdServerContext)
  public
    Tag: Integer;
    Queue: TIdThreadSafeStringList;
    ...
    constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil); override;
    destructor Destroy; override;
  end;

constructor TMyContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil);
begin
  inherited;
  Queue := TIdThreadSafeStringList.Create;
end;

destructor TMyContext.Destroy;
begin
  Queue.Free;
  inherited;
end;

.

procedure TForm1.FormCreate(Sender: TObject);
begin
  IdServer.ContextClass := TMyContext;
end;

procedure TForm1.IdServerConnect(AContext: TIdContext);
begin
  TMyContext(AContext).Queue.Clear;
  TMyContext(AContext).Tag := ...
end;

procedure TForm1.IdServerDisconnect(AContext: TIdContext);
begin
  TMyContext(AContext).Queue.Clear;
end;

procedure TForm1.IdServerExecute(AContext: TIdContext);
var
  Queue: TStringList;
  tmpList: TStringList;
begin
  ...
  tmpList := nil;
  try
    Queue := TMyContext(AContext).Queue.Lock;
    try
      if Queue.Count > 0 then
      begin
        tmpList := TStringList.Create;
        tmpList.Assign(Queue);
        Queue.Clear;
      end;
    finally
      TMyContext(AContext).Queue.Unlock;
    end;
    if tmpList <> nil then
      AContext.Connection.IOHandler.Write(tmpList);
  finally
    tmpList.Free;
  end;
  ...
end;

.

var
  tmpList: TList;
  i: Integer;
begin
  tmpList := IdServer.Contexts.LockList;
  try
    for i := 0 to tmpList.Count-1 do
      TMyContext(tmpList[i]).Queue.Add('Broadcast message');
  finally
    IdServer.Contexts.UnlockList;
  end;
end;

.

var
  tmpList: TList;
  i: Integer;
begin
  tmpList := IdServer.Contexts.LockList;
  try
    for i := 0 to tmpList.Count-1 do
    begin
      if TMyContext(tmpList[i]).Tag = idReceiver then
        TMyContext(tmpList[i]).Queue.Add('Message');
    end;
  finally
    IdServer.Contexts.UnlockList;
  end;
end;

2
我的回答确实回答了你最初的问题 - 考虑性能和线程安全的广播/转发的最佳方法是使用每个客户端队列。无论是广播还是转发,您仍然必须循环遍历“Contexts”列表以找到要发送到的客户端(除非您实现自己的线程安全查找,例如将多个“TIdContext”指针分组在一起)。队列有助于通过将大部分工作转移到每个客户端的“OnExecute”事件来最小化需要锁定“Contexts”列表的时间... - Remy Lebeau
使用TIdThreadSafeStringList只是一个例子。只要在访问它时有线程安全锁定它,您可以使用任何您想要的队列。例如,使用包含记录指针的TThreadList,其中记录包含您的实际数据。 - Remy Lebeau
那么你必须循环以达到每个人...看起来很可怕。使用UDP不是更简单、更快速吗? - NaN
1
TCP不支持广播,所以如果你想将相同的数据发送给所有人,就必须循环遍历所有活动连接。不,UDP并不一定是更好的选择。当然,UDP可以简化你的代码(单个发送可以广播到多个接收器),但UDP不能保证数据实际上是否到达目的地,即使到达了,UDP也不能保证数据到达的顺序与发送时相同。它之所以更快,只是因为UDP不像TCP那样确认数据。更快并不总是意味着更好。 - Remy Lebeau
@RemyLebeau,我完成了我的案例,我会同时使用它们!;-) http://stackoverflow.com/questions/18069948/should-i-be-afraid-to-use-udp-to-make-a-client-server-broadcast-talk 看看你是否同意我的观点...至少对于我的用例是这样。 - NaN
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接