How to Support Multiple TApdWinsockPort client Connections to a Single Server

Basic Concepts

The APRO TApdWinsockPort component does not allow multiple client connections to be established to a single server on the same port.  However, with a little work, it is possible to get around this issue.

The server will have a TApdWinsockPort that is listening on a known port number (i.e. the negotiation port).  When a client connection is established, the server sends a packet to the client containing a real port number and disconnects.  The client then connects with the real port number and does its work.

Setting up the Server

First, establish the port the clients will initially use for the negotiation and the range of ports that can be used for the live communications.  A new Winsock port is created for each port number in this range.  This also determines the number of simultaneous connections.

In this example, the clients will first connect to port 1000 and will be assigned a port between 1001 and 1100 for the real communications.  This range will allow for 99 simultaneous connections.

const

  NegotiationPort = 1000;

  StartingPort    = 1001;

  EndingPort      = 1100;

For each port, there are two things that need to be tracked.  First, and most obviously, is the TApdWinsockPort that will be created when the client connects.  In addition to this, a flag is kept that indicates if the port is actively being used or not.

For simplicity, the needed information is stored in a record, and an array is built to hold the data.  The array is indexed to match the port number, so a connection on port number 1001 will be entry 1001.

type 

  TPortInfo = record

    Port : TApdWinsockPort;  { The port used by the client connection }

    { Additional information can be stored in this record }

  end;

  TPortList = array [StartingPort..EndingPort] of TPortInfo;

There are other ways to track the ports and whether or not they are in use.  A linked list would also serve well for tracking the ports in use.

In addition, the TApdWinsockPort and the components that will use that port can be created on a data module.  An instance of data module would then be created whenever a connection was negotiated with the client.  The data modules can easily replace the Port field in the record.  For example, a data module that contained a TApdWinsockPort and two data packets could be placed in the TPortInfo array.  The code for handling the TApdWinsockPort would be in the data packet.  The following code shows how the data packet could be inserted in the TPortInfo structure.  When it comes time to create the port, TMyDataModule would be created instead.

type

  TMyDataModule = class(TDataModule)

    ApdWinsockPort1: TApdWinsockPort;

    ApdDataPacket1: TApdDataPacket;

    ApdDataPacket2: TApdDataPacket;

  private

  TPortInfo = record

    Port : TMyDataModule;

    { Additional information can be stored in this record }

  end;

  TPortList = array [StartingPort..EndingPort] of TPortInfo;

Finding and Creating Ports

Now that the port numbers have been defined and data structures have been created to hold the live connections, a couple of routines are needed.  The first of these finds a free port.  The second creates a port to use for the live connections.

FindFreePort tries to anticipate what the next available port is. If it cannot easily find an available port, a basic linear search is used.  It looks through the array of ports.  The first available port that is found is returned.  If all the ports are used, this will return –1.

function TMyServer.IsPortFree (PortNum : Integer) : Boolean;

begin

  Result := False;

  { Check for out of range ports }

  if (PortNum < StartingPort) or (PortNum > EndingPort) then

    Exit;

  { Unassigned ports are free }

  if not Assigned (FPorts[PortNum].Port) then begin

    Result := True;

    Exit;

  end;

  { Closed ports are free }

  if FPorts[PortNum].Port.Open = False then

    Result := True;

end;

function TMyServer.FindFreePort : Integer;

{ Locates the first free port.  Returns - 1 if no free ports are

  available.  Returns the port number of the port to use if a port is found. }

var

  i : Integer;

begin

  Result := -1;

  { Check to see if the anticipated next port is free }

  if (NextPort >= StartingPort) and

     (NextPort <= EndingPort) then begin

    if IsPortFree (NextPort) then begin

      Result := NextPort;

      Inc (NextPort);

      Exit;

    end;

  end;

  { If the anticipated next port is not free, search for any port that

    may be free }

  i := StartingPort;

  while i <= EndingPort do begin

    if IsPortFree (i) then begin

      Result := i;

      Exit;

    end;

    Inc (i);

  end;

end;

CreateNewPort is slightly more involved.  This function will first determine if the port number is valid.  If a port has not already been created, it will be created and initialized.  Any additional components, or data modules should be created and initialized here.

This method is called when a port number is being sent to the client.  Once a port number has been decided on, the port is marked as active (to prevent other connections from using it) until the client has connected to that port number and then disconnected.  In some cases, timeouts may be needed to allow ports that have not been used to return to the available port list.

