Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Porting old Delphi2007 application using TServerSocket....
#1
So I have this old service application written in Delphi7 and maintained in Delphi2007 until about 2012.
Now it won't run anymore in Windows10 and I have decided to try a port to FreePascal so it can run on Linux.
The Indy10 suite is available in Lazarus as a package via Online Package Manager.

The server uses a TCP socket connection for user interaction (mainly configuration and data retrieval) and it was implemented at the time using TServerSocket that came with Delphi.

This won't obviously port so I thought that I could use Indy10 instead for that interface.
TIdTcpServer seems to be a natural replacement except for the fact that the TServerSocket is event driven...

The existing structure is a TService descendant which implements the Windows service. I know how to handle conversion to Linux for that.

But I would like some help into how I can port the communications between the remote client and the server using TidTcpServer.

This server uses TServerSocket in order to handle the communications and it implements the following event procedures:
Code:
sckServer: TServerSocket;
....
procedure sckClientConnect(Sender: TObject; Socket: TCustomWinSocket);
procedure sckClientDisconnect(Sender: TObject; Socket: TCustomWinSocket);
procedure sckClientRead(Sender: TObject; Socket: TCustomWinSocket);
procedure sckClientError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer);

The sckClientConnect procedure handles the client connection and incoming commands as follows:

Code:
procedure TSuperStingRemoteServer.sckClientConnect(Sender: TObject; Socket: TCustomWinSocket);
{Create the remote handler and assign the communication to it}
var
  SSRC: TSSRemoteClientComm; //The communications object
  i: integer;
begin
  try
    FActivityTime := Now;
    i := sckServer.Socket.ActiveConnections;
    Log3RServer.StdLog('Active clients = ' + IntToStr(i));
    if i > 1 then
    begin
      Log3RServer.StdLog('Only one active client allowed!');
      Socket.Close;
      Exit;
    end;
    Log3RServer.StdLog('SSRemote - Client connect, IP=' + Socket.RemoteAddress + ' Host=' + Socket.RemoteHost);
    SSRC := TSSRemoteClientComm.Create(Socket, FRemoteServer);
    Socket.Data := SSRC;  {Keep pointer to handler with socket}
    SSRC.Log := Log3RComm;
    FRemoteServer.ClientCallback := SSRC.ClientCallback;
    Log3RServer.StdLog('SSRemote - initializing new SSRemote Client');
    SSRC.Initialize;
    SSRC.Log.StdLog('Socket communication channel opened to ' + Socket.RemoteAddress + ' Host='  + Socket.RemoteHost);
  except
    on E: Exception do
    begin
      Log3RServer.ErrLog('Exception during client connect: ' + E.Message);
    end;
  end;
end;

procedure TSuperStingRemoteServer.sckClientDisconnect(Sender: TObject;  Socket: TCustomWinSocket);
begin
  FRemoteServer.ClientCallback := NIL;
  LogStd('Client disconnect ' + Socket.RemoteAddress);
  FActivityTime := Now;
  if Socket.Data <> NIL then
  begin
    TSSRemoteClientComm(Socket.Data).OnDisconnect(Socket);
    TSSRemoteClientComm(Socket.Data).Free;
  end;
end;

procedure TSuperStingRemoteServer.sckClientRead(Sender: TObject;  Socket: TCustomWinSocket);
var
  RdData: string;
begin
  {Implement the socket data flow here by using the Data pointer to the handling object}
  RdData := Socket.ReceiveText;
  FActivityTime := Now;
  if Socket.Data <> NIL then
    TSSRemoteClientComm(Socket.Data).OnDataRead(RdData);
end;

procedure TSuperStingRemoteServer.sckClientError(Sender: TObject;
  Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer);
begin
  {Implement the socket error handling here}
  LogErr('Socket error detected, code: ' + IntToStr(ErrorCode));
  ErrorCode := 0; {To stop the socket from generating pop-up errors}
end;


So this system acts really just like a pass-through to the real communications object class dealing with all of the messaging and data transfers etc using the supplied socket when it was created.

Since Indy does not use events, how can I adapt this from TServerSocket to TidTcpServer?

