Question

I want to restrict the number of incoming connections that a TIdTcpServer can take, but the rule I need to apply is a little complex, so I don't think the MaxConnections property will work for me.

I have an application running N servers, each using a different protocol on a different port. I need to limit the total number of clients across all N servers. So for example, with 16 servers, and 16 clients allowed, I would allow one client on each, or 16 all on a single server.

It's possible I could manipulate the MaxConnections dynamically to fix this (e.g. set them all to zero when I determine we're 'full', and back to 16 or whatever when we're not full, but this feels a little too tricksy.

Is there some kind of virtual IsConnectionAllowed method that I can override with my own logic, or a suitable place in the connection process where I can raise an exception if I determine the limit has been exceeded?

No correct solution

OTHER TIPS

Create a new component - TIdTCPServerCollection for example - which is the "owner" of all server components.

In this component, declare a thread-safe property which stores the available - currently unused - connection count.

In the server connect and disconnect logic, decrement / increment this variable, and set MaxConnections to reflect the new limit.

One option might be to implement a custom TIdScheduler class that derives from one of the TIdSchedulerofThread... components and override its virtual AcquireYarn() method to either:

  1. raise an EAbort exception if the scheduler's ActiveYarns list has reached the max allowed number of connections. This might cause too tight a loop in TIdTCPServer listening threads, though. To mitigate that, you could put a small timer in the method and only raise the exception if the list remains maxed out for a short period of time.

  2. block the calling thread (the TIdTCPServer listening thread) until the ActiveYarns has fewer yarns than your max connection limit, then call the inherited method to return a new TIdYarn object normally.

For example:

type
  TMyScheduler = class(TIdSchedulerOfThreadDefault)
  public
    function AcquireYarn: TIdYarn; override;
  end;

function TMyScheduler.AcquireYarn: TIdYarn;
begin
  if not ActiveYarns.IsCountLessThan(SomeLimit) then
  begin
    Sleep(1000);
    if not ActiveYarns.IsCountLessThan(SomeLimit) then
      Abort;
  end;
  Result := inherited;
end;

Then assign a single instance of this class to the Scheduler property of all the servers. TIdTCPServer calls AcquireYarn() before accepting a new client connection.

Another option, for Windows only, would be to derive a new TIdStack class from TIdStackWindows and override its virtual Accept() method to use Winsock's WSAAccept() function instead of its accept() function. WSAAccept() allows you to assign a callback function that decides whether a new client is accepted or rejected based on criteria passed to the callback (QOS, etc). That callback could check a global counter you maintain for how many active connections are running (or just sum up all of the servers' active Contexts counts), and then return CF_REJECT if the limit has been reached, otherwise return CF_ACCEPT. You could then use the SetStackClass() function in the IdStack unit to assign your class as the active Stack for all Indy socket connections.

For example:

type
  TMyStack = class(TIdStackWindows)
  public
    function Accept(ASocket: TIdStackSocketHandle; var VIP: string; var VPort: TIdPort; var VIPVersion: TIdIPVersion): TIdStackSocketHandle; override;
  end;

function MyAcceptCallback(lpCallerId: LPWSABUF; lpCallerData: LPWSABUF; lpSQOS, pGQOS: LPQOS; lpCalleeId, lpCalleeData: LPWSABUF; g: PGROUP; dwCallbackData: DWORD_PTR): Integer; stdcall;
begin
  if NumActiveConnections >= SomeLimit then
    Result := CF_REJECT
  else
    Result := CF_ACCEPT;
end;

function TMyStack.Accept(ASocket: TIdStackSocketHandle; var VIP: string; var VPort: TIdPort; var VIPVersion: TIdIPVersion): TIdStackSocketHandle;
var
  LSize: Integer;
  LAddr: SOCKADDR_STORAGE;
begin
  LSize := SizeOf(LAddr);
  //Result := IdWinsock2.accept(ASocket, IdWinsock2.PSOCKADDR(@LAddr), @LSize);
  Result := IdWinsock2.WSAAccept(ASocket, IdWinsock2.PSOCKADDR(@LAddr), @LSize, @MyAcceptCallback, 0);
  if Result <> INVALID_SOCKET then begin
    case LAddr.ss_family of
      Id_PF_INET4: begin
        VIP := TranslateTInAddrToString(PSockAddrIn(@LAddr)^.sin_addr, Id_IPv4);
        VPort := ntohs(PSockAddrIn(@LAddr)^.sin_port);
        VIPVersion := Id_IPv4;
      end;
      Id_PF_INET6: begin
        VIP := TranslateTInAddrToString(PSockAddrIn6(@LAddr)^.sin6_addr, Id_IPv6);
        VPort := ntohs(PSockAddrIn6(@LAddr)^.sin6_port);
        VIPVersion := Id_IPv6;
      end;
      else begin
        CloseSocket(Result);
        Result := INVALID_SOCKET;
        IPVersionUnsupported;
      end;
    end;
  end;
end;

initialization
  SetStackClass(TMyStack);

This way, Indy will never see any rejected client connections at all, and you do not have to worry about implementing any other hacks inside of TIdTCPServer or its various dependencies. Everything will work normally and simply block as expected whenever TIdStack.Accept() does not return an accepted client.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top