Once a port is created, it is kept around.  When the client disconnects, only the Active flag is set to false.  After the first connection has been made to a specific port the later connections are slightly faster.  However, additional initialization may be needed in this method to make sure that things are ready for the later connections.

function TMyServer.CreateNewPort (PortNum : Integer) : Boolean;

{ Creates a new TApdWinsockPort in the FPorts port list.  The port's position

  in the array and port number are determined by the PortNum flag.  This

  will return false if the port cannot be created. }

begin

  { Assume that this will fail }

  Result := False;

  { Exit if the port number is bad or is already being used. }

  if (PortNum < StartingPort) or (PortNum > EndingPort) then

    Exit;

  { Exit if the port is already busy }

  if not IsPortFree (PortNum) then

    Exit;

  { If a port hasn't already been created for this port number, create it.

    Once a port is created, it is left in FPorts even if the client

    disconnects.  The next connection can use this port again. }

  if not Assigned (FPorts[PortNum].Port) then begin

    { Create the port }

    FPorts[PortNum].Port := TApdWinsockPort.Create (Self);

    { Set up the ports properties.}

    FPorts[PortNum].Port.wsAddress := edAddress.Text;

    FPorts[PortNum].Port.wsPort := IntToStr (PortNum);

    FPorts[PortNum].Port.DeviceLayer := dlWinsock;

    FPorts[PortNum].Port.WsMode := wsServer; {Needs AdSocket in uses clause}

    { Hook up the events that will be used by the port }

    FPorts[PortNum].Port.OnWsDisconnect := NegotiatedServerDisconnect;

    FPorts[PortNum].Port.OnWsError := NegotiatedServerError;

    FPorts[PortNum].Port.OnWsAccept := NegotiatedServerAccept;

    { Open the port so the client can connect to it }

    FPorts[PortNum].Port.Open := True;

  end;

  { A port was created }

  Result := True;

end;

Handling the clients initial connection

Whenever a client connects to the negotiation port, the connection is accepted, a free port to use for the live connection is found.  If needed the new port is created.  Finally a reply is sent to the client.  The OnWsAccept event of the server negotiation port will fire when a client wants to negotiate a port.

The OnWsAccept event is time sensitive, so the actual work of finding a new port, creating the live port and sending the reply to the client is done on a custom message handler.  The “How to build specialized data scanners using the OnTriggerAvail event” tech tip discusses how to create custom message handlers for APRO events.

The message is declared as a constant:

const

  mws_ServerConnect   = WM_USER + $302;

The message handler is declared in the protected section of the server class:

procedure mwsServerConnect (var Msg : TMessage); message mws_ServerConnect;

The OnWsAccept is very simple.  The connection is accepted and a message is posted to its own handle.  The message handler is where all the work will be done.

procedure TMyServer.sockServerWsAccept(Sender: TObject; Addr: TInAddr;

  var Accept: Boolean);

begin

  Accept := True;

  PostMessage (Handle, mws_ServerConnect, 0, 0);

end;