Hopefully I will not have to modify too much inside the handler itself provided the network interface shown above is ported with the messaging in mind...
Note that the sckClientRead retrieves the client command text and just shuffles it over to the handler's OnDatRead method that takes the text as parameter, so it should really be decently simple provided one can get the receive text event off of the TidTcpServer object....

Grateful for any suggestions, it was quite a while since I wrote this application (started in 2004)....
Reply
#2
Never mind, I finally found a useful webpage with some description on how to implement the simple client/server connection.

My needs are really not very complex, I need to implement a TCP server class that can accept connections and which will transfer incoming data to an already existing handler.

The handler will output some data to be sent back to the client so this TCP "relayer" must have a function which will transfer the data back o the client. Or a processing function returning the response to send back.
No data will ever be sent to the client without a prior request.

What I would like to know is if the TIdTCPServer object will trigger the OnExecute event when data arrive from the connected client?

Is there something one has to do in order for that to happen, like defining message delimiters?

The example I cut code from uses OnExecute to retrieve and process client data, but I seem not to get any extraction here...

Right now my OnExecute looks like this (ProcessRequest is a placeholder for a function that will take the client message payload and do whatever needed and then create a response to send back):

Code:
procedure TRemoteServer.ServerExecute(AContext: TIdContext);
var
  msg, reply: string;
begin
  // ... get incoming message from client and supply it to the consumer
  msg := AContext.Connection.IOHandler.ReadLn;
  // Right now just print to stdout
  writeln(msg);
  reply := ProcessRequest(msg);
  AContext.Connection.IOHandler.WriteLn(reply);
end;

The test client I have will send a login message following connection and it looks like this:

<STX>USER=username<CR>PWD=password<ETX>

So it is delimited using <STX> and <ETX> and the payload between these can be any normal ASCII characters incluting <CR>, <LF>, <TAB> etc.

All messages are delimited this way, so I would need something that can read incoming data up until the <ETX> char arrives, and then process them.

In the example IOHandler.Readln/IOHandler.Writeln is used and that is probably incorrect.

In my tests right now I will only write out the incoming data to STDOUT for checking.

So how can I get the content of the delimited message from the client in OnExecute?
Reply
#3
(07-05-2020, 01:08 PM)BosseB Wrote: So I have this old service application written in Delphi7 and maintained in Delphi2007 until about 2012.
Now it won't run anymore in Windows10

Why not? I have multiple services written in Delphi 6 (well, actually C++Builder 6, but it is the same VCL/RTL frameworks) and they work just fine on Windows 10.

(07-05-2020, 01:08 PM)BosseB Wrote: TIdTcpServer seems to be a natural replacement

Correct.

(07-05-2020, 01:08 PM)BosseB Wrote: Since Indy does not use events, how can I adapt this from TServerSocket to TidTcpServer?

The closest translation would look something like this:

Code:
sckServer: TIdTCPServer;
....
procedure sckServerConnect(AContext: TIdContext);
procedure sckServerDisconnect(AContext: TIdContext);
procedure sckServerExecute(AContext: TIdContext);
procedure sckServerException(AContext: TIdContext; AException: Exception);
...
procedure TSuperStingRemoteServer.sckServerConnect(AContext: TIdContext);
{Create the remote handler and assign the communication to it}
var
  SSRC: TSSRemoteClientComm; //The communications object
  i: integer;
  PeerIP, PeerHost: string;
