Watch, Follow, &
Connect with Us

For forums, blogs and more please visit our
Developer Tools Community.


Welcome, Guest
Guest Settings
Help

Thread: How to handle multiple HTTP sessions with Indy10 TIdHTTPServer


This question is answered.


Permlink Replies: 5 - Last Post: Jan 31, 2017 12:55 PM Last Post By: Remy Lebeau (Te...
Micha Ertkins

Posts: 3
Registered: 3/2/17
How to handle multiple HTTP sessions with Indy10 TIdHTTPServer  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jan 30, 2017 12:21 AM
Hi!
I'm working on a small HTTP server as an interface to my home automatization. I use the really nice Indy 10 implementaion TIdHTTPServer. So far so good. Now I ran in some problems when a client reloads the page while the prior request has not yet been accomplished. i.e. the event "OnCommandGet" gets triggert again BEFORE the last procedure call had returned. Then I get a EIdSocketError #10053 "software caused connection abort" that I cannot catch via try-except (???). Looks somehow that my OnCommandGet-routine is not "thread-save". Usually I would try to use an individual thread for each request, but I have no clue how that works here. I know about TIdHTTPServer.CreateSession and TIdHTTPCustomSessionList but I cannot figure out how they are to be used. Do I have to "CreateSession" each time I get the OnCommandGet-event? But that wouldn't be perfectly thread save in the end as well.

Here's my code:

procedure TForm1.ServerCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
      try
 
     if ARequestInfo.QueryParams<>'' then
        begin
        Memo1.Lines.Add(DateTimeToStr(Now)+': ReqParam "'+ARequestInfo.QueryParams+'"');
 
        AResponseInfo.ContentText:=ALLNET.ProcRequest(ARequestInfo.QueryParams);
        end
     else
            begin
            AResponseInfo.ContentText:='<HTML><BODY>Error: Client connection lost.</BODY></HTML>';
            Memo1.Lines.Add(DateTimeToStr(Now)+':           - Error: Client connection lost.');
            end;
 
      except
        on E: EIdSocketError do
            begin
            Memo1.Lines.Add(DateTimeToStr(Now)+': ['+IntToStr(cnt)+']  EXCEPTION!!.');
            end;
      end;
 
end;

Thanks for your help (if someone listens..)!!! :)