In the message handler, a free port is found and, if necessary, created.  The message handler will send one of three packets to the client.  If a port is available, it will send “PORT: <PortNum>”.  If no ports are available, if no ports are available, it will send “FULL”.  If a port is available, but for some reason, could not be created, it will send “FAIL: <PortNum>”.  Each packet is terminated by a linefeed (#10).  This makes it easy for the client to pick up the packet using the TApdDataPacket component.

procedure TMyServer.mwsServerConnect (var Msg : TMessage);

var

  NewPort : Integer;

begin

  NewPort := FindFreePort;

  if NewPort >= 0 then begin

    if CreateNewPort (NewPort) then

      sockServer.Output := 'PORT: ' + IntToStr (NewPort) + #10

    else

      sockServer.Output := 'FAIL: ' + IntToStr (NewPort) + #10;

  end else

    sockServer.Output := 'FULL' + #10;

  sockServer.Open := False;

  sockServer.Open := True;

end;

The closing and opening of the server at the end of the message handler is used to both force the client to disconnect and to reset the server for the next client to negotiate a port.

Using the negotiated port

The only further thing to do is to make sure the port is made available when the client disconnects or there is an error that could cause the connection to fail.  The following code on the real connection’s OnWsDisconnect event will free up the port for the next connection.  Remember that OnWsDisconnect was connected to the live port when it was created.

procedure TMyServer.NegotiatedServerDisconnect(Sender: TObject);

{ This event is fired when the client on the negotiated port disconnects.

  Set the Active flag for the port to False so another connection can use

  this port. }

var

  PortNum : Integer;

begin

  { Do not do anything if the form is being destroyed! }

  if not (csDestroying in Form1.ComponentState) then begin

    { Make sure the sender is a TApdWinsock }

    if Sender is TApdWinsockPort then begin

      try

        { Get the port number }

        PortNum := StrToInt (TApdWinsockPort (Sender).wsPort);

        if (NextPort < StartingPort) or (NextPort > EndingPort) or

           (IsPortFree (NextPort)) then

          NextPort := PortNum;

      except

        on E:EConvertError do

          { The port number was not numeric.  This should not happen. }

      end;

    end;

  end;

end;

For error conditions, the OnWsError event will fire.  When the live port is created, this event should be connected.  The following code will force the client to disconnect and free the port up whenever there is any error.

procedure TForm1.NegotiatedServerError(Sender: TObject; ErrCode: Integer);

{ This event is fired if an error occurs on the negotiated port.  In this

  case any error will force the client to disconnect. }

var

  PortNum : Integer;

begin

  if Sender is TApdWinsockPort then begin

    { Force the connection to drop }

    TApdWinsockPort (Sender).Open := False;

    TApdWinsockPort (Sender).Open := True;

  end;

end;

Setting up the Client

To negotiate a connection, the client must first connect to the negotiation port.  The server will reply with the new port number and the client must then connect to that port.

Getting the negotiated port number

The client needs to know the IP address of the server as well as the port that will be used for negotiation.  Establishing the initial contact is easy.  Just point the client to the server and open the port.

procedure TMyClient.ToggleServerConnection;

begin

  sockClient1.wsAddress := ServerAddress;

  sockClient1.wsPort := NegotiationPort;

  packetClient1.Enabled := True;

  sockClient1.Open := True;

end;

When the client connects to the server, a data packet will be returned and the port closed immediately afterwards.

The data packet is pretty simple.  The starting condition is scAnyData and the ending condition is ecString with a string of #10.  When the data packet fires, all that needs to be done is to collect the port number and wait for the port to close.

procedure TMyClient.packetClient1StringPacket(Sender: TObject; Data: String);

begin

  FNegotPort1 := -1;

  if Copy (Data, 1, 5) = 'PORT:' then

    FNegotPort1 := StrToInt (Copy (Data, 6, Length (Data) - 6));

  else if Copy (Data, 1, 4) = 'FULL' then begin

    // No ports are available

  end else if Copy (Data, 1, 4) = 'FAIL' then begin

    // A port could not be created

  end;

end;

After the data packet is fired, the port will close and the OnWsDisconnect event will fire.  If a negotiated port number has been collected, reconnect to the server using that port number.  Like the OnWsAccept event used by the server, this event is time sensitive.  A message is posted to do the actual work of reopening the port.

procedure TMyClient.sockClient1WsDisconnect(Sender: TObject);

begin

  if FNegotPort1 >= 0 then begin

    sockClient1.wsPort := IntToStr (FNegotPort1);

    FNegotPort1 := -1;

    PostMessage (Handle, mws_ClientReconnect, 0, 0);

  end else begin

    // There is no negotiated port number, so this must be disconnecting from

    // the negotiated port number.  There is no need to reconnect.

  end;

end;

As with the server, the custom message is declared as a constant and its handler is in the protected section.

const

  mws_ClientReconnect = WM_USER + $301; { Defines a message that will be

protected

   procedure mwsClientReconnect (var Msg : TMessage);

     message mws_ClientReconnect;

The message handler is very simple.  The negotiated port number was already set when the server disconnected.  It just needs to reconnect.

procedure TForm1.mwsClientReconnect (var Msg : TMessage);

begin

  sockClient1.Open := True;

end;

After the second connection, the OnWsConnect event will fire to indicate the connection negotiation is complete.  However, both the first and the second connections to the server will result in the OnWsConnect event firing.  Checking the port number on the OnWsConnect event indicates if this is a negotiation in progress that lead to the port firing, or if it is a live connection to do real work with the server.

procedure TForm1.sockClient1WsConnect(Sender: TObject);

begin

  if sockClient1.wsPort = NegotiationPort then

    Exit;

  // Do the real stuff here

end;

Disconnecting is easy.  Once the work has been done, just close the port.  The server will handle cleaning up the negotiated port.

The source for this project is here (8k).

This site is not affiliated, endorsed, or otherwise associated with the entity formerly known as TurboPower Software. The owners and maintainers of Aprozilla.com were merely meager employees of the aforementioned organization, providing this site out of the pure goodness of their collective hearts. SourceForge.net Logo

Last updated: July 22, 2003.