begin
  try
    FActivityTime := Now;

    with sckServer.Contexts.LockList do
    try
      i := Count;
    finally
      sckServer.Contexts.UnlockList;
    end;
    // make sure Log3RServer is thread-safe, otherwise you need to sync access to it!
    Log3RServer.StdLog('Active clients = ' + IntToStr(i));
    if i > 1 then
    begin
      Log3RServer.StdLog('Only one active client allowed!');
      AContext.Connection.Disconnect;
      Exit;
    end;

    PeerIP := AContext.Binding.PeerIP;
    PeerHost := GStack.HostByAddress(AContext.Binding.PeerIP, AContext.Binding.IPVersion);
    Log3RServer.StdLog('SSRemote - Client connect, IP=' + PeerIP + ' Host=' + PeerHost);

    AContext.Connection.IOHandler.DefStringEncoding := Indy8BitEncoding;

    SSRC := TSSRemoteClientComm.Create(AContext, FRemoteServer);
    AContext.Data := SSRC;  {Keep pointer to handler with socket}
    SSRC.Log := Log3RComm;
    FRemoteServer.ClientCallback := SSRC.ClientCallback;
    Log3RServer.StdLog('SSRemote - initializing new SSRemote Client');
    SSRC.Initialize;
    SSRC.Log.StdLog('Socket communication channel opened to ' + PeerIP + ' Host='  + PeerHost);
  except
    on E: Exception do
    begin
      Log3RServer.ErrLog('Exception during client connect: ' + E.Message);
      AContext.Connection.Disconnect;
      Exit;
    end;
  end;
end;

procedure TSuperStingRemoteServer.sckServerDisconnect(AContext: TIdContext);
begin
  FRemoteServer.ClientCallback := nil;
  LogStd('Client disconnect ' + AContext.Binding.PeerIP);
  FActivityTime := Now;
  if AContext.Data <> nil then
  begin
    TSSRemoteClientComm(AContext.Data).OnDisconnect(AContext);
    TSSRemoteClientComm(AContext.Data).Free;
    AContext.Data := nil;
  end;
end;

procedure TSuperStingRemoteServer.sckServerExecute(AContext: TIdContext);
var
  RdData: string;
begin
  {Implement the socket data flow here by using the Data pointer to the handling object}
  if AContext.Connection.IOHandler.InputBufferIsEmpty then
  begin
    AContext.Connection.IOHandler.CheckForDataOnSource(500);
    AContext.Connection.IOHandler.CheckForDisconnect;
    if AContext.Connection.IOHandler.InputBufferIsEmpty then Exit;
  end;
  RdData := AContext.Connection.IOHandler.InputBufferAsString;
  FActivityTime := Now;
  if AContext.Data <> nil then
    TSSRemoteClientComm(AContext.Data).OnDataRead(RdData);
end;

procedure TSuperStingRemoteServer.sckServerException(AContext: TIdContext; AException: Exception);
begin
  {Implement the socket error handling here}
  if not (AException is EIdConnClosedGracefully) then
    LogErr('Socket error detected: ' + AException.Message);
end;

(07-06-2020, 02:29 PM)BosseB Wrote: What I would like to know is if the TIdTCPServer object will trigger the OnExecute event when data arrive from the connected client?

Not when new data arrives, no. The OnExecute event handler is called in a continuous loop for the lifetime of the socket connection, regardless of the data. The handler is expected to do its own waiting for new data as needed, such as by using blocking socket I/O operations.

(07-06-2020, 02:29 PM)BosseB Wrote: The example I cut code from uses OnExecute to retrieve and process client data, but I seem not to get any extraction here...

<snip>

The test client I have will send a login message following connection and it looks like this:

<STX>USER=username<CR>PWD=password<ETX>

So it is delimited using <STX> and <ETX> and the payload between these can be any normal ASCII characters incluting <CR>, <LF>, <TAB> etc.

<snip>

So how can I get the content of the delimited message from the client in OnExecute?

You can call ReadLn() with a custom terminator. The default is LF (which also handles CRLF), but you can use any terminator you want, like ETX, eg:

Code:
procedure TRemoteServer.ServerExecute(AContext: TIdContext);
var
  msg, reply: string;