Remy Lebeau (Te...


Posts: 9,447
Registered: 12/23/01
Re: How to handle multiple HTTP sessions with Indy10 TIdHTTPServer
Correct
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jan 30, 2017 12:08 PM   in response to: Micha Ertkins in response to: Micha Ertkins
Micha Ertkins wrote:
Now I ran in some problems when a client reloads the page while
the prior request has not yet been accomplished. i.e. the event
"OnCommandGet" gets triggert again BEFORE the last procedure
call had returned.

That is certainly a possibility. TIdHTTPServer is multi-threaded, each TCP connection runs in its own thread. The only way for a browser to cancel an HTTP request in progress is for it to close its current connection. The browser would then have to make a new TCP connection before it can send a new HTTP request. So TIdHTTPServer could get the new request before it has reacted to the loss of the previous connection.

Then I get a EIdSocketError #10053 "software caused connection
abort"

On the new connection or the old connection? If it is on the old connection, then that error is to be expected when the browser kills its old connection. You should let TIdHTTPServer handle it so it can close down the old connection and shut down its worker thread.

that I cannot catch via try-except (???).

All Indy exceptions are catchable. If you are not able to catch it, you are probably trying to catch it in the wrong place.

Also, have a look at the server's OnException event, if you want to report unexpected errors.

Unless you have good reason to catch and handle exceptions, you really shouldn't. Let TIdHTTPServer handle them internally. If you do catch exceptions, at least re-raise any Indy exceptions you catch, unless you close the connections manually in your exception handlers.

Looks somehow that my OnCommandGet-routine is not "thread-save".

One problem is it is directly accessing your TMemo from outside of the main UI thread. You must synchronize with the main thread in order to access VCL/FMX UI controls safely.

Usually I would try to use an individual thread for each request, but I
have no clue how that works here.

TIdHTTPServer is already threaded. Each OnCommand... event is fired in a thread, where the TIdContext represents the particular client that each thread manages. Just know if a client uses HTTP keep-alives, and you have them enabled in TIdHTTPServer, it is possible to get multiple OnCommand... events in the same thread, though they will be fired in sequence, not in parallel.

I know about TIdHTTPServer.CreateSession and TIdHTTPCustomSessionList
but I cannot figure out how they are to be used. Do I have to "CreateSession"
each time I get the OnCommandGet event? But that wouldn't be perfectly
thread save in the end as well.

The best option is to let TIdHTTPServer manage the sessions for you. Just set TIdHTTPServer.SessionState to true (it is false by default). If you don't assign a handler to the TIdHTTPServer.OnCreateSession event, the server creates new sessions for you. Either way, sessions are available in the TIdHTTPRequestInfo.Session and TIdHTTPResponseInfo.Session properties in the OnCommand... events.

Here's my code:

Try this instead:

procedure TForm1.ServerCommandGet(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
  Msg: String;
begin
  if ARequestInfo.QueryParams <> '' then
  begin
    Msg := DateTimeToStr(Now) + ': ReqParam "' + ARequestInfo.QueryParams + "';
    TThread.Queue(nil,
      procedure
      begin
        Memo1.Lines.Add(S);
      end
    );
    AResponseInfo.ContentText := ALLNET.ProcRequest(ARequestInfo.QueryParams);
  end
  else
  begin
    AResponseInfo.ContentText := '<HTML><BODY>Error: No Query Params.</BODY></HTML>';
    Msg := DateTimeToStr(Now) + ': Error: No Query Params';
    TThread.Queue(nil,
      procedure
      begin
        Memo1.Lines.Add(S);
      end
    );
  end;
end;
 
procedure TForm1.ServerException(AContext: TIdContext; AException: Exception);
var
  Msg: String;
begin
  Msg := DateTimeToStr(Now) + ': [' + IntToStr(cnt) + '] EXCEPTION!! ' + AException.Message;
  TThread.Queue(nil,
    procedure
    begin
      Memo1.Lines.Add(Msg);
    end
  );
end;


--
Remy Lebeau (TeamB)
Micha Ertkins

Posts: 3
Registered: 3/2/17
Re: How to handle multiple HTTP sessions with Indy10 TIdHTTPServer  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jan 30, 2017 12:53 PM   in response to: Remy Lebeau (Te... in response to: Remy Lebeau (Te...
@Remy Lebeau: Wow, MAGIC! Thanx for this really great answer with a lot of excellent infos!
Your absolutely right - IdHTTPServer IS multi-threaded. I took the pleasure to run through the source code (lots of editions by you :) ) and found the TIdListenerThread = class(TIdThread), FListenerThreads := TIdThreadList.Create. So IdCustomTCPServer is encapsuled in TIdListenerThread which is in the FListenerThreads list. And the DoCommandGet is executed by the thread. Therefor I get my OnCommandGet event indeed from real Threads - BUT: they enter my OnCommandGet-routine (worst case) so shortly after each other, that my parameters (AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo;AResponseInfo: TIdHTTPResponseInfo) get overwritten by the 2nd incoming OnCommandGet-event.
I'll have to think of a nice solution for my pgm - and will report later...
Remy Lebeau (Te...


Posts: 9,447
Registered: 12/23/01
Re: How to handle multiple HTTP sessions with Indy10 TIdHTTPServer
Helpful
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jan 30, 2017 1:11 PM   in response to: Micha Ertkins in response to: Micha Ertkins
Micha wrote:

And the DoCommandGet is executed by the thread. Therefor I get
my OnCommandGet event indeed from real Threads - BUT: they enter
my OnCommandGet-routine (worst case) so shortly after each other

The only way that OnCommandGet can be re-entered while it is already running
is if the new call is coming from a different TCP connection, and thus from
a different thread. You have to be prepared to handle that in your event
code.

that my parameters (AContext: TIdContext; ARequestInfo:
TIdHTTPRequestInfo;AResponseInfo: TIdHTTPResponseInfo) get
overwritten by the 2nd incoming OnCommandGet-event.

No, they are not overwritten. They are all local to the particular thread
that is calling your OnCommandGet handler. It just happens that multiple
threads could be calling your OnCommandGet handler at the same time in parallel,
and each call will have its own set of local objects for the parameters.
Any overlapping/overwritting issues that you experience will be due to bugs
in your own code.

--
Remy Lebeau (TeamB)
Micha Ertkins

Posts: 3
Registered: 3/2/17
Re: How to handle multiple HTTP sessions with Indy10 TIdHTTPServer  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jan 31, 2017 12:33 PM   in response to: Remy Lebeau (Te... in response to: Remy Lebeau (Te...
@Remy: Great - problem solved! Thank you so much!
Now relying on the built-in exception handling of TIdHTTPServer & Co. I ignored the IDE Exception msgs for EIdSocketError and voilĂ  the user can be happy: no more msg boxes.
The only remaining issue was that my local vars which were holding the parameters of the OnCommandGet event (AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo) got corrupted by to two or more OnCommandGet events in (nearly) immediate succession so the preceding call hasn't returned yet.
I solved it with a sort of ring buffer (for now quick'n'dirty):

var
  OCGRecArray:Array[0..9] of OCGRec;
  OCGRecCnt:Integer = 0;
  OCGRecPtr:Integer = 0;
  EntryLock:Integer = 0;
 
(...)
 
function TForm1.WaitForEntryFree:Boolean;
begin
     Result:=False;
 
     repeat
        if EntryLock=0 then
           begin
           inc(EntryLock);
           Result:=True;
           Exit;
           end;
 
        Sleep(1);
  
        // maybe adding a time-out would be smart...
     until false;
end;
 
 
procedure TForm1.ConfirmExit;
begin
     dec(EntryLock);
end;
 
 
procedure TForm1.ServerCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
      OCGRecArray[OCGRecCnt].AContext:=AContext;
      OCGRecArray[OCGRecCnt].ARequestInfo:=ARequestInfo;
      OCGRecArray[OCGRecCnt].AResponseInfo:=AResponseInfo;
      inc(OCGRecCnt);
      if OCGRecCnt=10 then OCGRecCnt:=0; //hopefully not more than ten at a time... ;)
 
      WaitForEntryFree;
 
      // do something...
 
      // ... but use the correct parameters.
      OCGRecArray[OCGRecPtr].AResponseInfo.ContentText:=(....)
 
      inc(OCGRecPtr);
      if OCGRecPtr=10 then OCGRecPtr:=0;
 
      ConfirmExit;
end;
 


Sure, this is not a nice style yet and has room for optimization but for me it worked - so far.

Greetings!
Remy Lebeau (Te...


Posts: 9,447
Registered: 12/23/01
Re: How to handle multiple HTTP sessions with Indy10 TIdHTTPServer  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jan 31, 2017 12:55 PM   in response to: Micha Ertkins in response to: Micha Ertkins
Micha wrote:

The only remaining issue was that my local vars which were holding
the parameters of the OnCommandGet event (AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo)
got corrupted by to two or more OnCommandGet events in (nearly)
immediate succession so the preceding call hasn't returned yet.

If they were truely local variables (which they are actually not), they
could not be corrupted by multiple events running in parallel since they
would exist in different thread contexts. The ONLY way multiple threads
can corrupt each other's local variables is if you are sharing pointers
to the variables across thread boundaries.

You are not actually using local variables at all, though. You are actually
using variables that are members of your Form class instead. You have
multiple threads calling the same method on a single Form object. That requires
inter-thread synchronization to handle safe concurrent access to that object's
members across thread boundaries.

--
Remy Lebeau (TeamB)
Legend
Helpful Answer (5 pts)
Correct Answer (10 pts)

Server Response from: ETNAJIVE02