begin
  AContext.Connection.IOHandler.WaitFor(#2, True, False); // <-- optional
  msg := AContext.Connection.IOHandler.ReadLn(#3);
  reply := ProcessRequest(msg);
  AContext.Connection.IOHandler.Write(#2 + reply + #3);
end;

Reply
#4
Turns out I was close then using IOHandler.ReadLn(ETX), I did not wait for the STX to arrive, though.
Can't I wait for ETX instead so I know a complete message is available?
I noted that the ReadLn will throw away the ETX char only returning the stuff before.

Regarding the OnExecute call, is there some minimum time between the invocations?

And do I actually have to check if there is data before issuing the ReadLn call? Can OnExecute be called without any data having arrived?
Is there a way to check how much data there is in queue before actually trying to read it?

I assume that if I just skip the ReadLn call the data stays in the buffer so the next time around new data arriving gets added to the end of the buffer and can be retrieved with a later ReadLn call?

I think it will not be a big problem but I have noted when testing that there seems to be a lag in the handling, for example when I connect the client it connects immediately as witnessed by activity in the OnConnect, where I print out the connection message on the console, yet it takes several seconds or more before the client app shows the login dialog to the user in preparation for actually logging in. It is raised as soon as the client discovers the connection succeeded.

Same when sending the login request it is immediately shown in the console but the client takes some time to discover that a valid login has been accepted even though my test code sends the reply immediately after the console has been written to.

So if this is caused by the OnExecute firing only at a leisurely rate I need to figure out a better way to discover incoming data arriving...
Reply
#5
(07-06-2020, 11:19 PM)BosseB Wrote: Turns out I was close then using IOHandler.ReadLn(ETX), I did not wait for the STX to arrive, though.
Can't I wait for ETX instead so I know a complete message is available?

Yes, which is why I said the call to WaitFor(STX) is optional. But since the protocol dictates that every message begins with STX, it doesn't hurt to validate that actually happens. If every message is well-formed, as it should be for compliant clients, then the WaitFor() will be redundant. But for non-compliant clients, or corrupted communications, waiting for STX before then waiting for ETX can help with recovery efforts.

(07-06-2020, 11:19 PM)BosseB Wrote: I noted that the ReadLn will throw away the ETX char only returning the stuff before.

Yes, so if your processor needs the ETX then you will have to add it to the result, eg:

Code:
msg := AContext.Connection.IOHandler.ReadLn(#3) + #3;

(07-06-2020, 11:19 PM)BosseB Wrote: Regarding the OnExecute call, is there some minimum time between the invocations?

No. As soon as the OnExecute handler exits, it is called again immediately.

(07-06-2020, 11:19 PM)BosseB Wrote: And do I actually have to check if there is data before issuing the ReadLn call?

No. It will simply block the calling thread until the specified Terminator is available, or until the specified ReadTimeout elapses.

(07-06-2020, 11:19 PM)BosseB Wrote: Can OnExecute be called without any data having arrived?

Yes, which is why the handler is responsible for waiting for data.

(07-06-2020, 11:19 PM)BosseB Wrote: Is there a way to check how much data there is in queue before actually trying to read it?

The IOHandler.InputBuffer has a Size property. This indicates how many bytes Indy has already read from the socket and stored in the InputBuffer's memory buffer (which the rest of the IOHandler's reading methods then extract their bytes from).

If you want to check how many bytes are in the socket's internal buffer, you will have to access the underlying platform's socket API directly, such as using ioctlsocket(FIONBIO) on Windows. You can get the underlying platform socket handle from the AContext.Binding.Handle property.

(07-06-2020, 11:19 PM)BosseB Wrote: I assume that if I just skip the ReadLn call the data stays in the buffer so the next time around new data arriving gets added to the end of the buffer and can be retrieved with a later ReadLn call?

Yes.

(07-06-2020, 11:19 PM)BosseB Wrote: I think it will not be a big problem but I have noted when testing that there seems to be a lag in the handling, for example when I connect the client it connects immediately as witnessed by activity in the OnConnect, where I print out the connection message on the console, yet it takes several seconds or more before the client app shows the login dialog to the user in preparation for actually logging in. It is raised as soon as the client discovers the connection succeeded.

Same when sending the login request it is immediately shown in the console but the client takes some time to discover that a valid login has been accepted even though my test code sends the reply immediately after the console has been written to.

Did you have that same problem when using TServerSocket? It sounds like maybe the client's logic has a bug or design flaw in it, where it is not handling socket activity in a timely manner. Maye a buggy socket handler, or a laggy message queue, or something like that. I can't really help you with this without seeing the client's code.

(07-06-2020, 11:19 PM)BosseB Wrote: So if this is caused by the OnExecute firing only at a leisurely rate I need to figure out a better way to discover incoming data arriving...

That is not how the OnExecute event works. There is no "leisurely rate" on it.

I suggest you use a packet sniffer, like Wireshark, to watch the underlying TCP traffic in real-time, so that you can see whether TIdTCPServer is sending its replies in a delay manner after receiving client requests, or whether the client is updating its UI in a delayed manner after receiving the server's replies.

Reply
#6
(07-06-2020, 11:44 PM)rlebeau Wrote:
(07-06-2020, 11:19 PM)BosseB Wrote: Turns out I was close then using IOHandler.ReadLn(ETX), I did not wait for the STX to arrive, though.
Can't I wait for ETX instead so I know a complete message is available?

Yes, which is why I said the call to WaitFor(STX) is optional.  But since the protocol dictates that every message begins with STX, it doesn't hurt to validate that actually happens.  If every message is well-formed, as it should be for compliant clients, then the WaitFor() will be redundant.  But for non-compliant clients, or corrupted communications, waiting for STX before then waiting for ETX can help with recovery efforts.

I will try that out and see what happens.

Quote:
(07-06-2020, 11:19 PM)BosseB Wrote: Regarding the OnExecute call, is there some minimum time between the invocations?

No.  As soon as the OnExecute handler exits, it is called again immediately.

OK, so then if there is a WaitFor() in the code then it will stay here most of the time?
Even when the timeout hits it will come back here, right (since it is immediately re-called).
Is this call coming from a separate thread so it won't slow down the rest of the application, for example if there are two clients connected...

Quote:
(07-06-2020, 11:19 PM)BosseB Wrote: Is there a way to check how much data there is in queue before actually trying to read it?

The IOHandler.InputBuffer has a Size property.  This indicates how many bytes Indy has already read from the socket and stored in the InputBuffer's memory buffer (which the rest of the IOHandler's reading methods then extract their bytes from).

OK, then I will probably have a first check if there are at least 3 bytes in there (STX-Databyte-ETX) before going on. It would make the OnExecute exit much quicker most of the times, because I would think client data will not arrive very often, after all it will be triggered by a human clicking some action button in the client application.

Quote:If you want to check how many bytes are in the socket's internal buffer, you will have to access the underlying platform's socket API directly, such as using ioctlsocket(FIONBIO) on Windows.  You can get the underlying platform socket handle from the AContext.Binding.Handle property.

This is probably not necessary if I check for 3 or more bytes here...

Quote:
(07-06-2020, 11:19 PM)BosseB Wrote: I think it will not be a big problem but I have noted when testing that there seems to be a lag in the handling, for example when I connect the client it connects immediately as witnessed by activity in the OnConnect, where I print out the connection message on the console, yet it takes several seconds or more before the client app shows the login dialog to the user in preparation for actually logging in. It is raised as soon as the client discovers the connection succeeded.

Same when sending the login request it is immediately shown in the console but the client takes some time to discover that a valid login has been accepted even though my test code sends the reply immediately after the console has been written to.

Did you have that same problem when using TServerSocket?  It sounds like maybe the client's logic has a bug or design flaw in it, where it is not handling socket activity in a timely manner.  Maye a buggy socket handler, or a laggy message queue, or something like that.  I can't really help you with this without seeing the client's code.

Could well be, the client app was created some 16 years ago (for WinXP) and is a low-usage app, basically just for casual monitoring of activity and possibly modifying some configuration items.
So it is not a big deal.
The server's main task is managing recurring measurement operations on a remote site so mostly it is on its own.

Oh, regarding the Windows10 issue:
It is a problem with hardware compatibility. We have designed an interface box which attaches by USB to the server computer and now Windows has decided that the driver that has worked fine for many years is no longer allowed on the system! It is an FTDI USB-RS232 chip we integrated on the board.
So we face two ways of resolving this:
1) Redesign the hardware, meaning that all customers need to be updated if the server computer updates its Windows.
2) Port the server to run on Linux, which is what I am occupied with. Then we can replace the Windows PC server with a small Raspberry Pi box and keep all of the proprietary hardware we have used earlier.
The FTDI driver on Linux works just fine for all of the converters I have tested...
Reply
#7
Remy, thanks for your help!
I got the tests running fine now so I am going to replace the TServerSocket based code with the new connection based on TIdTCPServer.

I then also think I need to address the case when the server is shut down with clients connected...
But I am not sure how to handle that precisely...

Here is parts of what I have at the moment:
Code:
constructor TRemoteServer.Create;
begin
  //Create idTCPServer
  FServer := TIdTCPServer.Create(NIL);
  FServer.Active := False;

  //Allow only 2 connections
  FServer.MaxConnections := 2;

  //Define the callback functions
  FServer.OnConnect := ServerConnect;
  FServer.OnDisconnect := ServerDisconnect;
  FServer.OnExecute := ServerExecute;
  FServer.OnStatus := ServerStatus;
  FClientPort := GUEST_CLIENT_PORT;
end;

destructor TRemoteServer.Destroy;
begin
  if FServer.Active then
  begin
    if ClientCount > 0 then
    begin
      //Need to disconnect the clients so they get the message....
      //What to do here?

    end;
    FServer.Active := false;
  end;
  FServer.Free;
  inherited Destroy;
end;


function TRemoteServer.ClientCount(): integer;
var
  nClients : integer;
begin
  try
      //get number of connected clients
      nClients := FServer.Contexts.LockList.Count;
  finally
      FServer.Contexts.UnlockList;
  end;
  Result := nClients;
end;
Reply
#8
(07-07-2020, 10:33 AM)BosseB Wrote: OK, so then if there is a WaitFor() in the code then it will stay here most of the time?

Yes, if your traffic is light.

(07-07-2020, 10:33 AM)BosseB Wrote: Even when the timeout hits it will come back here, right (since it is immediately re-called).

Yes.

(07-07-2020, 10:33 AM)BosseB Wrote: Is this call coming from a separate thread so it won't slow down the rest of the application, for example if there are two clients connected...

Yes.  TIdTCPServer is multi-threaded.  Each client runs in its own thread.  The OnConnect, OnDisconnect, OnExecute, and OnException events are fired in the context of those threads.  That is why I said earlier that your Log3RServer logic needed to be thread-safe, since you could have multiple client threads trying to log at the same time.

(07-07-2020, 10:33 AM)BosseB Wrote: OK, then I will probably have a first check if there are at least 3 bytes in there (STX-Databyte-ETX) before going on. It would make the OnExecute exit much quicker most of the times

That is not necessary.  That will just lead to busy loops that eat up CPU cycles.  It would be better to just let the event handler block waiting for the message terminator to arrive.  The thread will be in an efficient sleep state during that time.  Stop thinking of TIdTCPServer in terms of events, like with TServerSocket.  Indy is a blocking library, so think in terms of functionality instead.  You want to process a complete message, so read a complete message and process it.  Let Indy block while it is waiting for the complete message to arrive.  Read a message, process it, exit, repeat.  That is how OnExecute is intended to be used in most situations.

(07-08-2020, 03:53 PM)BosseB Wrote: I then also think I need to address the case when the server is shut down with clients connected...
But I am not sure how to handle that precisely...

For the most part, TIdTCPServer handles that for you.  Simply deactivate the server (set Active=False) and it will disconnect all active clients for you.

In the case of socket operations that are blocked, they will simply fail when their connections are closed, typically raising exceptions.  Let those exceptions escape your event handlers so the server can terminate the client threads properly.  If you want to catch the exceptions (for logging, cleanup, etc), be sure to re-raise them when you are done, or at least Exit the handlers.

It is important that during a shutdown, execution flow return to TIdTCPServer.  The Active property setter blocks the calling thread waiting for all client threads to terminate.

Which means a gotcha to watch out for is if your event handlers synchronize with the same thread that is deactivating TIdTCPServer.  That will cause a deadlock if you use a synchronous sync, like TThread.Synchronize(), TIdSync, SendMessage(), etc.  In which case, you can either
  • use an asynchronous sync instead, like TThread.Queue(), TIdNotify, Post(Thread)Message(), etc.
  • deactivate the server from another thread, so that the thread which processes the syncs is not blocked.

(07-08-2020, 03:53 PM)BosseB Wrote:
Code:
destructor TRemoteServer.Destroy;
begin
  if FServer.Active then
  begin
    if ClientCount > 0 then
    begin
      //Need to disconnect the clients so they get the message....
      //What to do here?

    end;
    FServer.Active := false;
  end;
  FServer.Free;
  inherited Destroy;
end;

There is nothing to do there.  Just deactivate the server:

Code:
destructor TRemoteServer.Destroy;
begin
  FServer.Active := false; // <-- optional - TIdTCPServer's destructor will do this for you!
  FServer.Free;
  inherited Destroy;
end;

On the other hand, if you want to actually send a message to each client during a shutdown, that is a different matter. You would have to manually loop through the TIdTCPServer.Contexts list sending your messages to each active client as needed.

Which I don't generally suggest doing when your event handlers also send their own messages (replies to requests, etc), as this risks corrupting your communications having 2 separate threads sending messages to a given client potentially at the same time. Unless you synchronize your sends. But, the other problem with doing this kind of loop is that sending messages in a loop in another thread risks crashing the loop if an error occurs, or worse deadlocking the loop if it encounters a misbehaving client. That, and also the sends will be done in serial rather than in parallel (which is not really a problem when you have only a few clients connected, but it may be an issue if you have a lot of clients connected).

The alternative would be to set a flag somewhere during shutdown and then have your OnExecute handler look at that flag periodically when it is safe to do so (ie, between readings of new client messages), and if the flag is set then send a message as needed before disconnecting the client. That way, everything is kept in the context of the client's thread, and each client thread can handle its own shutdown.

Reply
#9
Thanks, I think that does it for me!
And there are never many clients connected, normally 0 or 1 but I have set a max of 2 clients.

Now I must rip out the old code and insert the new IdTCP server in its place...

But one clarification concerning the OnExecute handler:

If readln timeouts will it return an empty string to msg? I.e. it will leave the data in the buffer until the line ending char arrives?
I don't want to have partial packets to deal with...
Code:
msg := AContext.Connection.IOHandler.ReadLn(ETX, 10); //Timeout 10 ms

I guess it must work that way because otherwise one can never know if a partial packet is present when processing it (the ETX char is "eaten" by ReadLn)...
Reply
#10
(07-06-2020, 11:44 PM)rlebeau Wrote:
(07-06-2020, 11:19 PM)BosseB Wrote: I think it will not be a big problem but I have noted when testing that there seems to be a lag in the handling, for example when I connect the client it connects immediately as witnessed by activity in the OnConnect, where I print out the connection message on the console, yet it takes several seconds or more before the client app shows the login dialog to the user in preparation for actually logging in. It is raised as soon as the client discovers the connection succeeded.

Same when sending the login request it is immediately shown in the console but the client takes some time to discover that a valid login has been accepted even though my test code sends the reply immediately after the console has been written to.

Did you have that same problem when using TServerSocket?  It sounds like maybe the client's logic has a bug or design flaw in it, where it is not handling socket activity in a timely manner.  Maye a buggy socket handler, or a laggy message queue, or something like that.  I can't really help you with this without seeing the client's code.

Now after I have implemented the changes you suggested I also moved the application to Linux (Raspbian) and compiled it there. So I can compare the server behavior on Linux vs. on Windows.

It turns out that when exactly the same code is compiled and executed on Windows and on Linux the Linux version does not show this lag when using exactly the same client on Windows 10 as I used before.

I can run the server on both platforms and connect to each by changing the host value, and when connecting to the Linux server the response is immediate. The login dialog appears instantly, whereas if I connect to the Windows based server there is a delay of maybe 5 seconds before that happens.

I now believe it is caused by Windows10 "trying to be clever"....

Not an Indy or FPC issue.

And in any case it is not an issue to me for the whole exercise is to move the server to Linux anyway.
Thanks for your help!
Reply


Forum Jump:


Users browsing this thread: 2 Guest(s)